mrkeyoor.com_
Thu 06 Aug 02:47 UTC
npmSecurityupdated 06 Aug 2026

tweetnacl

TweetNaCl.js is a JavaScript port of TweetNaCl, Daniel J. Bernstein's 100-tweet implementation of the NaCl crypto library. It gives you five things and nothing else: authenticated public-key encryption (nacl.box, which is X25519 plus XSalsa20-Poly1305), authenticated secret-key encryption (nacl.secretbox), Ed25519 signatures (nacl.sign), SHA-512 hashing (nacl.hash), and raw X25519 scalar multiplication. Everything takes and returns Uint8Array; there is no string handling, no key serialisation format, and no protocol layer. It has no dependencies, works identically in browsers and Node, and was audited by Cure53 in early 2017 with no security problems found. The package ships two builds and defaults to nacl-fast.js, the optimised one.

Verdict

A correct, audited, public-domain implementation that earned its place before browsers had usable crypto, and it still works exactly as advertised. On any platform with WebCrypto, reach for the native APIs or the noble packages first; keep tweetnacl for XSalsa20-Poly1305 interop and for runtimes where nothing else fits.

API stability5/5The 1.0 API has not changed since 2018 and 1.0.3 has been the current release since February 2020. It is a port of a fixed reference implementation, so there is nothing to drift; code written against it years ago still runs unmodified.
Docs5/5The README documents every function and constant, and then does something rare: a Security Considerations section that spells out the missing key commitment, signature malleability, SHA-512 length extension, and the impossibility of guaranteed constant time in JavaScript. It also tells you to prefer WebCrypto where you can. Usage examples live in the wiki rather than the README.
Maintenance2/5No release since February 2020, though the repository was last pushed in August 2025 and carries 5 open issues. The Cure53 audit was 2017 and one bug was found and fixed after it. Frozen code implementing a frozen spec decays slowly, but nobody should assume a fix would ship quickly.
Ecosystem4/5Around 34.6M weekly downloads, mostly as a transitive dependency of wallet, chat, and protocol libraries that standardised on NaCl years ago. Add-ons exist for sealed boxes, crypto_auth, and streaming, but they are third-party forks with their own maintenance stories, and newer projects tend to pick the noble packages instead.

Use it if

  • You need XSalsa20-Poly1305 authenticated encryption specifically, because that is the one NaCl primitive WebCrypto does not implement and the one thing here you cannot get from the platform
  • You have to interoperate with something already speaking NaCl or libsodium's crypto_box and crypto_secretbox wire format, where the exact primitive and nonce layout are fixed by the other side
  • You need identical behaviour across browsers, Node, React Native, and old runtimes with no native modules, no WASM, and no build step, and you would rather have one 10 KB file than a matrix of platform code paths
  • You want a small, audited, public-domain implementation you can read end to end, which is a real argument for embedded or bundled-into-a-widget contexts
Skip it if

Setup reality

npm install tweetnacl and there is nothing else to configure: no dependencies, no native build, no postinstall, and a nacl.d.ts included for TypeScript. The package.json has main pointing at nacl-fast.js and no exports map or module field, so bundlers get one CommonJS file and tree shaking does not apply. Two runtime facts matter before you write code. First, randomness comes from crypto.getRandomValues in browsers or crypto.randomBytes in Node, and on a platform that provides neither, nacl.randomBytes, nacl.box.keyPair, and nacl.sign.keyPair throw while the deterministic functions keep working; nacl.setPRNG exists to plug in your own source, and it replaces the internal generator completely. Second, Buffer objects can be passed in because they are backed by Uint8Array, but everything comes back as a plain Uint8Array, and some returns are subarrays of an internal buffer, so convert with Buffer.from(array) and never Buffer.from(array.buffer) or you will read neighbouring bytes. The remaining setup work is protocol design: generating a fresh 24-byte nonce per message, transporting it alongside the ciphertext, and deciding how keys are encoded and stored, none of which the library does for you.

Patterns

Create key pairs for encryption and for signinggenerate-keypairs

import nacl from 'tweetnacl';

const enc = nacl.box.keyPair();
// enc.publicKey  32 bytes
// enc.secretKey  32 bytes

const signing = nacl.sign.keyPair();
// signing.publicKey  32 bytes
// signing.secretKey  64 bytes

Box and sign keys are different types and different lengths; do not reuse one pair for both. Both calls throw if the platform has no secure random source, so wrap them if you support exotic runtimes.

Encrypt to someone's public keypublic-key-encrypt

const nonce = nacl.randomBytes(nacl.box.nonceLength); // 24 bytes
const message = new TextEncoder().encode('hello');

const ciphertext = nacl.box(
  message, nonce, theirPublicKey, mySecretKey,
);
// ciphertext is message.length + nacl.box.overheadLength (16) bytes

const plaintext = nacl.box.open(
  ciphertext, nonce, myPublicKey, theirSecretKey,
);

box.open returns null on any authentication failure rather than throwing, so an if (!plaintext) check is mandatory. Treating null as an empty message is the classic way to turn a tamper detection into a silent accept.

Ship the nonce with the ciphertexttransport-the-nonce

