Skip to content

Wallet Security

This page details the cryptographic storage, session management, and encoding compatibility mechanisms used by the XE web wallet.

Seed encryption

Private keys (seeds) are encrypted at rest using AES-GCM with PBKDF2-derived keys.

Key derivation

Parameter Value
Algorithm PBKDF2
Hash SHA-256
Iterations 100,000
Key length 256 bits
Salt Random, 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.

Encryption

Parameter Value
Algorithm AES-GCM
IV Random, per encryption
Input 32-byte hex seed
Output Base64-encoded ciphertext

A fresh random IV is generated every time a seed is encrypted (on wallet creation, import, or passphrase change). This ensures that re-encrypting the same seed produces different ciphertext.

Important

The IV must be unique per encryption. Reusing an IV with the same key completely breaks AES-GCM's security guarantees.

Storage schema

Wallet data is stored in localStorage as a JSON object:

{
  "version": 1,
  "activeId": "w_1709123456789",
  "wallets": [
    {
      "id": "w_1709123456789",
      "name": "default",
      "encrypted": "<base64 ciphertext>",
      "salt": "<base64 salt>",
      "iv": "<base64 IV>"
    }
  ]
}
Field Description
version Schema version (currently 1)
activeId ID of the currently selected wallet
wallets[] Array of wallet entries
wallets[].id Unique ID (w_ + creation timestamp)
wallets[].name User-assigned display name
wallets[].encrypted Base64-encoded AES-GCM ciphertext of the seed
wallets[].salt Base64-encoded PBKDF2 salt
wallets[].iv Base64-encoded AES-GCM initialization vector

Note

No plaintext seed or private key is ever written to localStorage. Only the encrypted ciphertext, salt, and IV are persisted.

Session management

Idle timeout

The wallet automatically locks after 5 minutes (300,000 ms) of inactivity.

Monitored events:

  • mousemove
  • mousedown
  • keydown
  • touchstart
  • scroll

Any of these events resets the idle timer. When the timer expires:

  1. Lock the wallet (zero out all in-memory seeds)
  2. Stop the auto-receive poller
  3. Clear application state
  4. Redirect to the unlock screen

Lock behavior

When locked, all decrypted seeds are overwritten with zeros and dereferenced. The wallet transitions to a state where only the unlock form is accessible. Re-entering the passphrase decrypts all wallet seeds and resumes normal operation.

Key operations

Operation Description
Unlock Decrypt all wallet seeds using the passphrase. Derives AES keys via PBKDF2, decrypts each seed with AES-GCM.
Lock Zero out all in-memory seeds, stop poller, clear state.
Add wallet Generate or import a seed, encrypt with current passphrase, store in localStorage.
Remove wallet Delete a wallet entry from localStorage. Requires at least one wallet to remain.
Rename wallet Update the name field in localStorage.
Reveal seed Re-authenticate with passphrase, then display the decrypted seed.
Change passphrase Re-encrypt all wallet seeds with a new passphrase. Generates new salts and IVs.

Passphrase change

Changing the passphrase re-encrypts every wallet seed with fresh salt and IV. If the process is interrupted (e.g., browser crash), some wallets may be encrypted with the old passphrase and some with the new. The wallet handles this gracefully by attempting both on unlock.

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 the Go MarshalBlockCanonical function exactly:

Constant Value Shared between Go and JS
VERSION_BYTE 0x02 Yes
Send type 0x01 Yes
Receive type 0x02 Yes
Claim type 0x03 Yes
Lease type 0x04 Yes
LeaseAccept type 0x05 Yes
LeaseSettle type 0x06 Yes

Field ordering, padding, and endianness are identical. See Binary Encoding for the full specification.

See also