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

@cobranza-apps/crypto

v0.4.2

Published

Shared encryption (AES-256-GCM + HKDF) and deterministic hashing (HMAC-SHA256) library for Cobranza App microservices.

Downloads

88

Readme

@cobranza-apps/crypto

Shared encryption & deterministic hashing library for the Cobranza App platform. Single source of truth for protecting PII, financial, bank, and notification data across all NestJS microservices.

Node License

Overview / Purpose

@cobranza-apps/crypto is a framework-agnostic TypeScript library for Node.js (22.14.0+) providing authenticated encryption and deterministic hashing. It uses the built-in crypto module with zero runtime dependencies and enforces consistent security, best practices, and key-rotation readiness across all Cobranza App microservices.

What it does:

  • AES-256-GCM authenticated encryption with per-category HKDF-SHA256 key derivation. Output format is IV(12 bytes) + ciphertext + authTag(16 bytes), Base64-encoded.
  • Deterministic HMAC-SHA256 hashing for indexed PII lookups with constant-time verifyHash.
  • Combined encryptAndHash for fields needing both ciphertext storage and a hash index.
  • Version-aware decryption for seamless key rotation.

What it does NOT do (non-goals):

  • No password hashing — Argon2id/bcrypt belong in the Auth microservice.
  • No process.env reads; pass all config via CryptoConfig.
  • No business logic, database access, or hard NestJS dependency; an optional ./nestjs subpath provides CryptoModule and CryptoService when @nestjs/common is installed.
  • No browser or non-Node.js environments.

Status / Stability

All API methods are implemented; algorithms may evolve before v1.0.

Table of Contents

Requirements

  • Node.js 22.14.0 (see .nvmrc)
  • @cobranza-apps/entities — provides EncryptedValue type and @IsEncryptedField() decorator
  • @nestjs/common / @nestjs/config — optional peer dependencies (required only for the ./nestjs subpath)

Installation

npm install @cobranza-apps/crypto @cobranza-apps/entities

Getting Started

A minimal configuration and roundtrip in under one minute:

import { SecureCrypto, EncryptionKey } from '@cobranza-apps/crypto';

const crypto = new SecureCrypto({
  masterKey: process.env.COBRANZA_CRYPTO_MASTER_KEY!, // base64 32-byte key
  hashSalt: process.env.COBRANZA_CRYPTO_HASH_SALT!,   // base64 >= 32 bytes
  currentVersion: 1,
  defaultKeyName: EncryptionKey.PII,
});

const encrypted = crypto.encrypt('hello world', EncryptionKey.PII);
const plaintext = crypto.decrypt(encrypted);
console.assert(plaintext === 'hello world');

For the full walkthrough (key generation, hash, dual-column pattern) see Getting Started.

Configuration

The library accepts all configuration at instantiation time via CryptoConfig.

import { SecureCrypto, CryptoConfig, EncryptionKey } from '@cobranza-apps/crypto';

const cryptoConfig: CryptoConfig = {
  masterKey: process.env.COBRANZA_CRYPTO_MASTER_KEY!, // base64 32-byte key
  hashSalt:  process.env.COBRANZA_CRYPTO_HASH_SALT!,   // base64 >= 32 bytes
  currentVersion: 1,
  defaultKeyName: EncryptionKey.PII,
};

const crypto = new SecureCrypto(cryptoConfig);

Usage Examples

All examples assume a configured SecureCrypto instance (see Configuration).

Encrypt

const encrypted = crypto.encrypt('[email protected]', EncryptionKey.PII);
// encrypted: {
//   encryptedData: 'base64-encoded IV(12) + ciphertext + authTag(16)',
//   keyName: 'pii',
//   algorithm: 'aes-256-gcm',
//   version: 1,
// }

Decrypt

const original = crypto.decrypt(encrypted);
// '[email protected]'

// Roundtrip assertion
console.assert(original === '[email protected]', 'decrypt must restore plaintext');

Hash

const emailHash = crypto.hash('[email protected]');
// base64 HMAC-SHA256; deterministic, safe for indexed lookups

