@salvobee/crypto-vault
    Preparing search index...

    @salvobee/crypto-vault

    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/


    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.


    • 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)

    • 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


    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.


    <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.


    • 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.

    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)

    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.


    <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>

    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

    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);

    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.


    • 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.

    • 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.

    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.

    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.


    • 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.


    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.