Library / SDK
jedisct1/libsodium.js avatar
jedisct1/libsodium.js

libsodium.js ports the Sodium C library to WebAssembly and pure JavaScript

libsodium compiled to Webassembly and pure JavaScript, with convenient wrappers.

1,161 stars166 forksHTMLNOASSERTION

At a glance

What is it?
libsodium.js compiles the libsodium C library to WebAssembly and to pure JavaScript with Emscripten and adds generated wrappers, shipping a standard build of about 290 KB and a sumo build of about 375 KB minified and gzipped. It exposes the C API with buffer lengths inferred, plus helper functions for base64, hex and constant time comparison.
Who is it for?
libsodium.js fits a browser or Node.js application that needs the Sodium primitives the C library provides, including secretstream, X25519 and Ed25519, with a fixed and audited implementation rather than a hand rolled one, and it fits a team that needs a WebAssembly build of a cryptographic library without writing the Emscripten plumbing.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly HTML, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two builds of the same library, and the difference is size

libsodium.js is the sodium crypto library compiled to WebAssembly and pure JavaScript using Emscripten, with automatically generated wrappers to make it easy to use in web applications.

The size numbers are given up front, and they are the first thing to weigh. The standard version weighs about 290 KB and the sumo version about 375 KB, minified, gzipped, and including both the pure JS and the WebAssembly versions. Both can run in a web browser as well as server-side.

So the sumo build costs about 85 KB more gzipped, and the README explains what that buys. The browsers directory includes a single-file script that can be included in web pages and contains code for commonly used functions. The browsers-sumo directory is a superset of the previous script that contains all functions, including rarely used ones and undocumented ones.

That distinction matters more than it might sound. The sumo variants exist because libsodium's API surface is large and a significant part of it is the kind of thing almost nobody uses, the exotic key derivation variants, the less common hash functions, the low level primitives that the higher level functions are built from. Including them costs bandwidth on every page load, so the standard build omits them and the sumo build includes them for people who need the long tail.

The npm packages follow the same split, with libsodium-wrappers and libsodium-wrappers-sumo both published, and the module directories extend it to module systems, with modules, modules-sumo, modules-esm and modules-sumo-esm, the ESM ones using .mjs extensions.

The keyword list in the package.json names the primitives that matter, chacha20, poly1305, curve25519, ed25519, blake2, siphash, argon2 and ecc, which is a good short summary of what a modern application might reach for.

The wrappers infer buffer lengths, which is the whole ergonomic point

The API section says it in one line: the API exposed by the wrappers is identical to the one of the C library, except that buffer lengths never need to be explicitly given.

That single difference is what makes the port usable from JavaScript. In C, crypto_secretbox_easy takes a ciphertext buffer, a message, a message length, a nonce and a key, and getting the lengths right is the main source of bugs in C crypto code. The wrapper infers them from the typed arrays you pass, so the JavaScript call is closer to the intent and further from the C mistake.

The input types are stated too. Binary input buffers should be Uint8Array objects, however, if a string is given instead, the wrappers will automatically convert the string to an array containing a UTF-8 representation of the string.

On top of the C API there is a set of helper functions that are not in libsodium at all.

code
* `from_base64()`, `to_base64()` with an optional second parameter
  whose value is one of: `base64_variants.ORIGINAL`, `base64_variants.ORIGINAL_NO_PADDING`,
  `base64_variants.URLSAFE` or `base64_variants.URLSAFE_NO_PADDING`. Default is `base64_variants.URLSAFE_NO_PADDING`.
* `from_hex()`, `to_hex()`
* `from_string()`, `to_string()`
* `pad(<buffer>, <block size>)`, `unpad(<buffer>, <block size>)`
* `memcmp()` (constant-time check for equality, returns `true` or `false`)
* `compare()` (constant-time comparison. Values must have the same
  size. Returns `-1`, `0` or `1`)
* `memzero()` (applies to `Uint8Array` objects)
* `increment()` (increments an arbitrary-long number stored as a
  little-endian `Uint8Array` - typically to increment nonces)
* `add()` (adds two arbitrary-long numbers stored as little-endian
  `Uint8Array` vectors)
* `is_zero()` (constant-time, checks `Uint8Array` objects for all zeros)

The base64 default is worth noting, URLSAFE_NO_PADDING, which is the variant you want if the value is going into a URL or a filename, and it is the opposite of the default most encoding libraries use. The constant-time helpers are the ones that matter for security, memcmp, compare and is_zero, since a naive equality check on a secret leaks its value through timing. The arbitrary-long number helpers exist because nonces are often counters, and incrementing a counter as a byte vector avoids the precision problems of a JavaScript number.

The ready promise, and the exports that only exist after it resolves

The module usage has a wrinkle that trips up most people the first time.

Load the libsodium-wrappers module, and the returned object contains a.ready property, a promise that must be resolved before the sodium functions can be used.

js
import sodium from 'libsodium-wrappers';

await sodium.ready;

let key = sodium.crypto_secretstream_xchacha20poly1305_keygen();

