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
↓ peach-android-8c0dd8e3.tar.gzatlas at this save
files changed3 files
VERSION+1 −1app/src/main/java/com/peachpasswords/sync/PeachSyncService.kt+46 −5app/src/test/java/com/peachpasswords/sync/SyncServiceStartTest.kt+13 −0app/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 = !listenerStartedapp/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 }