npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

iso-crypto

v3.1.3

Published

Cryptography with one API in browsers and Node.js, over WebCrypto and node:crypto, for hashing, AES encryption (including GCM), PBKDF2 and HKDF key derivation, and ECDH.

Readme

iso-crypto

Cryptography with one API in browsers and Node.js, over WebCrypto and node:crypto, for hashing, AES encryption (including GCM), PBKDF2 and HKDF key derivation, and ECDH.

npm package License

Node.js and browsers both have cryptography built in, but with different APIs. Node.js has the node:crypto module, and browsers have WebCrypto (crypto.subtle), which also wants every key imported as a CryptoKey before it will use it.

iso-crypto provides one async API for both, for hashing, encryption, key derivation and ECDH. Package imports conditions pick node:crypto on Node.js and crypto.subtle in the browser, so the same code runs in both. Secrets, keys and IVs are plain Uint8Arrays or strings, so there's no importKey or CryptoKey to deal with.

  • encrypt defaults to AES-256-GCM, which detects tampering. decrypt rejects altered content or the wrong secret, and additional authenticated data can bind a ciphertext to its context.
  • deriveKey makes a key from a password with PBKDF2, or from a high-entropy secret with HKDF, on both platforms.
  • eccEncrypt and eccDecrypt encrypt between two parties in one call. They derive a shared secret from one party's private key and the other's public key, then encrypt with AES.
  • ECC keys can be generated, and public keys compressed or decompressed, on P-256, P-384 and P-521.
  • Some lower level utilities are included too, like text, hex, base64 and base64url encoding, and secure random bytes.

encrypt stretches its secret with a single hash, which isn't enough for a password, so derive keys from passwords with deriveKey first. There is no scrypt, Argon2, secp256k1 or X25519. For those, see @noble/hashes and @noble/curves.

Contents

Supported Algorithms

Hash

  • ✅ sha1
  • ✅ sha256
  • ✅ sha384
  • ✅ sha512

Symmetric Encryption

  • ✅ aes-128-cbc
  • ⚠️ aes-192-cbc (deprecated)
  • ✅ aes-256-cbc
  • ✅ aes-128-ctr
  • ⚠️ aes-192-ctr (deprecated)
  • ✅ aes-256-ctr
  • ✅ aes-128-gcm
  • ✅ aes-256-gcm

192-bit keys are deprecated, because Chromium-based browsers (Chrome, Edge, …) do not support them, so aes-192-* rejects there. They still work on Node.js, Firefox and Safari, so existing data stays readable, but use 128 or 256 bits for anything new. GCM only accepts 128 or 256 bits.

Key Derivation

  • ✅ PBKDF2
  • ✅ HKDF

ECDH (Asymmetric)

  • ✅ prime256v1
  • ✅ secp384r1
  • ✅ secp521r1

Install

npm i iso-crypto

No dependencies or peer dependencies. Requires Node.js 22.12 or later (engines); browsers use crypto.subtle.

Example

Encoding

import { decode, encode } from 'iso-crypto';

const buffer: Uint8Array = decode('This is my text');
const encoded = encode(buffer); // 'This is my text'

Random

import { randomBytes } from 'iso-crypto';

const rand: Uint8Array = await randomBytes(10);

Hash

import { encode, hash } from 'iso-crypto';

const hashed: Uint8Array = await hash('This is my text');
encode(hashed, 'hex'); // 04405de6b2dcefcd85a03503cefad765a88c8083c5fede2eb7e4354e5643e2df

const customHashed: Uint8Array = await hash(
    { text: 'abc123', encoding: 'hex' },
    {
        algorithm: 'SHA2',
        size: 512,
    }
);
encode(customHashed, 'base64'); // 8ff0IFBTdySF86lYF23zbCfmerD9ieLgid5Ak+5pRwffOgSY8yQ78HifX0lxZ0hKHZP4uZ0Nfr9dcwZmo5Mkfw==

Symmetric Encryption

import { decrypt, encode, encrypt } from 'iso-crypto';

const secret = 'Super duper secret password';

const encrypted = await encrypt({
    data: 'This is my message',
    secret,
});
const decrypted = encode(await decrypt({
    ...encrypted,
    secret,
})); // 'This is my message'

