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

@d10f/crypto

v1.2.0

Published

A thin wrapper around the Web Crypto API to manage cryptographic keys comfortably.

Downloads

17

Readme


A thin wrapper around the Web Crypto API to manage cryptographic keys comfortably and in a type safe way.

Features

  • [x] Fluid builder pattern for intuitive and easy to use interface with full TypeScript support.
  • [x] All common operations supported: key generation, import, derivation, wrapping, etc.
  • [x] Export keys to various portable formats: JSON, PEM, base64 encoded or a good old raw byte array.
  • [ ] Meaningful error messages, consistent across all browsers. [^1]
  • [ ] Pre-defined functions with sensible defaults for commonly used cryptography operations: encryption, key wrapping, key derivation, key hashing, etc. (including error handling).
  • [ ] Client-side key management.

[^1]: Different browsers display different messages for the same errors, and none of them very useful.

Usage

Installation

You can install this package from npm by running this command:

npm install @d10f/crypto

The CryptoKeyBuilder

The entry point to the library operations. It doesn't need to be instantiated; it only has static methods according to the type of key that you need to build. Use this to build any type of key that you need. Since it uses a fluid builder API, you can just let the IDE autocomplete the next method, and fill in the blanks.

import { CryptoKeyBuilder } from '@d10f/crypto';

Generate keys

The CryptoKeyBuilder.generateKey static method can be used to generate a single key, which can be used for things like encryption, wrapping or key derivation:

const key = await CryptoKeyBuilder.generateKey()
  .withAlgorithm({ name: 'AES-GCM', length: 256 })
  .useTo('encrypt', () => {})
  .build();

Similarly, the CryptoKeyBuilder.generateKeyPair static method can be used to generate a key pair, for cryptographic operations of asymmetric nature, such as sign and verify.

const { privateKey, publicKey } = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({ name: 'ECDSA', namedCurve: 'P-256' })
  .useTo('sign', () => {})
  .useTo('verify', () => {})
  .build();

Other methods exist that mirror the native web crypto API to import keys previously exported to an external format, derive new keys out of some other value or unwrap them if they have been wrapped appropriately.

Define the key usage

A key needs to have an action, otherwise it's useless. This is provided by the useTo method during the build process, which takes two arguments: a string describing the intended use, and a callback function that will be invoked when that happens.

This callback function has its context bound to the resulting CryptoKeyObject instance, which also has access to the cryptographic material necessary to perform any operation. For this reason, you need to use the function keyword when declaring this callback, and accept this as its first parameter:

function myEncryptFn(
  this: CryptoKeyObject<'AES-GCM'>,
  clearText: BufferSource,
  iv: BufferSource,
) {
  return window.crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    this.key,
    clearText,
  );
}

const encryptionKey = await CryptoKeyBuilder.generateKey()
  .withAlgorithm({ name: 'AES-GCM', length: 256 })
  .useTo('encrypt', myEncryptFn)
  .build();

In this example, this.key refers to the native CryptoKey object, which is passed to the crypto.subtle.encrypt function.

Algorithm-specific parameters

One of the motivations behind this project is to solve the problem of having to remember the myriad of parameters options that are dependent on the type of key and how its created. For example, let's create an RSA key pair:

const keyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({
    name: 'RSA-OAEP',
    hash: 'SHA-256',
    modulusLength: 4096,
    publicExponent: new Uint8Array([0x1, 0x0, 0x1]),
  })
  .useTo('encrypt', () => {})
  .useTo('encrypt', () => {})
  .useTo('wrapKey', () => {})
  .useTo('unwrapKey', () => {})
  .build();

The object passed to withAlgorithm is quite tedious to remember, type and even to read. Also, while there's some safety at the type level, it's possible to make a typo and provide an invalid value to the modulusLength or publicExponent properties inadvertently; an innocent mistake that can be disastrous and difficult to detect.

