Orion Wallet Encryption — Secure Key Storage in Rust
A misuse-resistant reference implementation for password-based wallet encryption in Rust: Argon2id key derivation, XChaCha20-Poly1305 authenticated encryption and zeroized memory — built because plaintext private keys on disk are negligence.
- Role
- Applied Cryptography Engineering
- Status
- Open Source
Architecture: Orion — password-authenticated wallet encryption
Private keys are the ultimate access credential on a blockchain: whoever holds the key controls the assets. Storing them insecurely isn't a bug risk — it's negligence.
The problem
The default Solana tooling writes keypairs as unencrypted JSON:
solana-keygen new --outfile ~/.config/solana/id.json
cat ~/.config/solana/id.json
# [123, 45, 67, ..., 201, 89] <-- PRIVATE KEY IN PLAINTEXT
Any process with read access to that file owns the wallet. The fix is encryption before the key touches disk — keyed by a human-memorable password, derived into real key material.
Why AEAD, not just encryption
Encryption alone doesn't detect tampering. Authenticated Encryption with Associated Data (AEAD) provides confidentiality, authenticity and integrity in a single pass: decrypting manipulated ciphertext fails authentication instead of returning garbage. That eliminates whole attack classes (e.g. padding oracles).
Design choices
XChaCha20-Poly1305 (IETF-standardized family) uses a 192-bit nonce, making accidental reuse negligible even with fully random nonces — the right default for stateless tools. Argon2id derives the encryption key from the password, memory-hard by design so brute-force attempts stay expensive. Orion supplies audited, misuse-resistant primitives instead of hand-rolled crypto.
The format
[ salt (32B) ] [ nonce (24B) ] [ ciphertext + tag (16B) ]
Encryption: random salt → Argon2id(password, salt) → 256-bit key → AEAD-seal under a random 192-bit nonce. Decryption reverses it; the Poly1305 tag verifies before any plaintext is returned.
use orion::{aead::{seal, open}, kdf::{derive_key, Password, Salt}};
use zeroize::Zeroize;
#[derive(Zeroize)]
#[zeroize(drop)]
struct SensitiveData(Vec<u8>);
// Derive a 256-bit key from password + salt (memory-hard).
let pw = Password::from_slice(password.as_bytes())?;
let derived = derive_key(&pw, &salt, 15, 1024, CHACHA_KEYSIZE as u32)?;
Round-trip tests assert identical keypairs; negative tests assert failure on wrong passwords, flipped bytes and truncated blobs — all of which must fail authentication, never return data.
Operational properties
Because the blob is useless without the password, backups can live in ordinary cloud storage. Key material is zeroized after use. Nothing secret ever reaches logs.
Result
A reference implementation now used within Symbiose to protect user assets across chains — and a documented decision record for why each primitive was chosen.
architecture
Four components: CLI (clap) orchestrates create/encrypt/decrypt; an Argon2id KDF derives a 256-bit key from password plus random 32-byte salt; XChaCha20-Poly1305 (orion::aead) encrypts under a 192-bit nonce; the on-disk format is [salt | nonce | ciphertext+tag]. Zeroize clears keys after use.
select a node to inspect responsibility, I/O, failure modes & security
- ▸Password-based key derivation (KDF) resistant to brute force
- ▸Authenticated encryption (AEAD): integrity and confidentiality in one pass
- ▸Misuse resistance: large nonces, no hidden pitfalls
- ▸Integrable into a multi-chain app (Solana first)
- ▸Zeroize: key material wiped from memory after use
- ▸Rust ecosystem only; no mandatory C bindings
- ▸KDF cost must not break UX (target: < 1s derivation)
- ▸Portable format — cloud-backup safe without the password
- ▸KDF cost (deliberately slow) vs. brute-force protection: slower unlock accepted
- ▸Blob portability vs. hardware-backed storage
- ▸Pure-Rust ecosystem (orion) vs. C-binding ecosystems like libsodium
- ▸Argon2id with memory-hard parameters and a random salt per file
- ▸AEAD: any bit manipulation breaks authentication before decryption returns data
- ▸zeroize on key slices; no key material in logs or structures
- ▸The blob is worthless without the password → safe for cloud backups
- ▸Explicit length fields guard against truncation when parsing the blob
- ▸Versioned blob format leaves room for future algorithm upgrades
- ▸Round-trip: encrypt→decrypt reproduces an identical keypair
- ▸Negative tests: wrong password, byte tampering, truncation
- ▸CLI binary for standalone use; library module inside Symbiose
- ▸No content in logs; only events such as 'encrypted N bytes'
- ▸Reference implementation for secure wallet storage in Rust
- ▸Misuse resistance demonstrated via XChaCha20-Poly1305 + Argon2id
- ▸Public repository: github.com/amariwan/symbiose-wallet-encryption
- ▸Case study documents KDF, AEAD, blob format, zeroization and negative tamper tests
- ▸Authenticated encryption is mandatory — encryption alone is not security
- ▸Document cryptographic decisions first; rule out 'roll your own' explicitly
problem
Solana private keys are routinely stored as plaintext JSON on disk — the official solana-keygen does it by default. Any process with filesystem read access can exfiltrate the wallet. The Rust ecosystem lacked a misuse-resistant, password-based encryption reference that multi-chain apps can integrate.
context
For Symbiose, a multi-chain application, wallet keys needed password-protected storage: the user remembers one password, the system derives cryptographically robust encryption from it. The outcome is a portable, backup-safe blob format and a reference implementation built on Orion.
requirements
constraints
key decisions
Why: A 192-bit nonce makes accidental reuse practically impossible in a stateless, password-based CLI where durable counters are easy to get wrong.
Less ubiquitous than AES-GCM, but safer for the misuse class this tool is designed to avoid.
Why: Argon2id is memory-hard and the modern default for password-derived keys.
Unlocks are deliberately slower, but brute-force attempts become more expensive.
Why: A high-level Rust crypto API removes low-level primitive misuse from the application code.
Less freedom to customize internals, but a much smaller cryptographic footgun surface.