Context
Findings from a read-only source-level audit of develop (2026-06-01). Note: develop HEAD is fully green on dfx01 (Analyze & Test, Coverage Floor, Visual Regression, BitBox quirks audit) — these are issues the automated gates do not catch. flutter was not run in the audit environment; items are code-evidenced with file:line and marked by confidence. Bundled because they all concern seed/key material at rest and lifecycle. Split into PRs as appropriate; track via checklist.
Suggested labels: bug.
Findings
S1 — Seed phrase leaks to OS app-switcher snapshot / screen recording (no FLAG_SECURE) — HIGH
S2 — "Delete Wallet" leaves the encrypted seed AND its decryption key on the device — HIGH
S3 — VerifySeedPage has no screenshot protection at all — MEDIUM
S4 — Auto-lock + mnemonic-drop keyed solely on AppLifecycleState.hidden — MEDIUM
S5 — flutter_secure_storage constructed without hardened options — MEDIUM
S6 — decryptSeed throws uncaught on malformed/truncated ciphertext — LOW
S7 — Seed-quiz word selection uses non-secure Random() — LOW
Checked & clean: mnemonic generation (bip39 secure RNG) + restore checksum; PIN PBKDF2-250k with persisted lockout (survives restart, biometric can't bypass); seed double-encrypted (AES-GCM in SQLCipher).
Context
Findings from a read-only source-level audit of
develop(2026-06-01). Note:developHEAD is fully green on dfx01 (Analyze & Test, Coverage Floor, Visual Regression, BitBox quirks audit) — these are issues the automated gates do not catch.flutterwas not run in the audit environment; items are code-evidenced with file:line and marked by confidence. Bundled because they all concern seed/key material at rest and lifecycle. Split into PRs as appropriate; track via checklist.Suggested labels:
bug.Findings
S1 — Seed phrase leaks to OS app-switcher snapshot / screen recording (no FLAG_SECURE) — HIGH
android/app/src/main/kotlin/.../MainActivity.ktsets noFLAG_SECURE; seed screens rely only on theno_screenshotpackage (lib/screens/create_wallet/create_wallet_view.dart:24,lib/screens/settings_seed/settings_seed_view.dart:21).grep FLAG_SECURE android/→ none.no_screenshotblocks manual screenshots but not the Android recents/app-switcher thumbnail or OS/3rd-party screen recording. With the seed revealed, the 12 words can be cached in recents or captured by recording.S2 — "Delete Wallet" leaves the encrypted seed AND its decryption key on the device — HIGH
lib/packages/storage/wallet_storage.dart:28-29deleteWalletdeletes only fromwalletAccountInfos, neverwalletInfos(holds the AES-GCM-encrypted mnemonic);_mnemonicEncryptionKeyinflutter_secure_storageis never removed. Path: settings →home_bloc._onDeleteCurrentWallet→deleteCurrentWallet→wallet_repository→deleteWallet.S3 —
VerifySeedPagehas no screenshot protection at all — MEDIUMlib/screens/verify_seed/verify_seed_page.dartnever callsscreenshotOff()(unlike the sibling seed screens), yet it handles the user's actual seed words (debug pre-fills them:verify_seed_cubit.dart:48-49).S4 — Auto-lock + mnemonic-drop keyed solely on
AppLifecycleState.hidden— MEDIUMlib/setup/lifecycle_initializer.dart:54-62: only_onHiddencallsWalletService.lockCurrentWallet()andPinAuthCubit.onAppHidden(); neitherinactivenorpausedarm the lock. Ifhiddenis skipped/coalesced on a platform transition, on resume_lastBackgroundTime == null→ the 5-min force-lock never fires and the in-memory mnemonic is not dropped.S5 —
flutter_secure_storageconstructed without hardened options — MEDIUMlib/packages/storage/secure_storage.dart:23const FlutterSecureStorage()(Android: noencryptedSharedPreferences: true; iOS: noKeychainAccessibility). This store holds the SQLCipher DB key, the mnemonic AES-GCM key, the PIN hash + lockout. These keys are not PIN-derived, so extracting them bypasses the PIN.S6 —
decryptSeedthrows uncaught on malformed/truncated ciphertext — LOWlib/packages/storage/secure_storage.dart:176-183:encoded.indexOf(':')thensubstring(0, colonIndex)→RangeErrorwhen no colon; GCM tag-mismatch throws uncaught. A corruptedwalletInfos.seedrow crashes the unlock path rather than surfacing a controlled error. (AES-GCM rejecting tampering is correct; this is robustness only.)S7 — Seed-quiz word selection uses non-secure
Random()— LOWlib/screens/verify_seed/cubit/verify_seed_cubit.dart:32Random().nextInt(...). Not key material (mnemonic itself uses bip39 secure RNG); only predicts which 4 of 12 words are challenged. PreferRandom.secure()for hygiene.Checked & clean: mnemonic generation (bip39 secure RNG) + restore checksum; PIN PBKDF2-250k with persisted lockout (survives restart, biometric can't bypass); seed double-encrypted (AES-GCM in SQLCipher).