The reason is that the module has to decide at runtime whether to use the WebAssembly build or the pure JavaScript one, and it has to load and instantiate the WebAssembly before any of the cryptographic functions exist.

That creates a second wrinkle, and the README spells it out. Cryptographic functions like crypto_secretbox_easy and constants like crypto_secretbox_KEYBYTES are dynamically added to the module at runtime after ready resolves. They cannot be imported as named exports and must be accessed via the default export.

js
import sodium from 'libsodium-wrappers';

await sodium.ready;
// Now crypto functions and constants are available on the sodium object
const key = sodium.crypto_secretbox_keygen();
const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES);

The named exports that do exist are the helper functions, which are static.

js
import { ready, free, from_hex, to_hex, from_string, to_string } from 'libsodium-wrappers';

await ready;
const bytes = from_hex('deadbeef');
console.log(to_hex(bytes));

So the split is between the helpers, which are known at build time and can be named imports, and the crypto surface, which is not known until the module has decided which implementation to load. A bundler cannot statically see that sodium.crypto_secretbox_easy exists, which is why the type definitions exist separately, at dist/modules-esm/libsodium-wrappers.d.mts for the ESM build and dist/modules/libsodium-wrappers.d.ts for the CommonJS one, and why the package exports map has a separate types entry under each of the import and require conditions.

The example the README gives uses the secretstream API, which is the one worth reading because it shows the push and pull model, the tag constants, and the manual state cleanup with sodium.free.

Loading it in a browser, and the encoding trap

The browser path is the older callback style, and it comes with one sharp edge that the README documents carefully.

The sodium.js file includes both the core libsodium functions and the higher-level JavaScript wrappers, and it can be loaded asynchronously. A sodium object should be defined in the global namespace with an onload property, the function to call after the wrapper is initialized.

html
<script>
    window.sodium = {
        onload: function (sodium) {
            let h = sodium.crypto_generichash(64, sodium.from_string('test'));
            console.log(sodium.to_hex(h));
        }
    };
</script>
<script src="sodium.js" async></script>

The trap is about inlining. If you inline the library directly into your HTML rather than loading it from a separate file, make sure your page declares UTF-8 encoding.

html
<meta charset="utf-8">

Without this, browsers may corrupt the embedded WebAssembly binary data during HTML parsing, leading to errors like failed to match magic number or HEAPU8 is undefined. Also ensure your server sends the charset=utf-8 header.

That is a genuinely obscure failure, and the reason is worth understanding. The WebAssembly binary is embedded in the script as a byte array, and if the browser parses the page as a different encoding than the bytes were written in, the byte array is corrupted before the WebAssembly loader ever sees it. The magic number error is the WebAssembly loader noticing that the first bytes are not the bytes it expects, and HEAPU8 is undefined is the same corruption seen from further along.

The compatibility list is broader than the callback style suggests, with Chrome from 16, Edge from 0.11, Firefox from 21, Opera from 15, Safari from 6, mobile Safari on iOS from 8.0, plus NodeJS and Bun. The README flags that older Safari and older mobile Safari produce incorrect results, and notes that this is comparable to the WebCrypto API, which is compatible with a similar number of browsers.

The build regenerates the wrappers from the library's own symbols

The Makefile shows how the wrappers are produced, and the answer is that they are not written by hand.

The wrapper inputs are gathered with a wildcard over the symbol and macro directories.