To address this, there are algorithm-specific parameters readily available, named after the algorithm and properties they represent. There are also functions available that you can pass as the second argument to the useTo method, which already provide all the correct parameters in their definitions.

import { CryptoKeyBuilder } from '@d10f/crypto';
import { encryptWithRsaOaepParams, decryptWithRsaOaepParams } from '@d10f/crypto/actions';
import { rsaOaepSha256Key, keyWrapRsaOaep, keyUnwrap } from '@d10f/crypto/params';

const keyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm(rsaOaepSha256Key)
  .useTo('encrypt', encryptWithRsaOaepParams)
  .useTo('decrypt', decryptWithRsaOaepParams)
  .useTo('wrapKey', keyWrapRsaOaep)
  .useTo('unwrapKey', unwrapKey)
  .build();

This improves readability and allows the TypeScript to provide proper type support when invoking the key usage methods received during the build process.

Keep in mind, however, that depending on your use case you probably want to write your own callback functions. Make sure to check the src/actions directory to get an idea of how these functions are implemented and use them for reference.

The CryptoKeyObject

The output of any build operation is an object of type CryptoKeyObject. This wraps the native CryptoKey that is actually used to perform cryptographic operations, and provides additional methods based on the key usages given to the key during the build process.

Following the example from the previous section, the encryption key was given an encrypt method. This can be accessed and invoked directly:

const message = "This is a secret";
const encoded = new TextEncoder().encode(message);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const encrypted = await encryptionKey.encrypt(encoded, iv);

The encryptionKey.encrypt method is fully typed; the TypeScript compiler would complain if none of the passed arguments were of type BufferSource.

Export key as a portable format

One additional method is available, exportAs, to convert the cryptographic key to an external format. For example, to export a private key as a PEM-encoded string, ready to be shared over the network:

const pemEncodedPrivateKey = await privateKey.exportAs('PEM');

Note that this might throw an error if the key was not built with the exportable property set to "true". Take a look at the examples below to see how you can use the CryptoKeyBuilder.importKey static method to create a key that was exported in this way.

Examples

Create an AES encryption & decryption key

/**
 * A simple example of an AES 256-bit encryption key.
 */
const key = await CryptoKeyBuilder.generateKey()
  .withAlgorithm(aesGcm256Key)
  .useTo('encrypt', encryptWithAesGcmParams)
  .useTo('decrypt', decryptWithAesGcmParams)
  .build();

/**
 * The method 'encrypt' is now available as a method on the key,
 * fully typed and and ready to be used.
 */
const secret = 'Evil world domination plan';
const buffer = new TextEncoder().encode(secret);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await key.encrypt(buffer, { iv });

/**
 * Some time in the future, Bob decides to check his notes on how to
 * dominate the world. Using the same key, he's able to decrypt them.
 */
const cleartext = await key.decrypt(ciphertext, { iv });

Derive shared secret key (ECDH)

/**
 * Alice and Bob generate a key pair each.
 */
const aliceKeyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({ name: 'ECDH', namedCurve: 'P-256' })
  .useTo('deriveKey', deriveEcdhKeyFn)
  .build();

const bobKeyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({ name: 'ECDH', namedCurve: 'P-256' })
  .useTo('deriveKey', deriveEcdhKeyFn)
  .build();

/**
 * Alice and Bob can share their own public key with each other. Now,
 * they can combine their own private key with the other's public key
 * to produce a new symmetric key to encrypt & decrypt messages with.
 * This key is typically referred to as the "shared" or "secret" key.
 */
const aliceSharedKey = await aliceKeyPair.privateKey
  .deriveKey()
  .withParams({ name: 'ECDH', public: bobKeyPair.publicKey.key })
  .derivedAlgorithm(aesGcm256Key)
  .useTo('encrypt', encryptWithAesGcmParams)
  .build();

