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

@efficimo/cipher

v0.1.0

Published

Cryptographic primitives — AES-256-GCM, PBKDF2, WebAuthn PRF key derivation, CipherVault observable

Readme

@efficimo/cipher

Cryptographic primitives for the browser and Node.js — AES-256-GCM encryption, PBKDF2 key derivation, WebAuthn PRF, and an observable vault that unlocks reactively.

Built on the Web Crypto API. No external crypto dependencies.

Installation

npm install @efficimo/cipher

@efficimo/observable is a peer dependency required by CipherVault:

npm install @efficimo/observable

Usage

Symmetric encryption (password-based)

import { generateSalt, deriveKey, encrypt, decrypt } from "@efficimo/cipher";

const salt = generateSalt();
const key = await deriveKey("my-passphrase", salt);

const payload = await encrypt("sensitive data", key);
// { ciphertext: "...", iv: "...", version: 1 }

const result = await decrypt(payload, key);
new TextDecoder().decode(result); // "sensitive data"

To store and later re-derive the same key, attach the salt to the payload:

const stored = { ...payload, salt: btoa(String.fromCharCode(...salt)) };

Key rotation

import { reEncrypt, deriveKey, generateSalt } from "@efficimo/cipher";

const newKey = await deriveKey("new-passphrase", generateSalt());
const rotated = await reEncrypt(payload, oldKey, newKey);

CipherVault — observable unlock state

CipherVault wraps a key in memory and exposes an ObservableValue<boolean> that components can subscribe to — no polling, no manual event listeners.

import { CipherVault, deriveKey, generateSalt } from "@efficimo/cipher";

const vault = new CipherVault();

vault.isUnlocked.subscribe((unlocked) => {
  console.log("vault is", unlocked ? "open" : "locked");
});

const key = await deriveKey("passphrase", generateSalt());
vault.unlock(key); // → subscriber fires with true

const payload = await vault.encrypt("top secret");
const result  = await vault.decrypt(payload);

vault.lock(); // → subscriber fires with false

The Cipher interface is implemented by CipherVault, so you can type parameters against it:

import type { Cipher } from "@efficimo/cipher";

async function saveNote(cipher: Cipher, text: string) {
  return cipher.encrypt(text);
}

WebAuthn PRF — biometric key derivation

Imported from @efficimo/cipher/webauthn. Same biometric on the same device always produces the same CryptoKey — without ever storing the key.

import { isBiometricSupported, BiometricAuth } from "@efficimo/cipher/webauthn";
import { CipherVault } from "@efficimo/cipher";

if (await isBiometricSupported()) {
  const auth  = new BiometricAuth();
  const vault = new CipherVault();

  // First time: register
  const credential = await auth.register({ userId: "u1", userName: "alice" });

  // Subsequent visits: authenticate → unlock vault
  const key = await auth.authenticate(credential.id);
  vault.unlock(key);

  const payload = await vault.encrypt("biometric-protected data");
}

Credential metadata (ID, userName) is stored in localStorage. The key itself is never stored.

Browser support (PRF extension): Chrome 116+, Edge 116+, Safari 17.4+. Firefox is not supported. Always call isBiometricSupported() before showing a biometric option, and fall back to password-based deriveKey() otherwise.

API

Core — @efficimo/cipher

function generateSalt(): Uint8Array

function deriveKey(
  password: string,
  salt    : Uint8Array,
  options ?: { iterations?: number; hash?: "SHA-256" | "SHA-512" }
): Promise<CryptoKey>

function encrypt(data: string | ArrayBuffer, key: CryptoKey): Promise<EncryptedPayload>
function decrypt(payload: EncryptedPayload,  key: CryptoKey): Promise<ArrayBuffer>

function reEncrypt(
  payload: EncryptedPayload,
  oldKey : CryptoKey,
  newKey : CryptoKey
): Promise<EncryptedPayload>

class CipherVault implements Cipher {
  readonly isUnlocked: ObservableValue<boolean>
  unlock(key: CryptoKey): void
  lock(): void
  encrypt(data: string | ArrayBuffer): Promise<EncryptedPayload>
  decrypt(payload: EncryptedPayload):  Promise<ArrayBuffer>
}

interface Cipher {
  encrypt(data: string | ArrayBuffer): Promise<EncryptedPayload>
  decrypt(payload: EncryptedPayload):  Promise<ArrayBuffer>
}

WebAuthn — @efficimo/cipher/webauthn

function isBiometricSupported(): Promise<boolean>

class BiometricAuth {
  register(options: {
    userId      : string
    userName    : string
    displayName?: string
    rpName     ?: string
  }): Promise<CredentialInfo>

  authenticate(credentialId: string): Promise<CryptoKey>
  listCredentials(): CredentialInfo[]
  removeCredential(credentialId: string): void
}

Types

type EncryptedPayload = {
  ciphertext: string   // base64 — AES-256-GCM ciphertext
  iv        : string   // base64 — 96-bit IV (never reused)
  salt     ?: string   // base64 — PBKDF2 salt (attached by caller)
  version   : number   // format version
}

type CredentialInfo = {
  id         : string  // credential ID (base64url)
  userName   : string
  displayName: string
  createdAt  : number  // Unix timestamp
}

Security notes

  • PBKDF2 iterations: 600 000 (OWASP 2024 recommendation). Higher than most legacy implementations.
  • Keys are non-extractable: deriveKey() sets extractable: false — keys cannot leave SubtleCrypto.
  • IV is never reused: encrypt() always generates a fresh IV via crypto.getRandomValues().
  • PRF salt is fixed per application: the same eval salt ensures deterministic key derivation across sessions.
  • Broadcasts / shared secrets: this library handles single-recipient encryption. For group or broadcast scenarios, symmetric key distribution is the caller's responsibility.

License

MIT