// Same input always produces the same hash
console.assert(crypto.hash('[email protected]') === emailHash);

verifyHash

const emailHash = crypto.hash('[email protected]');
const isValid = crypto.verifyHash('[email protected]', emailHash);
// true — constant-time comparison prevents timing attacks

const tampered = crypto.verifyHash('[email protected]', emailHash);
// false

encryptAndHash (recommended for PII columns)

Dual-column storage pattern — encrypted value for confidentiality, hash for indexed lookups:

const { encrypted, hash } = crypto.encryptAndHash('[email protected]', EncryptionKey.PII);

// Store `encrypted` (EncryptedValue) in the encrypted column
// Store `hash` (string) in the `*Hash` index column
// Later:
const decrypted = crypto.decrypt(encrypted);                        // '[email protected]'
const match = crypto.verifyHash('[email protected]', hash);          // true

reEncrypt (key rotation)

Decrypt and re-encrypt at the current key version, optionally under a different key name:

// Re-encrypt under the current version (same key name)
const refreshed = crypto.reEncrypt(encrypted);
// refreshed.version === crypto's currentVersion

// Re-encrypt under a different key
const moved = crypto.reEncrypt(encrypted, EncryptionKey.BANK_DATA);
// moved.keyName === 'bank_data'

See Key Rotation Guide for the full rotation workflow.

Bulk Operations

Encrypt or decrypt multiple fields of an object in a single call. Only the fields listed in the fieldMap are transformed; all other fields pass through unchanged. The input object is never mutated.

import { SecureCrypto, EncryptionKey } from '@cobranza-apps/crypto';
import type { BulkFieldMap } from '@cobranza-apps/crypto';

interface Customer {
  email: string;
  fullName: string;
  id: number;
}

const fieldMap: BulkFieldMap<Customer> = {
  email:    EncryptionKey.PII,
  fullName: EncryptionKey.PII,
};

// Encrypt every mapped string field — returns a shallow clone
const customer = { email: '[email protected]', fullName: 'Ana', id: 42 };
const encrypted = crypto.encryptObject(customer, fieldMap);
// encrypted.email    is an EncryptedValue
// encrypted.fullName is an EncryptedValue
// encrypted.id       === 42

// Decrypt every mapped EncryptedValue field — returns a shallow clone
const plaintext = crypto.decryptObject(encrypted, fieldMap);
// plaintext.email    === '[email protected]'
// plaintext.fullName === 'Ana'

Tip: encryptObject / decryptObject are ideal for DTO-to-entity mapping in NestJS services. Pair with @IsEncryptedField() from @cobranza-apps/entities for end-to-end type safety.

Cached Decryptor

Wrap this SecureCrypto instance with a TTL-bounded in-memory cache to avoid repeated AES-256-GCM decryption of hot records. Cache hits return the stored plaintext; misses delegate to decrypt.

// Build a cached decryptor with a 30-second TTL (default: 60 s)
const cached = crypto.withCache({ ttlMs: 30_000 });

const a = cached.decrypt(encrypted); // cache miss — delegates to SecureCrypto
const b = cached.decrypt(encrypted); // cache hit  — returns cached plaintext

cached.size();   // number of cached entries
cached.clear();  // invalidate all entries (call after key rotation)

Security note: Caching plaintext is an explicit, opt-in decision. Never share a CachedDecryptor across users or tenants. Invalidate on key rotation. See Security Best Practices.

Decryption cache (opt-in)

Cache decrypted plaintext in-memory with a TTL to avoid repeated decryption of hot records:

import { createDecryptionCache } from '@cobranza-apps/crypto';
import type { EncryptedValue } from '@cobranza-apps/entities';

const cache = createDecryptionCache(60_000); // 60 s TTL

function getCachedDecrypt(encrypted: EncryptedValue): string {
  const cached = cache.get(encrypted.encryptedData);
  if (cached !== undefined) return cached;
  const plaintext = crypto.decrypt(encrypted);
  cache.set(encrypted.encryptedData, plaintext);
  return plaintext;
}

