mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
fix: 1.0.2 hotfix — heal wallets broken by per-build random crypto secrets
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
This commit is contained in:
@@ -174,6 +174,42 @@ jobs:
|
|||||||
flutter pub get
|
flutter pub get
|
||||||
dart run tool/generate_new_secrets.dart
|
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
|
- name: Inject Trocador + WalletConnect + MoonPay secrets into lib/.secrets.g.dart
|
||||||
env:
|
env:
|
||||||
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
||||||
|
|||||||
@@ -221,6 +221,42 @@ jobs:
|
|||||||
- name: Generate per-module secrets.g.dart files (all empty defaults)
|
- name: Generate per-module secrets.g.dart files (all empty defaults)
|
||||||
run: dart run tool/generate_new_secrets.dart
|
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
|
- name: Inject Trocador affiliate secrets
|
||||||
env:
|
env:
|
||||||
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
||||||
|
|||||||
@@ -224,6 +224,42 @@ jobs:
|
|||||||
- name: Generate per-module secrets.g.dart files (empty defaults)
|
- name: Generate per-module secrets.g.dart files (empty defaults)
|
||||||
run: dart run tool/generate_new_secrets.dart
|
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
|
- name: Inject Trocador + WalletConnect + MoonPay secrets
|
||||||
env:
|
env:
|
||||||
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
||||||
|
|||||||
@@ -134,6 +134,42 @@ jobs:
|
|||||||
flutter pub get
|
flutter pub get
|
||||||
dart run tool/generate_new_secrets.dart
|
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
|
- name: Inject Trocador affiliate secrets into lib/.secrets.g.dart
|
||||||
env:
|
env:
|
||||||
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
||||||
|
|||||||
@@ -329,6 +329,42 @@ jobs:
|
|||||||
- name: Generate per-module secrets.g.dart files (empty defaults)
|
- name: Generate per-module secrets.g.dart files (empty defaults)
|
||||||
run: dart run tool/generate_new_secrets.dart
|
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
|
- name: Inject Trocador affiliate secrets into lib/.secrets.g.dart
|
||||||
env:
|
env:
|
||||||
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
TROCADOR_API_KEY: ${{ secrets.TROCADOR_API_KEY }}
|
||||||
|
|||||||
@@ -89,12 +89,31 @@ class AuthService with Store {
|
|||||||
Future<bool> authenticate(String pin) async {
|
Future<bool> authenticate(String pin) async {
|
||||||
final regularKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
|
final regularKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword);
|
||||||
final encodedRegularPin = await secureStorage.read(key: regularKey);
|
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) {
|
if (decodedRegularPin == pin) {
|
||||||
return true;
|
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
|
// Check for duress pin
|
||||||
final duressKey = generateStoreKeyFor(key: SecretStoreKey.duressPinCodePassword);
|
final duressKey = generateStoreKeyFor(key: SecretStoreKey.duressPinCodePassword);
|
||||||
final encodedDuressPin = await secureStorage.read(key: duressKey);
|
final encodedDuressPin = await secureStorage.read(key: duressKey);
|
||||||
@@ -106,6 +125,7 @@ class AuthService with Store {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
printV("Failed to decode duress pin: $e");
|
printV("Failed to decode duress pin: $e");
|
||||||
}
|
}
|
||||||
|
decodedDuressPin ??= decodedPinCodeLegacy(pin: encodedDuressPin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decodedDuressPin == pin) {
|
if (decodedDuressPin == pin) {
|
||||||
|
|||||||
@@ -43,7 +43,19 @@ class KeyService {
|
|||||||
'restore this wallet from its seed.');
|
'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<void> saveWalletPassword({required String walletName, required String password}) async {
|
Future<void> saveWalletPassword({required String walletName, required String password}) async {
|
||||||
|
|||||||
@@ -54,3 +54,70 @@ String decodeWalletPassword({required String password}) {
|
|||||||
|
|
||||||
return decrypt(source: source, key: _key);
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: hash_wallet
|
name: hash_wallet
|
||||||
description: Hash Bags — a noncustodial multi-currency wallet by Such Software LLC.
|
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
|
publish_to: none
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
Reference in New Issue
Block a user