make
WRAPPER_SYMBOLS := $(wildcard wrapper/symbols/*.json)
WRAPPER_MACROS := $(wildcard wrapper/macros/*.js)
WRAPPER_SHARED_INPUTS := wrapper/constants.json wrapper/shared-types.ts wrapper/types.ts $(WRAPPER_SYMBOLS)

So each C function in libsodium has a JSON description in wrapper/symbols/, the shared types and constants live in their own files, and the macros handle the cases the generator cannot express generically. The wrapper build, the doc build and the types build all consume the same shared inputs, which is what keeps the API documentation, the TypeScript definitions and the runtime wrappers from drifting apart.

The Makefile also notes something about concurrency that is a real constraint. The libsodium builds reconfigure the in-tree source and must not run concurrently. The libsodium directory is a git submodule, and the builds point at the in-tree libsodium-js and libsodium-js-sumo output directories, so the wrapper build consumes the already compiled library.

The minification is done with terser through bun, with the flags spelled out, mangle and compress with drop_console and passes of 3, and a separate module variant for the ESM outputs. The dev dependencies are minimal, @biomejs/biome for the lint and format of the TypeScript sources and terser for the minification, with the lint script scoped to test/*.ts and wrapper/*.ts.

The API documentation is generated too, with API.md and API_sumo.md at the root for the two builds, which is consistent with the wrappers being generated from the symbol descriptions rather than maintained by hand.

Where libsodium.js is the wrong choice

The first wrong case is when WebCrypto is enough. The README itself notes that the compatibility is comparable to the WebCrypto API. If what you need is AES-GCM, SHA-256, HMAC, RSA or ECDSA, the browser already has them, natively, with no payload and no build step, and a 290 KB dependency to get the same primitives is a poor trade.

The second wrong case is a strict bundle budget. 290 KB gzipped is not a small number for a web page, and for a marketing site or a content page it is absurd. The pure JavaScript build is the reason the number is that high, since shipping both implementations means the payload covers WebAssembly and a JavaScript fallback.

The third wrong case is a project that already standardises on a JavaScript crypto library. A JavaScript crypto library that already covers the primitives you need leaves little reason to add this one. This project wins when you want the full libsodium surface, particularly secretstream, which is a substantial piece of machinery to reimplement.

The fourth wrong case is a security review that wants the upstream library rather than a port. The C library is the artefact that gets audited. A JavaScript port that is automatically generated is a different thing, and a team whose policy requires the audited upstream artefact to be what runs in production needs to look at what the WebAssembly build actually contains. The README notes that signatures and other Edwards25519-based operations are compatible with WasmCrypto, which is a useful reference point for anyone cross-checking the implementation.

How it differs from the browser's built-in WebCrypto

The comparison worth making is with the WebCrypto API, since the README itself puts them side by side on browser coverage.

WebCrypto is a browser feature. It has no payload, it is implemented by the browser vendor, and it is a small and deliberately limited API. What it covers is the symmetric and asymmetric primitives that the web platform standardised, and it does not cover the rest of libsodium.

libsodium.js is a port of a specific library. The difference in approach is that you get the whole C API of a library whose design is deliberately conservative, and you get the parts WebCrypto does not have. The secretstream construction in the example is the clearest case, an AEAD with a stateful push and pull API and per-message tags, which is the right primitive for a protocol that carries a sequence of messages and needs to prevent reordering and truncation, and there is no WebCrypto equivalent.

The second difference is the constant-time helpers. The additional helpers list includes memcmp, compare and is_zero, all constant time, which is exactly the class of operation that WebCrypto does not provide because it exposes only high level primitives. A JavaScript application that needs to compare a MAC in constant time has to either use one of these or write it, and writing it correctly is the kind of thing you would rather not do.

The third difference is the pure JavaScript fallback, which WebCrypto has no analogue for. The README says both versions are included, so the library works where WebAssembly is unavailable, which is a real portability property for a library that has to run in unusual environments.

The fourth difference is where the boundary of the API sits. The wrappers keep the C API intact, so the documentation for libsodium itself is the documentation for this library, and that is a large body of existing material that a team does not have to write.

Editorial conclusion

libsodium.js fits a browser or Node.js application that needs the Sodium primitives the C library provides, including secretstream, X25519 and Ed25519, with a fixed and audited implementation rather than a hand rolled one, and it fits a team that needs a WebAssembly build of a cryptographic library without writing the Emscripten plumbing. It does not fit a project that only needs AES, SHA or HMAC, since the WebCrypto API covers those with no payload at all, and it does not fit a bundle budget that cannot absorb 290 KB. Before adopting it, decide between the standard and sumo builds, since sumo adds rarely used and undocumented functions, and note that the npm package licence is ISC while the repository as a whole carries a custom licence recorded in its LICENSE file, with the last release 0.8.4 from 2026-04-19 and the last push on 2026-09-25.

Frequently asked questions

How do I install libsodium.js?

The dist directory in the repository contains pre-built scripts for browsers, browsers-sumo, modules, modules-sumo, modules-esm and modules-sumo-esm, and the modules are also on npm as libsodium-wrappers and libsodium-wrappers-sumo. The browser build is a single file that can be included in a page with a script tag.

What do I need to await before using the crypto functions?

The module returns an object with a ready promise, and it must be resolved before the sodium functions can be used, because the cryptographic functions and constants are dynamically added to the module at runtime after ready resolves. They therefore cannot be imported as named exports and must be reached through the default export.

How do I load libsodium.js in a browser?

Define a sodium object in the global namespace with an onload callback and load the script asynchronously with a script tag. If you inline the library directly into your HTML you must declare a meta charset of utf-8, because otherwise the browser may corrupt the embedded WebAssembly binary and you get errors such as failed to match magic number or HEAPU8 is undefined.

What is the difference between the standard and sumo builds?

The standard build contains commonly used functions and weighs about 290 KB minified and gzipped. The sumo build is a superset that contains all functions, including rarely used and undocumented ones, and weighs about 375 KB. The same split exists for the module and ESM distributions and for the npm packages.

Which browsers and runtimes does libsodium.js support?

Chrome 16 and later, Edge 0.11 and later, Firefox 21 and later, Opera 15 and later, Safari 6 and later with older versions producing incorrect results, mobile Safari on iOS 8.0 and later with the same caveat, plus NodeJS and Bun. The README notes this is comparable to the WebCrypto API's browser coverage.

Official sources

  1. Issues
  2. jedisct1/libsodium.js on GitHub
  3. README
  4. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jedisct1-libsodium-js.svg)](https://hysenlabs.com/projects/jedisct1-libsodium-js)