5888cb598ccc
fix: recovery escrow missing on adopted vaults + re-auth gate for viewing words (v2.9.22)
- time
- 2026-08-01 21:00 UTC · 2 months ago
- machine
- koh2
- parent
- f4352f0fe746
↓ peach-android-5888cb59.tar.gzatlas at this save
files changed2 files
app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+10 −1app/src/main/java/com/peachpasswords/vault/VaultRepository.kt+51 −2app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+10 −1
@@ -375,7 +375,16 @@375375 modifier = Modifier.padding(top = 2.dp),376376 )377377 }378- SubtleButton("view", fg) { onNavigate(PeachNav.RecoveryView) }378+ SubtleButton("view", fg) {379+ if (bioActivity != null) {380+ com.peachpasswords.ui.lock.Biometrics.authenticate(381+ activity = bioActivity,382+ title = "View recovery words",383+ subtitle = "Confirm your identity to view your recovery words",384+ onSuccess = { onNavigate(PeachNav.RecoveryView) },385+ )386+ } else onNavigate(PeachNav.RecoveryView)387+ }379388 }380389 SubLabel("BREACH PROTECTION", fg)381390 val checkingBreaches by vm.checkingBreaches.collectAsState()app/src/main/java/com/peachpasswords/vault/VaultRepository.kt+51 −2
@@ -592,6 +592,27 @@592592 val (previousDerivedKey, previousRootKey) = adopted593593 previousDerivedKey?.let(VaultCrypto::zero)594594 previousRootKey?.let(VaultCrypto::zero)595+ // Write the recovery escrow so the forgot-password flow works on596+ // this device. The words are deterministic from the root key and597+ // were already confirmed on the source device.598+ runCatching {599+ val words = rootKeyToWords(rootKey!!)600+ val escrow = buildRecoveryEscrow(words)601+ val rkEscrow = buildRootKeyEscrow(words)602+ if (escrow != null || rkEscrow != null) {603+ val prior = store.loadSlotA()604+ if (prior != null) {605+ store.writeSlotA(prior.copy(606+ recoveryEscrow = escrow ?: prior.recoveryEscrow,607+ rootKeyEscrow = rkEscrow ?: prior.rootKeyEscrow,608+ ))609+ }610+ mutateState {611+ recoveryWordHash = recoveryWordsHash(words)612+ recoveryWordsSet = true613+ }614+ }615+ }595616 AdoptVaultResult.Success(vaultId)596617 } catch (e: Exception) {597618 AdoptVaultResult.Error(e.message ?: "adoption_create_failed")@@ -603,7 +624,9 @@603624 }604625 605626 suspend fun unlock(password: String): UnlockResult = mutex.withLock {606- unlockLocked(password, currentEpoch())627+ val result = unlockLocked(password, currentEpoch())628+ if (result is UnlockResult.Success) ensureRecoveryEscrow()629+ result607630 }608631 609632 /** Unlock body without acquiring the mutex — callers must already hold it. */@@ -1172,7 +1195,7 @@11721195 }11731196 val candidate = stagedKey ?: return UnlockResult.Error("unlock_key_missing")11741197 val result = adoptSlotA(candidate, payload, expectedEpoch)1175- if (result is UnlockResult.Success) stagedKey = null1198+ if (result is UnlockResult.Success) { stagedKey = null; ensureRecoveryEscrow() }11761199 result11771200 } finally {11781201 stagedKey?.let(VaultCrypto::zero)@@ -2120,6 +2143,32 @@21202143 /** True at rest if this vault has any recovery escrow the recovery flow can use. */21212144 fun hasRecoveryEscrow(): Boolean =21222145 store.loadSlotA()?.let { it.recoveryEscrow != null || it.rootKeyEscrow != null } ?: false2146+2147+ /**2148+ * One-time self-heal: if this vault has a root key but no recovery escrow2149+ * on disk (adopted vaults created before the escrow fix), build and persist2150+ * it now. Called on every successful unlock; no-ops once the escrow exists.2151+ */2152+ private fun ensureRecoveryEscrow() {2153+ if (hasRecoveryEscrow()) return2154+ val rk = mutateState { rootKey?.copyOf() } ?: return2155+ runCatching {2156+ val words = rootKeyToWords(rk)2157+ val escrow = buildRecoveryEscrow(words)2158+ val rkEscrow = buildRootKeyEscrow(words)2159+ if (escrow != null || rkEscrow != null) {2160+ val prior = store.loadSlotA() ?: return@runCatching2161+ store.writeSlotA(prior.copy(2162+ recoveryEscrow = escrow ?: prior.recoveryEscrow,2163+ rootKeyEscrow = rkEscrow ?: prior.rootKeyEscrow,2164+ ))2165+ mutateState {2166+ recoveryWordHash = recoveryWordsHash(words)2167+ recoveryWordsSet = true2168+ }2169+ }2170+ }.also { VaultCrypto.zero(rk) }2171+ }21232172 21242173 /**21252174 * Attempt to unlock the vault using the 12 BIP39 recovery [words] instead of