mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
CI ran tool/generate_new_secrets.dart unpinned, so every release binary shipped different AES keys/salts for the stored PIN and wallet passwords. Updating the app then failed to decrypt what the previous build stored: 'Invalid or corrupted pad block' at PIN entry and the 'Corrupted seeds' dialog, with wallet files fully intact. - entities/encrypt.dart: legacy-key fallback decode for PIN and wallet password blobs (candidate sets recovered from the shipped 1.0.1 binary; output validated to reject PKCS7 false positives) - key_service/auth_service: on successful legacy decode, re-encrypt under the pinned key — installs self-heal at first unlock - all build workflows: inject the eight pinned CRYPTO_* repo secrets after secret generation and hard-fail if any is missing, so crypto salts can never silently rotate again (values pinned via ~keys/set-gitea-crypto-secrets.sh) - bump 1.0.2+12
76 lines
3.1 KiB
Dart
76 lines
3.1 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.');
|
|
}
|
|
|
|
try {
|
|
return decodeWalletPassword(password: encodedPassword);
|
|
} catch (e) {
|
|
// Legacy-build fallback: blobs written by a build whose generated
|
|
// crypto secrets differed (pre-1.0.2 CI rotated them every build).
|
|
// On success, re-encode under the current key so this install heals.
|
|
final legacy = decodeWalletPasswordLegacy(password: encodedPassword);
|
|
if (legacy != null) {
|
|
await saveWalletPassword(walletName: walletName, password: legacy);
|
|
return legacy;
|
|
}
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|