1package com.peachpasswords.sync23import com.peachpasswords.vault.StoredVault4import com.peachpasswords.vault.VaultEntry5import com.peachpasswords.vault.VaultRepository6import com.peachpasswords.vault.VaultSettings7import com.peachpasswords.vault.VaultStorage8import kotlinx.coroutines.runBlocking9import org.junit.Assert.assertEquals10import org.junit.Assert.assertFalse11import org.junit.Assert.assertNull12import org.junit.Assert.assertTrue13import org.junit.Test1415private class FakeStorage : VaultStorage {16 private var slotA: StoredVault? = null17 private var slotB: String? = null18 private var ver: Long = 019 private var settings = VaultSettings()20 override fun loadSlotA() = slotA21 override fun hasVault() = slotA != null22 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }23 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }24 private var decoy: StoredVault? = null25 override fun loadDecoySlot() = decoy26 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }27 override fun hasDecoySlot() = decoy != null28 override fun clearDecoySlot() { decoy = null }29 override fun version() = ver30 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }31 override fun loadSettings() = settings32 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }33 override fun clearVault() { slotA = null; slotB = null; ver = 0; settings = VaultSettings() }34}3536class VaultSyncAdapterTest {37 private val meshRelay = "wss://relay.peachpasswords.com"38 private class FakeDeviceRegistry(39 devices: List<PairedDevice> = emptyList(),40 ) : PairedDeviceRegistry {41 private val byId = devices.associateBy { it.deviceId }.toMutableMap()42 private val revocations = mutableListOf<DeviceRevocation>()43 override fun getAll(): List<PairedDevice> = byId.values.toList()44 override fun get(id: String): PairedDevice? = byId[id]45 override fun upsert(device: PairedDevice) { byId[device.deviceId] = device }46 override fun remove(deviceId: String) { byId.remove(deviceId) }47 override fun listRevocations(): List<DeviceRevocation> = revocations.toList()48 override fun mergeRevocations(incoming: List<DeviceRevocation>): List<String> {49 val known = revocations.map { it.deviceId }.toSet()50 val fresh = incoming.filterNot { known.contains(it.deviceId) }51 revocations += fresh52 return fresh.map { it.deviceId }53 }54 override fun clearRevocation(deviceId: String) {55 revocations.removeAll { it.deviceId == deviceId }56 }57 }5859 private class FakePendingRegistry : PendingVouchedRegistry {60 val devices = mutableListOf<PendingVouchedDevice>()61 override fun getAll(): List<PendingVouchedDevice> = devices.toList()62 override fun upsert(device: PendingVouchedDevice) {63 devices.removeAll { it.deviceId == device.deviceId }64 devices += device65 }66 override fun remove(id: String) { devices.removeAll { it.deviceId == id } }67 }6869 private fun newAdapter(): Pair<VaultRepository, VaultSyncAdapter> {70 val repo = VaultRepository(FakeStorage())71 runBlocking { repo.createVault("correct horse") }72 return repo to VaultSyncAdapter(repo, ourDeviceId = "android-test")73 }7475 @Test fun manifest_and_vault_id_reflect_the_vault() = runBlocking {76 val (repo, adapter) = newAdapter()77 val e = repo.addEntry(VaultEntry(id = "", title = "GitHub", createdAt = 0, modifiedAt = 0, password = "p"))78 val manifest = adapter.buildEntryManifest()79 assertEquals(setOf(e.id), manifest.keys)80 assertTrue(adapter.computeVaultId().length == 64) 81 }8283 @Test fun envelope_round_trips_within_the_vault() = runBlocking {84 val (repo, adapter) = newAdapter()85 val e = repo.addEntry(VaultEntry(id = "", title = "Mail", createdAt = 0, modifiedAt = 0, password = "secret"))86 val envelopes = adapter.getEncryptedEntries(listOf(e.id))87 assertTrue(envelopes.containsKey(e.id))88 89 adapter.applyEntries(envelopes)90 assertEquals(1, repo.getEntries().size)91 }9293 @Test fun swapped_envelope_is_rejected_by_id_binding() = runBlocking {94 val (repo, adapter) = newAdapter()95 val e = repo.addEntry(VaultEntry(id = "", title = "Bank", createdAt = 0, modifiedAt = 0, password = "x"))96 val blob = adapter.getEncryptedEntries(listOf(e.id)).getValue(e.id)97 98 99 adapter.applyEntries(mapOf("attacker-chosen-id" to blob))100 assertNull(repo.getEntry("attacker-chosen-id"))101 assertEquals(1, repo.getEntries().size)102 }103104 105 106 107 108 @Test fun legacy_info_envelope_fails_closed() = runBlocking {109 val (repo, adapter) = newAdapter()110 val master = repo.syncMasterKey()!!111 val entryId = "11111111-2222-4333-8444-555555555555"112 val entry = VaultEntry(id = entryId, title = "LegacyOnly", createdAt = 0, modifiedAt = 0, password = "pw")113114 115 val legacyInfo = (entryId + "sync-entry-v1").toByteArray(Charsets.UTF_8)116 val gen = org.bouncycastle.crypto.generators.HKDFBytesGenerator(org.bouncycastle.crypto.digests.SHA256Digest())117 gen.init(org.bouncycastle.crypto.params.HKDFParameters(master, ByteArray(0), legacyInfo))118 val key = ByteArray(32).also { gen.generateBytes(it, 0, it.size) }119120 val json = kotlinx.serialization.json.Json { encodeDefaults = false; ignoreUnknownKeys = true }121 val plaintext = json.encodeToString(VaultEntry.serializer(), entry).toByteArray(Charsets.UTF_8)122 val nonce = ByteArray(12).also { java.security.SecureRandom().nextBytes(it) }123 val cipher = javax.crypto.Cipher.getInstance("AES/GCM/NoPadding")124 cipher.init(125 javax.crypto.Cipher.ENCRYPT_MODE,126 javax.crypto.spec.SecretKeySpec(key, "AES"),127 javax.crypto.spec.GCMParameterSpec(128, nonce),128 )129 cipher.updateAAD("peach-sync-entry-v1".toByteArray(Charsets.UTF_8))130 val ct = cipher.doFinal(plaintext)131 val legacyEnvelope = java.util.Base64.getEncoder().encodeToString(nonce + ct)132133 adapter.applyEntries(mapOf(entryId to legacyEnvelope))134 135 assertNull(repo.getEntry(entryId))136 }137138 @Test fun signed_device_manifest_round_trips_through_canonical_signature() = runBlocking {139 val master = ByteArray(32) { it.toByte() }140 val signing = deriveSyncSigningKeypair(master, "extension-issuer")141 val manifest = buildSignedDeviceManifest(142 issuerDeviceId = "extension-issuer",143 devices = listOf(144 ManifestDeviceEntry(145 deviceId = "android-peer",146 deviceName = "Phone",147 deviceType = "android",148 longTermPublicKeyHex = "aa".repeat(32),149 signingPublicKeyHex = "bb".repeat(32),150 pairedAt = 123L,151 ),152 ),153 signingPrivateKey = signing.privateKey,154 nowMs = 1_000L,155 )156157 val verified = verifyDeviceManifest(158 manifest,159 signing.publicKeyRaw,160 nowMs = 1_000L,161 )162163 assertEquals("extension-issuer", verified?.issuerDeviceId)164 assertEquals("android-peer", verified?.devices?.single()?.deviceId)165 assertTrue(verifyDeviceManifest(manifest.copy(signatureHex = "00".repeat(64)), signing.publicKeyRaw, nowMs = 1_000L) == null)166 }167168 @Test fun `revocation evicts the device and permanently blocks re-vouching`() = runBlocking {169 val repo = VaultRepository(FakeStorage())170 repo.createVault("correct horse")171 val master = repo.syncMasterKey()!!172 val issuerSigning = deriveSyncSigningKeypair(master, "extension-issuer")173 val paired = FakeDeviceRegistry(174 listOf(175 PairedDevice(176 deviceId = "extension-issuer",177 deviceName = "Peach Browser",178 deviceType = "extension",179 longTermPublicKeyHex = "11".repeat(32),180 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),181 trustSource = "direct",182 pairedAt = 1L,183 ),184 PairedDevice(185 deviceId = "stolen-laptop",186 deviceName = "Old Laptop",187 deviceType = "extension",188 longTermPublicKeyHex = "dd".repeat(32),189 signingPublicKeyHex = "ee".repeat(32),190 trustSource = "direct",191 pairedAt = 2L,192 ),193 ),194 )195 val pending = FakePendingRegistry()196 val adapter = VaultSyncAdapter(repo, ourDeviceId = "android-self", pairedDevices = paired, pendingVouched = pending)197198 val revocation = buildSignedDeviceManifest(199 issuerDeviceId = "extension-issuer",200 devices = emptyList(),201 signingPrivateKey = issuerSigning.privateKey,202 revoked = listOf(203 DeviceRevocation(204 deviceId = "stolen-laptop",205 longTermPublicKeyHex = "dd".repeat(32),206 revokedAt = System.currentTimeMillis(),207 revokedByDeviceId = "extension-issuer",208 ),209 ),210 )211 adapter.applyIncomingDeviceManifest(revocation, ByteArray(32) { 0x11 })212213 assertNull(paired.get("stolen-laptop"))214215 216 217 val revouch = buildSignedDeviceManifest(218 issuerDeviceId = "extension-issuer",219 devices = listOf(220 ManifestDeviceEntry(221 deviceId = "stolen-laptop",222 deviceName = "Old Laptop",223 deviceType = "extension",224 longTermPublicKeyHex = "dd".repeat(32),225 signingPublicKeyHex = "ee".repeat(32),226 pairedAt = System.currentTimeMillis() + 60_000L,227 ),228 ),229 signingPrivateKey = issuerSigning.privateKey,230 )231 adapter.applyIncomingDeviceManifest(revouch, ByteArray(32) { 0x11 })232233 assertNull(paired.get("stolen-laptop"))234 assertEquals(0, pending.devices.size)235 master.fill(0)236 }237238 @Test fun `a peer cannot revoke this device`() = runBlocking {239 val repo = VaultRepository(FakeStorage())240 repo.createVault("correct horse")241 val master = repo.syncMasterKey()!!242 val issuerSigning = deriveSyncSigningKeypair(master, "extension-issuer")243 val paired = FakeDeviceRegistry(244 listOf(245 PairedDevice(246 deviceId = "extension-issuer",247 deviceName = "Peach Browser",248 deviceType = "extension",249 longTermPublicKeyHex = "11".repeat(32),250 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),251 trustSource = "direct",252 pairedAt = 1L,253 ),254 ),255 )256 val pending = FakePendingRegistry()257 val adapter = VaultSyncAdapter(repo, ourDeviceId = "android-self", pairedDevices = paired, pendingVouched = pending)258259 val selfRevoke = buildSignedDeviceManifest(260 issuerDeviceId = "extension-issuer",261 devices = emptyList(),262 signingPrivateKey = issuerSigning.privateKey,263 revoked = listOf(264 DeviceRevocation(265 deviceId = "android-self",266 longTermPublicKeyHex = "ff".repeat(32),267 revokedAt = System.currentTimeMillis(),268 revokedByDeviceId = "extension-issuer",269 ),270 ),271 )272 adapter.applyIncomingDeviceManifest(selfRevoke, ByteArray(32) { 0x11 })273274 assertTrue(paired.listRevocations().none { it.deviceId == "android-self" })275 master.fill(0)276 }277278 @Test fun incoming_extension_manifest_auto_admits_routable_same_vault_peer() = runBlocking {279 val repo = VaultRepository(FakeStorage())280 repo.createVault("correct horse")281 val master = repo.syncMasterKey()!!282 val issuerSigning = deriveSyncSigningKeypair(master, "extension-issuer")283 val paired = FakeDeviceRegistry(284 listOf(285 PairedDevice(286 deviceId = "extension-issuer",287 deviceName = "Peach Browser",288 deviceType = "extension",289 longTermPublicKeyHex = "11".repeat(32),290 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),291 trustSource = "direct",292 pairedAt = 1L,293 ),294 ),295 )296 val pending = FakePendingRegistry()297 val adapter = VaultSyncAdapter(repo, ourDeviceId = "android-self", pairedDevices = paired, pendingVouched = pending)298 val manifest = buildSignedDeviceManifest(299 issuerDeviceId = "extension-issuer",300 devices = listOf(301 ManifestDeviceEntry(302 deviceId = "new-phone",303 deviceName = "June's Phone",304 deviceType = "android",305 longTermPublicKeyHex = "aa".repeat(32),306 signingPublicKeyHex = "bb".repeat(32),307 relayUrl = meshRelay,308 pairedAt = 44L,309 ),310 ),311 signingPrivateKey = issuerSigning.privateKey,312 )313314 adapter.applyIncomingDeviceManifest(manifest, ByteArray(32) { 0x11 })315316 val admitted = paired.get("new-phone")317 assertEquals("vouched", admitted?.trustSource)318 assertEquals(meshRelay, admitted?.relayUrl)319 assertTrue(admitted?.rendezvousSecretHex?.matches(Regex("[0-9a-f]{64}")) == true)320 assertTrue(pending.devices.isEmpty())321 master.fill(0)322 }323 324325326327328329330 @Test fun `a manifest from a directly paired phone is accepted`() = runBlocking {331 val repo = VaultRepository(FakeStorage())332 repo.createVault("correct horse")333 val master = repo.syncMasterKey()!!334 val issuerSigning = deriveSyncSigningKeypair(master, "phone-issuer")335 val paired = FakeDeviceRegistry(336 listOf(337 PairedDevice(338 deviceId = "phone-issuer",339 deviceName = "Other Phone",340 deviceType = "android",341 longTermPublicKeyHex = "11".repeat(32),342 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),343 trustSource = "direct",344 pairedAt = 1L,345 ),346 ),347 )348 val pending = FakePendingRegistry()349 val adapter = VaultSyncAdapter(repo, ourDeviceId = "android-self", pairedDevices = paired, pendingVouched = pending)350 val manifest = buildSignedDeviceManifest(351 issuerDeviceId = "phone-issuer",352 devices = listOf(353 ManifestDeviceEntry(354 deviceId = "laptop-b",355 deviceName = "Laptop B",356 deviceType = "extension",357 longTermPublicKeyHex = "aa".repeat(32),358 signingPublicKeyHex = "bb".repeat(32),359 relayUrl = meshRelay,360 pairedAt = 44L,361 ),362 ),363 signingPrivateKey = issuerSigning.privateKey,364 )365366 adapter.applyIncomingDeviceManifest(manifest, ByteArray(32) { 0x11 })367368 assertEquals("vouched", paired.get("laptop-b")?.trustSource)369 assertEquals(meshRelay, paired.get("laptop-b")?.relayUrl)370 assertTrue(pending.devices.isEmpty())371 master.fill(0)372 }373374 375376377378379 @Test fun `a vouched issuer cannot introduce further devices`() = runBlocking {380 val repo = VaultRepository(FakeStorage())381 repo.createVault("correct horse")382 val master = repo.syncMasterKey()!!383 val issuerSigning = deriveSyncSigningKeypair(master, "vouched-issuer")384 val paired = FakeDeviceRegistry(385 listOf(386 PairedDevice(387 deviceId = "vouched-issuer",388 deviceName = "Vouched Device",389 deviceType = "extension",390 longTermPublicKeyHex = "11".repeat(32),391 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),392 trustSource = "vouched",393 pairedAt = 1L,394 ),395 ),396 )397 val pending = FakePendingRegistry()398 val adapter = VaultSyncAdapter(repo, ourDeviceId = "android-self", pairedDevices = paired, pendingVouched = pending)399 val manifest = buildSignedDeviceManifest(400 issuerDeviceId = "vouched-issuer",401 devices = listOf(402 ManifestDeviceEntry(403 deviceId = "smuggled-device",404 deviceName = "Smuggled",405 deviceType = "extension",406 longTermPublicKeyHex = "aa".repeat(32),407 signingPublicKeyHex = "bb".repeat(32),408 relayUrl = meshRelay,409 pairedAt = 44L,410 ),411 ),412 signingPrivateKey = issuerSigning.privateKey,413 )414415 adapter.applyIncomingDeviceManifest(manifest, ByteArray(32) { 0x11 })416417 assertEquals("a vouched device must not chain trust onward", 0, pending.devices.size)418 master.fill(0)419 }420421 @Test fun `a self asserted manifest issuer cannot override the authenticated peer`() = runBlocking {422 val repo = VaultRepository(FakeStorage())423 repo.createVault("correct horse")424 val master = repo.syncMasterKey()!!425 val claimedSigning = deriveSyncSigningKeypair(master, "claimed-issuer")426 val paired = FakeDeviceRegistry(427 listOf(428 PairedDevice(429 deviceId = "claimed-issuer",430 deviceName = "Claimed Browser",431 deviceType = "extension",432 longTermPublicKeyHex = "11".repeat(32),433 signingPublicKeyHex = pubkeyToHex(claimedSigning.publicKeyRaw),434 trustSource = "direct",435 pairedAt = 1L,436 ),437 PairedDevice(438 deviceId = "authenticated-peer",439 deviceName = "Authenticated Phone",440 deviceType = "android",441 longTermPublicKeyHex = "22".repeat(32),442 signingPublicKeyHex = "33".repeat(32),443 trustSource = "direct",444 pairedAt = 2L,445 ),446 ),447 )448 val pending = FakePendingRegistry()449 val adapter = VaultSyncAdapter(450 repo,451 ourDeviceId = "android-self",452 pairedDevices = paired,453 pendingVouched = pending,454 )455 val manifest = buildSignedDeviceManifest(456 issuerDeviceId = "claimed-issuer",457 devices = listOf(458 ManifestDeviceEntry(459 deviceId = "smuggled-device",460 deviceName = "Smuggled",461 deviceType = "extension",462 longTermPublicKeyHex = "aa".repeat(32),463 signingPublicKeyHex = "bb".repeat(32),464 relayUrl = meshRelay,465 pairedAt = 44L,466 ),467 ),468 signingPrivateKey = claimedSigning.privateKey,469 )470471 adapter.applyIncomingDeviceManifest(manifest, ByteArray(32) { 0x22 })472473 assertEquals(0, pending.devices.size)474475 476 477 478 adapter.applyIncomingDeviceManifest(manifest, ByteArray(32) { 0x11 })479 assertEquals("vouched", paired.get("smuggled-device")?.trustSource)480 assertTrue(pending.devices.isEmpty())481 master.fill(0)482 }483484 @Test fun `a revoked long term key cannot walk back in under a fresh deviceId`() = runBlocking {485 val repo = VaultRepository(FakeStorage())486 repo.createVault("correct horse")487 val master = repo.syncMasterKey()!!488 val issuerSigning = deriveSyncSigningKeypair(master, "phone-issuer")489 val stolenKey = "dd".repeat(32)490 val paired = FakeDeviceRegistry(491 listOf(492 PairedDevice(493 deviceId = "phone-issuer",494 deviceName = "Pixel",495 deviceType = "android",496 longTermPublicKeyHex = "11".repeat(32),497 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),498 trustSource = "direct",499 pairedAt = 1L,500 ),501 PairedDevice(502 deviceId = "stolen-laptop",503 deviceName = "Old Laptop",504 deviceType = "extension",505 longTermPublicKeyHex = stolenKey,506 signingPublicKeyHex = "ee".repeat(32),507 trustSource = "direct",508 pairedAt = 2L,509 ),510 ),511 )512 val pending = FakePendingRegistry()513 val adapter = VaultSyncAdapter(repo, ourDeviceId = "android-self", pairedDevices = paired, pendingVouched = pending)514515 adapter.applyIncomingDeviceManifest(516 buildSignedDeviceManifest(517 issuerDeviceId = "phone-issuer",518 devices = emptyList(),519 signingPrivateKey = issuerSigning.privateKey,520 revoked = listOf(521 DeviceRevocation(522 deviceId = "stolen-laptop",523 longTermPublicKeyHex = stolenKey,524 revokedAt = System.currentTimeMillis(),525 revokedByDeviceId = "phone-issuer",526 ),527 ),528 ),529 ByteArray(32) { 0x11 },530 )531 assertNull(paired.get("stolen-laptop"))532533 534 535 adapter.applyIncomingDeviceManifest(536 buildSignedDeviceManifest(537 issuerDeviceId = "phone-issuer",538 devices = listOf(539 ManifestDeviceEntry(540 deviceId = "stolen-laptop-mk2",541 deviceName = "New Laptop",542 deviceType = "extension",543 longTermPublicKeyHex = stolenKey,544 signingPublicKeyHex = "ee".repeat(32),545 relayUrl = meshRelay,546 pairedAt = System.currentTimeMillis(),547 ),548 ),549 signingPrivateKey = issuerSigning.privateKey,550 ),551 ByteArray(32) { 0x11 },552 )553 assertEquals(0, pending.devices.size)554 assertNull(paired.get("stolen-laptop-mk2"))555556 557 558 adapter.applyIncomingDeviceManifest(559 buildSignedDeviceManifest(560 issuerDeviceId = "phone-issuer",561 devices = listOf(562 ManifestDeviceEntry(563 deviceId = "stolen-laptop-mk2",564 deviceName = "New Laptop",565 deviceType = "extension",566 longTermPublicKeyHex = "99".repeat(32),567 signingPublicKeyHex = "ee".repeat(32),568 relayUrl = meshRelay,569 pairedAt = System.currentTimeMillis(),570 ),571 ),572 signingPrivateKey = issuerSigning.privateKey,573 ),574 ByteArray(32) { 0x11 },575 )576 assertEquals("vouched", paired.get("stolen-laptop-mk2")?.trustSource)577 assertTrue(pending.devices.isEmpty())578 master.fill(0)579 }580581 @Test fun `a revoked key cannot be written onto a device we still trust`() = runBlocking {582 val repo = VaultRepository(FakeStorage())583 repo.createVault("correct horse")584 val master = repo.syncMasterKey()!!585 val issuerSigning = deriveSyncSigningKeypair(master, "phone-issuer")586 val revokedKey = "dd".repeat(32)587 val paired = FakeDeviceRegistry(588 listOf(589 PairedDevice(590 deviceId = "phone-issuer",591 deviceName = "Pixel",592 deviceType = "android",593 longTermPublicKeyHex = "11".repeat(32),594 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),595 trustSource = "direct",596 pairedAt = 1L,597 ),598 PairedDevice(599 deviceId = "trusted-tablet",600 deviceName = "Tablet",601 deviceType = "android",602 longTermPublicKeyHex = "77".repeat(32),603 signingPublicKeyHex = "ee".repeat(32),604 trustSource = "direct",605 pairedAt = 2L,606 ),607 ),608 )609 val adapter = VaultSyncAdapter(610 repo,611 ourDeviceId = "android-self",612 pairedDevices = paired,613 pendingVouched = FakePendingRegistry(),614 )615616 adapter.applyIncomingDeviceManifest(617 buildSignedDeviceManifest(618 issuerDeviceId = "phone-issuer",619 devices = emptyList(),620 signingPrivateKey = issuerSigning.privateKey,621 revoked = listOf(622 DeviceRevocation(623 deviceId = "stolen-laptop",624 longTermPublicKeyHex = revokedKey,625 revokedAt = System.currentTimeMillis(),626 revokedByDeviceId = "phone-issuer",627 ),628 ),629 ),630 ByteArray(32) { 0x11 },631 )632633 634 adapter.applyIncomingDeviceManifest(635 buildSignedDeviceManifest(636 issuerDeviceId = "phone-issuer",637 devices = listOf(638 ManifestDeviceEntry(639 deviceId = "trusted-tablet",640 deviceName = "Tablet",641 deviceType = "android",642 longTermPublicKeyHex = revokedKey,643 signingPublicKeyHex = "ee".repeat(32),644 pairedAt = 2L,645 ),646 ),647 signingPrivateKey = issuerSigning.privateKey,648 ),649 ByteArray(32) { 0x11 },650 )651 assertEquals("77".repeat(32), paired.get("trusted-tablet")?.longTermPublicKeyHex)652 master.fill(0)653 }654655 656 @Test fun `revocation matches on id or key but never on the empty-key sentinel`() {657 val registry = FakeDeviceRegistry()658 registry.mergeRevocations(659 listOf(660 DeviceRevocation("gone", "dd".repeat(32), 1L, "issuer"),661 DeviceRevocation("keyless", NO_KEY_SENTINEL, 2L, "issuer"),662 ),663 )664 assertTrue(registry.isRevoked("gone"))665 assertTrue(registry.isRevoked("fresh-id", "DD".repeat(32)))666 assertTrue(registry.isRevoked("keyless"))667 assertFalse(registry.isRevoked("fresh-id", "99".repeat(32)))668 669 670 assertFalse(registry.isRevoked("innocent", NO_KEY_SENTINEL))671 }672673 @Test fun `public key decoder requires exactly 32 bytes`() {674 assertEquals(32, hexToPubkey("ab".repeat(32)).size)675 assertTrue(runCatching { hexToPubkey("ab".repeat(31)) }.isFailure)676 assertTrue(runCatching { hexToPubkey("ab".repeat(33)) }.isFailure)677 assertEquals(64, hexToBytes("ab".repeat(64)).size)678 }679}