const customAlgEncrypted = await encrypt(
    {
        data: 'This is my response',
        secret,
    },
    {
        hash: { algorithm: 'SHA1' },
        encryption: {
            cipher: 'AES',
            size: 128,
            mode: 'CBC',
        },
    }
);
const customAlgDecrypted = encode(await decrypt(
    {
        ...customAlgEncrypted,
        secret,
    },
    {
        hash: { algorithm: 'SHA1' },
        encryption: {
            cipher: 'AES',
            size: 128,
            mode: 'CBC',
        },
    }
)); // 'This is my response'

Authenticated encryption with a password

import { decrypt, deriveKey, encrypt, randomBytes } from 'iso-crypto';

// Store the salt alongside the encrypted data
const salt = await randomBytes(16);
const key = await deriveKey({ secret: 'correct horse battery staple', salt });

// The key is already the right size, so skip hashing it
const encrypted = await encrypt({ data: 'This is my message', secret: key }, { hash: 'raw' });

// Rejects if `encrypted` was altered, or the key is wrong
const decrypted = await decrypt({ ...encrypted, secret: key }, { hash: 'raw' });

ECC

import { eccDecrypt, eccEncrypt, encode, generateEccPrivateKey, generateEccPublicKey } from 'iso-crypto';

const anne: Uint8Array = await generateEccPrivateKey();
const bob: Uint8Array = await generateEccPrivateKey();

const fromAnneEncrypted = await eccEncrypt({
    data: 'Hello, would you like a cup to tea?',
    privateKey: anne,
    publicKey: generateEccPublicKey(bob),
});
const fromAnneDecrypted = encode(await eccDecrypt({
    ...fromAnneEncrypted,
    privateKey: bob,
})); // 'Hello, would you like a cup to tea?'

const fromBobEncrypted = await eccEncrypt(
    {
        data: 'I am more of a coffee drinker myself',
        privateKey: bob,
        publicKey: generateEccPublicKey(anne),
    },
    {
        encryption: {
            cipher: 'AES',
            size: 128,
            mode: 'GCM',
        },
    }
);
const fromBobDecrypted = encode(await eccDecrypt(
    {
        ...fromBobEncrypted,
        privateKey: anne,
    },
    {
        encryption: {
            cipher: 'AES',
            size: 128,
            mode: 'GCM',
        },
    }
)); // 'I am more of a coffee drinker myself'

Usage

iso-crypto is an ESM module. That means it must be imported. To load from a CJS module, use dynamic import const { eccEncrypt } = await import('iso-crypto');.

Most cryptographic methods return instances of Uint8Array, instead of strings. Those can encoded as text via the encode method. In NodeJS the response is often actually a Buffer which extends Uint8Array.

Any method that takes input "text" (InputText, see Types) allows 3 formats:

  • string - inferred as UTF8 encoding
  • Uint8Array - "raw" encoding
  • { text: string; encoding: string } - text with a custom encoding
    • Supported encodings: hex, base64, base64url, utf8

A Uint8Array input must be typed Uint8Array<ArrayBuffer> (what new Uint8Array(n), decode and every iso-crypto output give you). A value typed as a plain Uint8Array (Uint8Array<ArrayBufferLike>) is a type error, because it could be backed by a SharedArrayBuffer.

Algorithm options are literal types. An options object declared on its own widens 'AES' to string and 128 to number, which is a type error when passed in. Annotate it (const encryption: Encryption = { ... }), add as const, or write it inline in the call. The Sizes, Ciphers, Modes and Algorithms enums are exported and accepted in place of the literals.

Any method that performs hashing can take the Hash algorithm as an option. The default algorithm is SHA256. In any case where hashing should not be used (e.g. custom hashing already implemented), declare the raw option.

e.g.

import { encrypt } from 'iso-crypto';

await encrypt(
    {
        data: 'My message',
        secret: 'Already hashed',
    },
    {
        hash: 'raw',
    }
);

Any method that performs symmetric encryption can take the encryption algorithm as an option. The default algorithm is aes-256-gcm ({ cipher: 'AES', size: 256, mode: 'GCM' }). Before iso-crypto 3 it was aes-256-ctr, see Upgrading to 3.0.

Similarly the default ECC algorithm is prime256v1 ({ curve: 'p256' }).

Many algorithms require keys of a fixed size. The general approach is to hash the input, then adjust the bytes to fit: buffers that are too large are stripped from their beginning to match the desired size. With CBC and CTR, buffers that are too small have 0 prepended, as they always have. GCM rejects them instead (a raw key must be exactly the key size, and a hash at least that long), since padding silently weakens the key.

It is important to note that encryption IS NOT the same as compression. In fact encryption methods will generally be slightly larger than the original input.

