6ab683d0e9fd
fix: Size Android duress by exact wire encoding (v2.9.11)
- time
- 2026-07-30 23:31 UTC · 2 months ago
- machine
- koh2
- parent
- c6c7bcb3aa5d
↓ peach-android-6ab683d0.tar.gzatlas at this save
files changed5 files
VERSION+1 −1app/build.gradle+1 −1app/src/main/java/com/peachpasswords/sync/VaultAdopt.kt+2 −1app/src/test/java/com/peachpasswords/sync/RelayAdoptionReceiverTest.kt+87 −0app/src/test/java/com/peachpasswords/sync/VaultAdoptSnapshotTest.kt+6 −1app/build.gradle+1 −1
@@ -32,7 +32,7 @@3232 minSdk 263333 targetSdk 353434 // Keep the Android code in Gradle; the semantic name comes from root VERSION.35- versionCode 24015635+ versionCode 2401573636 versionName semanticVersion3737 3838 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"app/src/main/java/com/peachpasswords/sync/VaultAdopt.kt+2 −1
@@ -545,7 +545,8 @@545545 546546 if (547547 duressFrame != null &&548- AdoptJson.encodeToString(duressFrame).encodeToByteArray().size > SNAPSHOT_FRAME_WIRE_CEILING548+ AdoptJson.encodeToString(AdoptionFrame.serializer(), duressFrame)549+ .encodeToByteArray().size > SNAPSHOT_FRAME_WIRE_CEILING549550 ) {550551 throw SnapshotDuressFrameTooLargeException()551552 }app/src/test/java/com/peachpasswords/sync/RelayAdoptionReceiverTest.kt+87 −0
@@ -8,6 +8,7 @@88 import org.junit.Assert.assertNotNull99 import org.junit.Assert.assertTrue1010 import org.junit.Test11+import kotlin.system.measureTimeMillis1112 1213 /**1314 * Item 2 (REVIEW_DEFERRED_ITEMS_BRIEF.md): receiver inline-snapshot happy-path.@@ -230,5 +231,91 @@230231 assertFalse("markSynced must NOT be called when no snapshot was applied", receiverMarkedSynced)231232 assertNotNull(receiverStored)232233 assertNotNull(sourceStored)234+ }235+236+ @Test fun `receiver skips the inline wait when the grant is not snapshot capable`() = runBlocking {237+ val (sourceSocket, receiverSocket) = socketPair()238+ val token = "AAECAwQFBgcICQoLDA0ODw"239+ val relay = "wss://relay.example.test"240+ val root = ByteArray(32) { (it + 7).toByte() }241+ val sourceKeys = deriveLocalSyncKeypair(root, "old-source-id")242+ val receiverKeys = deriveLocalSyncKeypair(root, "receiver-id")243+ val sourceIdentity = identity("old-source", sourceKeys.publicKey)244+ val receiverIdentity = identity("receiver", receiverKeys.publicKey)245+ var receiverApplied = false246+247+ val sourceJob = async {248+ sourceSocket.receive()249+ sourceSocket.send(250+ org.json.JSONObject().put("type", "PEACH_HELLO_ACK")251+ .put("ephemeralPublicKeyHex", Rendezvous.bytesToHex(sourceKeys.publicKey))252+ .toString().toByteArray(),253+ )254+ val noise = noiseNKResponder(255+ sourceKeys.secretKey,256+ sourceKeys.publicKey,257+ sourceSocket::send,258+ sourceSocket::receive,259+ )260+ try {261+ val channel = object : PairingTransport {262+ override suspend fun send(msg: ByteArray) = sourceSocket.send(noise.encrypt(msg))263+ override suspend fun receive(): ByteArray = noise.decrypt(sourceSocket.receive())264+ override suspend fun close() = sourceSocket.close()265+ }266+ assertTrue(receiveFrame(channel) is AdoptionFrame.Request)267+ assertTrue(receiveFrame(channel) is AdoptionFrame.Confirm)268+ sendFrame(channel, AdoptionFrame.Grant(269+ rootKeyHex = Rendezvous.bytesToHex(root),270+ vaultId = computeVaultId(root),271+ license = "PPK-test",272+ source = AdoptionFrame.Source(273+ deviceId = sourceIdentity.deviceId,274+ deviceName = sourceIdentity.deviceName,275+ deviceType = sourceIdentity.deviceType.wireValue,276+ longTermPublicKeyHex = sourceIdentity.longTermPublicKeyHex,277+ signingPublicKeyHex = sourceIdentity.signingPublicKeyHex,278+ ),279+ snapshotCapable = false,280+ ))281+ assertTrue(receiveFrame(channel) is AdoptionFrame.Identity)282+ sendFrame(channel, AdoptionFrame.Complete)283+ assertTrue(receiveFrame(channel) is AdoptionFrame.Applied)284+ } finally {285+ noise.close()286+ }287+ }288+289+ var received: PairedDevice? = null290+ val elapsedMs = measureTimeMillis {291+ received = runRelayAdoptionReceiverSession(292+ receiverSocket,293+ PairingInvitation(relay, token),294+ object : AdoptionReceiverDeps {295+ override val deviceName = "receiver"296+ override val deviceType = DeviceType.ANDROID297+ override fun generateDeviceId() = "session-only-request"298+ override suspend fun confirmSas() = true299+ override suspend fun createVaultFromRootKey(rootKeyHex: String) =300+ computeVaultId(hexToPubkey(rootKeyHex))301+ override suspend fun deriveLocalIdentity() = receiverIdentity302+ override suspend fun upsertPairedDevice(device: PairedDevice) = Unit303+ override suspend fun applySnapshot(304+ entries: Map<String, String>,305+ tombstones: Map<String, Long>,306+ duressSlot: String?,307+ ) {308+ receiverApplied = true309+ }310+ override fun onError(error: String) = throw AssertionError(error)311+ },312+ confirm = { true },313+ )314+ }315+316+ assertNotNull(received)317+ sourceJob.await()318+ assertFalse(receiverApplied)319+ assertTrue("legacy grant should skip the 15s snapshot wait", elapsedMs < 5_000)233320 }234321 }app/src/test/java/com/peachpasswords/sync/VaultAdoptSnapshotTest.kt+6 −1
@@ -356,7 +356,12 @@356356 override suspend fun receive(): ByteArray = source.receive()357357 override suspend fun close() {}358358 }359- val duressJson = "x".repeat(800 * 1024) // 800 KiB duress, exceeds 768 KiB ceiling359+ val ceiling = 768 * 1024360+ val emptyWireSize = AdoptJson.encodeToString(361+ AdoptionFrame.serializer(),362+ AdoptionFrame.Snapshot(1, 2, emptyMap(), emptyMap(), ""),363+ ).encodeToByteArray().size364+ val duressJson = "x".repeat(ceiling - emptyWireSize + 1)360365 val error = runCatching {361366 sendVaultSnapshot(countingSource, mapOf("id-0" to "small"), emptyMap(), duressSlot = duressJson)362367 }.exceptionOrNull()