1package com.peachpasswords.sync23import com.peachpasswords.vault.StoredVault4import com.peachpasswords.vault.CreateVaultResult5import 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.assertTrue12import org.junit.Test13import java.util.concurrent.CountDownLatch14import java.util.concurrent.TimeUnit15import java.util.concurrent.atomic.AtomicReference161718class VaultSyncManifestEpochRaceTest {19 private class Storage : VaultStorage {20 private var slotA: StoredVault? = null21 private var slotB: String? = null22 private var version = 0L23 private var settings = VaultSettings()24 override fun loadSlotA() = slotA25 override fun hasVault() = slotA != null26 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }27 override fun loadSlotB() = slotB28 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }29 override fun loadDecoySlot(): StoredVault? = null30 override fun writeDecoySlot(slot: StoredVault): Boolean = true31 override fun hasDecoySlot(): Boolean = false32 override fun clearDecoySlot() = Unit33 override fun version() = version34 override fun stampLocalMutation(): Long { version += 1; return version }35 override fun loadSettings() = settings36 override fun writeSettings(settings: VaultSettings): Boolean { this.settings = settings; return true }37 override fun clearVault() { slotA = null; slotB = null; version = 0; settings = VaultSettings() }38 }3940 private class Registry(initial: List<PairedDevice>) : PairedDeviceRegistry {41 private val devices = initial.associateBy { it.deviceId }.toMutableMap()42 private val revocations = mutableListOf<DeviceRevocation>()43 var batchCalls = 04445 override fun getAll(): List<PairedDevice> = devices.values.toList()46 override fun get(id: String): PairedDevice? = devices[id]47 override fun upsert(device: PairedDevice) { devices[device.deviceId] = device }48 override fun remove(deviceId: String) { devices.remove(deviceId) }49 override fun listRevocations(): List<DeviceRevocation> = revocations.toList()50 override fun applyManifestBatch(batch: DeviceManifestRegistryBatch): Boolean {51 batchCalls += 152 batch.removals.forEach(devices::remove)53 batch.upserts.forEach { devices[it.deviceId] = it }54 revocations += batch.incomingRevocations55 return true56 }57 }5859 private class Pending : PendingVouchedRegistry {60 override fun getAll(): List<PendingVouchedDevice> = emptyList()61 override fun upsert(device: PendingVouchedDevice) = Unit62 override fun remove(id: String) = Unit63 }6465 @Test66 fun stale_manifest_is_rejected_as_a_whole_batch_after_lock_and_reunlock() = runBlocking {67 val repo = VaultRepository(Storage())68 assertTrue(repo.createVault("correct horse battery staple") is CreateVaultResult.Success)69 repo.lock()7071 val master = repo.syncMasterKey() 72 assertEquals(null, master)73 assertTrue(repo.unlock("correct horse battery staple") is com.peachpasswords.vault.UnlockResult.Success)74 val syncMaster = repo.syncMasterKey()!!75 val issuerSigning = deriveSyncSigningKeypair(syncMaster, "issuer")76 val registry = Registry(77 listOf(78 PairedDevice(79 deviceId = "issuer",80 deviceName = "Issuer",81 deviceType = "android",82 longTermPublicKeyHex = "11".repeat(32),83 signingPublicKeyHex = pubkeyToHex(issuerSigning.publicKeyRaw),84 trustSource = "direct",85 pairedAt = 1L,86 ),87 PairedDevice(88 deviceId = "old-device",89 deviceName = "Old",90 deviceType = "extension",91 longTermPublicKeyHex = "dd".repeat(32),92 signingPublicKeyHex = "ee".repeat(32),93 trustSource = "direct",94 pairedAt = 1L,95 ),96 ),97 )98 val adapter = VaultSyncAdapter(repo, "android-self", registry, Pending())99 val manifest = buildSignedDeviceManifest(100 issuerDeviceId = "issuer",101 devices = listOf(102 ManifestDeviceEntry(103 deviceId = "new-device",104 deviceName = "New",105 deviceType = "extension",106 longTermPublicKeyHex = "aa".repeat(32),107 signingPublicKeyHex = "bb".repeat(32),108 relayUrl = "wss://relay.peachpasswords.com",109 pairedAt = 2L,110 ),111 ),112 signingPrivateKey = issuerSigning.privateKey,113 revoked = listOf(114 DeviceRevocation(115 deviceId = "old-device",116 longTermPublicKeyHex = "dd".repeat(32),117 revokedAt = System.currentTimeMillis(),118 revokedByDeviceId = "issuer",119 ),120 ),121 )122123 val enteredFence = CountDownLatch(1)124 val releaseFence = CountDownLatch(1)125 try {126 repo.beforeSessionEpochCommit = {127 enteredFence.countDown()128 check(releaseFence.await(5, TimeUnit.SECONDS)) { "manifest did not release" }129 }130 val applyFailure = AtomicReference<Throwable?>()131 val applyDone = CountDownLatch(1)132 Thread {133 try {134 runBlocking {135 adapter.applyIncomingDeviceManifest(manifest, ByteArray(32) { 0x11 })136 }137 } catch (t: Throwable) {138 applyFailure.set(t)139 } finally {140 applyDone.countDown()141 }142 }.start()143 assertTrue(enteredFence.await(5, TimeUnit.SECONDS))144145 val relockDone = CountDownLatch(1)146 Thread {147 repo.lock()148 runBlocking {149 assertTrue(repo.unlock("correct horse battery staple") is com.peachpasswords.vault.UnlockResult.Success)150 }151 relockDone.countDown()152 }.start()153 assertTrue(relockDone.await(5, TimeUnit.SECONDS))154 releaseFence.countDown()155 assertTrue(applyDone.await(5, TimeUnit.SECONDS))156 assertEquals(null, applyFailure.get())157 assertEquals("the stale epoch must never reach the registry", 0, registry.batchCalls)158 assertTrue(registry.get("old-device") != null)159 assertTrue(registry.get("new-device") == null)160 assertTrue(registry.listRevocations().isEmpty())161 } finally {162 releaseFence.countDown()163 repo.beforeSessionEpochCommit = {}164 }165 }166}