Ideally any cryptographic output should have statistically uniform distribution of their outputs. That means every byte in the output is equally likely to be 00 as it is FF, and everything in between.

As such, if you intend to combine encryption with compression, it is important to apply compression before encryption, to take advantage of repeated and common patterns in the input.

Many methods in this module deal with "private" or "secret" values. Proper handling of those values are not explicitly covered by this module. Make sure you understand how to securely transfer and access these values before deploying encryption in production, as improper handling and data leaks can effectively render even the most secure encryption algorithms moot.

Upgrading to 3.0

  • The default encryption is AES-256-GCM, instead of AES-256-CTR. GCM detects tampering, so decrypt rejects altered content or the wrong secret. Data encrypted with the old default, by encrypt or eccEncrypt without an encryption option, no longer decrypts with the default: it rejects rather than returning garbage. Pass unsafeLegacyEncryption, the old default, to decrypt or eccDecrypt to read it:

    import { decrypt, unsafeLegacyEncryption } from 'iso-crypto';
    
    await decrypt({ encrypted, iv, secret }, { encryption: unsafeLegacyEncryption });

    Data encrypted with an explicit encryption option is unaffected. Consider re-encrypting old data with the new default.

  • decode rejects invalid hex. Hex text with characters other than 0-9 and a-f throws a TypeError, where those characters used to decode as 0. Valid hex decodes as before.

  • 192-bit AES keys are deprecated, as Chromium-based browsers do not support them. They still work for CBC and CTR where supported.

Implementation Details

These are far from thorough explanations of cryptographic functions, but hopefully serve as enough to justify design decisions and explain method interfaces.

Hashing

Hashing algorithms are a "one way" programs, that output a fixed-size buffer that is effectively random for a given input. Re-hashing the same input with the algorithm always returns the same output, but given the output it is virtually impossible to guess a valid input. Similarly it is virtually impossible to "guess" the output without fully applying the algorithm.

Symmetric Encryption

Symmetric encryption algorithms take a message (a.k.a. data) and a "key" (a.k.a. secret) and produce a seemingly random output that represents that message. This output is effectively safe to expose publicly, as the original message is cryptographically hidden.

The same key (this is what makes it "symmetric") can then be used decrypt the output and retrieve the original message.

The key length is specified by the algorithm (e.g. aes-256-ctr requires 32 bytes), which may not necessarily match the actual length of the secret provided. Similarly the provided secret may not have "uniform distribution" of bytes (e.g. Password123 only has bytes between 30-7A). To mediate, the secret is hashed, and has bytes removed from (or, for CBC and CTR, prepended to) the beginning of the buffer to match. A single hash is not enough for passwords: derive the key with deriveKey and pass it with { hash: 'raw' }.

These algorithms also require an effectively random "Initialization Vector" (a.k.a. iv). This buffer is not necessarily private, but it must not be known ahead of time, and should never be reused. Encryption methods in iso-crypto will generate this iv internally from a random number generator (see randomBytes) and exposed in the output alongside the encrypted data.

Given that the same secret it used to both encrypt and decrypt a message, it is not appropriate for sending messages between multiple parties. Instead, it should be used only in cases where the data is being stored/transferred by an untrusted third party and then re-read by original party (e.g. storing sensitive files on a third party server).

Asymmetric Encryption

Asymmetric encryption differs from Symmetric encryption in regards to the "secret". In Asymmetric encryption, messages are secured with both a private key (known only to one party, treated as secret) and a public key (derived from a private key. Safe, if not encouraged, to share publicly).

ECDH

Presently, the only form of asymmetric encryption implemented by this module is Elliptic Curve Diffie-Hellman (ECDH).

How ECC works

Without going into extreme details of curve mathematics, the algorithm is based on points on curve. "Adding" (for simplicity, traditional mathematical terms/symbols are used, but the logic itself is not as trivial as traditional addition) points on a curve is a straightforward equation. "Multiplying" a point can only be achieved by adding the original point N times, which can be simplified into binary logic (doubling + adding). These new points also lie on the curve (and are therefore verifiable).

For example P1 * 10 can be simplified as P1 * ((8 * 1) + (4 * 0) + (2 * 1) + (1 * 0)) -> P1 * 8 + P1 * 2. Note that P1 * 2 can be rewritten as P1 + P1 (straightforward) and P1 * 4 can be rewritten as P2 = P1 * 2; P2 * 2 (re-using a cached value). As such, multiplying a Point can be solved in O(log(n)) time (multiplying by 2^256 takes ~ 256 "steps") which is very acceptable.

