Skip to content

Latest commit

 

History

History
220 lines (162 loc) · 5.56 KB

File metadata and controls

220 lines (162 loc) · 5.56 KB

@happyvertical/secrets

Envelope encryption SDK for per-tenant secret management with pluggable backends.

Installation

npm install @happyvertical/secrets
# or
pnpm add @happyvertical/secrets

Claude Code Context

Install Claude Code context files for AI-assisted development:

npx have-secrets-context

This copies the package's AGENT.md documentation and metadata.json metadata to your project's .claude/ directory, enabling Claude to provide better assistance when working with this package.

Overview

This package provides envelope encryption primitives for secure, per-tenant secret management. It uses a two-tier key hierarchy:

Application Master Key (AMK) - from environment variable
    └── wraps → Tenant Data Encryption Keys (TDEKs) - per tenant, stored in DB
                    └── encrypts → Secret values (AES-256-GCM)

Quick Start

import { getSecretStore } from '@happyvertical/secrets';
import { getDatabase } from '@happyvertical/sql';

// Set up database and AMK
const db = await getDatabase({ type: 'sqlite', url: ':memory:' });
process.env.MY_SECRET_KEY = crypto.randomBytes(32).toString('hex');

// Create secret store
const store = await getSecretStore({
  type: 'database',
  db,
  amk: {
    provider: 'env',
    keyEnvVar: 'MY_SECRET_KEY',
    keyId: 'amk-v1',
  },
});

// Create tenant key
await store.createTenantKey('tenant-123');

// Encrypt a secret
const envelope = await store.encrypt('tenant-123', 'api-key', 'sk_live_xxx');

// Decrypt the secret
const { value } = await store.decrypt('tenant-123', envelope);
console.log(value); // 'sk_live_xxx'

Core Concepts

Envelope Encryption

Envelope encryption separates data encryption from key management:

  1. Application Master Key (AMK): A 32-byte key stored securely (env var, KMS, etc.)
  2. Tenant Data Encryption Keys (TDEKs): Per-tenant keys wrapped by the AMK
  3. Secret Values: Encrypted with tenant's TDEK using AES-256-GCM

This architecture enables:

  • Per-tenant key isolation
  • Key rotation without re-encrypting all secrets
  • Secure key storage (only wrapped keys in database)

SecretStore Interface

interface SecretStore {
  // Encrypt a secret for a tenant
  encrypt(tenantId: string, secretName: string, plaintext: string): Promise<EncryptedEnvelope>;

  // Decrypt a secret
  decrypt(tenantId: string, envelope: EncryptedEnvelope): Promise<DecryptedSecret>;

  // Tenant key management
  getTenantKey(tenantId: string): Promise<TenantDataEncryptionKey | null>;
  createTenantKey(tenantId: string): Promise<TenantDataEncryptionKey>;
  rotateTenantKey(tenantId: string): Promise<TenantDataEncryptionKey>;

  // Event subscription
  subscribe(listener: SecretStoreEventListener): Unsubscribe;
}

API Reference

getSecretStore(options)

Factory function to create a secret store instance.

const store = await getSecretStore({
  type: 'database',
  db: databaseInstance,
  keysTable: 'tenant_encryption_keys', // optional, default
  amk: {
    provider: 'env',
    keyEnvVar: 'SMRT_SECRET_MASTER_KEY',
    keyId: 'production-amk-v1',
  },
});

EnvelopeEncryption

Low-level encryption primitives:

import { EnvelopeEncryption } from '@happyvertical/secrets';

// Generate a data key
const dataKey = EnvelopeEncryption.generateDataKey();

// Wrap the key with AMK
const wrapped = EnvelopeEncryption.wrapKey(dataKey, amk);

// Encrypt data
const encrypted = EnvelopeEncryption.encryptData('secret', dataKey);

// Decrypt data
const plaintext = EnvelopeEncryption.decryptData(
  encrypted.ciphertext,
  encrypted.iv,
  encrypted.authTag,
  dataKey,
);

Key Rotation

Rotate tenant keys without service interruption:

// Old keys are retained for decryption
const newKey = await store.rotateTenantKey('tenant-123');

// Old envelopes still decrypt (using retained key version)
const decrypted = await store.decrypt('tenant-123', oldEnvelope);

// New envelopes use the new key
const newEnvelope = await store.encrypt('tenant-123', 'new-secret', 'value');

Events

Subscribe to encryption/decryption events:

const unsubscribe = store.subscribe((event) => {
  console.log(`${event.type} for tenant ${event.tenantId}`);
});

// Later: unsubscribe
unsubscribe();

Event types:

  • secret.encrypted - Secret was encrypted
  • secret.decrypted - Secret was decrypted
  • key.created - Tenant key was created
  • key.rotated - Tenant key was rotated

Security Considerations

  1. AMK Protection: Store the Application Master Key securely

    • Use environment variables for simple deployments
    • Use AWS KMS, HashiCorp Vault, or Azure Key Vault for production
  2. Key Isolation: Each tenant has a unique TDEK

    • Compromise of one tenant's data doesn't expose others
    • Keys can be rotated independently
  3. AES-256-GCM: Authenticated encryption

    • Provides confidentiality and integrity
    • 12-byte IV, 16-byte auth tag
  4. Memory Safety: Key buffers are zeroed after use

    • Reduces exposure window for sensitive key material

Error Handling

import {
  AMKUnavailableError,
  TenantKeyMissingError,
  EncryptionError,
  DecryptionError,
} from '@happyvertical/secrets';

try {
  await store.encrypt('tenant-123', 'secret', 'value');
} catch (error) {
  if (error instanceof AMKUnavailableError) {
    // AMK not configured or inaccessible
  } else if (error instanceof TenantKeyMissingError) {
    // Tenant doesn't have a key - create one first
  } else if (error instanceof EncryptionError) {
    // Encryption failed
  }
}

License

MIT