mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
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).
64 lines
2.7 KiB
Dart
64 lines
2.7 KiB
Dart
import 'package:hash_wallet/core/secure_storage.dart';
|
|
import 'package:hash_wallet/entities/secret_store_key.dart';
|
|
import 'package:hash_wallet/entities/encrypt.dart';
|
|
|
|
class KeyService {
|
|
KeyService(this._secureStorage);
|
|
|
|
final SecureStorage _secureStorage;
|
|
|
|
Future<String> getWalletPassword({required String walletName}) async {
|
|
final key =
|
|
generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
|
|
|
|
// 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 {
|
|
final key =
|
|
generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
|
|
final encodedPassword = encodeWalletPassword(password: password);
|
|
|
|
await _secureStorage.write(key: key, value: encodedPassword);
|
|
}
|
|
|
|
Future<void> deleteWalletPassword({required String walletName}) async {
|
|
final key =
|
|
generateStoreKeyFor(key: SecretStoreKey.moneroWalletPassword, walletName: walletName);
|
|
|
|
await _secureStorage.delete(key: key);
|
|
}
|
|
}
|