Skip to content

Latest commit

 

History

History
147 lines (108 loc) · 5.23 KB

File metadata and controls

147 lines (108 loc) · 5.23 KB

Wallet Import & Backup: Parser Hardening

starforge wallet import --file and starforge backup restore read files that came from outside the tool — a colleague's export, a CI artifact, a USB stick. That makes their parsers a trust boundary, so they live in src/utils/wallet_import.rs, separated from prompting, disk access, and the config store, and are driven by three continuously-run fuzz harnesses.


What is enforced

Size

Limit Value Checked
Backup document 4 MiB Before the JSON parser runs
Encrypted bundle 8 MiB Before base64 decoding
Wallets per backup 1,000 After parsing, before validation
Wallet name 64 characters Per entry

Size gates run first, so an oversized file cannot drive a large allocation inside the JSON parser.

Backup documents

  • Must be a JSON object with version, exported_at, and a non-empty wallets array.
  • version must be exactly "1". A newer version is refused with a message naming both versions rather than being partially read.
  • Wallet names must be unique within the file.
  • Each entry's public_key must be a 56-character G… StrKey; a secret_key, when present, must be a 56-character S… StrKey or a well-formed encrypted bundle.
  • network must be non-empty.

Encrypted bundles

A bundle is salt:nonce:ciphertext, optionally followed by Argon2 parameters (:mem:iterations or :mem:iterations:parallelism).

Field Requirement
salt Valid base64, decoding to exactly 16 bytes
nonce Valid base64, decoding to exactly 12 bytes
ciphertext Valid base64, at least 16 bytes (one AES-GCM tag)
mem, iterations, parallelism Decimal u32, greater than zero

The structure is checked before the passphrase prompt, so a corrupt file fails immediately instead of after an Argon2 key derivation.

Unicode

Wallet names are rejected when they contain characters that are invisible or that reorder rendering — they can make one wallet's name look exactly like another's:

Range Characters
U+0000U+001F, U+007F Control characters
U+200BU+200F Zero-width space through right-to-left mark
U+202AU+202E Bidirectional embedding and override
U+2066U+2069 Bidirectional isolates
U+00AD Soft hyphen
U+FEFF Zero-width no-break space

Non-ASCII names are accepted with a warning, because earlier releases allowed any Unicode alphanumeric and rejecting them outright would make old backups unreadable. The warning flags the homograph risk (Cyrillic а renders like Latin a):

⚠ wallet 'аlice' has a non-ASCII name, which can render identically to another name

Error messages

A rejection quotes the wallet name and the reason, never the key material — error text lands in terminals, CI logs, and bug reports.


Behaviour changes

Encrypted bundles with custom Argon2 parameters now import correctly

Encryption detection used to be raw.matches(':').count() == 2, which only recognises the 3-part bundle. A backup encrypted with custom Argon2 parameters (starforge wallet export after --mem / --iterations / --parallelism) has 5 or 6 parts, so it was handed to the JSON parser and failed with a misleading Invalid backup JSON format.

Detection now follows the bundle grammar. If you have a backup that failed to import with that error, retry it — no re-export is needed.

A JSON document is never treated as a bundle

Classification returns "plaintext" for anything starting with { or [, regardless of how many colons the values contain. Previously a JSON document with exactly two colons could trigger a passphrase prompt for a passphrase that does not exist.

New rejections

Files that previously imported and now do not:

Input Now rejected because
A backup with more than 1,000 wallets Above the per-file limit
A wallet name longer than 64 characters Above the name limit
A wallet name containing bidi or zero-width characters Deceptive name
A backup document above 4 MiB Above the size limit

These were previously accepted, so a file hitting one of them was already unusual. Split an oversized backup, or rename the offending wallet before exporting.


Fuzzing

cargo fuzz run fuzz_wallet_backup_parse       -- -dict=fuzz/dicts/wallet_backup.dict
cargo fuzz run fuzz_wallet_import_envelope    -- -dict=fuzz/dicts/wallet_backup.dict
cargo fuzz run fuzz_wallet_backup_structured

cargo fuzz needs a nightly toolchain. The same invariants run on stable in tests/wallet_import_property_tests.rs, so every PR checks them without nightly:

cargo test --test wallet_import_property_tests
PROPTEST_CASES=10000 cargo test --test wallet_import_property_tests

See FUZZING_GUIDE.md for the harness inventory, the seed corpora, and the invariants each target asserts.


See also