function seal(message, theirPub, mySec) {
  const nonce = nacl.randomBytes(nacl.box.nonceLength);
  const box = nacl.box(message, nonce, theirPub, mySec);
  const out = new Uint8Array(nonce.length + box.length);
  out.set(nonce);
  out.set(box, nonce.length);
  return out;
}

function unseal(payload, theirPub, mySec) {
  const nonce = payload.slice(0, nacl.box.nonceLength);
  const box = payload.slice(nacl.box.nonceLength);
  return nacl.box.open(box, nonce, theirPub, mySec);
}

Nonces are not secret, only unique. A fresh random 24-byte nonce per message is safe; a counter is safe only if you can guarantee it never resets or forks. Reusing a nonce under the same key pair leaks the relationship between both plaintexts.

Encrypt with a shared symmetric keysecret-key-encrypt

const key = nacl.randomBytes(nacl.secretbox.keyLength);   // 32
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength); // 24

const box = nacl.secretbox(message, nonce, key);
const opened = nacl.secretbox.open(box, nonce, key);

secretbox is XSalsa20-Poly1305, the one primitive here that WebCrypto still does not offer. It is not key-committing: a crafted ciphertext can open to valid plaintexts under two different keys, which matters if you ever try keys in a loop to identify a recipient.

Reuse the Diffie-Hellman result across many messagesprecompute-shared-key

const shared = nacl.box.before(theirPublicKey, mySecretKey);

const box1 = nacl.box.after(msg1, nonce1, shared);
const box2 = nacl.box.after(msg2, nonce2, shared);

const back = nacl.box.open.after(box1, nonce1, shared);

box.before does the expensive X25519 step once. In a chat or session loop this is the difference between a few hundred and tens of thousands of operations per second, since scalarMult dominates the cost of box.

Sign and verify without copying the messagedetached-signatures

const signature = nacl.sign.detached(message, signing.secretKey); // 64 bytes

const ok = nacl.sign.detached.verify(
  message, signature, signing.publicKey,
);
if (!ok) throw new Error('bad signature');

Prefer detached over nacl.sign, which returns signature and message concatenated and forces you to copy the payload twice. Ed25519 here is malleable: a third party can derive a different valid signature for the same message, so never use a signature as a deduplication key.

Use the combined form when the format demands itcombined-signatures

const signed = nacl.sign(message, secretKey);       // 64-byte sig + message
const message2 = nacl.sign.open(signed, publicKey); // null if invalid

sign.open returns null rather than throwing on a bad signature, the same trap as box.open. This form exists for NaCl wire compatibility; new protocols should use the detached functions.

Derive a signing key pair from a stored 32-byte seeddeterministic-keys-from-seed

const seed = loadSecret(); // 32 bytes of real entropy, not a password
const pair = nacl.sign.keyPair.fromSeed(seed);

// Rebuild from a saved 64-byte secret key instead:
const same = nacl.sign.keyPair.fromSecretKey(pair.secretKey);

The README warns against fromSeed for general use because the seed must carry full entropy. Feeding it a password or a hash of one gives you a key only as strong as the password, with no stretching in between.

Hash with SHA-512hash-bytes

const digest = nacl.hash(new TextEncoder().encode('message'));
console.log(digest.length); // 64

Plain SHA-512, which means length-extension attacks apply: never build a MAC as hash(secret || message). Use nacl.secretbox for authenticated encryption or an HMAC from WebCrypto if you need a keyed digest.

Compare secrets without a timing leakconstant-time-compare

const equal = nacl.verify(tokenFromRequest, expectedToken);
if (!equal) reject();

Returns false for zero-length inputs and for length mismatches, and only then compares contents. Never use === or a loop with an early return on secret material; the comparison time tells an attacker how many bytes they guessed right.

Convert between strings, base64, and Uint8Arrayencode-strings-and-keys

// text
const bytes = new TextEncoder().encode('hello');
const text = new TextDecoder().decode(bytes);

// base64 for transport, Node
const b64 = Buffer.from(publicKey).toString('base64');
const key = new Uint8Array(Buffer.from(b64, 'base64'));

Use Buffer.from(uint8array), which copies. Buffer.from(uint8array.buffer) shares memory and, because some tweetnacl returns are subarrays of a larger internal buffer, hands you bytes that are not part of your value.

Supply your own PRNG on a platform without onecustom-random-source

nacl.setPRNG((x, n) => {
  const bytes = myHardwareEntropy(n); // must be cryptographic
  for (let i = 0; i < n; i++) x[i] = bytes[i];
});

This replaces the internal generator entirely, for the whole process. It is an escape hatch for exotic runtimes only; anything derived from Math.random or a timestamp makes every key the library generates predictable.

Alternatives

PackageRegistryPick it when
@noble/curvesnpmYou want audited, actively maintained, tree-shakeable Ed25519 and X25519 with modern TypeScript types.
@noble/ciphersnpmYou need XSalsa20-Poly1305 or ChaCha20-Poly1305 from a package that is still shipping releases.
libsodium-wrappersnpmYou want the full libsodium surface, including sealed boxes, Argon2, and generichash, and can accept a WASM payload.
tweetnacl-utilnpmYou are staying on tweetnacl and just need the string and base64 conversion helpers it deliberately omits.