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

envelock

v0.1.0

Published

Framework-agnostic envelope encryption for Node.js with pluggable KDF strategies and NestJS support.

Downloads

297

Readme

envelock

Framework-agnostic envelope encryption for Node.js. A random data key encrypts your payload. A key derived from a secret encrypts that data key. The secret never has to be stored.

AES-256-GCM is built in. Key derivation is pluggable: PBKDF2 and scrypt ship with Node, Argon2id is optional. NestJS can inject the same core through a dynamic module.

The design is motivated by anonymous storage: systems that have no account and no server-side identity, only an API key. The write-up in docs/README.md covers why plaintext storage, a server-managed key, and using the API key itself as the cipher key each fall short, and how an API-key-derived envelope (KEK wraps a random DEK, nothing long-lived is stored) provides per-user isolation, key rotation by re-wrapping, and the limits of that model. An analysis in ASD-STE100 Simplified Technical English is in docs/analysis.md.

Install

npm install envelock

Argon2id needs the optional native package:

npm install argon2

NestJS usage needs @nestjs/common (optional peer dependency).

How it works

secret (API key, token, …)
        │
        ▼
   KDF + salt  ──►  KEK          (derived per request, never stored)
        │
        ▼
   AES-256-GCM ──►  encrypted DEK   (persisted)
        │
        ▼
   AES-256-GCM ──►  ciphertext      (your data)
  1. Create. Generate an API key, derive a key-encryption key (KEK) from it, generate a data-encryption key (DEK), and encrypt the DEK with the KEK.
  2. Store. Persist the API key hash, the salt, and the encrypted DEK. Return the raw API key to the caller once.
  3. Use. Hash the presented API key to look up the record, re-derive the KEK, decrypt the DEK, then encrypt or decrypt data. Zero the DEK when you are done.

GCM authentication tags reject a wrong key and tampered ciphertext.

Quick start

import { EnvelopeCore, PBKDF2Strategy } from 'envelock';

const envelope = new EnvelopeCore({
  apiKey: { prefix: 'ev' },
  kdf: { strategy: new PBKDF2Strategy() },
});

// Once, when issuing a key
const { apiKey, hash } = envelope.generateApiKey();
const material = await envelope.generateKeyMaterial(apiKey);

// Persist `hash`, `material.salt`, and `material.encryptedDek`.
// Hand `apiKey` to the caller. Do not store it.

// On each request
const dek = await envelope.resolveDataKey(apiKey, material);
try {
  const encrypted = envelope.encrypt(Buffer.from('payload'), dek);
  const plain = envelope.decrypt(encrypted, dek);
} finally {
  dek.fill(0);
}

EncryptedPayload is three Buffers: ciphertext, iv, and authTag. Encode them (for example base64) before writing to a database, and decode them before calling decrypt or resolveDataKey.

NestJS

EnvelopeEncryptionModule is global by default and exports EnvelopeEncryptionService, which is EnvelopeCore with dependency injection.

import { Module } from '@nestjs/common';
import { EnvelopeEncryptionModule, PBKDF2Strategy } from 'envelock';

@Module({
  imports: [
    EnvelopeEncryptionModule.forRoot({
      apiKey: { prefix: 'ev' },
      kdf: { strategy: new PBKDF2Strategy() },
    }),
  ],
})
export class AppModule {}

Async registration, for example from ConfigService:

EnvelopeEncryptionModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    apiKey: { prefix: config.getOrThrow('API_KEY_PREFIX') },
    kdf: { strategy: new PBKDF2Strategy() },
  }),
});

Pass isGlobal: false when the module should stay local to the importing module.

constructor(private readonly envelope: EnvelopeEncryptionService) {}

Choosing a KDF

| Strategy | Dependency | Use when | | --- | --- | --- | | PBKDF2Strategy | none | The secret is high-entropy (API keys, random tokens). Default is 600,000 iterations of HMAC-SHA256. | | ScryptStrategy | none | You want a memory-hard KDF without a native addon. Default cost is N = 16384, r = 8, p = 1. | | Argon2idStrategy | argon2 | You want the memory-hard default for passwords. Default is 64 MiB, 3 iterations, parallelism 1. |

PBKDF2 is fast to attack on GPUs when the secret is a user-chosen password. Use scrypt or Argon2id for those secrets.

import { ScryptStrategy, Argon2idStrategy } from 'envelock';

new ScryptStrategy({ N: 16384, r: 8, p: 1 });
new Argon2idStrategy({ memoryCost: 65536, timeCost: 3, parallelism: 1 });

Instantiating Argon2idStrategy without argon2 installed throws when derive runs, with a message that names the missing package.

API

EnvelopeCore

| Method | Returns | Role | | --- | --- | --- | | generateApiKey() | { apiKey, hash } | Random key {prefix}_{secret} plus its SHA-256 hex hash. | | hashApiKey(apiKey) | string | SHA-256 hex digest for lookup. | | generateKeyMaterial(secret) | { salt, encryptedDek } | Salt, KEK, DEK, and encrypted DEK. KEK and raw DEK are zeroed before return. | | resolveDataKey(secret, material) | Buffer | Re-derives the KEK, decrypts the DEK, zeroes the KEK. Caller zeroes the DEK. | | encrypt(data, key) | EncryptedPayload | AES-256-GCM with a fresh IV. | | decrypt(payload, key) | Buffer | Throws when the auth tag does not match. | | generateDEK() / encryptDEK() / decryptDEK() | | Lower-level DEK helpers. generateKeyMaterial and resolveDataKey cover the usual path. | | generateSalt() / deriveKEK(secret, salt) | | Lower-level KDF helpers. |

Config

interface EnvelopeConfig {
  apiKey: {
    prefix: string;
    secretLength?: number; // default 32 bytes, encoded base64url
  };
  kdf: {
    strategy: KDFStrategy;
  };
  aes?: {
    algorithm?: string; // default 'aes-256-gcm'
    ivLength?: number;  // default 12
    keyLength?: number; // default 32
  };
}

Bring your own KDFStrategy by implementing derive(secret, salt) and generateSalt().

What to persist

| Value | Store it? | | --- | --- | | Raw API key | No. Return it once. | | API key hash | Yes. Lookup key. | | Salt | Yes. Required to re-derive the KEK. | | Encrypted DEK (ciphertext, iv, authTag) | Yes. | | KEK or raw DEK | No. Derive and decrypt in memory, then buffer.fill(0). |

License

MIT