Skip to main content

@happyvertical/encryption

Unified encryption and cryptography operations with adapter-based architecture supporting PGP, NaCl, and Node.js crypto.

Tests License TypeScript

Featuresโ€‹

  • ๐Ÿ” Multiple Encryption Methods: PGP/OpenPGP, NaCl/libsodium, Node.js crypto
  • ๐Ÿ“ Text, Files & Buffers: Encrypt any data format with a unified API
  • ๐Ÿ“ง Email Integration: Specialized PGP/MIME email encryption
  • ๐Ÿ”‘ Key Management: Generate, import, export, and manage encryption keys
  • โœ๏ธ Digital Signatures: Sign and verify data with RSA, ECDSA, and EdDSA
  • ๐ŸŽฏ Type-Safe: Full TypeScript support with comprehensive type definitions
  • โšก High Performance: Fast NaCl encryption, efficient streaming for large files
  • ๐Ÿงช Well-Tested: 209 tests covering all adapters and use cases

Installationโ€‹

npm install @happyvertical/encryption
# or
pnpm add @happyvertical/encryption
# or
yarn add @happyvertical/encryption

Claude Code Contextโ€‹

Install Claude Code context files for AI-assisted development:

npx have-encryption-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.

Quick Startโ€‹

PGP Encryptionโ€‹

import { getEncryption } from '@happyvertical/encryption';

// Create PGP encryption instance
const pgp = await getEncryption({ type: 'pgp' });

// Generate keypair
const keypair = await pgp.generateKeyPair({
name: 'Alice',
email: 'alice@example.com',
passphrase: 'secure-passphrase',
type: 'rsa',
keySize: 4096
});

// Initialize with keys
const encryption = await getEncryption({
type: 'pgp',
publicKey: keypair.publicKey,
privateKey: keypair.privateKey,
passphrase: 'secure-passphrase'
});

// Encrypt and decrypt text
const encrypted = await encryption.encryptText('Secret message');
const decrypted = await encryption.decryptText(encrypted);

// Encrypt and decrypt files
await encryption.encryptFile('document.pdf', 'document.pdf.pgp');
await encryption.decryptFile('document.pdf.pgp', 'document.pdf');

NaCl Encryption (Fast & Modern)โ€‹

import { getEncryption } from '@happyvertical/encryption';

// Generate keypair
const nacl = await getEncryption({ type: 'nacl' });
const keypair = await nacl.generateKeyPair();

// Symmetric encryption (secretbox)
const encryption = await getEncryption({
type: 'nacl',
secretKey: keypair.secretKey
});

const encrypted = await encryption.encryptText('Secret message');
const decrypted = await encryption.decryptText(encrypted);

// Encrypt files (very fast!)
await encryption.encryptFile('data.json', 'data.json.enc');
await encryption.decryptFile('data.json.enc', 'data.json');

Node.js Crypto (AES & RSA)โ€‹

import { getEncryption } from '@happyvertical/encryption';

// AES-256-GCM encryption
const aes = await getEncryption({
type: 'node',
algorithm: 'aes-256-gcm',
keyDerivation: {
password: 'user-password',
salt: 'unique-salt',
iterations: 100000
}
});

const encrypted = await aes.encryptText('Secret message');
const decrypted = await aes.decryptText(encrypted);

// RSA encryption
const rsa = await getEncryption({
type: 'node',
algorithm: 'rsa-oaep'
});

const keypair = await rsa.generateKeyPair({
type: 'rsa',
keySize: 2048
});

const rsaEncryption = await getEncryption({
type: 'node',
algorithm: 'rsa-oaep',
publicKey: keypair.publicKey,
privateKey: keypair.privateKey
});

const encrypted = await rsaEncryption.encryptText('Small message');
const decrypted = await rsaEncryption.decryptText(encrypted);

Vault snapshot envelopesโ€‹

import { openVaultSnapshot, sealVaultSnapshot } from '@happyvertical/encryption/vault-snapshot';

sealVaultSnapshot(snapshot, { key, keyId, context, compression }) and openVaultSnapshot(envelope, { context, resolveKey }) are synchronous Node.js byte APIs. The key is a raw 32-byte user key dedicated to snapshot envelopes. The key ID is an opaque 16-byte identifier encoded as 32 lowercase hexadecimal digits in TypeScript. Supply the expected { accountId, deviceId, sequence } independently when opening. Scope resolveKey(keyId) to that account and return undefined for an unknown ID. A syntactically valid envelope can trigger one lookup with an untrusted ID. Opening returns bytes only after authentication and validation succeed.

Compression is optional and defaults to none. Use zlib for snapshots where revealing the resulting size is acceptable. Envelope length is public in both modes, and compression can reveal relationships between attacker-controlled and secret bytes. Account and device IDs and the sequence are encrypted. Callers that require replay or rollback protection must track which sequences are acceptable in their trusted state. Every seal generates a fresh random nonce.

Version 1 wire formatโ€‹

The envelope is a binary byte array with a 47-byte clear header followed by the combined output of NaCl XSalsa20-Poly1305 secretbox (crypto_secretbox_easy in libsodium). The output begins with its 16-byte authentication tag and continues with the encrypted message to end of input.

