1package com.peachpasswords.license23import java.math.BigInteger4import java.security.SecureRandom5import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator6import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters7import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters8import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters9import org.bouncycastle.crypto.signers.Ed25519Signer10import org.junit.Assert.assertEquals11import org.junit.Assert.assertFalse12import org.junit.Assert.assertNotNull13import org.junit.Assert.assertTrue14import org.junit.Test1516171819202122232425class LicenseCodecTest {2627 private val alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"2829 private class KeyPairFixture {30 val privateKey: Ed25519PrivateKeyParameters31 val publicKeyHex: String32 constructor() {33 val generator = Ed25519KeyPairGenerator()34 generator.init(Ed25519KeyGenerationParameters(SecureRandom()))35 val pair = generator.generateKeyPair()36 privateKey = pair.private as Ed25519PrivateKeyParameters37 publicKeyHex = (pair.public as Ed25519PublicKeyParameters).encoded.joinToString("") { "%02x".format(it) }38 }3940 fun sign(payload: ByteArray): ByteArray {41 val signer = Ed25519Signer()42 signer.init(true, privateKey)43 signer.update(payload, 0, payload.size)44 return signer.generateSignature()45 }46 }4748 private fun tagPayload(flipProduct: Boolean = false, flipPlan: Boolean = false, flipVersion: Boolean = false) =49 ByteArray(LicenseCodec.PAYLOAD_SIZE).also {50 it[0] = if (flipProduct) 0x02 else LicenseCodec.PRODUCT_TAG51 it[1] = if (flipPlan) 0x02 else LicenseCodec.PLAN_TAG52 it[2] = if (flipVersion) 0x02 else LicenseCodec.VERSION_TAG53 it[7] = 20 54 }5556 private fun key(payload: ByteArray, signature: ByteArray): String =57 "PPK-" + base58Encode(payload + signature)5859 private fun base58Encode(bytes: ByteArray): String {60 var n = BigInteger(1, bytes)61 val out = StringBuilder()62 while (n > BigInteger.ZERO) {63 val (quotient, remainder) = n.divideAndRemainder(BigInteger.valueOf(58))64 out.append(alphabet[remainder.toInt()])65 n = quotient66 }67 return out.reverse().toString()68 }6970 @Test fun a_correctly_signed_blob_is_accepted() {71 val fixture = KeyPairFixture()72 val payload = tagPayload()73 val license = key(payload, fixture.sign(payload))7475 assertTrue("fixture must be a valid licence shape", license.length - 4 <= 104)76 assertTrue(LicenseCodec.validate(license, fixture.publicKeyHex))77 assertNotNull(LicenseCodec.issuedAtMillis(license))78 }7980 @Test fun a_flipped_signature_byte_is_rejected() {81 val fixture = KeyPairFixture()82 val payload = tagPayload()83 val signature = fixture.sign(payload)84 signature[10] = (signature[10].toInt() xor 0x01).toByte()8586 assertFalse(LicenseCodec.validate(key(payload, signature), fixture.publicKeyHex))87 }8889 @Test fun a_mutated_payload_is_rejected() {90 val fixture = KeyPairFixture()91 val payload = tagPayload()92 val signature = fixture.sign(payload)93 payload[7] = (payload[7] + 1).toByte() 9495 assertFalse(LicenseCodec.validate(key(payload, signature), fixture.publicKeyHex))96 }9798 @Test fun a_wrong_product_plan_or_version_tag_is_rejected_before_signature_work() {99 val fixture = KeyPairFixture()100 listOf(101 tagPayload(flipProduct = true),102 tagPayload(flipPlan = true),103 tagPayload(flipVersion = true),104 ).forEach { payload ->105 106 assertFalse(LicenseCodec.validate(key(payload, fixture.sign(payload)), fixture.publicKeyHex))107 }108 }109110 @Test fun over_length_and_illegal_character_input_is_rejected() {111 val fixture = KeyPairFixture()112 val payload = tagPayload()113 val valid = key(payload, fixture.sign(payload))114115 assertFalse("no PPK- prefix", LicenseCodec.validate(valid.removePrefix("PPK-"), fixture.publicKeyHex))116 assertFalse("over-length body", LicenseCodec.validate("PPK-" + "1".repeat(400), fixture.publicKeyHex))117 assertFalse("illegal base58 character", LicenseCodec.validate("PPK-0OIl" + valid, fixture.publicKeyHex))118 assertEquals(null, LicenseCodec.issuedAtMillis("PPK-0OIl"))119 }120}