Security note: The cache is opt-in and bounded by TTL, not by a hard size limit. Callers should size TTLs to their memory budget. Never share a cache across users or tenants. Invalidate on key rotation.

Key introspection

crypto.hasKey('pii');             // true
crypto.getAvailableKeys();        // ['pii','company_pii','bank_data','notification','general']

Note: Ciphertext is non-deterministic (random 12-byte IV); the hash output is deterministic. See Testing Utilities for why exact ciphertext assertions are not part of test vectors.

Real-World Scenarios

  • Email (PII)encryptAndHash('[email protected]', EncryptionKey.PII) for dual-column storage + lookup-by-hash.
  • Tax ID (Company PII)encryptAndHash('RFC-ABCD123456', EncryptionKey.COMPANY_PII) with dedup via hash + verifyHash.
  • Bank description (Bank Data)encrypt('Payment for invoice...', EncryptionKey.BANK_DATA) encrypt-only, decrypt on read.

See Real-World Scenarios for full code examples with the dual-column pattern and lookup-by-hash.

API Summary

| Method | Parameters | Returns | Description | Status | |--------|-----------|---------|-------------|--------| | constructor | config: CryptoConfig | SecureCrypto | Validates and stores config | functional | | encrypt | plaintext: string, keyName: EncryptionKey | EncryptedValue | Encrypts a string using AES-256-GCM with HKDF-derived key | functional | | decrypt | data: EncryptedValue | string | Decrypts an EncryptedValue, supporting any version with an available key | functional | | hash | plaintext: string | string | Produces a deterministic HMAC-SHA256 hash | functional | | verifyHash | plaintext: string, hash: string | boolean | Constant-time hash verification | functional | | encryptAndHash | plaintext: string, keyName: EncryptionKey | { encrypted: EncryptedValue, hash: string } | Combined encryption + hashing for indexed PII fields | functional | | reEncrypt | encrypted: EncryptedValue, targetKeyName?: EncryptionKey \| string | EncryptedValue | Decrypts and re-encrypts at the current version, optionally under a new key | functional | | encryptObject | obj: T, fieldMap: BulkFieldMap<T> | T | Encrypts every string field listed in fieldMap; returns a shallow clone | functional | | decryptObject | obj: T, fieldMap: BulkFieldMap<T> | T | Decrypts every EncryptedValue field listed in fieldMap; returns a shallow clone | functional | | withCache | options?: { ttlMs?: number } | CachedDecryptor | Returns a TTL-cached decryptor bound to this instance | functional | | hasKey | name: string | boolean | Checks whether a key derivation config exists for the given name | functional | | getAvailableKeys | — | string[] | Returns all configured key names | functional |

Cache Utilities

| Export | Kind | Description | | --- | --- | --- | | TtlCache | class | Generic in-memory TTL cache with lazy eviction | | createDecryptionCache | function | Factory for a TtlCache<string, string> keyed by encrypted payload | | DecryptionCache | type | Alias for TtlCache<string, string> | | createDecryptionCacheWrapper | function | SecureCrypto-aware cache-through decrypt wrapper | | CachedDecryptor | interface | Cache-through decryptor returned by withCache | | DecryptionCacheOptions | interface | Options for createDecryptionCacheWrapper | | SecureCryptoDecryptor | interface | Minimal decryptor contract accepted by the wrapper | | BulkFieldMap | type | Per-field key mapping for encryptObject / decryptObject | | AuditLogger | interface | Optional observability hooks (onEncrypt, onDecrypt) invoked after successful crypto operations |

For the full interface contract, see brief.md §4.

NestJS Integration Guide

The library ships a built-in CryptoModule and CryptoService at the @cobranza-apps/crypto/nestjs subpath. Both synchronous (forRoot) and asynchronous (forRootAsync with ConfigService) registration are available:

import { CryptoModule } from '@cobranza-apps/crypto/nestjs';
import { EncryptionKey } from '@cobranza-apps/crypto';

// Sync:
CryptoModule.forRoot({ masterKey: '...', hashSalt: '...', currentVersion: 1, defaultKeyName: EncryptionKey.PII });

