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

@salvobee/crypto-vault

v3.0.0

Published

Browser crypto vault: AES-GCM 256, chunked blobs, optional gzip, base64url packaging.

Readme

Crypto Vault

Universal, zero-deps encryption for Browser + Node.js. Encrypt strings and files (small or huge) with AES-GCM-256, optional gzip, and ship the result as a single Base64URL string—perfect for APIs, DBs, and copy-paste sharing.

📚 API docs: https://salvobee.github.io/crypto-vault/


Why Crypto Vault?

Most apps don’t need a heavyweight crypto stack—they need something portable, boring-reliable, and easy to ship:

  • 🔐 Strong, authenticated encryption (AES-GCM-256 via Web Crypto / Node WebCrypto)
  • 🧩 Large file support with automatic chunking (images, videos, PDFs…)
  • 🗜️ Optional compression (gzip) when it helps; silently skipped if not supported
  • 🔤 Base64URL packaging so ciphertext travels as plain text anywhere
  • 🌐 One library for modern browsers and Node 18+

Concept: Crypto Vault turns any input into an opaque, versioned, self-described blob you can store as text and decrypt only with the right key—on any modern runtime.


Common use cases

  • End-to-end encrypted notes & messages (store as text in your DB)
  • Secure media vaults (photos, videos, PDFs) with streaming-friendly chunks
  • Client-side encryption before upload (privacy by default)
  • Sharing secrets across devices/teammates (wrap keys or use passphrase+salt)
  • Portable encrypted backups (download a key, keep data anywhere)

At a glance (benefits)

  • Zero deps (lean, audit-friendly)
  • Stable format with magic header + version (future-proof)
  • Simple APIs for strings & blobs
  • TypeScript typings + TSDoc
  • Works offline—no external services needed

Table of contents


Install

npm i @salvobee/crypto-vault
# or
pnpm add @salvobee/crypto-vault
# or
yarn add @salvobee/crypto-vault

ESM only. Works in modern browsers (HTTPS/localhost) and Node.js ≥ 18.


Quickstart (30 seconds)

<script type="module">
  import {
    generateAesKey,
    encryptString, decryptToString,
    encryptBlob,  decryptToBlob,
  } from "@salvobee/crypto-vault";

  // 1) Generate a key (do this once, store it safely)
  const key = await generateAesKey();

  // 2) Encrypt / decrypt a string
  const packed = await encryptString("Hello vault!", key, { compress: true });
  const text   = await decryptToString(packed, key);

  // 3) Encrypt / decrypt a file/blob
  const file = new File(["hello"], "hello.txt", { type: "text/plain" });
  const packedBlob = await encryptBlob(file, key);             // Base64URL
  const blob       = await decryptToBlob(packedBlob, key);     // Blob
</script>

That’s it. You now have ciphertext you can safely store/send as plain text.


How it works (high level)

  • AES-GCM-256 for confidentiality + integrity.
  • Fresh random IV per message/chunk (GCM best practice).
  • Optional gzip (Compression Streams in browsers, zlib in Node).
  • A compact binary container (with magic bytes, version, flags, JSON meta, payload) is Base64URL-encoded so you can store it anywhere as text.

API overview

Full signatures & details: API docshttps://salvobee.github.io/crypto-vault/

  • Key management

    • generateAesKey()
    • exportKeyToBase64(key) / importKeyFromBase64(b64)
    • deriveKeyFromPassphrase(passphrase, saltU8, iterations?)
    • RSA helpers for sharing: generateRsaKeyPair(), wrapKeyForRecipient(), unwrapKeyForRecipient(), and RSA import/export helpers
  • High-level primitives

    • encryptString() / decryptToString()
    • encryptBlob() / decryptToBlob() (Blob | ArrayBuffer | Buffer, auto-chunking)
  • Utilities

    • downloadText(filename, text) (browser download / Node Buffer)
    • toBase64Url(u8) / fromBase64Url(b64u)

Guides & recipes

Passphrase-derived keys

If you can’t persist a random AES key, derive it from a passphrase + a stable salt. Store the salt with the ciphertext as Base64URL.

import {
  SALT_BYTES,
  deriveKeyFromPassphrase,
  encryptString,
  decryptToString,
  toBase64Url,
  fromBase64Url,
} from "@salvobee/crypto-vault";

const passphrase = "correct horse battery staple";

// Generate once, store alongside ciphertext
const salt = crypto.getRandomValues(new Uint8Array(SALT_BYTES));
const saltB64 = toBase64Url(salt);

// Derive & encrypt
const key = await deriveKeyFromPassphrase(passphrase, salt);
const ciphertext = await encryptString("Hello vault!", key);

// Later: restore salt & derive again to decrypt
const keyAgain = await deriveKeyFromPassphrase(passphrase, fromBase64Url(saltB64));
const plain = await decryptToString(ciphertext, keyAgain);

🔁 Same passphrase + salt → same AES key. Keep both to re-derive; share both if collaborators decrypt with a shared passphrase.


Show decrypted image/video in a page

<input type="file" id="pick" accept="image/*,video/*" />
<img id="img" style="display:none;max-width:100%" />
<video id="vid" controls style="display:none;max-width:100%"></video>

<script type="module">
import { generateAesKey, encryptBlob, decryptToBlob } from "@salvobee/crypto-vault";

const key = await generateAesKey();

document.getElementById("pick").addEventListener("change", async (e) => {
  const file = e.target.files?.[0];
  if (!file) return;

  const packed = await encryptBlob(file, key, { compress: true });
  const blob = await decryptToBlob(packed, key);
  const url = URL.createObjectURL(blob);

  const img = document.getElementById("img");
  const vid = document.getElementById("vid");
  img.style.display = vid.style.display = "none";

  if (blob.type.startsWith("image/")) { img.src = url; img.style.display = "block"; }
  else if (blob.type.startsWith("video/")) { vid.src = url; vid.style.display = "block"; }
});
</script>

