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.
| 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.
- Must be a JSON object with
version,exported_at, and a non-emptywalletsarray. versionmust 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_keymust be a 56-characterG…StrKey; asecret_key, when present, must be a 56-characterS…StrKey or a well-formed encrypted bundle. networkmust be non-empty.
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.
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+0000–U+001F, U+007F |
Control characters |
U+200B–U+200F |
Zero-width space through right-to-left mark |
U+202A–U+202E |
Bidirectional embedding and override |
U+2066–U+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
A rejection quotes the wallet name and the reason, never the key material — error text lands in terminals, CI logs, and bug reports.
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.
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.
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.
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_structuredcargo 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_testsSee FUZZING_GUIDE.md for the harness inventory, the seed corpora, and the invariants each target asserts.
- WALLET_ENCRYPTION_FIX.md — the encryption format itself
- SECURITY_LOGGING_GUIDE.md — what may be logged
- docs/COMMAND_REFERENCE.md — the
walletcommand