comportment
v0.1.1
Published
Context-scoped field encryption. Derive independent keys from one root secret via HKDF scope labels, with deterministic hash lookups and password blinding. Zero external dependencies.
Downloads
90
Maintainers
Readme
comportment
Context-scoped field encryption — derive independent keys from one root secret via HKDF scope labels, with deterministic hash lookups and password blinding. Zero external dependencies — built entirely on node:crypto.
What this does
Encrypt structured data fields with independent per-scope keys derived from a single root secret via HKDF. Compromising one scope's derived key does not expose data in other scopes. Hash-based lookup columns allow searching encrypted data without decryption. The pattern applies to any structured data with compartmentalization needs.
- Three encryption scopes — EMAIL, NAME, CONTENT with independent HKDF-derived AES-256-GCM keys
- Scope isolation — email ciphertext cannot be decrypted with the name key, and vice versa
- Hash-based lookups — HMAC-SHA256 for deterministic search without decryption
- Password blinding — PBKDF2 output HMAC'd with OOB salt; DB alone cannot verify guesses
- Dual-salt rotation — transparent transition when rotating the auth salt
- Noise flags — HMAC-based markers indistinguishable from real user flags without the salt
- Legacy format support — transparent upgrade from unblinded PBKDF2 and scrypt hashes
Install
npm install comportmentRequires Node.js >= 18.
Quick start
import {
init,
encryptEmail, decryptEmail, hashEmail,
encryptName, decryptName,
hashPassword, verifyPassword,
generateNoiseFlag, checkNoiseFlag,
} from 'comportment';
// Configure salts (hex strings or Buffers)
init({
piiSalt: process.env.OOB_SALT_PII, // for encryption scopes
authSalt: process.env.OOB_SALT_AUTH, // for password blinding + hash lookups
noiseSalt: process.env.OOB_SALT_NOISE, // for noise flag generation
});
// Encrypt PII by scope
const emailBlob = encryptEmail('[email protected]');
const nameBlob = encryptName('Alice Liddell');
// Decrypt
decryptEmail(emailBlob); // => '[email protected]'
decryptName(nameBlob); // => 'Alice Liddell'
// Hash-based lookup (deterministic, for DB indexing)
const emailHash = hashEmail('[email protected]');
// Store emailHash alongside emailBlob; query by hash, decrypt on match
// Password hashing with OOB-salt blinding
const hash = await hashPassword('correct-horse-battery-staple');
// => 'pbkdf2-blind:600000:salt:blindedHash'
const { valid, needsUpgrade } = await verifyPassword('correct-horse-battery-staple', hash);
// Noise flags (indistinguishable without salt)
const flag = generateNoiseFlag(true); // noise user
checkNoiseFlag(flag); // => true (only with correct noiseSalt)Scope isolation
Each scope derives an independent AES-256-GCM key from the PII salt via HKDF-SHA256:
| Scope | HKDF Label | Use |
|-------|-----------|-----|
| EMAIL | bp-pii-email-v1 | Email addresses |
| NAME | bp-pii-name-v1 | Display names, team names, usernames |
| CONTENT | bp-pii-content-v1 | Projects, access rules, service links |
Callers import only the scope functions they need. Code that handles emails cannot decrypt project content.
Salt rotation
Dual-salt support for zero-downtime auth salt rotation:
// Before rotation: hash with current salt
init({ authSalt: CURRENT_SALT });
const hash = await hashPassword('password');
// After rotation: new salt + old salt for transition
init({ authSalt: NEW_SALT, oldAuthSalt: CURRENT_SALT });
// Verification tries new salt first, falls back to old
const { valid, needsUpgrade } = await verifyPassword('password', hash);
// valid: true, needsUpgrade: true — re-hash with new saltAPI reference
Configuration
init(config)— provide salt values. Keys:piiSalt,authSalt,noiseSalt,oldAuthSalt,oldOobSalt
Scoped encryption
encryptEmail(plaintext)/decryptEmail(blob)— EMAIL scopeencryptName(plaintext)/decryptName(blob)— NAME scopeencryptContent(plaintext)/decryptContent(blob)— CONTENT scope
Hash-based lookups
hashEmail(email)— HMAC-SHA256, case-normalized, returns hexhashDisplayName(name)— HMAC-SHA256 with domain separation, returns hex or nullhashToken(token)— HMAC-SHA256 with token-specific derivation
Password hashing
hashPassword(password)— returnspbkdf2-blind:iterations:salt:blindedHashverifyPassword(password, stored)— returns{ valid, needsUpgrade }getCurrentIterations()— returns current PBKDF2 iteration count (600000)
Noise flags
generateNoiseFlag(isNoise)— returnssalt:hmacstringcheckNoiseFlag(flag)— returns boolean (requires correctnoiseSalt)
Security model
- OOB salt separation — the root secret (PII salt) never stored in the DB. DB alone cannot decrypt.
- Per-scope HKDF derivation — three independent keys from one root. Scope compromise is isolated.
- Non-deterministic encryption — random 12-byte nonce per encrypt; same plaintext produces different ciphertext
- Password blinding — HMAC(authSalt, PBKDF2(password)) means stolen DB + offline cracking yields nothing without the OOB salt
- Timing-safe verification —
timingSafeEqualfor all hash comparisons - Nonce birthday bound — ~2^32 encryptions per scope key before random nonce collision risk; rotate the PII salt to reset (see SECURITY.md)
- Legacy format upgrade — transparently verifies and flags old
pbkdf2:and scrypt hashes for re-hashing
Tests
npm test27 tests using node:test — no test framework dependency.
License
AGPL-3.0-or-later with additional terms. See LICENSE.
For closed-source or proprietary use, see LICENSE-COMMERCIAL.md.
