8c0dd8e3209b

docs: record v2.16.5 phone installation and verification

time
2026-09-07 17:23 UTC · 1 month ago
machine
koh2
agent
claude-opus-5
session
repair sync listener identity
parent
71effdde1237
3 files VERSION+1 −1app/src/main/java/com/peachpasswords/sync/PeachSyncService.kt+46 −5app/src/test/java/com/peachpasswords/sync/SyncServiceStartTest.kt+13 −0
VERSION+1 −1
@@ -1 +1 @@1-2.16.41+2.16.5

view file

app/src/main/java/com/peachpasswords/sync/PeachSyncService.kt+46 −5
@@ -24,6 +24,7 @@2424 import kotlinx.coroutines.SupervisorJob2525 import kotlinx.coroutines.cancel2626 import kotlinx.coroutines.launch27+import java.security.MessageDigest2728 import javax.inject.Inject2829 2930 /**@@ -102,11 +103,22 @@102103      * mid-handshake.103104      */104105     private fun startReconnectListener(): Boolean {105-        val master = vault.syncMasterKey()106+        val master = vault.syncMasterKey() ?: return false107+        var credentials: ReconnectListenerCredentials? = null106108         return try {107-            if (master != null) lockedSyncSpool.refresh(vault)108-            val (keys, identity) = lockedSyncSpool.identity() ?: return false109-            cloudRelayListener.start(scope, keys.secretKey, keys.publicKey, identity) {109+            lockedSyncSpool.refresh(vault)110+            credentials = deriveUnlockedListenerCredentials(111+                masterKey = master,112+                deviceId = getOrCreateDeviceId(this),113+                deviceName = getDeviceName(this),114+                relayUrl = RelaySettingsStore.readRelayUrl(this),115+            )116+            cloudRelayListener.start(117+                scope,118+                credentials.keys.secretKey,119+                credentials.keys.publicKey,120+                credentials.identity,121+            ) {110122                 if (!vault.isUnlocked.value) lockedSyncSpool.adapter() else VaultSyncAdapter(111123                     repo = vault,112124                     ourDeviceId = getOrCreateDeviceId(this),@@ -116,7 +128,8 @@116128                 )117129             }118130         } finally {119-            master?.let(VaultCrypto::zero)131+            VaultCrypto.zero(master)132+            credentials?.keys?.secretKey?.let(VaultCrypto::zero)120133         }121134     }122135 @@ -136,6 +149,34 @@136149             context.stopService(Intent(context, PeachSyncService::class.java))137150         }138151     }152+}153+154+internal data class ReconnectListenerCredentials(155+    val keys: SyncIdentityKeypair,156+    val identity: LocalDeviceIdentity,157+)158+159+internal fun deriveUnlockedListenerCredentials(160+    masterKey: ByteArray,161+    deviceId: String,162+    deviceName: String,163+    relayUrl: String = Rendezvous.DEFAULT_RELAY_URL,164+): ReconnectListenerCredentials {165+    val keys = deriveLocalSyncKeypair(masterKey, deviceId)166+    val signing = deriveSyncSigningKeypair(masterKey, deviceId)167+    val identity = LocalDeviceIdentity(168+        deviceId = deviceId,169+        deviceName = deviceName,170+        deviceType = DeviceType.ANDROID,171+        longTermPublicKeyHex = pubkeyToHex(keys.publicKey),172+        signingPublicKeyHex = pubkeyToHex(signing.publicKeyRaw),173+        relayUrl = relayUrl,174+    )175+    if (!MessageDigest.isEqual(keys.publicKey, hexToPubkey(identity.longTermPublicKeyHex))) {176+        VaultCrypto.zero(keys.secretKey)177+        throw IllegalStateException("derived reconnect identity mismatch")178+    }179+    return ReconnectListenerCredentials(keys, identity)139180 }140181 141182 internal fun shouldRefreshExistingWaiters(listenerStarted: Boolean): Boolean = !listenerStarted

view file

app/src/test/java/com/peachpasswords/sync/SyncServiceStartTest.kt+13 −0
@@ -1,6 +1,7 @@11 package com.peachpasswords.sync22 33 import org.junit.Assert.assertEquals4+import org.junit.Assert.assertArrayEquals45 import org.junit.Assert.assertTrue56 import org.junit.Test67 @@ -64,5 +65,17 @@6465     @Test fun `only a nudge to an existing listener replaces its waiters`() {6566         assertEquals(false, shouldRefreshExistingWaiters(listenerStarted = true))6667         assertEquals(true, shouldRefreshExistingWaiters(listenerStarted = false))68+    }69+70+    @Test fun `an unlocked vault derives transient listener credentials`() {71+        val root = ByteArray(32) { (it + 1).toByte() }72+        val expected = deriveSyncIdentityKeypair(root, "phone-id")73+        val credentials = deriveUnlockedListenerCredentials(root, "phone-id", "Phone")74+75+        assertEquals("phone-id", credentials.identity.deviceId)76+        assertEquals("Phone", credentials.identity.deviceName)77+        assertEquals(pubkeyToHex(expected.publicKey), credentials.identity.longTermPublicKeyHex)78+        assertArrayEquals(expected.publicKey, credentials.keys.publicKey)79+        assertArrayEquals(expected.secretKey, credentials.keys.secretKey)6780     }6881 }

view file