libsodium-wrappers
libsodium-wrappers is the recommended standard JavaScript wrapper around libsodium, compiled to WebAssembly with a pure-JavaScript fallback for browsers, Node.js, and Bun. It exposes high-level authenticated encryption, public-key boxes, signatures, hashes, secure random bytes, streaming encryption, binary encoders, and constant-time helpers through Uint8Array-friendly functions. The module now ships CommonJS, ESM, and TypeScript declarations, but cryptographic functions are attached only after its asynchronous ready promise resolves.
libsodium-wrappers is a strong choice when a protocol genuinely needs libsodium across JavaScript runtimes. Do not install it casually for one ordinary hash or browser cipher, and do not mistake a large primitive catalog for key management or secure protocol design.
Use it if
- You need the same well-reviewed libsodium primitives in browsers and server-side JavaScript
- You need XChaCha20-Poly1305, secretstream, sealed boxes, or Ed25519 features not consistently offered by Web Crypto
- Your protocol already specifies libsodium-compatible byte formats and algorithms
- You can budget for asynchronous initialization, a large crypto payload, and explicit key lifecycle design
- You only need common browser cryptography already covered by Web Crypto and bundle weight matters: version 0.8.4 measures 151.6 KB gzipped through the package-size endpoint
- You expect password hashing from this package: the README says crypto_pwhash is available only in libsodium-wrappers-sumo, which is larger, reserves more memory, and includes undocumented, deprecated, low-level symbols
- You want a library to manage keys, rotation, envelopes, storage, recovery, or protocol design; this wrapper supplies primitives and byte helpers, not an application security architecture
- You cannot accommodate async startup or WebAssembly constraints: every cryptographic function and constant is dynamically attached after sodium.ready resolves, and browser delivery can be affected by CSP or corrupted inline WASM text
- You target React Native directly: the README points to the separate react-native-libsodium binding rather than claiming this browser and server build is the native-mobile path
Setup reality
Install libsodium-wrappers, import its default export, and await sodium.ready once before touching any crypto function or constant. Helpers such as from_hex can be named imports in ESM, but cryptographic functions cannot because they are added dynamically at runtime. The package includes ESM, CommonJS, and corresponding TypeScript declarations, plus a libsodium dependency containing compiled code. No native compiler is needed for the npm artifact, but WebAssembly initialization is asynchronous and a restrictive browser Content Security Policy may require deployment work. The README warns that inlining the browser build without UTF-8 metadata or a charset=utf-8 response can corrupt its embedded WASM bytes; serving the separate artifact is safer. The standard package deliberately excludes crypto_pwhash, so password-based encryption requires the sumo package or a different KDF implementation. Inputs and outputs are mainly Uint8Array values. Design an encoding and framing format that stores algorithm version, nonce, ciphertext, and any associated-data requirements unambiguously. Nonces are not secret but must obey each primitive's uniqueness rules. Keys are your responsibility: generate them with the matching keygen function, store them outside source control, rotate them deliberately, and never substitute user passwords for keys. memzero can overwrite a Uint8Array, but JavaScript copies, strings, logs, serialization, and garbage collector behavior prevent a general guarantee that every secret copy disappears. Stateful hash and secretstream APIs allocate WASM heap state; the README says to call sodium.free for state types without an automatic finalizer. Authentication failure should terminate processing, not fall back to plaintext or a second unauthenticated decoder.
Patterns
Wait for cryptographic functions to loadinitialize-module
import sodium from 'libsodium-wrappers';
await sodium.ready;
const key = sodium.crypto_secretbox_keygen();Crypto functions and constants are attached after ready resolves. They cannot be imported as ESM named exports even though conversion helpers can.
Generate secure random bytesgenerate-random-bytes
await sodium.ready;
const token = sodium.randombytes_buf(32);
const tokenText = sodium.to_base64(
token,
sodium.base64_variants.URLSAFE_NO_PADDING
);Use randombytes_buf for security tokens. Do not replace it with Math.random or derive independent secrets by slicing one human password.
Encrypt and frame a secretbox messageencrypt-secretbox
await sodium.ready;
const key = sodium.crypto_secretbox_keygen();
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);
const ciphertext = sodium.crypto_secretbox_easy('private message', nonce, key);
const packed = {
nonce: sodium.to_base64(nonce),
ciphertext: sodium.to_base64(ciphertext),
};Store the nonce with the ciphertext. The nonce is not secret, but reuse with the same key breaks the primitive's security. Store the key separately.
Authenticate and decrypt a secretbox messagedecrypt-secretbox
await sodium.ready;
const nonce = sodium.from_base64(packed.nonce);
const ciphertext = sodium.from_base64(packed.ciphertext);
const plaintext = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
const message = sodium.to_string(plaintext);Authentication failure throws. Treat it as tampering or corruption and stop; never return the ciphertext, guessed plaintext, or an unauthenticated fallback.
Bind metadata with XChaCha20-Poly1305encrypt-with-associated-data
await sodium.ready;
const key = sodium.crypto_aead_xchacha20poly1305_ietf_keygen();
const nonce = sodium.randombytes_buf(
sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
);
const aad = sodium.from_string('tenant=acme;v=1');
const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
'payload', aad, null, nonce, key
);The same associated data must be supplied during decryption. It is authenticated but not encrypted, so do not put secrets in it.
Create a BLAKE2b generic hashhash-data
await sodium.ready;
const digest = sodium.crypto_generichash(
32,
sodium.from_string('content to hash'),
null
);
console.log(sodium.to_hex(digest));crypto_generichash is not a password hash. The standard package excludes crypto_pwhash, and fast hashes are unsafe for storing human passwords.
Create and verify a detached signaturesign-message
await sodium.ready;
const { publicKey, privateKey } = sodium.crypto_sign_keypair();
const message = sodium.from_string('release manifest');
const signature = sodium.crypto_sign_detached(message, privateKey);
const valid = sodium.crypto_sign_verify_detached(signature, message, publicKey);A signature authenticates the holder of the private key but does not encrypt the message. Distribute public keys through a trusted channel.
Encrypt anonymously to one recipientseal-for-recipient
await sodium.ready;
const recipient = sodium.crypto_box_keypair();
const sealed = sodium.crypto_box_seal('for recipient only', recipient.publicKey);
const opened = sodium.crypto_box_seal_open(
sealed, recipient.publicKey, recipient.privateKey
);Sealed boxes do not authenticate a sender. Use a signature or an authenticated sender-recipient box protocol when sender identity matters.
Round-trip URL-safe base64encode-binary-values
await sodium.ready;
const variant = sodium.base64_variants.URLSAFE_NO_PADDING;
const text = sodium.to_base64(bytes, variant);
const restored = sodium.from_base64(text, variant);The default is already URLSAFE_NO_PADDING, but specifying the variant makes a stored or network protocol explicit and prevents decoder mismatches.
Compare equal-length secrets in constant timecompare-secrets
await sodium.ready;
const expected = sodium.from_hex(expectedHex);
const received = sodium.from_hex(receivedHex);
const matches = expected.length === received.length &&
sodium.memcmp(expected, received);memcmp requires equal-size values. Validate lengths before calling it and avoid converting secrets to strings for ordinary equality checks.
Overwrite a key buffer after useerase-key-buffer
await sodium.ready;
const key = sodium.crypto_secretbox_keygen();
try {
useKey(key);
} finally {
sodium.memzero(key);
}memzero overwrites this Uint8Array only. Copies, serialized forms, logs, strings, engine internals, and other references may still contain the key.
Finalize and free a secretstream stateencrypt-message-stream
await sodium.ready;
const key = sodium.crypto_secretstream_xchacha20poly1305_keygen();
const { state, header } =
sodium.crypto_secretstream_xchacha20poly1305_init_push(key);
try {
const finalChunk = sodium.crypto_secretstream_xchacha20poly1305_push(
state, 'last chunk', null,
sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL
);
send(header, finalChunk);
} finally {
sodium.free(state);
}secretstream state is an opaque WASM heap handle. The README specifically requires free() for this API after the last operation; do not use the handle afterward.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @noble/ciphers | npm | You want audited TypeScript-oriented pure-JavaScript cipher primitives and can use its narrower algorithm set |
| tweetnacl | npm | You need a much smaller, frozen NaCl-compatible API and do not need newer libsodium features |
| sodium-native | npm | Your service is Node-only and prefers native libsodium performance despite native addon installation requirements |
| libsodium-wrappers-sumo | npm | You specifically require crypto_pwhash or another symbol omitted from the recommended standard build |