Given the complication of multiplication, there is no known "division" operator in ECC. As such, an algorithm can agree on an original point on a curve. Then the "private key" is simply a random number between the curves "domain" (prime256v1 domain is between 0 and 2^256). The "public key" is the resulting point on the curve after multiplication.

A public key technically represents a point (both X and Y coordinates). However, since the equation for the curve is known, it is possible to provide only the X coordinate, and a single byte indicating whether the associated Y coordinate is odd or even. This is known as the "compressed" form of a public key.

Given one parties public key (a point), and another's private key (an integer), these can again be multiplied to produce a "shared secret". Due to the communicative properties of addition/multiplication, both combinations of PublicA * PrivateB AND PrivateA * PublicB produces the same shared secret.

Implementation

This module focuses on the point mathematics required by ECC (relying on native modules as much as possible). Once a shared secret can be calculated, encryption is performed by the symmetric encryption algorithms provided by this module.

With GCM, the AES key is derived from the shared secret with HKDF-SHA256 (no salt, with iso-crypto ECDH <curve> AES-<size>-GCM as the info). The shared secret is a curve coordinate rather than uniformly random bytes, and the info keeps one key pair from producing the same key for two algorithms.

With CBC and CTR, the shared secret is used as the key directly (trimmed to the key size), as it always has been, so existing data stays readable. No hash option applies to ECC encryption.

Any public keys returned by this module will use the compressed form of a public key by default, however both forms are accepted as input.

API

randomBytes(size: number)

Produces a promise of Uint8Array of length size filled with random bytes.

decode(input: InputText)

Decodes any input as Uint8Array, given the provided encoding (see InputText).

Throws a TypeError for hex text with characters other than 0-9 and a-f (in either case). Odd-length hex implies a leading 0.

encode(input: InputText, outputEncoding?: 'hex' | 'base64' | 'base64url' | 'utf8')

Encodes any input (ideally as Uint8Array, but potentially as a differently encoded text) as the desired encoding.

Defaults to UTF8.

decodeObject(input: Record<string, string>)

Decodes each attribute in an object to Uint8Array. Convenience method around manually calling decode for each attribute.

Potentially useful for converting serialized text (e.g. JSON containing hex-decoded data) to usable buffers.

encodeObject(input: Record<string, Uint8Array>)

Encodes each attribute in an object to string. Convenience method around manually calling encode for each attribute.

Potentially useful for serializing decoded text (JSON does not like raw Uint8Array).

hash(input: InputText, algorithm?: Hash)

Asynchronously hashes the input, and returns the output buffer. Defaults to SHA256 algorithm. With 'raw' the input is returned unchanged (as bytes).

algorithm is { algorithm: 'SHA1' } or { algorithm: 'SHA2', size: 256 | 384 | 512 }.

encrypt(params: { data: InputText; secret: InputText }, options?: { hash?: Hash; encryption?: Encryption })

Asynchronously encrypts the input using the provided algorithms (defaults to aes-256-gcm). Hashes the secret internally with the provided hashing algorithm (defaults to SHA256).

Returns both the encrypted data as a Uint8Array, as well as the internally generated iv.

Both of these properties will need to be provided to the decrypt method in order to decrypt the original data.

decrypt(params: { encrypted: InputText; iv: InputText; secret: InputText }, options?: { hash?: Hash; encryption?: Encryption })

Asynchronously decrypts the data using the provided algorithms. Make sure that these are the same algorithms used to encrypt the data originally.

Both the encrypted + iv comes from the output of encrypt, alongside the original secret.

With GCM, the last 16 bytes of encrypted are the authentication tag (the same layout as Web Crypto), and decrypt rejects if the content, IV or secret don't match what was encrypted.

Additional authenticated data

With GCM, encrypt, decrypt, eccEncrypt and eccDecrypt take an additionalData option: data that is authenticated but not encrypted, and not part of the output. Decryption rejects unless it is given the same value, which binds a ciphertext to its context, e.g. a record ID, so it can't be swapped into another record.

const encryption = { cipher: 'AES', size: 256, mode: 'GCM' } as const;
const encrypted = await encrypt({ data, secret }, { encryption, additionalData: `user:${userId}` });

// Rejects if `encrypted` was stored under a different user
await decrypt({ ...encrypted, secret }, { encryption, additionalData: `user:${userId}` });

Other modes don't authenticate anything, so they throw a TypeError rather than ignore it.

deriveKey(params: { secret: InputText; salt: InputText }, options?: Pbkdf2Options | HkdfOptions)

