LockedSyncSpoolModelTest.kt

4.4 KB · raw

1package com.peachpasswords.sync23import kotlinx.coroutines.runBlocking4import org.bouncycastle.crypto.params.X25519PrivateKeyParameters5import org.junit.Assert.assertArrayEquals6import org.junit.Assert.assertEquals7import org.junit.Assert.assertFalse8import org.junit.Assert.assertNull9import org.junit.Assert.assertThrows10import org.junit.Test1112class LockedSyncSpoolModelTest {13    @Test fun cumulative_inbox_counts_are_bounded_when_state_is_rebuilt() {14        val entries = (0..SyncLimits.MAX_ENTRY_COUNT).associate { index ->15            "entry-$index" to LockedSyncCachedEntry(1, "AA==")16        }17        val tombstones = (0..SyncLimits.MAX_TOMBSTONE_COUNT).associate { index ->18            "tombstone-$index" to 1L19        }2021        assertThrows(IllegalArgumentException::class.java) { state(inboxEntries = entries) }22        assertThrows(IllegalArgumentException::class.java) { state(inboxTombstones = tombstones) }23    }2425    @Test fun keystore_wrapped_identity_survives_a_cold_locked_restart() {26        val private = X25519PrivateKeyParameters(ByteArray(32) { (it + 1).toByte() }, 0)27        val restored = lockedSyncIdentity(28            state(29                longTermPublicKeyHex = pubkeyToHex(private.generatePublicKey().encoded),30                longTermPrivateKeyHex = pubkeyToHex(private.encoded),31            ),32            "wss://relay.example",33        )3435        requireNotNull(restored)36        assertArrayEquals(private.encoded, restored.first.secretKey)37        assertArrayEquals(private.generatePublicKey().encoded, restored.first.publicKey)38        assertEquals("device", restored.second.deviceId)39        assertEquals("wss://relay.example", restored.second.relayUrl)40    }4142    @Test fun locked_identity_rejects_a_private_key_that_does_not_match_the_public_key() {43        assertNull(44            lockedSyncIdentity(45                state(longTermPrivateKeyHex = "c".repeat(64)),46                Rendezvous.DEFAULT_RELAY_URL,47            ),48        )49    }5051    @Test fun locked_adapter_refuses_every_device_manifest_operation() {52        val adapter = lockedAdapter()5354        // A locked session must not be able to mutate trust in either direction.55        assertFalse(adapter.supportsDeviceManifest)56        assertThrows(VaultLockedException::class.java) {57            runBlocking { adapter.buildOutgoingDeviceManifest() }58        }59        assertThrows(VaultLockedException::class.java) {60            runBlocking { adapter.applyIncomingDeviceManifest(manifest(), ByteArray(32)) }61        }62    }6364    @Test fun locked_adapter_still_stages_ciphertext_for_the_next_unlock() = runBlocking {65        val entries = mutableMapOf<String, String>()66        val tombstones = mutableMapOf<String, Long>()67        val adapter = LockedSyncVaultAdapter(68            vaultId = "a".repeat(64),69            readSnapshot = { null },70            stageEntriesInto = { staged, _ -> entries.putAll(staged) },71            stageTombstonesInto = { tombstones.putAll(it) },72        )7374        adapter.applyEntries(mapOf("entry-1" to "AA=="), mapOf("entry-1" to 5L))75        adapter.applyTombstones(mapOf("entry-2" to 7L))7677        assertEquals(mapOf("entry-1" to "AA=="), entries)78        assertEquals(mapOf("entry-2" to 7L), tombstones)79    }8081    private fun lockedAdapter() = LockedSyncVaultAdapter(82        vaultId = "a".repeat(64),83        readSnapshot = { null },84        stageEntriesInto = { _, _ -> },85        stageTombstonesInto = { _ -> },86    )8788    private fun manifest() = SignedDeviceManifest(89        body = DeviceManifestBody(90            manifestVersion = DEVICE_MANIFEST_VERSION,91            issuerDeviceId = "issuer",92            issuedAt = 1L,93            devices = emptyList(),94        ),95        signatureHex = "00".repeat(64),96    )9798    private fun state(99        inboxEntries: Map<String, LockedSyncCachedEntry> = emptyMap(),100        inboxTombstones: Map<String, Long> = emptyMap(),101        longTermPublicKeyHex: String = "b".repeat(64),102        longTermPrivateKeyHex: String? = null,103    ) = LockedSyncSpool(104        vaultId = "a".repeat(64),105        deviceId = "device",106        deviceName = "Android",107        localVersion = 1,108        longTermPublicKeyHex = longTermPublicKeyHex,109        longTermPrivateKeyHex = longTermPrivateKeyHex,110        signingPublicKeyHex = "d".repeat(64),111        entries = emptyMap(),112        tombstones = emptyMap(),113        inboxEntries = inboxEntries,114        inboxTombstones = inboxTombstones,115    )116}