Encryption¶
Two layers of encryption:
- KeyManager encryption — encrypts private keys in memory / in the DB with a password. Wallet can be locked/unlocked without re-syncing.
- WalletDB encryption — encrypts the entire DB blob for portable backups.
Key derivation¶
Both layers use:
- Argon2id — 64 MiB memory, 3 iterations, 4-lane parallelism. Resists GPU / ASIC password cracking.
- AES-256-GCM — authenticated encryption with a 12-byte random IV per encryption.
- Salt — 16 random bytes stored alongside the verification hash.
- Verification hash — derived from the password, used to reject wrong passwords without a decryption attempt.
KeyManager encryption¶
setPassword¶
lock¶
unlock¶
changePassword¶
const km = client.getKeyManager();
// Protect a fresh wallet
await km.setPassword('my-secure-password');
km.isEncrypted(); // true
km.isUnlocked(); // true — just-set password leaves wallet unlocked
// Lock — clears cached key from memory
km.lock();
km.isUnlocked(); // false
// Unlock
const ok = await km.unlock('my-secure-password');
if (!ok) { /* wrong password */ }
// Change
await km.changePassword('old-password', 'new-password');
A locked wallet can still read public-key state (sub-addresses, audit key) but cannot sign transactions. Attempting sendTransaction on a locked wallet throws.
Encryption parameters for persistence¶
const { salt, verificationHash } = km.getEncryptionParams();
// persist alongside the wallet DB
// On restore:
km.setEncryptionParams(savedSalt, savedVerificationHash);
await km.unlock(password);
The WalletDB table encryption_metadata stores these automatically — you only need the above calls when bypassing the WalletDB wrapper.
WalletDB encryption¶
For off-host backup or transfer:
Export¶
const blob = await client.getWalletDB().exportEncrypted('backup-pw');
// Uint8Array — safe to save to disk, upload, email
import fs from 'node:fs/promises';
await fs.writeFile('wallet-backup.enc', blob);
Or download in the browser:
const url = URL.createObjectURL(new Blob([blob]));
const a = document.createElement('a');
a.href = url; a.download = 'wallet-backup.enc';
a.click();
URL.revokeObjectURL(url);
Load¶
import { WalletDB, isEncryptedDatabase } from 'navio-sdk';
const raw = await fs.readFile('wallet-backup.enc');
const buf = new Uint8Array(raw);
if (!isEncryptedDatabase(buf)) throw new Error('not encrypted');
const db = await WalletDB.loadEncrypted(buf, 'backup-pw');
const keyManager = await db.loadWallet();
Stand-alone encryption primitives¶
The SDK exports the underlying primitives for application-layer use:
import {
encrypt, decrypt,
deriveKey,
serializeEncryptedData, deserializeEncryptedData,
createPasswordVerification, verifyPassword,
randomBytes,
} from 'navio-sdk';
// Encrypt arbitrary bytes
const plaintext = new TextEncoder().encode('secret');
const encrypted = await encrypt(plaintext, 'password');
// Decrypt
const decrypted = await decrypt(encrypted, 'password');
console.log(new TextDecoder().decode(decrypted)); // 'secret'
// Serialise to base64 JSON for storage
const json = JSON.stringify(serializeEncryptedData(encrypted));
// Password verification helper (no data to decrypt)
const salt = randomBytes(16);
const hash = await createPasswordVerification('password', salt);
const isValid = await verifyPassword('password', salt, hash);
encrypt / decrypt accept a password or a pre-derived key (from deriveKey) — reuse the derived key across multiple encrypt/decrypt calls to avoid recomputing Argon2id.
Security notes¶
- Password strength matters. Argon2id is slow, but a weak password (
password123) is still brute-forceable. Use a password manager; prefer ≥ 20 chars or a high-entropy passphrase. - Salt is not secret. Store it alongside the ciphertext in the clear.
- Verification hash is not the key. Knowing the verification hash does not allow decrypting the wallet — it only lets attackers accelerate offline password cracking against the same wallet. Store encrypted wallet backups as securely as the mnemonic.
- IV reuse is catastrophic for GCM. The SDK uses a fresh 12-byte random IV per encryption — never re-use one manually.
- Memory hygiene.
km.lock()clears the cached derived key. In JS you cannot fully zero memory, so best-effort is all we can do; for the strictest environments, destroy the client and re-initialise.