Fix v3 keystore migration for legacy empty scrypt salt#541
Merged
Conversation
Legacy WalletCore keystores (pre-2025) sometimes wrote an empty scrypt salt. The v4 migrator's v3 reader rejected salts shorter than 16 bytes during deserialization -- before any decryption -- so migration failed on every launch and the wallet's secret became unviewable. The seed is intact. Relax the v3 reader to accept any salt length up to the maximum; the MAC and the migrate_v3 binding verification still gate correctness, and v4 always writes a fresh 16-byte Argon2 salt. Also (cause-agnostic): surface keystore-read failures instead of a silent blank phrase (Android) and as an actionable message (iOS), and rethrow CancellationException in the Android secret-data flow. Tests: empty-salt v3 reader round-trip (+ wrong-password/oversized negatives) and iOS/Android end-to-end migration of an empty-salt keystore.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The oldest wallets (created by WalletCore ~2024-11) have an empty scrypt salt (
"salt": "") in their v3 keystore file. The v4 migrator's v3 reader rejected any salt shorter thanMIN_SALT_LEN(16) during JSON deserialization — before any decryption — withCorruptFile("invalid v3 hex length"). Migration runs on every launch and fails identically every time, so the v4 file is never written and the wallet's secret phrase becomes unviewable (e.g. "Not Found" on iOS).The seed is intact — scrypt accepts an empty salt and the original app read these files fine. This is over-strict validation, not data loss or password loss. Confirmed against a real affected file (structurally intact: 16-byte iv, 32-byte mac, only the salt zeroed).
Fix (core)
Relax the v3 reader to accept any salt length up to
MAX_SALT_LEN(lower bound → 0), keeping the upper bound. Correctness is still gated by:AuthenticationFailed, before any plaintext is returned), andmigrate_v3's binding verification (the decrypted secret must derive the wallet id, else the staged v4 file is deleted and the v3 file preserved).v4 always writes a fresh 16-byte Argon2id salt on a separate path, so going-forward security is unchanged. A scoped audit confirmed the salt floor is the only over-rejection for the Gem population (n/r/p/dklen/kdf/cipher/version all match production exactly).
Recovery: shipping this is the recovery — the installed app re-runs migration on launch (holding the keychain password) and completes it. Conditional only on the keychain password still being present (strong evidence it is: the failure is at salt parsing, not decryption).
Also included (cause-agnostic safety nets — help any keystore-read failure)
WalletSecretDataValue.isError); cancellation is rethrown (no falseisErroron coroutine cancellation).getOrCreatePasswordkeeps its existing fail-closed behavior (thrown keychain errors propagate; never wipes the password).Tests
gemstone migrate_v3_empty_salt_mnemonic_round_tripMigrateV3KeystoreTests.migrateV3WithEmptyScryptSaltMigrateV3KeystoreServiceTest.migrateV3WithEmptyScryptSalt_createsV4AndRecoversKeyGetWalletSecretDataImplTest(error surfacing + cancellation).Verified:
cargo test -p gem_keystore --features v3(27),cargo test -p gemstone migrate_v3(6),clippy -D warningsclean; iOS KeystoreTests + MigrateV3KeystoreTests; Android unit + instrumented (emulator). The same empty-salt issue affects both platforms (shared WalletCore origin + shared core reader) and the fix covers both.