mrkeyoor.com_
Sun 20 Sept 07:02 UTC
npmSecurityupdated 20 Sept 2026

tweetnacl review

tweetnacl 1.0.3 is a JavaScript port of the small NaCl cryptographic API. It accepts `Uint8Array` values and exposes XSalsa20-Poly1305 secret boxes, X25519 plus XSalsa20-Poly1305 public-key boxes, Ed25519 signatures, X25519 scalar multiplication, SHA-512, random bytes, and byte comparison. The npm entry loads the faster implementation. Version 1.0.3 is a 2020 security fix for incorrect signature generation caused by carry calculation with integers beyond 32-bit bitwise behavior; verification and the encryption APIs were not affected by that bug.

26.0Mdownloads / wk
Verdict

tweetnacl 1.0.3 installed in 0.6 seconds as 1 dependency-free package, occupied 1 MB, bundled to 10.8 KB gzipped in our browser test, and returned 0 audit findings. Install it for an existing NaCl-compatible protocol; for new designs, prefer Web Crypto, Sodium, or JOSE so primitive selection and wire rules are not yours to invent.

We installed it

Lab card: what happened when we installed tweetnaclScreenshot of tweetnacl documentation
Install✓ · 0.6s1 package on disk · 1 MB
ImportESM import works · require() works · CommonJS package
Browser10.8 KBgzipped (32.7 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does tweetnacl install cleanly?

Yes. In a fresh container with an empty cache, npm install tweetnacl finished in 0.6s, leaving 1 package and 1 MB on disk. npm audit reported no known vulnerabilities.

How much does tweetnacl add to a browser bundle?

10.8 KB gzipped (32.7 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does tweetnacl work with both ESM and CommonJS?

Yes. Both import 'tweetnacl' and require('tweetnacl') worked in Node 22 in our run. The package is published as CommonJS.

Does tweetnacl include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

tweetnacl or libsodium-wrappers: which should you use?

libsodium-wrappers: Use it for a wider maintained Sodium API with password hashing, sealed boxes, and additional modern constructions. tweetnacl 1.0.3 installed in 0.6 seconds as 1 dependency-free package, occupied 1 MB, bundled to 10.8 KB gzipped in our browser test, and returned 0 audit findings.

When should you not use tweetnacl?

You are designing a new application protocol. TweetNaCl supplies primitives, not message framing, key rotation, replay protection, identity binding, or negotiation.

API stability5/5The 1.0 API has remained unchanged since 2017: byte arrays enter named primitives and fixed-size byte arrays, booleans, or `null` come back. Version 1.0.3 fixes signature generation without changing signatures or call shapes. That stability also reflects a frozen surface. There is no exports map, and callers should avoid undocumented low-level functions exposed for older third-party projects.
Docs4/5The README defines every function, byte length, return value, random-source rule, Buffer copying trap, and failure sentinel. Its security section plainly states missing key commitment, signature malleability, SHA-512 length extension, side-channel limits, and memory-erasure limits. Examples are primitive-level; the project intentionally does not document a complete message protocol, key lifecycle, or storage format.
Maintenance2/5npm 1.0.3 was published on 10 February 2020, and the repository's latest push was 15 August 2025. GitHub shows 1,921 stars, 6 combined open issues and pull requests, and the repository is not archived. The last release fixed a serious signing error, but more than 6 years without another npm release is a meaningful maintenance risk for new security-sensitive adoption.
Ecosystem4/5npm counted 36,032,209 downloads for 19 through 25 August 2026. The package works in Node and modern browsers, has no runtime dependencies, bundles TypeScript declarations, and preserves NaCl-compatible byte-oriented primitives. Web Crypto now overlaps X25519, Ed25519, and SHA-512, while Sodium and JOSE offer higher-level or more current choices for new systems.

Discussed on

  1. hnTinySSH is a small SSH server using NaCl, TweetNaCl322 points
  2. hnTinySSH is a small SSH server using NaCl, TweetNaCl129 points
  3. hnSPARKNaCl: A verified, fast re-implementation of TweetNaCl70 points
  4. hnTweetNaCl.js57 points
  5. hnTweetNaCL16 points

Use it if

  • An existing protocol explicitly requires NaCl-compatible `secretbox`, `box`, or TweetNaCl Ed25519 byte formats.
  • The same small primitive API must run in Node and browsers with no runtime dependencies.
  • Inputs and outputs are already byte arrays, and your protocol separately defines framing, key identity, nonce allocation, and versioning.
  • You need XSalsa20-Poly1305, which the README notes is the main primitive here that Web Crypto does not supply.
Skip it if

Setup reality

We installed tweetnacl 1.0.3 in a fresh Node 22 Bookworm sandbox in 0.6 seconds. It left 1 package and 1 MB on disk. npm audit found 0 known vulnerabilities at every severity. The package has 0 direct dependencies and 0 peer dependencies, is 196 KB unpacked, includes TypeScript declarations, and uses the Unlicense. It is CommonJS with no exports map; both require() and ESM import worked in our checks.

All APIs consume and return Uint8Array, so text encoding and wire framing are your work. Our esbuild browser import measured 32.7 KB minified and 10.8 KB gzipped. In Node, Buffer inputs work because they extend Uint8Array, but outputs remain byte arrays. Use Buffer.from(view) to copy the visible bytes; constructing from view.buffer can expose unrelated bytes outside a subarray. Validate decoded key, nonce, and signature lengths before calling a primitive.

Nonce uniqueness is a protocol requirement. secretbox needs a fresh 24-byte nonce for each message under one key, and box needs one for each message under a key pair. Nonces are public and may travel beside ciphertext. Key generation requires a cryptographic random source from Web Crypto or Node crypto; it throws when none exists. setPRNG replaces that source globally, so it is unsuitable for Math.random or an unreviewed callback.

Authenticated decryption returns null on a wrong key, wrong nonce, or changed ciphertext. Check for null, since an empty Uint8Array is valid plaintext. The README also documents signature malleability, SHA-512 length-extension exposure, missing key commitment, and the inability of JavaScript engines to promise physical constant-time execution or reliable memory erasure. Those are protocol constraints, not issues npm audit can detect.

Patterns

Create separate box and signing keys generate-key-pairs

import nacl from 'tweetnacl';

const boxKeys = nacl.box.keyPair();
const signKeys = nacl.sign.keyPair();

storeBoxPublicKey(boxKeys.publicKey);
storeSigningPublicKey(signKeys.publicKey);

Box and Ed25519 keys have different purposes and secret-key lengths; never reuse 1 pair for both jobs.

Encrypt to a recipient key encrypt-public-key-box

const message = new TextEncoder().encode('hello');
const nonce = nacl.randomBytes(nacl.box.nonceLength);
const ciphertext = nacl.box(
  message, nonce, recipientPublicKey, senderSecretKey,
);

Transmit the 24-byte nonce with the ciphertext and never repeat it for the same sender and recipient key pair.

Reject a modified public-key box open-public-key-box

const plaintext = nacl.box.open(
  ciphertext, nonce, senderPublicKey, recipientSecretKey,
);
if (plaintext === null) {
  throw new Error('authentication failed');
}

`box.open` returns `null` on failure; a 0-byte `Uint8Array` is valid plaintext and must remain distinct.

Prefix ciphertext with its nonce frame-box-message

const payload = new Uint8Array(nonce.length + ciphertext.length);
payload.set(nonce, 0);
payload.set(ciphertext, nonce.length);

const receivedNonce = payload.slice(0, nacl.box.nonceLength);
const receivedBox = payload.slice(nacl.box.nonceLength);

Nonce bytes are public, but your protocol must also authenticate external version, sender, and content-type metadata.

Protect bytes with a shared key encrypt-secretbox

const key = nacl.randomBytes(nacl.secretbox.keyLength);
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);
const ciphertext = nacl.secretbox(message, nonce, key);

const opened = nacl.secretbox.open(ciphertext, nonce, key);
if (opened === null) throw new Error('invalid secretbox');

XSalsa20-Poly1305 here is not key-committing, which matters if a receiver tests more than 1 candidate key.

Reuse a box shared key precompute-shared-key

const shared = nacl.box.before(peerPublicKey, localSecretKey);
const first = nacl.box.after(messageA, nonceA, shared);
const second = nacl.box.after(messageB, nonceB, shared);
const opened = nacl.box.open.after(first, nonceA, shared);

Every message still needs its own 24-byte nonce, and the precomputed value must be protected like any other secret key.

Create and verify a detached signature sign-detached

const signature = nacl.sign.detached(message, secretKey);
const valid = nacl.sign.detached.verify(message, signature, publicKey);
if (!valid) throw new Error('signature rejected');

Version 1.0.3 fixes incorrect signature generation; signature bytes are malleable and should not be unique record ids.

Recover a key pair from a seed derive-signing-key

if (seed.length !== nacl.sign.seedLength) {
  throw new Error('expected 32-byte seed');
}
const pair = nacl.sign.keyPair.fromSeed(seed);

The 32-byte seed needs full cryptographic entropy; a password or one ordinary password hash is not a substitute.

Calculate a SHA-512 digest hash-bytes

const message = new TextEncoder().encode('event');
const digest = nacl.hash(message);
if (digest.length !== nacl.hash.hashLength) throw new Error('bad digest');

This returns a raw 64-byte SHA-512 digest; do not construct a MAC as `hash(secret || message)` because length extension applies.

Compare non-empty equal-length values compare-byte-secrets

const same = candidate.length === expected.length &&
  candidate.length > 0 &&
  nacl.verify(candidate, expected);
if (!same) throw new Error('token mismatch');

`nacl.verify` returns false for 0-length arrays and mismatched lengths, while JavaScript prevents a physical constant-time guarantee.

Copy a public key to base64 encode-key-base64

const encoded = Buffer.from(publicKey).toString('base64');
const decoded = new Uint8Array(Buffer.from(encoded, 'base64'));
if (decoded.length !== nacl.box.publicKeyLength) {
  throw new Error('invalid public key');
}

`Buffer.from(view)` copies the visible 32 bytes; `Buffer.from(view.buffer)` can include bytes outside a returned subarray.

Install a platform random source supply-secure-prng

nacl.setPRNG((target, length) => {
  const bytes = readHardwareRandom(length);
  target.set(bytes.subarray(0, length));
});

`setPRNG` replaces the random source globally; use it only for a reviewed cryptographic generator and never `Math.random`.

Alternatives

PackageRegistryPick it when
libsodium-wrappersnpmUse it for a wider maintained Sodium API with password hashing, sealed boxes, and additional modern constructions.
@noble/ed25519npmUse it when the requirement is specifically a modern audited Ed25519 implementation rather than the NaCl primitive set.
josenpmUse it for interoperable JWT, JWS, JWE, JWK, and OAuth-facing key formats instead of inventing message framing.

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.