Asynchronously derives a key of a fixed size from a secret. Use the result as the secret of encrypt/decrypt with { hash: 'raw' }.

salt is a random value (e.g. randomBytes(16)) stored alongside whatever the key protects, so equal secrets derive different keys.

| Option | Type | Default | Description | | ------------ | -------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | algorithm | 'PBKDF2' \| 'HKDF' | 'PBKDF2' | PBKDF2 is deliberately slow, for passwords. HKDF is fast, for secrets that are already random, such as an ECDH shared secret. | | hash | HashAlgorithm | SHA256 | Hash the derivation is built on: { algorithm: 'SHA1' } or { algorithm: 'SHA2', size: 256 \| 384 \| 512 }. Unlike Hash, 'raw' is not accepted. | | size | number | 256 | Size of the key in bits, a multiple of 8. | | iterations | number | 600_000 | PBKDF2 only. OWASP's recommendation for PBKDF2 with SHA256. | | info | InputText | '' | HKDF only. Context for the key, so one secret can derive several unrelated keys. |

generateEccPrivateKey(curve?: Curve)

Asynchronously generates an ECC private key as a Uint8Array.

generateEccPublicKey(privateKey: InputText, curve?: Curve)

Generates the corresponding public key for the provided private key.

compressEccPublicKey(publicKey: InputText, curve?: Curve)

Calculates the corresponding compressed public key for the provided public key.

NOOP if public key is already compressed (idempotent).

Generally available for convenience. All public keys outputted by ecc methods are compressed by default, and accept both forms as input.

decompressEccPublicKey(publicKey: InputText, curve?: Curve)

Calculate the corresponding uncompressed public key for the provided public key.

NOOP if public key is already uncompressed (idempotent).

Throws if a compressed key isn't a point on the curve (or isn't a compressed key at all). In the browser, eccEncrypt and eccDecrypt decompress the publicKey they're given, so they reject such keys the same way.

eccEncrypt(params: { data: InputText; privateKey: InputText; publicKey: InputText }, options?: { curve?: Curve; encryption?: Encryption })

Asynchronously determines the shared secret between public + private key, and encrypts the data using the specified encryption algorithm (defaults to aes-256-gcm).

Ensure that the keys are generated using the same curve algorithm provided.

Note that the publicKey should be the "receiver's" public key, not simply the public key for the provided privateKey ("sender").

Returns both the encrypted + iv data (see encrypt) as well as the publicKey corresponding to the input privateKey for convenience.

eccDecrypt(params: { encrypted: InputText; iv: InputText; privateKey: InputText; publicKey: InputText }, options?: { curve?: Curve; encryption?: Encryption })

Asynchronously decrypts the data from eccEncrypt using the receiver's privateKey and the sender's publicKey.

Ensure that the provided curve + encryption algorithm is the same as used to encrypt.

Types

Each is exported as a type, e.g. import type { InputText } from 'iso-crypto'.

InputText

Type used to represent allowed text/data input.

string | Uint8Array<ArrayBuffer> | { text: string; encoding: 'hex' | 'base64' | 'base64url' | 'utf8' } | { text: Uint8Array<ArrayBuffer>; encoding: 'raw' }.

Uint8ArrayBuffer is the Uint8Array<ArrayBuffer> member.

To verify the "buffer" input, pass the text to decode (which is done internally).

A plain string is inferred to be UTF8 encoded.

Hash

Type used to represent a specific hash algorithm.

{ algorithm: 'SHA1'; size?: 160 } | { algorithm: 'SHA2'; size: 256 | 384 | 512 } | 'raw'.

To avoid hashing (as it is sometimes used internally by algorithms) provide the 'raw' option to return the input unchanged. HashAlgorithm is the same type without 'raw'.

Generally defaults to { algorithm: 'SHA2', size: 256 }.

Encryption

Type used to represent a specific symmetric encryption algorithm.

{ cipher: 'AES'; size: 128 | 192 | 256; mode: 'CBC' | 'CTR' } | { cipher: 'AES'; size: 128 | 256; mode: 'GCM' }.

A size of 192 is deprecated, see Symmetric Encryption.

Defaults to { cipher: 'AES', size: 256, mode: 'GCM' }. unsafeLegacyEncryption is the default before iso-crypto 3, { cipher: 'AES', size: 256, mode: 'CTR' }.

Curve

Type used to represent a specific ECC curve. 'p256' | 'p384' | 'p521' (prime256v1, secp384r1, secp521r1).

Generally defaults to p256.