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:
jwinterm
2026-08-29 14:53:18 -04:00
parent 8db45c5842
commit d8aab70fa7
9 changed files with 282 additions and 3 deletions
+36
View File
@@ -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 }}
+36
View File
@@ -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 }}
@@ -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 }}
+36
View File
@@ -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 }}
+36
View File
@@ -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 }}