From d8aab70fa749a3338506a0559cd0585f5a9f3b68 Mon Sep 17 00:00:00 2001 From: jwinterm Date: Sat, 29 Aug 2026 14:53:18 -0400 Subject: [PATCH] =?UTF-8?q?fix:=201.0.2=20hotfix=20=E2=80=94=20heal=20wall?= =?UTF-8?q?ets=20broken=20by=20per-build=20random=20crypto=20secrets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .github/workflows/build-android.yml | 36 ++++++++++++ .github/workflows/build-ios-sim.yml | 36 ++++++++++++ .github/workflows/build-ios-testflight.yml | 36 ++++++++++++ .github/workflows/build-linux.yml | 36 ++++++++++++ .github/workflows/build-windows.yml | 36 ++++++++++++ lib/core/auth_service.dart | 22 ++++++- lib/core/key_service.dart | 14 ++++- lib/entities/encrypt.dart | 67 ++++++++++++++++++++++ pubspec_description.yaml | 2 +- 9 files changed, 282 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 730062b89..6a6f259c4 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -174,6 +174,42 @@ jobs: flutter pub get dart run tool/generate_new_secrets.dart + + - name: Pin crypto secrets (never ship random salts) + env: + CRYPTO_SALT: ${{ secrets.CRYPTO_SALT }} + CRYPTO_KEYCHAIN_SALT: ${{ secrets.CRYPTO_KEYCHAIN_SALT }} + CRYPTO_KEY: ${{ secrets.CRYPTO_KEY }} + CRYPTO_WALLET_SALT: ${{ secrets.CRYPTO_WALLET_SALT }} + CRYPTO_SHORT_KEY: ${{ secrets.CRYPTO_SHORT_KEY }} + CRYPTO_BACKUP_SALT: ${{ secrets.CRYPTO_BACKUP_SALT }} + CRYPTO_BACKUP_KEYCHAIN_SALT: ${{ secrets.CRYPTO_BACKUP_KEYCHAIN_SALT }} + CRYPTO_WALLET_GROUP_SALT: ${{ secrets.CRYPTO_WALLET_GROUP_SALT }} + run: | + set -e + # generate_new_secrets.dart mints RANDOM salts each run. Shipping + # those breaks decryption of everything the previous build stored + # (2026-08-29 "Corrupted seeds" incident). Overwrite with the pinned + # values and refuse to build if any is missing. + for v in CRYPTO_SALT CRYPTO_KEYCHAIN_SALT CRYPTO_KEY CRYPTO_WALLET_SALT CRYPTO_SHORT_KEY CRYPTO_BACKUP_SALT CRYPTO_BACKUP_KEYCHAIN_SALT CRYPTO_WALLET_GROUP_SALT; do + eval "val=\${$v:-}" + [ -n "$val" ] || { echo "FATAL: $v is empty — refusing to ship random crypto secrets"; exit 1; } + done + sed -i.bak \ + -e "s|const salt = '.*';|const salt = '${CRYPTO_SALT}';|" \ + -e "s|const keychainSalt = '.*';|const keychainSalt = '${CRYPTO_KEYCHAIN_SALT}';|" \ + -e "s|const key = '.*';|const key = '${CRYPTO_KEY}';|" \ + -e "s|const walletSalt = '.*';|const walletSalt = '${CRYPTO_WALLET_SALT}';|" \ + -e "s|const shortKey = '.*';|const shortKey = '${CRYPTO_SHORT_KEY}';|" \ + -e "s|const backupSalt = '.*';|const backupSalt = '${CRYPTO_BACKUP_SALT}';|" \ + -e "s|const backupKeychainSalt = '.*';|const backupKeychainSalt = '${CRYPTO_BACKUP_KEYCHAIN_SALT}';|" \ + -e "s|const walletGroupSalt = '.*';|const walletGroupSalt = '${CRYPTO_WALLET_GROUP_SALT}';|" \ + lib/.secrets.g.dart + rm -f lib/.secrets.g.dart.bak + for c in salt keychainSalt key walletSalt shortKey backupSalt backupKeychainSalt walletGroupSalt; do + grep -qE "^const $c = '[0-9a-f]{8,}';" lib/.secrets.g.dart || { echo "FATAL: $c not pinned"; exit 1; } + done + echo "crypto secrets pinned (values not printed)" - name: Inject Trocador + WalletConnect + MoonPay secrets into lib/.secrets.g.dart env: TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} diff --git a/.github/workflows/build-ios-sim.yml b/.github/workflows/build-ios-sim.yml index d2ee3a16f..9a54442bf 100644 --- a/.github/workflows/build-ios-sim.yml +++ b/.github/workflows/build-ios-sim.yml @@ -221,6 +221,42 @@ jobs: - name: Generate per-module secrets.g.dart files (all empty defaults) run: dart run tool/generate_new_secrets.dart + + - name: Pin crypto secrets (never ship random salts) + env: + CRYPTO_SALT: ${{ secrets.CRYPTO_SALT }} + CRYPTO_KEYCHAIN_SALT: ${{ secrets.CRYPTO_KEYCHAIN_SALT }} + CRYPTO_KEY: ${{ secrets.CRYPTO_KEY }} + CRYPTO_WALLET_SALT: ${{ secrets.CRYPTO_WALLET_SALT }} + CRYPTO_SHORT_KEY: ${{ secrets.CRYPTO_SHORT_KEY }} + CRYPTO_BACKUP_SALT: ${{ secrets.CRYPTO_BACKUP_SALT }} + CRYPTO_BACKUP_KEYCHAIN_SALT: ${{ secrets.CRYPTO_BACKUP_KEYCHAIN_SALT }} + CRYPTO_WALLET_GROUP_SALT: ${{ secrets.CRYPTO_WALLET_GROUP_SALT }} + run: | + set -e + # generate_new_secrets.dart mints RANDOM salts each run. Shipping + # those breaks decryption of everything the previous build stored + # (2026-08-29 "Corrupted seeds" incident). Overwrite with the pinned + # values and refuse to build if any is missing. + for v in CRYPTO_SALT CRYPTO_KEYCHAIN_SALT CRYPTO_KEY CRYPTO_WALLET_SALT CRYPTO_SHORT_KEY CRYPTO_BACKUP_SALT CRYPTO_BACKUP_KEYCHAIN_SALT CRYPTO_WALLET_GROUP_SALT; do + eval "val=\${$v:-}" + [ -n "$val" ] || { echo "FATAL: $v is empty — refusing to ship random crypto secrets"; exit 1; } + done + sed -i.bak \ + -e "s|const salt = '.*';|const salt = '${CRYPTO_SALT}';|" \ + -e "s|const keychainSalt = '.*';|const keychainSalt = '${CRYPTO_KEYCHAIN_SALT}';|" \ + -e "s|const key = '.*';|const key = '${CRYPTO_KEY}';|" \ + -e "s|const walletSalt = '.*';|const walletSalt = '${CRYPTO_WALLET_SALT}';|" \ + -e "s|const shortKey = '.*';|const shortKey = '${CRYPTO_SHORT_KEY}';|" \ + -e "s|const backupSalt = '.*';|const backupSalt = '${CRYPTO_BACKUP_SALT}';|" \ + -e "s|const backupKeychainSalt = '.*';|const backupKeychainSalt = '${CRYPTO_BACKUP_KEYCHAIN_SALT}';|" \ + -e "s|const walletGroupSalt = '.*';|const walletGroupSalt = '${CRYPTO_WALLET_GROUP_SALT}';|" \ + lib/.secrets.g.dart + rm -f lib/.secrets.g.dart.bak + for c in salt keychainSalt key walletSalt shortKey backupSalt backupKeychainSalt walletGroupSalt; do + grep -qE "^const $c = '[0-9a-f]{8,}';" lib/.secrets.g.dart || { echo "FATAL: $c not pinned"; exit 1; } + done + echo "crypto secrets pinned (values not printed)" - name: Inject Trocador affiliate secrets env: TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} diff --git a/.github/workflows/build-ios-testflight.yml b/.github/workflows/build-ios-testflight.yml index 26d81cba1..c195f8d7d 100644 --- a/.github/workflows/build-ios-testflight.yml +++ b/.github/workflows/build-ios-testflight.yml @@ -224,6 +224,42 @@ jobs: - name: Generate per-module secrets.g.dart files (empty defaults) run: dart run tool/generate_new_secrets.dart + + - name: Pin crypto secrets (never ship random salts) + env: + CRYPTO_SALT: ${{ secrets.CRYPTO_SALT }} + CRYPTO_KEYCHAIN_SALT: ${{ secrets.CRYPTO_KEYCHAIN_SALT }} + CRYPTO_KEY: ${{ secrets.CRYPTO_KEY }} + CRYPTO_WALLET_SALT: ${{ secrets.CRYPTO_WALLET_SALT }} + CRYPTO_SHORT_KEY: ${{ secrets.CRYPTO_SHORT_KEY }} + CRYPTO_BACKUP_SALT: ${{ secrets.CRYPTO_BACKUP_SALT }} + CRYPTO_BACKUP_KEYCHAIN_SALT: ${{ secrets.CRYPTO_BACKUP_KEYCHAIN_SALT }} + CRYPTO_WALLET_GROUP_SALT: ${{ secrets.CRYPTO_WALLET_GROUP_SALT }} + run: | + set -e + # generate_new_secrets.dart mints RANDOM salts each run. Shipping + # those breaks decryption of everything the previous build stored + # (2026-08-29 "Corrupted seeds" incident). Overwrite with the pinned + # values and refuse to build if any is missing. + for v in CRYPTO_SALT CRYPTO_KEYCHAIN_SALT CRYPTO_KEY CRYPTO_WALLET_SALT CRYPTO_SHORT_KEY CRYPTO_BACKUP_SALT CRYPTO_BACKUP_KEYCHAIN_SALT CRYPTO_WALLET_GROUP_SALT; do + eval "val=\${$v:-}" + [ -n "$val" ] || { echo "FATAL: $v is empty — refusing to ship random crypto secrets"; exit 1; } + done + sed -i.bak \ + -e "s|const salt = '.*';|const salt = '${CRYPTO_SALT}';|" \ + -e "s|const keychainSalt = '.*';|const keychainSalt = '${CRYPTO_KEYCHAIN_SALT}';|" \ + -e "s|const key = '.*';|const key = '${CRYPTO_KEY}';|" \ + -e "s|const walletSalt = '.*';|const walletSalt = '${CRYPTO_WALLET_SALT}';|" \ + -e "s|const shortKey = '.*';|const shortKey = '${CRYPTO_SHORT_KEY}';|" \ + -e "s|const backupSalt = '.*';|const backupSalt = '${CRYPTO_BACKUP_SALT}';|" \ + -e "s|const backupKeychainSalt = '.*';|const backupKeychainSalt = '${CRYPTO_BACKUP_KEYCHAIN_SALT}';|" \ + -e "s|const walletGroupSalt = '.*';|const walletGroupSalt = '${CRYPTO_WALLET_GROUP_SALT}';|" \ + lib/.secrets.g.dart + rm -f lib/.secrets.g.dart.bak + for c in salt keychainSalt key walletSalt shortKey backupSalt backupKeychainSalt walletGroupSalt; do + grep -qE "^const $c = '[0-9a-f]{8,}';" lib/.secrets.g.dart || { echo "FATAL: $c not pinned"; exit 1; } + done + echo "crypto secrets pinned (values not printed)" - name: Inject Trocador + WalletConnect + MoonPay secrets env: TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 00c8063d0..30e20a11e 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -134,6 +134,42 @@ jobs: flutter pub get dart run tool/generate_new_secrets.dart + + - name: Pin crypto secrets (never ship random salts) + env: + CRYPTO_SALT: ${{ secrets.CRYPTO_SALT }} + CRYPTO_KEYCHAIN_SALT: ${{ secrets.CRYPTO_KEYCHAIN_SALT }} + CRYPTO_KEY: ${{ secrets.CRYPTO_KEY }} + CRYPTO_WALLET_SALT: ${{ secrets.CRYPTO_WALLET_SALT }} + CRYPTO_SHORT_KEY: ${{ secrets.CRYPTO_SHORT_KEY }} + CRYPTO_BACKUP_SALT: ${{ secrets.CRYPTO_BACKUP_SALT }} + CRYPTO_BACKUP_KEYCHAIN_SALT: ${{ secrets.CRYPTO_BACKUP_KEYCHAIN_SALT }} + CRYPTO_WALLET_GROUP_SALT: ${{ secrets.CRYPTO_WALLET_GROUP_SALT }} + run: | + set -e + # generate_new_secrets.dart mints RANDOM salts each run. Shipping + # those breaks decryption of everything the previous build stored + # (2026-08-29 "Corrupted seeds" incident). Overwrite with the pinned + # values and refuse to build if any is missing. + for v in CRYPTO_SALT CRYPTO_KEYCHAIN_SALT CRYPTO_KEY CRYPTO_WALLET_SALT CRYPTO_SHORT_KEY CRYPTO_BACKUP_SALT CRYPTO_BACKUP_KEYCHAIN_SALT CRYPTO_WALLET_GROUP_SALT; do + eval "val=\${$v:-}" + [ -n "$val" ] || { echo "FATAL: $v is empty — refusing to ship random crypto secrets"; exit 1; } + done + sed -i.bak \ + -e "s|const salt = '.*';|const salt = '${CRYPTO_SALT}';|" \ + -e "s|const keychainSalt = '.*';|const keychainSalt = '${CRYPTO_KEYCHAIN_SALT}';|" \ + -e "s|const key = '.*';|const key = '${CRYPTO_KEY}';|" \ + -e "s|const walletSalt = '.*';|const walletSalt = '${CRYPTO_WALLET_SALT}';|" \ + -e "s|const shortKey = '.*';|const shortKey = '${CRYPTO_SHORT_KEY}';|" \ + -e "s|const backupSalt = '.*';|const backupSalt = '${CRYPTO_BACKUP_SALT}';|" \ + -e "s|const backupKeychainSalt = '.*';|const backupKeychainSalt = '${CRYPTO_BACKUP_KEYCHAIN_SALT}';|" \ + -e "s|const walletGroupSalt = '.*';|const walletGroupSalt = '${CRYPTO_WALLET_GROUP_SALT}';|" \ + lib/.secrets.g.dart + rm -f lib/.secrets.g.dart.bak + for c in salt keychainSalt key walletSalt shortKey backupSalt backupKeychainSalt walletGroupSalt; do + grep -qE "^const $c = '[0-9a-f]{8,}';" lib/.secrets.g.dart || { echo "FATAL: $c not pinned"; exit 1; } + done + echo "crypto secrets pinned (values not printed)" - name: Inject Trocador affiliate secrets into lib/.secrets.g.dart env: TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 4fcf9255d..34cdf3b14 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -329,6 +329,42 @@ jobs: - name: Generate per-module secrets.g.dart files (empty defaults) run: dart run tool/generate_new_secrets.dart + + - name: Pin crypto secrets (never ship random salts) + env: + CRYPTO_SALT: ${{ secrets.CRYPTO_SALT }} + CRYPTO_KEYCHAIN_SALT: ${{ secrets.CRYPTO_KEYCHAIN_SALT }} + CRYPTO_KEY: ${{ secrets.CRYPTO_KEY }} + CRYPTO_WALLET_SALT: ${{ secrets.CRYPTO_WALLET_SALT }} + CRYPTO_SHORT_KEY: ${{ secrets.CRYPTO_SHORT_KEY }} + CRYPTO_BACKUP_SALT: ${{ secrets.CRYPTO_BACKUP_SALT }} + CRYPTO_BACKUP_KEYCHAIN_SALT: ${{ secrets.CRYPTO_BACKUP_KEYCHAIN_SALT }} + CRYPTO_WALLET_GROUP_SALT: ${{ secrets.CRYPTO_WALLET_GROUP_SALT }} + run: | + set -e + # generate_new_secrets.dart mints RANDOM salts each run. Shipping + # those breaks decryption of everything the previous build stored + # (2026-08-29 "Corrupted seeds" incident). Overwrite with the pinned + # values and refuse to build if any is missing. + for v in CRYPTO_SALT CRYPTO_KEYCHAIN_SALT CRYPTO_KEY CRYPTO_WALLET_SALT CRYPTO_SHORT_KEY CRYPTO_BACKUP_SALT CRYPTO_BACKUP_KEYCHAIN_SALT CRYPTO_WALLET_GROUP_SALT; do + eval "val=\${$v:-}" + [ -n "$val" ] || { echo "FATAL: $v is empty — refusing to ship random crypto secrets"; exit 1; } + done + sed -i.bak \ + -e "s|const salt = '.*';|const salt = '${CRYPTO_SALT}';|" \ + -e "s|const keychainSalt = '.*';|const keychainSalt = '${CRYPTO_KEYCHAIN_SALT}';|" \ + -e "s|const key = '.*';|const key = '${CRYPTO_KEY}';|" \ + -e "s|const walletSalt = '.*';|const walletSalt = '${CRYPTO_WALLET_SALT}';|" \ + -e "s|const shortKey = '.*';|const shortKey = '${CRYPTO_SHORT_KEY}';|" \ + -e "s|const backupSalt = '.*';|const backupSalt = '${CRYPTO_BACKUP_SALT}';|" \ + -e "s|const backupKeychainSalt = '.*';|const backupKeychainSalt = '${CRYPTO_BACKUP_KEYCHAIN_SALT}';|" \ + -e "s|const walletGroupSalt = '.*';|const walletGroupSalt = '${CRYPTO_WALLET_GROUP_SALT}';|" \ + lib/.secrets.g.dart + rm -f lib/.secrets.g.dart.bak + for c in salt keychainSalt key walletSalt shortKey backupSalt backupKeychainSalt walletGroupSalt; do + grep -qE "^const $c = '[0-9a-f]{8,}';" lib/.secrets.g.dart || { echo "FATAL: $c not pinned"; exit 1; } + done + echo "crypto secrets pinned (values not printed)" - name: Inject Trocador affiliate secrets into lib/.secrets.g.dart env: TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }} diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 919f6acb2..77f37e347 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -89,12 +89,31 @@ class AuthService with Store { Future authenticate(String pin) async { final regularKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedRegularPin = await secureStorage.read(key: regularKey); - final decodedRegularPin = decodedPinCode(pin: encodedRegularPin!); + if (encodedRegularPin == null) { + return false; + } + + String? decodedRegularPin; + try { + decodedRegularPin = decodedPinCode(pin: encodedRegularPin); + } catch (e) { + printV("Primary pin decode failed (legacy-build blob?): $e"); + } if (decodedRegularPin == pin) { return true; } + // Legacy-build fallback: the stored blob was encrypted by a build whose + // generated crypto secrets differed (pre-1.0.2 CI rotated them every + // build). If a legacy key decodes to the entered PIN, accept it and + // re-encode the blob under the current key so this install heals. + final legacyDecodedPin = decodedPinCodeLegacy(pin: encodedRegularPin); + if (legacyDecodedPin != null && legacyDecodedPin == pin) { + await secureStorage.write(key: regularKey, value: encodedPinCode(pin: pin)); + return true; + } + // Check for duress pin final duressKey = generateStoreKeyFor(key: SecretStoreKey.duressPinCodePassword); final encodedDuressPin = await secureStorage.read(key: duressKey); @@ -106,6 +125,7 @@ class AuthService with Store { } catch (e) { printV("Failed to decode duress pin: $e"); } + decodedDuressPin ??= decodedPinCodeLegacy(pin: encodedDuressPin); } if (decodedDuressPin == pin) { diff --git a/lib/core/key_service.dart b/lib/core/key_service.dart index 4eb60db3d..098d18d92 100644 --- a/lib/core/key_service.dart +++ b/lib/core/key_service.dart @@ -43,7 +43,19 @@ class KeyService { 'restore this wallet from its seed.'); } - return decodeWalletPassword(password: encodedPassword); + 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 saveWalletPassword({required String walletName, required String password}) async { diff --git a/lib/entities/encrypt.dart b/lib/entities/encrypt.dart index 7695bda8b..94a621006 100644 --- a/lib/entities/encrypt.dart +++ b/lib/entities/encrypt.dart @@ -54,3 +54,70 @@ String decodeWalletPassword({required String password}) { return decrypt(source: source, key: _key); } + +// --------------------------------------------------------------------------- +// Legacy-build compatibility (1.0.2 hotfix). +// +// CI builds before 1.0.2 regenerated the crypto secrets on EVERY build +// (tool/generate_new_secrets.dart runs unpinned in the workflow), so each +// shipped binary encrypted the stored PIN / wallet passwords under different +// keys. Updating the app then broke decryption of everything the previous +// build had stored: "Invalid or corrupted pad block" at PIN entry and the +// "Corrupted seeds" dialog, with wallet files themselves perfectly intact. +// +// The values below are recovered from shipped release binaries (string table +// of libapp.so). On a primary decrypt failure the callers retry with these +// and re-encrypt under the current pinned key on success, so every install +// self-heals on its first successful unlock. A wrong key can slip past +// PKCS7 unpadding (~0.4%), so decoded output is validated before acceptance. + +// 32-hex `key` candidates (PIN blobs). Includes every plausible constant from +// the 1.0.1 Android string table; wrong entries fail validation harmlessly. +const _legacyPinKeys = [ + '51f918406bc771b5827e2479ce24dae0', + '5a0e830aa102994045c19a4fef0d3156', + '76d489cc1eb5704e6e4bea65bb2618b3', + 'bba3031d10f37f741c9bcd2b26227cba', + 'd6031998d1b3bbfebf59cc9bbff9aee1', + '5eeefca380d02919dc2c6558bb6d8a5d', +]; + +// shortKey+walletSalt concatenations (wallet-password blobs), 1.0.1 Android. +const _legacyWalletPasswordKeys = [ + 'b77ed95e692db912ff0463821f37e159', + 'be2a818b190ab41010f0e2871f37e159', +]; + +String? _tryDecrypt({required String source, required String key}) { + try { + final decrypted = decrypt(source: source, key: key); + final printable = + decrypted.isNotEmpty && decrypted.codeUnits.every((c) => c >= 0x20 && c < 0x7f); + return printable ? decrypted : null; + } catch (_) { + return null; + } +} + +/// Decode a stored wallet password written by a build with rotated secrets. +/// Returns null when no legacy key fits. +String? decodeWalletPasswordLegacy({required String password}) { + for (final key in _legacyWalletPasswordKeys) { + final decrypted = _tryDecrypt(source: password, key: key); + if (decrypted != null) return decrypted; + } + return null; +} + +/// Decode a stored PIN blob written by a build with rotated secrets. +/// The plaintext layout is 32-char salt + digits; returns null if nothing fits. +String? decodedPinCodeLegacy({required String pin}) { + for (final key in _legacyPinKeys) { + final decrypted = _tryDecrypt(source: pin, key: key); + if (decrypted == null || decrypted.length <= 32) continue; + final candidate = decrypted.substring(32); + final digitsOnly = candidate.codeUnits.every((c) => c >= 0x30 && c <= 0x39); + if (digitsOnly) return candidate; + } + return null; +} diff --git a/pubspec_description.yaml b/pubspec_description.yaml index 98e20af27..0b0904565 100644 --- a/pubspec_description.yaml +++ b/pubspec_description.yaml @@ -1,6 +1,6 @@ name: hash_wallet description: Hash Bags — a noncustodial multi-currency wallet by Such Software LLC. -version: 1.0.1+11 +version: 1.0.2+12 publish_to: none environment: