libsodium-wrappers review
libsodium-wrappers 0.8.4 exposes the standard libsodium cryptographic API to browsers, Node, and Bun through WebAssembly with a JavaScript fallback. Its default object gains authenticated encryption, signatures, hashes, sealed boxes, random-byte generation, secret streams, encoders, and memory helpers after sodium.ready resolves. Inputs and outputs normally use Uint8Array. The 0.8.4 release fixed ESM interop with libsodium factory exports and added a regression test; it includes the libsodium 1.0.22 update shipped in 0.8.3.
libsodium-wrappers 0.8.4 installed in 0.6 seconds and 2 MB on our box, but its full browser import reached 147.7 KB gzipped, so the dependency makes sense for protocols that specifically need libsodium across runtimes. Choose Web Crypto or a smaller package for a single common primitive, and do not adopt this wrapper before key management and ciphertext framing are designed.
We installed it
| Install | ✓ · 0.6s | 2 packages on disk · 2 MB |
| Import | ✓ | ESM import works · require() works · CommonJS package with exports map |
| Browser | 147.7 KB | gzipped (424 KB minified), bundled with esbuild |
| Types | ✓ | TypeScript types bundled |
| Known vulns | 0 | 0 critical · 0 high · 0 moderate · 0 low (npm audit) |
Answers from our run
Does libsodium-wrappers install cleanly?
Yes. In a fresh container with an empty cache, npm install libsodium-wrappers finished in 0.6s, leaving 2 packages and 2 MB on disk. npm audit reported no known vulnerabilities.
How much does libsodium-wrappers add to a browser bundle?
147.7 KB gzipped (424 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.
Does libsodium-wrappers work with both ESM and CommonJS?
Yes. Both import 'libsodium-wrappers' and require('libsodium-wrappers') worked in Node 22 in our run. The package is published as CommonJS with an exports map.
Does libsodium-wrappers include TypeScript types?
Yes, type declarations ship inside the package, so no @types install is needed.
libsodium-wrappers or @noble/ciphers: which should you use?
@noble/ciphers: Use it for a smaller TypeScript-focused set of pure JavaScript ciphers when cross-language libsodium compatibility is unnecessary. libsodium-wrappers 0.8.4 installed in 0.6 seconds and 2 MB on our box, but its full browser import reached 147.7 KB gzipped, so the dependency makes sense for protocols that specifically need libsodium across runtimes.
When should you not use libsodium-wrappers?
Web Crypto already covers the required browser operation; our full import measured 147.7 KB gzipped, which is expensive for one ordinary primitive
Use it if
- A browser and server must produce byte-compatible ciphertexts or signatures using the same libsodium protocol
- Your design calls for XChaCha20-Poly1305, secretstream, sealed boxes, or another primitive that Web Crypto does not expose consistently
- You need generated TypeScript declarations for a broad libsodium API in both ESM and CommonJS projects
- The application already has a reviewed plan for key creation, storage, rotation, framing, and authentication failures
- Web Crypto already covers the required browser operation; our full import measured 147.7 KB gzipped, which is expensive for one ordinary primitive
- Password hashing is the requirement; crypto_pwhash exists only in libsodium-wrappers-sumo, whose README warns about extra memory and low-level symbols
- You expect key storage, envelopes, rotation, recovery, or protocol choices from a package; this wrapper exposes primitives and binary conversion helpers
- The runtime cannot await asynchronous WebAssembly setup or comply with its delivery constraints; every crypto call must wait for sodium.ready
- React Native is the deployment target; the project lists react-native-libsodium as the matching native binding
Setup reality
Our fresh libsodium-wrappers 0.8.4 install completed in 0.6 seconds and left two packages using 2 MB. The wrapper declared one direct dependency, no peers, 568 KB unpacked, and an ISC license. npm audit reported 0 known vulnerabilities. TypeScript declarations were bundled. An import-all browser build measured 424 KB minified and 147.7 KB gzipped, enough weight to matter on a user-facing route.
No native compiler, account, key, or config file is needed for the published npm artifact. Import the default sodium object and await sodium.ready before reading a crypto constant or calling a primitive. Version 0.8.4 has an exports map; CommonJS require() and ESM import both worked in our sandbox. Helper functions have named ESM exports, but dynamically attached cryptographic functions must come from the initialized default object.
Keys, nonces, and framing belong to the application. Generate keys with the matching keygen function, keep them out of source control, and store the nonce beside its ciphertext. Define a format with an algorithm version and any associated data so future code can parse old records. The standard build omits crypto_pwhash. Do not replace a key with a human password or use crypto_generichash as a password-storage function.
Stateful APIs allocate handles in WebAssembly memory. The README requires sodium.free() after secretstream and XOF state types that lack an automatic finalizer; finalized state must never be reused. Authentication failure ends processing and should not trigger a plaintext fallback. memzero overwrites one Uint8Array, while strings, logs, serialized copies, and other references can retain secret material. When inlining the browser artifact, UTF-8 metadata and a charset=utf-8 response prevent corruption of embedded WASM bytes.
Patterns
Wait for the wrapper before using crypto initialize-sodium
import sodium from 'libsodium-wrappers';
await sodium.ready;
const key = sodium.crypto_secretbox_keygen();Cryptographic functions and constants appear on the default object after ready resolves. Version 0.8.4 fixed ESM factory interoperability.
Generate a URL-safe random token create-random-token
await sodium.ready;
const bytes = sodium.randombytes_buf(32);
const token = sodium.to_base64(
bytes,
sodium.base64_variants.URLSAFE_NO_PADDING
);randombytes_buf uses the library's secure random source. Math.random is unsuitable for tokens or keys.
Encrypt with a fresh secretbox nonce encrypt-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 record', nonce, key);
const record = {
nonce: sodium.to_base64(nonce),
ciphertext: sodium.to_base64(ciphertext)
};Persist the nonce beside the ciphertext and keep the key elsewhere. Reusing one nonce with the same key breaks secretbox security.
Authenticate before decoding plaintext decrypt-secretbox
await sodium.ready;
const nonce = sodium.from_base64(record.nonce);
const ciphertext = sodium.from_base64(record.ciphertext);
const bytes = sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
const plaintext = sodium.to_string(bytes);A failed open means the bytes are corrupt or unauthenticated. Stop processing instead of trying a plaintext decoder.
Authenticate metadata with XChaCha20-Poly1305 encrypt-with-aad
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=42;schema=1');
const ciphertext = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
'payload', aad, null, nonce, key
);Decryption needs identical associated data. AAD is authenticated and remains visible, so it must not contain a secret.
Compute a BLAKE2b digest hash-bytes
await sodium.ready;
const digest = sodium.crypto_generichash(
32,
sodium.from_string('artifact contents'),
null
);
console.log(sodium.to_hex(digest));crypto_generichash is a fast general-purpose hash. It is not suitable for storing passwords; crypto_pwhash is absent from the standard package.
Sign and verify a manifest sign-bytes
await sodium.ready;
const keys = sodium.crypto_sign_keypair();
const manifest = sodium.from_string('release=2026.08');
const signature = sodium.crypto_sign_detached(manifest, keys.privateKey);
const valid = sodium.crypto_sign_verify_detached(
signature, manifest, keys.publicKey
);A detached signature proves possession of the private key and leaves the message readable. Public-key distribution still needs a trusted channel.
Create an anonymous sealed box seal-for-recipient
await sodium.ready;
const recipient = sodium.crypto_box_keypair();
const sealed = sodium.crypto_box_seal('recipient data', recipient.publicKey);
const opened = sodium.crypto_box_seal_open(
sealed, recipient.publicKey, recipient.privateKey
);A sealed box hides the message for one recipient but supplies no sender identity. Add a signature when the receiver must authenticate the sender.
Round-trip explicit base64 variants encode-binary
await sodium.ready;
const variant = sodium.base64_variants.URLSAFE_NO_PADDING;
const encoded = sodium.to_base64(bytes, variant);
const decoded = sodium.from_base64(encoded, variant);Store the chosen variant in the protocol definition. Producer and consumer must agree about alphabet and padding.
Compare fixed-length byte arrays compare-secrets
await sodium.ready;
const expected = sodium.from_hex(expectedHex);
const received = sodium.from_hex(receivedHex);
const equal = expected.length === received.length &&
sodium.memcmp(expected, received);memcmp performs a constant-time equality check on equal-length arrays. Validate lengths before calling it.
Overwrite a key buffer clear-key-buffer
await sodium.ready;
const key = sodium.crypto_secretbox_keygen();
try {
await encryptRecords(key);
} finally {
sodium.memzero(key);
}memzero clears this Uint8Array. It cannot erase copies in logs, strings, serialized objects, engine internals, or other references.
Finalize and release secretstream state finish-secret-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 has no automatic finalizer. The README requires free() after the last operation and forbids using that state handle again.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| @noble/ciphers | npm | Use it for a smaller TypeScript-focused set of pure JavaScript ciphers when cross-language libsodium compatibility is unnecessary |
| tweetnacl | npm | Use it for a compact and frozen NaCl-style surface when newer libsodium algorithms are outside the protocol |
| sodium-native | npm | Use it in Node-only services that accept a native addon in exchange for direct native libsodium bindings |
| libsodium-wrappers-sumo | npm | Use it only when crypto_pwhash or another symbol missing from the standard build is required |
More security guides
cryptography · pyjwt · jose · requests-oauthlib · oauthlib · dompurify · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