// Async with ConfigService:
CryptoModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({ /* ... */ }),
});

See the full How to Configure in NestJS guide for CryptoModule registration, interceptor pattern, DTO integration, rotation, testing, and deployment.

EncryptionKey is from this library; @IsEncryptedField() and EncryptedValue are from @cobranza-apps/entities.

For a complete end-to-end example (module + DTO + service + subscriber + test) see Full NestJS Integration Example, including Bulk Multi-Field Encryption with encryptObject / decryptObject.

Security Best Practices

The following practices are critical when handling sensitive data. Follow them to avoid data leaks, key compromise, and compliance violations.

Key Storage

  • Load masterKey and hashSalt from a secrets manager / vault / KMS at boot via ConfigService; never hardcode or commit them.
  • Keep masterKey and hashSalt as distinct secrets with independent rotation lifecycles.
  • Restrict secret access to the service identity; never expose via logs, traces, error responses, or client payloads.
  • Use separate secrets per environment (dev/staging/prod). The testing subpath uses fixed zero keys and MUST NOT be used in production.

Logging Rules

  • Never log plaintext, decrypted values, the master key, derived keys, the hash salt, IVs, or the full EncryptedValue.encryptedData payload.
  • Log only non-sensitive error messages — the library throws closed errors without secret material.
  • When logging request/response bodies that may contain EncryptedValue fields, redact or omit those fields.
  • keyName and version are acceptable in internal operational telemetry but should not appear in user-facing logs.

General Rules

  • Fail closed: errors are thrown; never returned as partial results.
  • IV is 12 random bytes per encryption; never reused.
  • Hash verification uses constant-time comparison (crypto.timingSafeEqual).
  • Use encryptAndHash (not hash alone) when the field also needs confidentiality.
  • Follow the full Key Rotation Guide and the NestJS deployment guidance.

Security Checklist

Quick production-readiness checklist:

  • [ ] Never hardcode or commit secrets — load masterKey and hashSalt from a vault / KMS.
  • [ ] Never expose keys, plaintext, IVs, or encryptedData in logs or error responses.
  • [ ] Use encryptAndHash (not hash alone) when the field needs confidentiality.
  • [ ] Use constant-time verifyHash for hash comparisons, never ===.
  • [ ] Run a background reEncrypt job after incrementing currentVersion.
  • [ ] Isolate the decryption cache per request/process; never share across users.

Full checklist: Security Checklist.

Observability & Auditing

Pass an optional AuditLogger via CryptoConfig.auditLogger to receive non-sensitive metadata after every successful encrypt or decrypt call. Hooks receive only keyName and versionnever plaintext, ciphertext, keys, IVs, or any other sensitive material. This is enforced at the type level: the AuditLogger interface signatures have no parameter capable of carrying sensitive payload data.

A logger that throws is silently swallowed so a misbehaving implementation can never break a crypto operation.

import { SecureCrypto, EncryptionKey } from '@cobranza-apps/crypto';
import type { AuditLogger, CryptoConfig } from '@cobranza-apps/crypto';

const auditLogger: AuditLogger = {
  onEncrypt(keyName, version) {
    metrics.increment('crypto.encrypt', { keyName, version });
  },
  onDecrypt(keyName, version) {
    metrics.increment('crypto.decrypt', { keyName, version });
  },
};

const config: CryptoConfig = {
  masterKey: process.env.COBRANZA_CRYPTO_MASTER_KEY!,
  hashSalt: process.env.COBRANZA_CRYPTO_HASH_SALT!,
  currentVersion: 1,
  defaultKeyName: EncryptionKey.PII,
  auditLogger, // <-- optional observability hooks
};

const crypto = new SecureCrypto(config);

Security note: Audit hooks are the only extension point that crosses the SecureCrypto boundary. The contract guarantees that no sensitive data (plaintext, ciphertext, derived keys, IVs, auth tags) is ever passed to consumer code. Log keyName and version freely in internal telemetry, but avoid emitting them in user-facing responses.

Key Rotation Guide

