mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
fix(ios): recover wallet password across Keychain param changes; no crash
getWalletPassword force-unwrapped the secure-storage read, so a null iOS Keychain result (item written by an older build under different Keychain params, e.g. after a TestFlight update) hard-crashed the app and the wallet showed "Corrupted seeds" — reported by a tester whose older Wownero and BTC wallets broke on update. Now it: - falls back to the legacy readNoIOptions path (queries the OTHER iOS Keychain accessibility), which can AUTO-RECOVER such wallets without a seed restore; - falls back to the #__<name>_bak__# backup password the Monero migration writes before re-encrypting; - throws a catchable error instead of a null-check crash, so the loader can offer seed/restore recovery rather than the app dying. Only changes behavior on the previously-crashing null path — strictly safer. Not the file cipher (iOS is always Salsa20) nor the Monero migration (Wownero/BTC don't run it): the wallet files are intact, only the Keychain password was unreadable. flutter_secure_storage stays pinned (ca897a08) and the accessibility stays first_unlock; those must not change without a migration (see docs/internal-notes.md).
This commit is contained in:
@@ -10,8 +10,40 @@ class KeyService {
|
||||
Future<String> getWalletPassword({required String walletName}) async {
|
||||
final key =
|
||||
generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
|
||||
final encodedPassword = await _secureStorage.read(key: key);
|
||||
return decodeWalletPassword(password: encodedPassword!);
|
||||
|
||||
// Primary read (current iOS Keychain accessibility).
|
||||
var encodedPassword = await _secureStorage.read(key: key);
|
||||
|
||||
// Fallback 1: the legacy read path queries the OTHER iOS Keychain
|
||||
// accessibility. A wallet whose password item was written by an older
|
||||
// build with different Keychain params reads back null on the primary
|
||||
// path — this recovers it. That is the "corrupted seeds after a TestFlight
|
||||
// update" failure (Wownero/BTC/etc.: wallet files intact, Keychain entry
|
||||
// unreadable under the new params).
|
||||
encodedPassword ??= await _secureStorage.readNoIOptions(key: key);
|
||||
|
||||
// Fallback 2: the backup password the Monero password migration saves
|
||||
// under a side key BEFORE it re-encrypts, in case the main save did not
|
||||
// persist (see WalletLoadingService.updateMoneroWalletPassword).
|
||||
if (encodedPassword == null) {
|
||||
final bakKey = generateStoreKeyFor(
|
||||
key: SecretStoreKey.moneroWalletPassword,
|
||||
walletName: '#__${walletName}_bak__#');
|
||||
encodedPassword = await _secureStorage.read(key: bakKey) ??
|
||||
await _secureStorage.readNoIOptions(key: bakKey);
|
||||
}
|
||||
|
||||
// Fail with a clear, CATCHABLE error instead of a null-check crash, so the
|
||||
// loader can surface the seed/restore recovery path rather than the app
|
||||
// hard-crashing ("crashed and wouldn't let me in").
|
||||
if (encodedPassword == null) {
|
||||
throw Exception(
|
||||
'Wallet password not found in secure storage for "$walletName". '
|
||||
'The keychain entry may have been lost across an app update; '
|
||||
'restore this wallet from its seed.');
|
||||
}
|
||||
|
||||
return decodeWalletPassword(password: encodedPassword);
|
||||
}
|
||||
|
||||
Future<void> saveWalletPassword({required String walletName, required String password}) async {
|
||||
|
||||
Reference in New Issue
Block a user