Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ repositories {

@Suppress("VulnerableLibrariesLocal")
dependencies {
implementation(libs.bundles.ktor)
compileOnly(libs.paper.api)
implementation(libs.bundles.kotlinx)

testImplementation(libs.kotlin.test)
testImplementation(libs.junit.jupiter)
Expand Down
20 changes: 6 additions & 14 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
[versions]
kotlin = "2.4.0"
kotlinx = "1.11.0"
paper-api = "1.17-R0.1-SNAPSHOT"
paper-api-compat-v1-v17 = "1.17-R0.1-SNAPSHOT"
paper-api-compat-v1_v18v2 = "1.18.2-R0.1-SNAPSHOT"
Expand All @@ -11,7 +12,6 @@ minecraft = "1.21.8"

gdPublish = "1.0.0-SNAPSHOT"

ktor = "3.5.0"
junit-jupiter = "6.0.1"
mockbukkit = "1.13.0"
maven-resolver-provider = "3.8.1"
Expand All @@ -23,11 +23,8 @@ run-task = "3.0.2"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }

ktor-client = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" }
kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx" }

paper-api = { module = "io.papermc.paper:paper-api", version.ref = "paper-api" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
Expand All @@ -36,17 +33,12 @@ maven-resolver-provider = { module = "org.apache.maven:maven-resolver-provider",
maven-resolver-connector-basic = { module = "org.apache.maven.resolver:maven-resolver-connector-basic", version.ref = "maven-resolver" }
maven-resolver-transport-http = { module = "org.apache.maven.resolver:maven-resolver-transport-http", version.ref = "maven-resolver" }

[bundles]
kotlinx = ["kotlinx-coroutines", "kotlinx-serialization"]

[plugins]
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
shadow = { id = "com.gradleup.shadow", version.ref = "shadow" }
run-paper = { id = "xyz.jpenilla.run-paper", version.ref = "run-task" }
gdPublish = { id = "com.guavadealer.plugins.GDPublish", version.ref = "gdPublish" }

[bundles]
ktor = [
"ktor-client",
"ktor-client-cio",
"ktor-client-content-negotiation",
"ktor-serialization",
]
61 changes: 33 additions & 28 deletions src/main/kotlin/com/nmcrate/key/NMKey.kt
Original file line number Diff line number Diff line change
@@ -1,26 +1,23 @@
package com.nmcrate.key

import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.future.await
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.bukkit.plugin.java.JavaPlugin
import java.net.InetAddress
import java.net.NetworkInterface
import java.net.URI
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.security.KeyFactory
import java.security.MessageDigest
import java.security.Signature
import java.security.spec.X509EncodedKeySpec
import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.net.http.*
import java.time.Duration

/**
* Singleton utility for managing license key validation and hardware fingerprinting for NMCrate plugins.
Expand All @@ -30,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap
* internal cache for public keys and license strings to minimize network overhead and I/O operations.
*
* @author Idan Nehama (GuavaDealer)
* @author QrackyDev (Qracky)
* @since 1.0.0
*/
object NMKey {
Expand All @@ -41,18 +39,15 @@ object NMKey {
*/
const val DEFAULT_API_URL = "https://www.nmcrate.com/api/nmkey/v1"

val requestTimeout: Duration = Duration.ofMillis(7500)

private val publicKeys = ConcurrentHashMap<String, String>()
private val cachedKeys = ConcurrentHashMap<String, String>()
private val json = Json { ignoreUnknownKeys = true }
private val client by lazy {
HttpClient(CIO) {
install(ContentNegotiation) { json(json) }
install(HttpTimeout) {
connectTimeoutMillis = 3_000
requestTimeoutMillis = 7_500
socketTimeoutMillis = 7_500
}
}
HttpClient.newBuilder()
.connectTimeout(Duration.ofMillis(3000))
.build()
}
private val keyFactory = KeyFactory.getInstance("Ed25519")
private val base64Decoder: Base64.Decoder = Base64.getDecoder()
Expand Down Expand Up @@ -126,10 +121,14 @@ object NMKey {
val nonce = UUID.randomUUID().toString()

pl.logger.info("NMKey: sending validation request to $DEFAULT_API_URL/validate.")
val res = client.post("$DEFAULT_API_URL/validate") {
contentType(ContentType.Application.Json)
setBody(KeyRequest(pluginId, key, fp, nonce))
}.body<KeyResponse>()
val body = json.encodeToString(KeyRequest(pluginId, key, fp, nonce))
val request = HttpRequest.newBuilder()
.uri(URI("$DEFAULT_API_URL/validate"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.header("Content-Type", "application/json")
.build()
val responseBody = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).get().body()
val res = json.decodeFromString<KeyResponse>(responseBody)

val canonical = "v1|$pluginId|$key|$fp|${res.status}|$nonce|${res.issuedAt}"
val valid = res.status.equals("valid", ignoreCase = true) &&
Expand Down Expand Up @@ -169,10 +168,12 @@ object NMKey {
runCatching {
runBlocking {
val key = readKey(pl) ?: return@runBlocking
client.post("$DEFAULT_API_URL/release") {
contentType(ContentType.Application.Json)
setBody(KeyRequest(pluginId, key, fingerprint(pl)))
}
val body = json.encodeToString(KeyRequest(pluginId, key, fingerprint(pl)))
val request = HttpRequest.newBuilder()
.uri(URI("$DEFAULT_API_URL/releases"))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build()
client.sendAsync(request, HttpResponse.BodyHandlers.discarding())
}
}
}
Expand Down Expand Up @@ -232,9 +233,13 @@ object NMKey {
}

pl.logger.info("NMKey: fetching public key from $DEFAULT_API_URL/public-key.")
val fetched = client.get("$DEFAULT_API_URL/public-key") { parameter("pluginId", pluginId) }
.body<String>()
.removePemPublicKeyHeaders()
val encodedPluginId = URLEncoder.encode(pluginId, StandardCharsets.UTF_8)
val request = HttpRequest.newBuilder()
.uri(URI("$DEFAULT_API_URL/public-key?pluginId=$encodedPluginId"))
.timeout(requestTimeout)
.GET()
.build()
val fetched = client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).await().body().removePemPublicKeyHeaders()

publicKeys[pluginId] = fetched
pl.logger.info("NMKey: public key fetched and cached.")
Expand Down Expand Up @@ -267,7 +272,7 @@ object NMKey {
.digest(raw.toByteArray(StandardCharsets.UTF_8))
.joinToString("") { byte -> "%02x".format(byte) }
.take(32)
} catch (e: Exception) {
} catch (_: Exception) {
return "0"
}
}
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/com/nmcrate/key/NMKeyExtensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import org.bukkit.plugin.java.JavaPlugin
* @param valid Whether the key validation was successful.
*
* @author Idan Nehama (GuavaDealer)
* @author QrackyDev (Qracky)
* @since 1.0.0
*/
class NMKeySession internal constructor(
Expand Down
2 changes: 1 addition & 1 deletion src/test/resources/nmkey.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
NMK-LXROK2W3-2LUCI374-BVLKLO75
NMK-LXROK2W3-2LUCI374-BVLKLO75
2 changes: 1 addition & 1 deletion src/test/resources/paper-plugin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ version: '${version}'

main: com.nmcrate.key.tests.NMKeyTestPlugin
api-version: '1.21.8'
load: POSTWORLD
load: STARTUP