Wallet Security
Wallet Security
This page details the cryptographic storage, session handling, and encoding compatibility mechanisms used by the wallet section of the node's embedded web UI.
Seed encryption
Seeds are encrypted at rest using AES-GCM with PBKDF2-derived keys, entirely through the browser's Web Crypto API.
Key derivation
| Parameter | Value |
|---|---|
| Algorithm | PBKDF2 |
| Hash | SHA-256 |
| Iterations | 600,000 |
| Key length | 256 bits |
| Salt | Random 16 bytes, per wallet |
The user's passphrase is fed through PBKDF2 to produce a 256-bit AES key. Each wallet has its own random salt, so the same passphrase produces different keys for different wallets. The iteration count is stored on each wallet entry and used on decrypt, so entries written by an older version still open.
Encryption
| Parameter | Value |
|---|---|
| Algorithm | AES-GCM |
| IV | Random 12 bytes, per encryption |
| Input | 32-byte seed |
| Output | Base64-encoded ciphertext |
A fresh random IV is generated every time a seed is encrypted. Decryption failure -- including a wrong passphrase, which trips the AES-GCM authentication tag -- surfaces as "incorrect passphrase"; a successfully decrypted seed is additionally checked to be exactly 32 bytes.
[!IMPORTANT] Important The IV must be unique per encryption. Reusing an IV with the same key completely breaks AES-GCM's security guarantees.
Storage schema
The vault is stored in localStorage under the key xe.wallets as a JSON object:
{
"wallets": [
{
"name": "main",
"address": "<64 hex characters>",
"salt": "<base64 salt>",
"iv": "<base64 IV>",
"encrypted_seed": "<base64 ciphertext>",
"iterations": 600000,
"created_at": 1709123456789
}
],
"active": 0
}| Field | Description |
|---|---|
wallets[] | Array of wallet entries |
active | Index of the currently selected wallet |
wallets[].name | User-assigned display name |
wallets[].address | The account address, cached so the vault can be listed while locked |
wallets[].salt | Base64-encoded PBKDF2 salt |
wallets[].iv | Base64-encoded AES-GCM initialization vector |
wallets[].encrypted_seed | Base64-encoded AES-GCM ciphertext of the seed |
wallets[].iterations | PBKDF2 iteration count used for this entry |
wallets[].created_at | Creation timestamp (milliseconds) |
[!NOTE] Note No plaintext seed or private key is ever written to
localStorage. Only the address, the encrypted ciphertext, the salt and the IV are persisted.
Legacy migration
An older wallet version stored a plaintext seed under xe.wallet.seed. When one is
found, the UI offers a migration form: choosing a passphrase encrypts the seed into the
vault and deletes the plaintext key.
Session handling
Unlock cache
On unlock, the passphrase is cached in sessionStorage under xe.session.unlock so
navigating between wallet pages in the same tab does not re-prompt. The cache lives until
whichever comes first:
- an explicit lock,
- the tab closing (
sessionStorageis per-tab), - a hard TTL of 30 minutes since unlock.
A cached passphrase that no longer decrypts the active vault -- a different vault, or a changed passphrase -- is dropped on first use.
[!WARNING] What the cache costs Holding the passphrase in
sessionStorageis roughly equivalent in risk to holding the decrypted seed in JS memory: both are reachable by any code running in the same origin. The vault on disk stays encrypted either way. Use the lock button on shared machines.
Reveal seed
Displaying a seed always re-authenticates: the passphrase must be re-entered on the reveal form, and the revealed value can be hidden again without a reload.
Canonical encoding compatibility
The wallet must produce byte-identical canonical block encodings to the Go node for hashing and signing to work correctly. The JavaScript implementation mirrors MarshalBlockCanonical for the two block types the wallet builds:
| Constant | Value |
|---|---|
| Version byte | 0x02 |
| Send type byte | 0x01 |
| Receive type byte | 0x02 |
| Max memo bytes | 64 |
Field ordering, zero-padding, and endianness are identical, and the block hash is taken over the network ID followed by the canonical bytes -- so a block signed for one network cannot be replayed on another. See Binary Encoding for the full specification.
See also
- Wallet Overview -- feature summary and tech stack
- Wallet Features -- detailed feature reference
- Cryptography -- ed25519 and hashing primitives
- Binary Encoding -- canonical block encoding