const secret = 'Hello, Bob!';
const buffer = new TextEncoder().encode(secret);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await aliceSharedKey.encrypt(buffer, { iv });

/**
 * Bob receives the encrypted message from Alice, and takes the same
 * steps to derive the shared key. He can now decrypt the message.
 * Note that in this example we have two variables named differently,
 * "aliceSharedKey" and "bobSharedKey", but they hold the exact same
 * cryptographic material.
 */
const bobSharedKey = await bobKeyPair.privateKey
  .deriveKey()
  .withParams({ name: 'ECDH', public: aliceKeyPair.publicKey.key })
  .derivedAlgorithm(aesGcm256Key)
  .useTo('decrypt', decryptWithAesGcmParams)
  .build();

const cleartext = await bobSharedKey.decrypt(ciphertext, {
  iv,
});

Derive AES key from shared secret (HKDF)

/**
 * This is similar to the example above where Alice and Bob exchange
 * their respective public keys to derive a shared secret. This time,
 * however, the shared secret is passed to a key derivation function
 * to obtain the final key.
 *
 * Note that this is done in two separate steps but it could also be
 * simplified further, depending on the `deriveSharedSecret` function,
 * defined further down the code.
 */
const aliceKeyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({ name: 'ECDH', namedCurve: 'P-384' })
  .useTo('deriveBits', deriveSharedSecret)
  .build();

const bobKeyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({ name: 'ECDH', namedCurve: 'P-384' })
  .useTo('deriveBits', deriveSharedSecret)
  .build();

/**
 * Alice and Bob are able to derive the raw bytes of the shared key,
 * which is then used to derive another encryption key with higher
 * entropy.
 */
const sharedSecretBytes = await aliceKeyPair.privateKey.deriveBits(
  bobKeyPair.publicKey,
  384,
);

const sharedSecretKey = await sharedSecretBytes
  .deriveKey()
  .withParams({
    ...hkdfSha256,
    salt: window.crypto.getRandomValues(new Uint8Array(16)),
    info: new TextEncoder().encode('associated data'),
  })
  .derivedAlgorithm(aesGcm256Key)
  .useTo('encrypt', encryptWithAesGcmParams)
  .useTo('decrypt', decryptWithAesGcmParams)
  .build();

/**
 * This can be used to encrypt/decrypt messages as needed.
 */
const secret = 'Hello, Bob!';
const buffer = new TextEncoder().encode(secret);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await sharedSecretKey.encrypt(buffer, { iv });

const cleartext = await sharedSecretKey.decrypt(ciphertext, {
  iv,
});

async function deriveSharedSecret(
  this: CryptoKeyObject<'ECDH'>,
  publicKey: CryptoKeyObject<'ECDH'>,
  length: number,
) {
  const bits = await deriveBitsFromEcdhKey.call(this, publicKey, length);

  return CryptoKeyBuilder.importKey()
    .as('raw')
    .from(bits)
    .withAlgorithm({ name: 'HKDF' })
    .useTo('deriveKey', function (this: CryptoKeyObject<'HKDF'>) {
      return CryptoKeyBuilder.deriveKey().fromKey(this);
    })
    .build();
}

Derive AES key from shared secret (X25519)

/**
 * This example also shows hwo to derive an encryption key after
 * performing a key exchange, but using the X25519 algorithm, instead
 * of ECDH.
 *
 * It's nearly identical to the previous example shown. Only the
 * callback function deriving the key is different.
 */

const aliceKeyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({ name: 'X25519' })
  .useTo('deriveKey', deriveSharedKey)
  .build();

const bobKeyPair = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm({ name: 'X25519' })
  .useTo('deriveKey', deriveSharedKey)
  .build();

const salt = crypto.getRandomValues(new Uint8Array(32));
const info = new TextEncoder().encode(
  'X25519 key agreement for an AES-GCM-256 key',
);

const aliceSharedSecret = await aliceKeyPair.privateKey.deriveKey(
  bobKeyPair.publicKey,
  salt,
  info,
);