Persist & reload keys

import { generateAesKey, exportKeyToBase64, importKeyFromBase64 } from "@salvobee/crypto-vault";

const key  = await generateAesKey();
const b64  = await exportKeyToBase64(key);   // save (IndexedDB, download, server…)
const key2 = await importKeyFromBase64(b64); // restore later

Encrypt/Decrypt buffers in Node

import { generateAesKey, encryptBlob, decryptToBlob } from "@salvobee/crypto-vault";
import { readFileSync, writeFileSync } from "node:fs";

const key = await generateAesKey();

const input = readFileSync("input.pdf");
const packed = await encryptBlob(input, key, { compress: true });
const outBuf = await decryptToBlob(packed, key, { output: "buffer" });

writeFileSync("output.pdf", outBuf);

Share AES keys (RSA-OAEP wrap/unwrap)

Use RSA only to wrap the AES key—not for bulk data.

import {
  generateAesKey,
  generateRsaKeyPair,
  wrapKeyForRecipient,
  unwrapKeyForRecipient,
  exportPublicKeyToBase64, importPublicKeyFromBase64,
  exportPrivateKeyToBase64, importPrivateKeyFromBase64,
  exportKeyToBase64,
} from "@salvobee/crypto-vault";

// Alice
const alicePair = await generateRsaKeyPair();
const alicePubB64 = await exportPublicKeyToBase64(alicePair.publicKey);
const alicePrivB64 = await exportPrivateKeyToBase64(alicePair.privateKey);

// Bob wraps an AES key for Alice
const dataKey = await generateAesKey();
const alicePub = await importPublicKeyFromBase64(alicePubB64);
const wrappedForAlice = await wrapKeyForRecipient(alicePub, dataKey);

// Alice unwraps
const alicePriv = await importPrivateKeyFromBase64(alicePrivB64);
const aliceDataKey = await unwrapKeyForRecipient(wrappedForAlice, alicePriv);

// Sanity check
console.assert(
  (await exportKeyToBase64(dataKey)) === (await exportKeyToBase64(aliceDataKey))
);

Trade-offs: extractable RSA private keys make backups easy; RSA-OAEP-4096 is heavy → use it only for key exchange; rotate keys for better forward secrecy.


Performance tips

  • Base64URL overhead ≈ 33%. For very large media, consider splitting at the application level (e.g., 1–5 MB slices).
  • Compression mainly helps text and some binaries; most images/videos are already compressed. The default compress: "auto" policy automatically skips compression for content types that gain nothing (JPEG, PNG, WebP, AVIF, HEIC, video/*, audio/mpeg|aac|opus|ogg|flac, ZIP, GZIP, 7z, RAR, PDF, EPUB, OOXML). Force the behaviour with compress: true or compress: false. Compression is also skipped silently if the runtime lacks Compression Streams.

Security notes

  • AES-GCM = confidentiality + integrity.
  • Never reuse IVs with the same key (the library generates fresh 96-bit IVs per message/chunk).
  • Prefer random keys (generateAesKey) for maximum entropy; use passphrase-derived keys only when necessary.
  • Keep keys out of logs/analytics; never hard-code secrets.
  • For multi-user sharing, wrap the AES key with public-key crypto rather than sharing the AES key in the clear.

Container format

Every ciphertext is a Base64URL string wrapping a compact binary container:

[MAGIC "WCV1"][VERSION 1B][FLAGS 1B][ALG_ID 1B][META_LEN 4B BE][META JSON][PAYLOAD]
  • VERSION: 0x02 (current). The reader still parses 0x01 containers for backward compatibility, but new ciphertexts are always written as v2.

  • FLAGS (v2): bit0 reserved (always 0), bit1=chunked. In v1 bit0 indicated whether the payload was compressed; v2 moves that information into the encrypted envelope.

  • ALG_ID: 0x01 = AES-GCM-256

  • META JSON (clear, v2 — minimal dispatch fields only):

    • text: { type:"text", alg:"AES-GCM", iv }
    • blob (single): { type:"blob", alg:"AES-GCM", single:true, iv }
    • blob (chunked): { type:"blob", alg:"AES-GCM", chunked:true }
  • PAYLOAD (v2):

    • single: AES-GCM ciphertext of [u32be env_len][env JSON][raw bytes], where env carries { mime, size, compressed } for blobs (or { compressed } for text).
    • chunked: an envelope frame [len 4B BE][iv 12B][ct+tag] (carrying { mime, size, compressed, chunkSize }) followed by data frames [len 4B BE][iv 12B][ct+tag] per chunk.

Why v2?

In v1 the meta JSON was sent in the clear and included mime, size, and compressed. For private media vaults that's already metadata an attacker can profile from the bucket without ever seeing the key. v2 hides those fields inside the AES-GCM authentication scope; the clear meta only carries what the reader needs before decryption (algorithm, IV, framing). v1 containers can still be read.


Troubleshooting

  • RangeError: too many function arguments Likely attempted to Base64-encode a massive buffer using spread. Use the built-in toBase64Url which is chunk-safe.

  • DecompressionStream not available Your browser doesn’t support Compression Streams; encryption still works (without gzip).

  • Operation is not supported Web Crypto often requires HTTPS or localhost.


License & Acknowledgements

MIT — see LICENSE.

Built with ❤️ on standard Web Crypto API and Compression Streams API so your encrypted content stays portable—and easy to store as text.