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(): Boolean { slotA = null; slotB = null; version = 0; settings = VaultSettings(); return true }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(99 repo,100 "android-self",101 registry,102 Pending(),103 localCredentials = randomLocalIdentityCredentials("android-self"),104 )105 val manifest = buildSignedDeviceManifest(106 issuerDeviceId = "issuer",107 devices = listOf(108 ManifestDeviceEntry(109 deviceId = "new-device",110 deviceName = "New",111 deviceType = "extension",112 longTermPublicKeyHex = "aa".repeat(32),113 signingPublicKeyHex = "bb".repeat(32),114 relayUrl = "wss://relay.peachpasswords.com",115 pairedAt = 2L,116 ),117 ),118 signingPrivateKey = issuerSigning.privateKey,119 revoked = listOf(120 DeviceRevocation(121 deviceId = "old-device",122 longTermPublicKeyHex = "dd".repeat(32),123 revokedAt = System.currentTimeMillis(),124 revokedByDeviceId = "issuer",125 ),126 ),127 )128129 val enteredFence = CountDownLatch(1)130 val releaseFence = CountDownLatch(1)131 try {132 repo.beforeSessionEpochCommit = {133 enteredFence.countDown()134 check(releaseFence.await(5, TimeUnit.SECONDS)) { "manifest did not release" }135 }136 val applyFailure = AtomicReference<Throwable?>()137 val applyDone = CountDownLatch(1)138 Thread {139 try {140 runBlocking {141 adapter.applyIncomingDeviceManifest(manifest, ByteArray(32) { 0x11 })142 }143 } catch (t: Throwable) {144 applyFailure.set(t)145 } finally {146 applyDone.countDown()147 }148 }.start()149 assertTrue(enteredFence.await(5, TimeUnit.SECONDS))150151 val relockDone = CountDownLatch(1)152 Thread {153 repo.lock()154 runBlocking {155 assertTrue(repo.unlock("correct horse battery staple") is com.peachpasswords.vault.UnlockResult.Success)156 }157 relockDone.countDown()158 }.start()159 assertTrue(relockDone.await(5, TimeUnit.SECONDS))160 releaseFence.countDown()161 assertTrue(applyDone.await(5, TimeUnit.SECONDS))162 assertEquals(null, applyFailure.get())163 assertEquals("the stale epoch must never reach the registry", 0, registry.batchCalls)164 assertTrue(registry.get("old-device") != null)165 assertTrue(registry.get("new-device") == null)166 assertTrue(registry.listRevocations().isEmpty())167 } finally {168 releaseFence.countDown()169 repo.beforeSessionEpochCommit = {}170 }171 }172}