const aliceEncryptionKey = await aliceSharedSecret
  .deriveKey()
  .derivedAlgorithm({ name: 'AES-GCM', length: 256 })
  .useTo('encrypt', encryptWithAesGcmParams)
  .build();

const secret = 'Hello, Bob!';
const buffer = new TextEncoder().encode(secret);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await aliceEncryptionKey.encrypt(buffer, { iv });

/**
 * Later, Bob takes the exact same steps to produce the same shared
 * encryption key.
 */
const bobSharedSecret = await bobKeyPair.privateKey.deriveKey(
  aliceKeyPair.publicKey,
  salt,
  info,
);

const bobEncryptionKey = await bobSharedSecret
  .deriveKey()
  .derivedAlgorithm({ name: 'AES-GCM', length: 256 })
  .useTo('decrypt', decryptWithAesGcmParams)
  .build();

const cleartext = await bobEncryptionKey.decrypt(ciphertext, {
  iv,
});

async function deriveSharedKey(
  this: CryptoKeyObject<'X25519'>,
  publicKey: CryptoKeyObject<'X25519'>,
  salt: BufferSource,
  info: BufferSource,
) {
  const hkdfParams: HkdfParams = {
    name: 'HKDF',
    hash: 'SHA-256',
    salt,
    info,
  };

  return CryptoKeyBuilder.deriveKey()
    .fromKey(this)
    .withParams({ name: 'X25519', public: publicKey.key })
    .derivedAlgorithm(hkdfParams)
    .useTo('deriveKey', function (this: CryptoKeyObject<'HKDF'>) {
      return CryptoKeyBuilder.deriveKey()
        .fromKey(this)
        .withParams(hkdfParams);
    })
    .build();
}

Derive AES key from password (PBKDF2)

/**
 * Assume the user has provided a password through an input field.
 */
const password = 'Sup3r_$ecret';
const encoded = new TextEncoder().encode(password);

/**
 * Derive key material to derive the encryption key from.
 */
const keyMaterial = await CryptoKeyBuilder.importKey()
  .as('raw')
  .from(encoded)
  .withAlgorithm({ name: 'PBKDF2' })
  .useTo('deriveKey', derivePbkdf2KeyFn)
  .build();

const encryptionKey = await keyMaterial
  .deriveKey()
  .withParams({
    ...pbkdf2Sha256,
    salt: window.crypto.getRandomValues(new Uint8Array(32)),
  })
  .derivedAlgorithm(aesGcm256Key)
  .useTo('encrypt', encryptWithAesGcmParams)
  .useTo('decrypt', decryptWithAesGcmParams)
  .build();

const secret = 'some personal data';
const buffer = new TextEncoder().encode(secret);
const iv = window.crypto.getRandomValues(new Uint8Array(16));
const ciphertext = await encryptionKey.encrypt(buffer, { iv });
const cleartext = await encryptionKey.decrypt(ciphertext, { iv });

Encrypt & export a key (wrap it) in a portable format

const { privateKey } = await CryptoKeyBuilder.generateKeyPair()
  .withAlgorithm(rsaOaepSha256Key)
  .useTo('encrypt', encryptWithRsaOaepParams)
  .useTo('decrypt', decryptWithRsaOaepParams)
  .canExportKey()
  .build();

const wrappingKey = await CryptoKeyBuilder.generateKey()
  .withAlgorithm(aesGcm256Key)
  .useTo('wrapKey', AES_GCM_WRAP)
  .useTo('unwrapKey', unwrapKey)
  .build();

const privateWrappedKey = await wrappingKey.wrapKey(privateKey, 'PEM', {
  iv: window.crypto.getRandomValues(new Uint8Array(12)),
});

This example exports an RSA private signing key in a PEM-encoded format.

License

GNU General Public License v3.0 only

See COPYING to see the full text.