This library rotates derived keys by incrementing currentVersion (single masterKey; the version is part of the HKDF info, so a new version yields a new derived key from the same master key). Historical records stay decryptable because each EncryptedValue carries its version.

  1. Increment COBRANZA_CRYPTO_KEY_VERSION (e.g. 1 -> 2). No new master key.
  2. Deploy — new encryptions carry version: 2; existing records keep their original version.
  3. Run an external background job to migrate old records: crypto.reEncrypt(oldEncrypted) decrypts at the payload's version and re-encrypts at currentVersion in one call. See the reEncrypt example.
  4. Verify all records migrated (no records left on the old version).
  5. Hash columns need no migration — hashes are keyed by hashSalt, not version.
  6. Clear the decryption cache if in use (cache.clear()).

Rotating the actual master-key material is a larger, out-of-library migration (decrypt-all with the old key, re-encrypt-all with the new key). See the full Key Rotation Guide.

Performance Considerations

  • Internal HKDF cache: caches derived per-category keys in memory keyed by ${keyName}:v${version}. Repeated encrypt/decrypt calls with the same key name and version do not re-derive.
  • Plaintext caching: may cache decrypted values in-memory with a short TTL using createDecryptionCache (see Decryption cache). The cache is opt-in and bounded by TTL, not by a hard size limit. Callers should size TTLs to their memory budget. Isolate per request or process — never shared across users or tenants. Invalidate on key rotation.
  • Hashing performance: hash / verifyHash are deterministic and idempotent — safe to call repeatedly without caching.
  • Bulk re-encryption: during key rotation, run re-encryption as an external background job with batching / rate-limiting.
  • Ciphertext overhead: each value adds IV(12) + authTag(16) = 28 bytes before Base64 (~33% inflation). Size columns accordingly.
  • Synchronous cost: crypto calls block the event loop; negligible for PII-size fields (<1 KB), offload large payloads to a worker thread if latency-sensitive.

Full guide: Performance Considerations.

Testing

Consumer testing

Jest consumers can use the testing subpath:

import { getTestCrypto, SecureCryptoTestModule } from '@cobranza-apps/crypto/testing';
import { EncryptionKey } from '@cobranza-apps/crypto';

const crypto = getTestCrypto();
const { encrypted, hash } = crypto.encryptAndHash('[email protected]', EncryptionKey.PII);

Note: Ciphertext is non-deterministic (AES-256-GCM uses a random 12-byte IV). The expectedEncryptedShape field on each vector provides a deterministic structural assertion (algorithm, keyName, version, encryptedDataByteLength). Exact ciphertext correctness is verified via encrypt-decrypt roundtrip. See Testing Utilities for details.

  • getTestCrypto() returns a SecureCrypto with fixed, deterministic keys -- safe to publish; never usable in production.
  • test-vectors.ts provides 11 deterministic vectors with real expectedHash literals and expectedEncryptedShape for structural assertions.
  • SecureCryptoTestModule is a NestJS-friendly provider config (spreadable into Test.createTestingModule); it does not require @nestjs/testing as a dependency of this library.

Library test suite

npm test
npm run test:watch

The library's own test suite uses Jest + ts-jest.

Development

npm install
npm run build   # tsc -> dist/
npm run lint    # eslint --ext .ts
npm test        # jest

Package layout

src/
  index.ts
  config.ts
  audit.ts
  crypto.service.ts
  crypto.service.audit.ts
  crypto.service.bulk-guards.ts
  crypto.service.encryption.ts
  crypto.service.facade-guards.ts
  crypto.service.guards.ts
  crypto.service.hashing.ts
  crypto.service.keys.ts
  crypto.service.validation.ts
  hkdf.ts
  hkdf.types.ts
  utils.ts
  testing/
    index.ts
    test-vectors.ts
    encrypted-shape.ts
  nestjs/
    crypto-config.interface.ts
    crypto.module.ts
    crypto.service.ts
    index.ts
tests/
dist/
docs/

Guides

License

Released to the public domain under The Unlicense. See LICENSE for details.


AI agents: read AGENTS.md and follow the Critical Workflow before contributing. Project info lives in .agent/project-info/.