1package com.peachpasswords.sync23import org.junit.Assert.assertNotNull4import org.junit.Assert.assertNull5import org.junit.Test6import org.junit.Assert.assertEquals789101112class DeviceManifestSupportTest {13 private val master = ByteArray(32) { it.toByte() }14 private val signing = deriveSyncSigningKeypair(master, "issuer")1516 1718192021 private val knownIssuerKeys: (String) -> Set<String> = { setOf(pubkeyToHex(signing.publicKeyRaw)) }2223 private fun validEntry(deviceId: String = "peer", pairedAt: Long = 1L) = ManifestDeviceEntry(24 deviceId = deviceId,25 deviceName = "Peer",26 deviceType = "android",27 longTermPublicKeyHex = "aa".repeat(32),28 signingPublicKeyHex = "bb".repeat(32),29 pairedAt = pairedAt,30 )3132 private fun manifestWith(issuedAt: Long, devices: List<ManifestDeviceEntry> = listOf(validEntry())) =33 buildSignedDeviceManifest(34 issuerDeviceId = "issuer",35 devices = devices,36 signingPrivateKey = signing.privateKey,37 nowMs = issuedAt,38 )3940 @Test fun rejects_issued_at_long_min_value() {41 42 43 val manifest = manifestWith(issuedAt = Long.MIN_VALUE)44 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_700_000_000_000L))45 }4647 @Test fun rejects_issued_at_too_old() {48 val manifest = manifestWith(issuedAt = 1L)49 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_700_000_000_000L))50 }5152 @Test fun rejects_issued_at_too_far_in_future() {53 val now = 1_700_000_000_000L54 55 val manifest = manifestWith(issuedAt = now + 5 * 60_000L + 1L)56 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = now))57 }5859 @Test fun accepts_issued_at_within_window() {60 val now = 1_700_000_000_000L61 val manifest = manifestWith(issuedAt = now)62 assertNotNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = now))63 }6465 @Test fun rejects_oversized_device_id_string() {66 val manifest = manifestWith(67 issuedAt = 1_000L,68 devices = listOf(validEntry(deviceId = "x".repeat(10_000))),69 )70 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_000L))71 }7273 @Test fun caps_oversized_device_name_at_verify() {74 75 76 77 val now = 1_700_000_000_000L78 val body = DeviceManifestBody(79 manifestVersion = DEVICE_MANIFEST_VERSION,80 issuerDeviceId = "issuer",81 issuedAt = now,82 devices = listOf(validEntry().copy(deviceName = "N".repeat(200))),83 revoked = emptyList(),84 )85 val signature = signEd25519(86 signing.privateKey,87 canonicalDeviceManifestBody(body).toByteArray(Charsets.UTF_8),88 )89 val verified = verifyDeviceManifest(90 SignedDeviceManifest(body, pubkeyToHex(signature)),91 signing.publicKeyRaw,92 knownIssuerKeys,93 nowMs = now,94 )95 assertNotNull(verified)96 assertEquals("N".repeat(MAX_DEVICE_NAME_CHARS), verified!!.devices[0].deviceName)97 }9899 @Test fun caps_device_name_at_build() {100 val manifest = manifestWith(101 issuedAt = 1_000L,102 devices = listOf(validEntry().copy(deviceName = "B".repeat(200))),103 )104 assertEquals("B".repeat(MAX_DEVICE_NAME_CHARS), manifest.body.devices[0].deviceName)105 }106107 @Test fun rejects_negative_paired_at() {108 val manifest = manifestWith(109 issuedAt = 1_000L,110 devices = listOf(validEntry(pairedAt = -1L)),111 )112 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_000L))113 }114115 @Test fun rejects_too_many_devices() {116 val manifest = buildSignedDeviceManifest(117 issuerDeviceId = "issuer",118 devices = List(300) { validEntry(deviceId = "peer-$it") },119 signingPrivateKey = signing.privateKey,120 nowMs = 1_000L,121 )122 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_000L))123 }124125 @Test fun rejects_oversized_issuer_device_id() {126 val manifest = manifestWith(issuedAt = 1_000L)127 val oversized = manifest.copy(body = manifest.body.copy(issuerDeviceId = "y".repeat(2_000)))128 assertNull(verifyDeviceManifest(oversized, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_000L))129 }130131 @Test fun rejects_negative_revoked_at() {132 val manifest = buildSignedDeviceManifest(133 issuerDeviceId = "issuer",134 devices = emptyList(),135 signingPrivateKey = signing.privateKey,136 nowMs = 1_000L,137 revoked = listOf(138 DeviceRevocation(139 deviceId = "peer",140 longTermPublicKeyHex = "aa".repeat(32),141 revokedAt = -1L,142 revokedByDeviceId = "issuer",143 ),144 ),145 )146 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_000L))147 }148 @Test fun revocation_signature_binds_immutable_fields() {149 val record = signDeviceRevocation(150 DeviceRevocation("peer", "aa".repeat(32), 42L, "issuer"),151 signing.privateKey,152 )153 assertEquals(154 """{"deviceId":"peer","longTermPublicKeyHex":"${"aa".repeat(32)}","recordVersion":2,"revokedAt":42,"revokedByDeviceId":"issuer"}""",155 canonicalDeviceRevocation(record),156 )157 assertEquals(true, verifyDeviceRevocation(record, signing.publicKeyRaw))158 assertEquals(false, verifyDeviceRevocation(record.copy(deviceId = "attacker"), signing.publicKeyRaw))159 }160 @Test fun forwarding_preserves_original_revocation_signature() {161 val original = signDeviceRevocation(162 DeviceRevocation("peer", "aa".repeat(32), 42L, "issuer"),163 signing.privateKey,164 )165 val forwarder = deriveSyncSigningKeypair(master, "forwarder")166 val forwarded = buildSignedDeviceManifest(167 issuerDeviceId = "forwarder",168 devices = emptyList(),169 signingPrivateKey = forwarder.privateKey,170 nowMs = 1_000L,171 revoked = listOf(original),172 )173 assertEquals(original, forwarded.body.revoked.single())174 assertNotNull(verifyDeviceManifest(forwarded, forwarder.publicKeyRaw, knownIssuerKeys, nowMs = 1_000L))175 }176177 @Test fun rejects_a_revocation_signed_by_an_untrusted_issuer_key() {178 179 180 181 182 val attacker = deriveSyncSigningKeypair(ByteArray(32) { (it + 9).toByte() }, "attacker")183 val forged = signDeviceRevocation(184 DeviceRevocation("victim", "aa".repeat(32), 42L, "attacker"),185 attacker.privateKey,186 )187 assertEquals(true, verifyDeviceRevocation(forged, attacker.publicKeyRaw))188189 val manifest = buildSignedDeviceManifest(190 issuerDeviceId = "issuer",191 devices = emptyList(),192 signingPrivateKey = signing.privateKey,193 nowMs = 1_000L,194 revoked = listOf(forged),195 )196 assertNull(197 verifyDeviceManifest(manifest, signing.publicKeyRaw, knownIssuerKeys, nowMs = 1_000L),198 )199 }200201 @Test fun rejects_a_revocation_when_the_issuer_key_cannot_be_resolved() {202 val record = signDeviceRevocation(203 DeviceRevocation("peer", "aa".repeat(32), 42L, "issuer"),204 signing.privateKey,205 )206 val manifest = buildSignedDeviceManifest(207 issuerDeviceId = "issuer",208 devices = emptyList(),209 signingPrivateKey = signing.privateKey,210 nowMs = 1_000L,211 revoked = listOf(record),212 )213 214 215 216 assertNull(verifyDeviceManifest(manifest, signing.publicKeyRaw, { emptySet() }, nowMs = 1_000L))217 }218}