package com.peachpasswords.license import java.math.BigInteger import java.security.SecureRandom import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator import org.bouncycastle.crypto.params.Ed25519KeyGenerationParameters import org.bouncycastle.crypto.params.Ed25519PrivateKeyParameters import org.bouncycastle.crypto.params.Ed25519PublicKeyParameters import org.bouncycastle.crypto.signers.Ed25519Signer import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotNull import org.junit.Assert.assertTrue import org.junit.Test /** * PB-26-147: nothing covered the licence validation boundary — the one write * path whose previous defect was a missing validation call. * * The verifier's public key is a parameter (defaulting to the compiled-in key), * so these tests mint blobs with their own keypair. That is what makes the * rejections meaningful: a verifier that rejected everything would pass a * negative-only suite. */ class LicenseCodecTest { private val alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" private class KeyPairFixture { val privateKey: Ed25519PrivateKeyParameters val publicKeyHex: String constructor() { val generator = Ed25519KeyPairGenerator() generator.init(Ed25519KeyGenerationParameters(SecureRandom())) val pair = generator.generateKeyPair() privateKey = pair.private as Ed25519PrivateKeyParameters publicKeyHex = (pair.public as Ed25519PublicKeyParameters).encoded.joinToString("") { "%02x".format(it) } } fun sign(payload: ByteArray): ByteArray { val signer = Ed25519Signer() signer.init(true, privateKey) signer.update(payload, 0, payload.size) return signer.generateSignature() } } private fun tagPayload(flipProduct: Boolean = false, flipPlan: Boolean = false, flipVersion: Boolean = false) = ByteArray(LicenseCodec.PAYLOAD_SIZE).also { it[0] = if (flipProduct) 0x02 else LicenseCodec.PRODUCT_TAG it[1] = if (flipPlan) 0x02 else LicenseCodec.PLAN_TAG it[2] = if (flipVersion) 0x02 else LicenseCodec.VERSION_TAG it[7] = 20 // issue date, days } private fun key(payload: ByteArray, signature: ByteArray): String = "PPK-" + base58Encode(payload + signature) private fun base58Encode(bytes: ByteArray): String { var n = BigInteger(1, bytes) val out = StringBuilder() while (n > BigInteger.ZERO) { val (quotient, remainder) = n.divideAndRemainder(BigInteger.valueOf(58)) out.append(alphabet[remainder.toInt()]) n = quotient } return out.reverse().toString() } @Test fun a_correctly_signed_blob_is_accepted() { val fixture = KeyPairFixture() val payload = tagPayload() val license = key(payload, fixture.sign(payload)) assertTrue("fixture must be a valid licence shape", license.length - 4 <= 104) assertTrue(LicenseCodec.validate(license, fixture.publicKeyHex)) assertNotNull(LicenseCodec.issuedAtMillis(license)) } @Test fun a_flipped_signature_byte_is_rejected() { val fixture = KeyPairFixture() val payload = tagPayload() val signature = fixture.sign(payload) signature[10] = (signature[10].toInt() xor 0x01).toByte() assertFalse(LicenseCodec.validate(key(payload, signature), fixture.publicKeyHex)) } @Test fun a_mutated_payload_is_rejected() { val fixture = KeyPairFixture() val payload = tagPayload() val signature = fixture.sign(payload) payload[7] = (payload[7] + 1).toByte() // issue date moved after signing assertFalse(LicenseCodec.validate(key(payload, signature), fixture.publicKeyHex)) } @Test fun a_wrong_product_plan_or_version_tag_is_rejected_before_signature_work() { val fixture = KeyPairFixture() listOf( tagPayload(flipProduct = true), tagPayload(flipPlan = true), tagPayload(flipVersion = true), ).forEach { payload -> // Correctly signed, so only the tag check can reject it. assertFalse(LicenseCodec.validate(key(payload, fixture.sign(payload)), fixture.publicKeyHex)) } } @Test fun over_length_and_illegal_character_input_is_rejected() { val fixture = KeyPairFixture() val payload = tagPayload() val valid = key(payload, fixture.sign(payload)) assertFalse("no PPK- prefix", LicenseCodec.validate(valid.removePrefix("PPK-"), fixture.publicKeyHex)) assertFalse("over-length body", LicenseCodec.validate("PPK-" + "1".repeat(400), fixture.publicKeyHex)) assertFalse("illegal base58 character", LicenseCodec.validate("PPK-0OIl" + valid, fixture.publicKeyHex)) assertEquals(null, LicenseCodec.issuedAtMillis("PPK-0OIl")) } }