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

ParameterValue
AlgorithmPBKDF2
HashSHA-256
Iterations600,000
Key length256 bits
SaltRandom 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

ParameterValue
AlgorithmAES-GCM
IVRandom 12 bytes, per encryption
Input32-byte seed
OutputBase64-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
}
FieldDescription
wallets[]Array of wallet entries
activeIndex of the currently selected wallet
wallets[].nameUser-assigned display name
wallets[].addressThe account address, cached so the vault can be listed while locked
wallets[].saltBase64-encoded PBKDF2 salt
wallets[].ivBase64-encoded AES-GCM initialization vector
wallets[].encrypted_seedBase64-encoded AES-GCM ciphertext of the seed
wallets[].iterationsPBKDF2 iteration count used for this entry
wallets[].created_atCreation 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 (sessionStorage is 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 sessionStorage is 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:

ConstantValue
Version byte0x02
Send type byte0x01
Receive type byte0x02
Max memo bytes64

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