Header offsetSizeValue
04ASCII HVSE (48 56 53 45)
41Version 01
51Algorithm 01 (XSalsa20-Poly1305 secretbox)
61Compression 00 for raw bytes or 01 for zlib
716Raw opaque key ID bytes, in the same order as the hex encoding
2324Secretbox nonce

The authenticated message is this exact concatenation:

clear header (47 bytes)
account ID UTF-8 byte length (uint8) || account ID UTF-8 bytes
device ID UTF-8 byte length (uint8) || device ID UTF-8 bytes
sequence (uint64 big-endian)
payload (to end of message)

Each ID is 1 to 255 bytes of valid UTF-8, with exact byte matching and no Unicode normalization. For Kotlin, convert byte lengths to unsigned values and interpret the eight sequence bytes as a big-endian ULong (TypeScript bigint). Compression 01 is one RFC 1950 zlib stream with DEFLATE, checksum and no preset dictionary. A JVM reader can use Inflater(false) with an output limit and check finished() and zero remaining input. Different compressors may produce different valid payload bytes.

Reject unknown header values, envelope lengths outside 122 to 8,454,774 bytes, and payload lengths outside the mode limits. A v1 envelope holds at most 8,388,608 decoded bytes (8 MiB). The raw payload has the same cap. The compressed payload cap is 8,454,144 bytes. These are fixed v1 limits for a single in-memory envelope. Open secretbox with the clear nonce and the one key identified by the header. Compare the authenticated header and context prefix against the clear header and independently supplied expected context, then decode the payload. For zlib, reject truncated streams, dictionaries, invalid checksums, trailing bytes and output above the decoded limit.

The v1 interoperability vectors were generated with libsodium via PyNaCl and Python zlib. They include the test key, raw IDs and sequence, encoded context, snapshot, nonce, encoded payload and full envelope for both modes. The fixed keys and nonces are for testing.

Common Use Casesโ€‹

Email Encryption (PGP/MIME)โ€‹

import { getEncryption } from '@happyvertical/encryption';

const pgp = await getEncryption({
type: 'pgp',
publicKey: recipientPublicKey,
privateKey: myPrivateKey,
passphrase: 'my-passphrase'
});

// Encrypt email message
const encryptedEmail = await pgp.encryptEmail({
from: { address: 'sender@example.com' },
to: [{ address: 'recipient@example.com' }],
subject: 'Confidential Information',
text: 'Secret message content',
attachments: [
{ filename: 'document.pdf', content: pdfBuffer }
]
}, {
sign: true,
armor: true
});

// Send with your email provider
// await mailbox.send(encryptedEmail);

Database Field Encryptionโ€‹

import { getEncryption } from '@happyvertical/encryption';

const encryption = await getEncryption({
type: 'node',
algorithm: 'aes-256-gcm',
keyDerivation: {
password: process.env.DB_ENCRYPTION_PASSWORD,
salt: 'db-encryption-salt',
iterations: 100000
}
});

// Encrypt sensitive fields
async function saveUser(user) {
const encryptedSSN = await encryption.encryptText(user.ssn);
const encryptedCreditCard = await encryption.encryptText(user.creditCard);

await db.insert('users', {
id: user.id,
name: user.name, // Plain text
ssn: encryptedSSN,
credit_card: encryptedCreditCard
});
}

// Decrypt when retrieving
async function getUser(id) {
const row = await db.selectOne('users', { where: { id } });

return {
id: row.id,
name: row.name,
ssn: await encryption.decryptText(row.ssn),
creditCard: await encryption.decryptText(row.credit_card)
};
}

File Backup Encryptionโ€‹

import { getEncryption } from '@happyvertical/encryption';

const encryption = await getEncryption({
type: 'nacl',
secretKey: backupSecretKey
});

// Encrypt backup files
await encryption.encryptFile(
'/data/backup-2024-01.tar.gz',
'/encrypted-backups/backup-2024-01.tar.gz.enc'
);

// Decrypt when needed
await encryption.decryptFile(
'/encrypted-backups/backup-2024-01.tar.gz.enc',
'/restored/backup-2024-01.tar.gz'
);

Digital Signaturesโ€‹

import { getEncryption } from '@happyvertical/encryption';

const pgp = await getEncryption({
type: 'pgp',
privateKey: myPrivateKey,
passphrase: 'my-passphrase'
});

// Sign message
const message = 'Important announcement';
const signature = await pgp.sign(message, {
detached: true,
armor: true
});

// Verify signature
const valid = await pgp.verify(message, signature, {
publicKey: signerPublicKey
});

if (valid) {
console.log('โœ“ Signature verified - message is authentic');
} else {
console.log('โœ— Invalid signature - message may be tampered');
}

API Overviewโ€‹

Factory Functionโ€‹

function getEncryption(options: GetEncryptionOptions): Promise<Encryption>

Encryption Interfaceโ€‹

All adapters implement this unified interface:

