Libsodium Encryption
2026-04-24•10 min read•Saul Vo
encryptioncryptographye2ee
Libsodium Encryption
Mục lục
- Libsodium Overview
- Installation
- Secret-Key Encryption
- Public-Key Encryption
- Sealed Boxes (Anonymous Encryption)
- Key Derivation
- Password Hashing
- Authentication
- Complete Examples
1. Libsodium Overview
What is Libsodium?
┌─────────────────────────────────────────────────────────────────────┐
│ Libsodium Overview │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Libsodium is a modern, easy-to-use cryptography library: │
│ │
│ ✓ Cross-platform (C, bindings for 20+ languages) │
│ ✓ Memory-hard hashing (resistant to GPU/ASIC) │
│ ✓ Easy API design (high-level functions) │
│ ✓ Side-channel resistant implementations │
│ ✓ Well-audited and widely used │
│ ✓ Used by Signal, Wire, and many others │
│ │
│ Node.js/Bun: npm install libsodium-wrappers │
│ Browser: Bundled via WebAssembly │
│ │
└─────────────────────────────────────────────────────────────────────┘
API Categories
| Category | Functions | Use Case |
|---|---|---|
| Secret-key encryption | crypto_secretbox_* | Symmetric encryption |
| Public-key encryption | crypto_box_* | Key exchange + encryption |
| Sealed boxes | crypto_sealedbox_* | Anonymous encryption |
| Authentication | crypto_auth, crypto_onetimeauth | MACs, streaming |
| Password hashing | crypto_pwhash_* | Argon2, password storage |
| Key derivation | crypto_kdf_* | HKDF-like derivation |
| Scalars | crypto_scalarmult_* | ECDH-like operations |
2. Installation
Node.js
npm install libsodium-wrappers// TypeScript usage
import * as sodium from 'libsodium-wrappers';
async function initSodium() {
await sodium.ready;
return sodium;
}
// Use throughout your code
const msg = 'Hello, World';
const key = sodium.crypto_secretbox_keygen();
const encrypted = sodium.crypto_secretbox_easy(msg, key);
const decrypted = sodium.crypto_secretbox_open_easy(encrypted, key);Browser (WASM)
// libsodium-wrappers auto-detects and uses WASM in browser
import * as sodium from 'libsodium-wrappers';
await sodium.ready;
// All same APIs work in browser
const encrypted = sodium.crypto_secretbox_easy(msg, key);Bun Native
// Bun has native libsodium support
import { crypto_secretbox_keygen, crypto_secretbox_easy } from 'bun';
const key = crypto_secretbox_keygen();
const encrypted = crypto_secretbox_easy(msg, key);3. Secret-Key Encryption
Quick Example
import * as sodium from 'libsodium-wrappers';
async function secretBoxExample() {
await sodium.ready;
// Generate random 256-bit key
const key = sodium.crypto_secretbox_keygen();
// Generate random 24-byte nonce (unique per message!)
const nonce = sodium.randombytes_buf(24);
const message = 'Secret message';
// Encrypt
const ciphertext = sodium.crypto_secretbox_easy(message, nonce, key);
// Decrypt
const plaintext = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
console.log('Message:', message);
console.log('Ciphertext:', sodium.to_hex(ciphertext));
console.log('Decrypted:', plaintext);
}
secretBoxExample();API Reference
// Key generation (256-bit)
const key = sodium.crypto_secretbox_keygen();
// Alternative: derive key from password
const key = sodium.crypto_kdf_derive_from_key(
32, // Output length
1, // Subkey ID
'application_id', // Context/subkey ID
masterKey
);
// Encrypt message
const ciphertext = sodium.crypto_secretbox_easy(
message: string | Uint8Array,
nonce: Uint8Array, // 24 bytes
key: Uint8Array // 32 bytes
): Uint8Array;
// Decrypt message
const plaintext = sodium.crypto_secretbox_open_easy(
ciphertext: Uint8Array,
nonce: Uint8Array,
key: Uint8Array
): Uint8Array;
// Detached mode (for large messages)
const { ciphertext, mac } = sodium.crypto_secretbox_detach(
message,
nonce,
key
);
// Send: nonce + mac + ciphertext (separately)Constants
console.log({
KEYBYTES: sodium.crypto_secretbox_KEYBYTES, // 32
NONCEBYTES: sodium.crypto_secretbox_NONCEBYTES, // 24
MACBYTES: sodium.crypto_secretbox_MACBYTES, // 16
PRIMITIVE: sodium.crypto_secretbox_PRIMITIVE // 'x25519-xsalsa20-poly1305'
});4. Public-Key Encryption
Key Exchange + Encryption
import * as sodium from 'libsodium-wrappers';
async function boxExample() {
await sodium.ready;
// Generate key pairs
const aliceKeyPair = sodium.crypto_box_keypair();
const bobKeyPair = sodium.crypto_box_keypair();
// Alice's side: Encrypt for Bob
const nonce = sodium.randombytes_buf(24);
const message = 'Secret message for Bob';
// Method 1: Using sender's private + recipient's public key
const ciphertext = sodium.crypto_box_easy(
message,
nonce,
bobKeyPair.publicKey,
aliceKeyPair.privateKey
);
// Method 2: Pre-compute shared key (more efficient for multiple messages)
const sharedKey = sodium.crypto_box_beforenm(
bobKeyPair.publicKey,
aliceKeyPair.privateKey
);
const ciphertext2 = sodium.crypto_box_easy_afternm(message, nonce, sharedKey);
// Bob's side: Decrypt with his private + Alice's public key
const plaintext = sodium.crypto_box_open_easy(
ciphertext,
nonce,
aliceKeyPair.publicKey,
bobKeyPair.privateKey
);
console.log('Original:', message);
console.log('Decrypted:', plaintext);
}
boxExample();API Reference
// Generate key pair (25519)
const keyPair = sodium.crypto_box_keypair();
// keyPair.publicKey (32 bytes)
// keyPair.privateKey (32 bytes)
// Seeded key pair (deterministic)
const keyPair = sodium.crypto_box_seed_keypair(seed);
// Pre-compute shared key (for many messages to same recipient)
const sharedKey = sodium.crypto_box_beforenm(
recipientPublicKey: Uint8Array,
senderPrivateKey: Uint8Array
): Uint8Array; // 32 bytes
// Encrypt
const ciphertext = sodium.crypto_box_easy(
message: string | Uint8Array,
nonce: Uint8Array,
recipientPublicKey: Uint8Array,
senderPrivateKey: Uint8Array
): Uint8Array;
// Encrypt with pre-computed shared key
const ciphertext = sodium.crypto_box_easy_afternm(
message,
nonce,
sharedKey
): Uint8Array;
// Decrypt
const plaintext = sodium.crypto_box_open_easy(
ciphertext,
nonce,
senderPublicKey: Uint8Array,
recipientPrivateKey: Uint8Array
): Uint8Array;
// Decrypt with pre-computed shared key
const plaintext = sodium.crypto_box_open_easy_afternm(
ciphertext,
nonce,
sharedKey
): Uint8Array;
// Detached mode
const { ciphertext, mac } = sodium.crypto_box_detach(...);
const plaintext = sodium.crypto_box_open_detach(..., mac);5. Sealed Boxes (Anonymous Encryption)
One-Way Encryption (Sender Anonymity)
import * as sodium from 'libsodium-wrappers';
async function sealedBoxExample() {
await sodium.ready;
// Bob generates his key pair (public key only needed for sealing)
const bobKeyPair = sodium.crypto_box_keypair();
// Alice seals message with just Bob's public key (no sender key needed)
const message = 'Anonymous message to Bob';
const sealed = sodium.crypto_sealedbox_seal(message, bobKeyPair.publicKey);
// Bob opens with his private key (sender is anonymous)
const plaintext = sodium.crypto_sealedbox_open(sealed, bobKeyPair);
console.log('Sealed (hex):', sodium.to_hex(sealed));
console.log('Decrypted:', plaintext);
}
sealedBoxExample();When to Use Sealed Boxes
| Use Case | Recommended |
|---|---|
| User-to-user messaging | crypto_box_* |
| Anonymous tips/messages | crypto_sealedbox_* |
| Encrypted storage | crypto_secretbox_* |
| Key exchange | crypto_box_beforenm |
API Reference
// Sealed box (anonymous sender)
// Only recipient's public key is needed
const sealed = sodium.crypto_sealedbox_seal(
message: string | Uint8Array,
recipientPublicKey: Uint8Array
): Uint8Array;
// Open sealed box (requires recipient's key pair)
const plaintext = sodium.crypto_sealedbox_open(
sealed: Uint8Array,
recipientKeyPair: KeyPair
): Uint8Array;
// Note: sealed boxes are slightly larger than normal boxes
// because they include an ephemeral sender key6. Key Derivation
Master Key + Subkeys
import * as sodium from 'libsodium-wrappers';
async function kdfExample() {
await sodium.ready;
// Generate 32-byte master key
const masterKey = sodium.randombytes_buf(32);
// Derive multiple subkeys for different purposes
const encryptionKey = sodium.crypto_kdf_derive_from_key(
32, // Output length
1, // Subkey ID (arbitrary number)
'encryption', // Context (8 bytes max)
masterKey
);
const macKey = sodium.crypto_kdf_derive_from_key(
32,
2,
'authentication',
masterKey
);
const fileKey = sodium.crypto_kdf_derive_from_key(
32,
3,
'file-encryption',
masterKey
);
// Contexts should be unique per purpose
// The same subkey ID with different context produces different keys
}
kdfExample();API Reference
// Derive subkey from master key
const subkey = sodium.crypto_kdf_derive_from_key(
length: number, // 1-64 bytes (but typically 32)
subkeyId: number, // Arbitrary number to distinguish subkeys
context: string, // 8 bytes max, MUST be unique per context
masterKey: Uint8Array // 32 bytes
): Uint8Array;
// Constants
console.log({
KEYBYTES: sodium.crypto_kdf_KEYBYTES, // 32
CONTEXTBYTES: sodium.crypto_kdf_CONTEXTBYTES, // 8
BYTES_MIN: sodium.crypto_kdf_BYTES_MIN, // 16
BYTES_MAX: sodium.crypto_kdf_BYTES_MAX // 64
});7. Password Hashing
Argon2id Implementation
import * as sodium from 'libsodium-wrappers';
async function passwordHashExample() {
await sodium.ready;
const password = 'secure_password_123';
// Hash password
const hash = sodium.crypto_pwhash_str(
password,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE
);
// Verify password
const valid = sodium.crypto_pwhash_str_verify(hash, password);
console.log('Hash:', hash);
console.log('Valid:', valid); // true
// Wrong password
const invalid = sodium.crypto_pwhash_str_verify(hash, 'wrong_password');
console.log('Invalid:', invalid); // false
}
passwordHashExample();API Reference
// Hash password with Argon2id (recommended)
const hash = sodium.crypto_pwhash_str(
password: string | Uint8Array,
opsLimit: number, // CPU cost (use preset constants)
memLimit: number // Memory cost (use preset constants)
): string; // Returns formatted hash string
// Verify password against hash
const valid = sodium.crypto_pwhash_str_verify(
hash: string,
password: string | Uint8Array
): boolean;
// Low-level API for more control
const hash = sodium.crypto_pwhash(
outputLength: number, // 16+ bytes recommended
password: string | Uint8Array,
salt: Uint8Array, // 16 bytes
opsLimit: number,
memLimit: number,
algorithm: number // ARGON2ID13 (default) or ARGON2ID14
): Uint8Array;
// Preset constants
console.log({
OPS_LIMIT_INTERACTIVE: sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
OPS_LIMIT_MODERATE: sodium.crypto_pwhash_OPSLIMIT_MODERATE,
OPS_LIMIT_SENSITIVE: sodium.crypto_pwhash_OPSLIMIT_SENSITIVE,
MEM_LIMIT_INTERACTIVE: sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
MEM_LIMIT_MODERATE: sodium.crypto_pwhash_MEMLIMIT_MODERATE,
MEM_LIMIT_SENSITIVE: sodium.crypto_pwhash_MEMLIMIT_SENSITIVE
});Hash Format
$argon2id$v=19$m=65536,t=2,p=1$
$gfdhjkghfdjkghfdjkghfdjkghfdjkg$
$hfdgjkdfhgjkdhfgjkdhfkgjhdkgjhd=
Format: $argon2id$v=19$m=32768,t=3,p=4$ + salt + hash
8. Authentication
HMAC-SHA512
import * as sodium from 'libsodium-wrappers';
async function authExample() {
await sodium.ready;
// Generate authentication key
const key = sodium.crypto_auth_keygen();
const message = 'Authenticated message';
// Create authentication tag
const tag = sodium.crypto_auth_fast(message, key);
// Verify (same key)
const valid = sodium.crypto_auth_verify(tag, message, key);
console.log('Valid:', valid); // true
// Tampered message
const tampered = sodium.crypto_auth_verify(tag, 'Tampered', key);
console.log('Tampered:', tampered); // false
}
authExample();One-Time Auth (Poly1305)
import * as sodium from 'libsodium-wrappers';
async function oneTimeAuthExample() {
await sodium.ready;
// Poly1305 key (generated separately per message)
const key = sodium.crypto_onetimeauth_keygen();
const message = 'Message for one-time auth';
// Authenticate
const tag = sodium.crypto_onetimeauth_fast(message, key);
// Verify
const valid = sodium.crypto_onetimeauth_verify(tag, message, key);
console.log('Valid:', valid); // true
}
oneTimeAuthExample();9. Complete Examples
E2EE Messaging with Libsodium
import * as sodium from 'libsodium-wrappers';
interface EncryptedMessage {
version: number;
from: string;
to: string;
nonce: string;
ciphertext: string;
}
interface KeyPair {
publicKey: Uint8Array;
privateKey: Uint8Array;
}
// Initialize
let sodiumInstance: typeof sodium;
async function init() {
await sodium.ready;
sodiumInstance = sodium;
}
// Generate user identity
function generateIdentity(): KeyPair {
return sodiumInstance.crypto_box_keypair();
}
// Export public key (for sharing)
function exportPublicKey(keyPair: KeyPair): string {
return sodiumInstance.to_hex(keyPair.publicKey);
}
// Import public key
function importPublicKey(hexKey: string): Uint8Array {
return sodiumInstance.from_hex(hexKey);
}
// Encrypt message
function encryptMessage(
message: string,
sender: KeyPair,
recipientPublicKey: Uint8Array
): EncryptedMessage {
const nonce = sodiumInstance.randombytes_buf(24);
const ciphertext = sodiumInstance.crypto_box_easy(
message,
nonce,
recipientPublicKey,
sender.privateKey
);
return {
version: 1,
from: exportPublicKey(sender),
to: sodiumInstance.to_hex(recipientPublicKey),
nonce: sodiumInstance.to_hex(nonce),
ciphertext: sodiumInstance.to_hex(ciphertext)
};
}
// Decrypt message
function decryptMessage(
encrypted: EncryptedMessage,
recipient: KeyPair
): string {
const nonce = sodiumInstance.from_hex(encrypted.nonce);
const senderPublicKey = sodiumInstance.from_hex(encrypted.from);
const ciphertext = sodiumInstance.from_hex(encrypted.ciphertext);
const plaintext = sodiumInstance.crypto_box_open_easy(
ciphertext,
nonce,
senderPublicKey,
recipient.privateKey
);
return sodiumInstance.to_string(plaintext);
}
// Session key derivation (for many messages)
function deriveSessionKey(
sender: KeyPair,
recipientPublicKey: Uint8Array
): Uint8Array {
return sodiumInstance.crypto_box_beforenm(recipientPublicKey, sender.privateKey);
}
// Full example
async function main() {
await init();
// Alice and Bob generate identities
const alice = generateIdentity();
const bob = generateIdentity();
// Alice sends encrypted message to Bob
const message = 'Hey Bob, secret message!';
const encrypted = encryptMessage(message, alice, bob.publicKey);
console.log('Encrypted:', JSON.stringify(encrypted, null, 2));
// Bob decrypts
const decrypted = decryptMessage(encrypted, bob);
console.log('Decrypted:', decrypted);
// Verify
console.log('Match:', decrypted === message); // true
}Encrypted Local Storage
import * as sodium from 'libsodium-wrappers';
interface EncryptedStorage {
salt: string;
nonce: string;
ciphertext: string;
}
async function createEncryptedStorage(
data: Record<string, any>,
password: string
): Promise<EncryptedStorage> {
await sodium.ready;
// Derive key from password
const salt = sodium.randombytes_buf(16);
const key = sodium.crypto_pwhash(
32,
password,
salt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_DEFAULT
);
// Encrypt data
const nonce = sodium.randombytes_buf(24);
const plaintext = JSON.stringify(data);
const ciphertext = sodium.crypto_secretbox_easy(plaintext, nonce, key);
return {
salt: sodium.to_hex(salt),
nonce: sodium.to_hex(nonce),
ciphertext: sodium.to_hex(ciphertext)
};
}
async function decryptStorage(
storage: EncryptedStorage,
password: string
): Promise<Record<string, any>> {
await sodium.ready;
const salt = sodium.from_hex(storage.salt);
const nonce = sodium.from_hex(storage.nonce);
const ciphertext = sodium.from_hex(storage.ciphertext);
const key = sodium.crypto_pwhash(
32,
password,
salt,
sodium.crypto_pwhash_OPSLIMIT_INTERACTIVE,
sodium.crypto_pwhash_MEMLIMIT_INTERACTIVE,
sodium.crypto_pwhash_ALG_DEFAULT
);
try {
const plaintext = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
return JSON.parse(sodium.to_string(plaintext));
} catch {
throw new Error('Decryption failed - wrong password?');
}
}
// Usage
const storage = await createEncryptedStorage(
{ apiKeys: { openai: 'sk-xxx' }, settings: { theme: 'dark' } },
'user_password'
);
// Store securely (encryptedStorage.salt, nonce, ciphertext in IndexedDB/localStorage)
console.log('Storage:', storage);Security Notes
Key Security
- Never expose private keys to network or logs
- Store keys encrypted with strong password-derived keys
- Use unique nonces for every encryption operation
- Destroy keys when no longer needed
Password Hashing
- Use Argon2id (via
crypto_pwhash_str) - Use INTERACTIVE or SENSITIVE limits for production
- Never use MD5/SHA1 for passwords
Random Numbers
// Always use sodium's random functions
const random = sodium.randombytes_buf(32); // 32 random bytes
const random16 = sodium.randombytes_random(); // 0-65535
const randomUniform = sodium.randombytes_uniform(100); // 0-99