1package com.peachpasswords.license23import java.math.BigInteger4import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters5import org.bouncycastle.crypto.signers.Ed25519Signer678910111213141516171819internal object LicenseCodec {20 private const val PUBLIC_KEY_HEX =21 "c7dc52d2c484696cf8566dbe6cfe69025d114cb47f6cf53c25088ecfd539bdfa"2223 private const val BASE58_ALPHABET =24 "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"2526 27 private const val MAX_BASE58_LENGTH = 1042829 const val PAYLOAD_SIZE = 1230 const val SIGNATURE_SIZE = 6431 const val TOTAL_SIZE = 763233 const val PRODUCT_TAG = 0x01.toByte()34 const val PLAN_TAG = 0x01.toByte()35 const val VERSION_TAG = 0x01.toByte()3637 38 fun validate(licenseKey: String, publicKeyHex: String = PUBLIC_KEY_HEX): Boolean {39 return try {40 val decoded = decodeLicenseKey(licenseKey) ?: return false41 if (decoded.size != TOTAL_SIZE) return false42 val payload = decoded.copyOfRange(0, PAYLOAD_SIZE)43 val signature = decoded.copyOfRange(PAYLOAD_SIZE, TOTAL_SIZE)44 45 46 if (payload[0] != PRODUCT_TAG) return false47 if (payload[1] != PLAN_TAG) return false48 if (payload[2] != VERSION_TAG) return false49 verifySignature(payload, signature, publicKeyHex)50 } catch (_: Exception) {51 false52 }53 }5455 56 fun issuedAtMillis(licenseKey: String): Long? {57 val decoded = decodeLicenseKey(licenseKey) ?: return null58 if (decoded.size != TOTAL_SIZE) return null59 val payload = decoded.copyOfRange(0, PAYLOAD_SIZE)60 if (payload[0] != PRODUCT_TAG || payload[1] != PLAN_TAG || payload[2] != VERSION_TAG) return null61 val days = (payload[3].toInt() and 0xff) or62 ((payload[4].toInt() and 0xff) shl 8) or63 ((payload[5].toInt() and 0xff) shl 16) or64 ((payload[6].toInt() and 0xff) shl 24)65 return days.toLong() * 86_400_000L66 }6768 private fun decodeLicenseKey(key: String): ByteArray? {69 if (!key.startsWith("PPK-")) return null70 val encoded = key.removePrefix("PPK-").replace("-", "")71 return base58Decode(encoded)72 }7374 private fun base58Decode(input: String): ByteArray? {75 if (input.length > MAX_BASE58_LENGTH) return null76 return try {77 var n = BigInteger.ZERO78 for (char in input) {79 val index = BASE58_ALPHABET.indexOf(char)80 if (index < 0) return null81 n = n.multiply(BigInteger.valueOf(58)).add(BigInteger.valueOf(index.toLong()))82 }83 val bytes = n.toByteArray()84 if (bytes.isNotEmpty() && bytes[0] == 0.toByte()) bytes.copyOfRange(1, bytes.size) else bytes85 } catch (_: Exception) {86 null87 }88 }8990 private fun verifySignature(payload: ByteArray, signature: ByteArray, publicKeyHex: String): Boolean {91 return try {92 val verifier = Ed25519Signer()93 verifier.init(false, Ed25519PublicKeyParameters(hexToBytes(publicKeyHex), 0))94 verifier.update(payload, 0, payload.size)95 verifier.verifySignature(signature)96 } catch (_: Exception) {97 false98 }99 }100101 private fun hexToBytes(hex: String): ByteArray {102 val len = hex.length103 val data = ByteArray(len / 2)104 for (i in 0 until len step 2) {105 data[i / 2] = ((Character.digit(hex[i], 16) shl 4) +106 Character.digit(hex[i + 1], 16)).toByte()107 }108 return data109 }110}