interface Encryption {
// Text operations
encryptText(text: string, options?: EncryptOptions): Promise<string>;
decryptText(encrypted: string, options?: DecryptOptions): Promise<string>;

// File operations
encryptFile(inputPath: string, outputPath: string, options?: EncryptOptions): Promise<void>;
decryptFile(inputPath: string, outputPath: string, options?: DecryptOptions): Promise<void>;

// Buffer operations
encryptBuffer(buffer: Buffer, options?: EncryptOptions): Promise<Buffer>;
decryptBuffer(buffer: Buffer, options?: DecryptOptions): Promise<Buffer>;

// Key management
generateKeyPair(options?: KeyPairOptions): Promise<KeyPair>;
importKey(key: string | Buffer, options?: ImportKeyOptions): Promise<Key>;
exportKey(key: Key, options?: ExportKeyOptions): Promise<string | Buffer>;

// Signing (optional)
sign?(data: string | Buffer, options?: SignOptions): Promise<string | Buffer>;
verify?(data: string | Buffer, signature: string | Buffer, options?: VerifyOptions): Promise<boolean>;

// Email operations (PGP only)
encryptEmail?(message: EmailMessage, options?: EncryptEmailOptions): Promise<EmailMessage>;
decryptEmail?(message: EmailMessage, options?: DecryptEmailOptions): Promise<DecryptedEmail>;

// Adapter info
getCapabilities(): Promise<EncryptionCapabilities>;
getAdapter(): AdapterType;
}

Adapter Comparisonโ€‹

FeaturePGPNaClNode Crypto
Text Encryptionโœ…โœ…โœ…
File Encryptionโœ…โœ…โœ…
Email Encryptionโœ…โŒโŒ
Digital Signaturesโœ…โœ…โœ…
Multiple Recipientsโœ…โŒโŒ
SymmetricโŒโœ…โœ…
Asymmetricโœ…โœ…โœ…
SpeedMediumFastFast
Key SizeLargeSmallMedium
Best ForEmail, compatibilityPerformance, modern appsStandard algorithms, built-in

When to Use Each Adapterโ€‹

PGP/OpenPGP - Best for:

  • Email encryption (PGP/MIME standard)
  • Multi-recipient scenarios
  • Compatibility with existing PGP infrastructure
  • Long-term archival

NaCl/libsodium - Best for:

  • High-performance applications
  • Modern cryptography
  • File encryption
  • API token encryption
  • Real-time data encryption

Node.js Crypto - Best for:

  • Standard algorithms (AES, RSA)
  • No external dependencies needed
  • Password-based encryption (PBKDF2)
  • Enterprise requirements (FIPS compliance)

Security Considerationsโ€‹

Key Storageโ€‹

โŒ DO NOT:

  • Store private keys in plain text
  • Commit keys to version control
  • Log keys or passphrases
  • Transmit keys over insecure channels

โœ… DO:

  • Use environment variables for keys
  • Encrypt private keys with strong passphrases
  • Use hardware security modules (HSMs) for production
  • Implement key rotation policies
  • Use key derivation for password-based encryption

Best Practicesโ€‹

  1. Key Length:

    • RSA: Minimum 2048 bits (4096 recommended)
    • AES: 256 bits
    • ECC: curve25519 or P-384
  2. Algorithms:

    • โœ… Prefer: AES-256-GCM, RSA-OAEP, ECDH, NaCl
    • โŒ Avoid: DES, 3DES, MD5, SHA1
  3. Key Derivation:

    • PBKDF2: Minimum 100,000 iterations
    • Use unique salts per encryption
    • Consider Argon2 for new applications
  4. Encrypted Data Integrity:

    • Use authenticated encryption (GCM, Poly1305)
    • Always verify signatures before trusting data
    • Implement replay protection

Testingโ€‹

Run the test suite:

npm test

Run tests for a specific adapter:

npm test -- test/unit/pgp.test.ts
npm test -- test/unit/nacl.test.ts
npm test -- test/unit/node.test.ts

Run file operations tests:

npm test -- test/unit/file-operations.test.ts

Performance Benchmarksโ€‹

Approximate performance for 100KB data:

AdapterEncryptionDecryptionKey Generation
PGP (RSA 4096)~200ms~100ms~3000ms
NaCl (secretbox)~2ms~2ms~1ms
AES-256-GCM~5ms~5msN/A
RSA-OAEP 2048~50ms~100ms~500ms

Benchmarks run on M1 MacBook Pro. Your results may vary.

Documentationโ€‹

  • AGENT.md - Comprehensive developer guide with detailed API reference, architecture, and implementation phases
  • API Types - TypeScript type definitions
  • Error Handling - Custom error classes

Examplesโ€‹

See the test directory for comprehensive examples:

Contributingโ€‹

Contributions are welcome! Please see the root CONTRIBUTING.md for guidelines.

Developmentโ€‹

# Install dependencies
pnpm install

# Run tests
npm test

# Run tests in watch mode
npm test -- --watch

# Build package
npm run build

# Lint and format
npm run lint
npm run format

Licenseโ€‹

MIT License - see LICENSE for details.

Supportโ€‹


Built with โค๏ธ by HappyVertical