@rotorsoft/act-crypto
v0.1.4
Published
Authenticated envelope encryption helper for act adapters
Maintainers
Readme
@rotorsoft/act-crypto
Authenticated envelope encryption (AES-256-GCM, versioned wire format) for adapters in the @rotorsoft/act ecosystem.
Why this package
The PII column on events is the first caller. @rotorsoft/act-pg and @rotorsoft/act-sqlite both accept an optional pii_encryption constructor option that delegates to this package — the adapter encrypts the column on commit and decrypts on read, with an operator-controlled key.
Adapter-layer encryption fits the deployments TDE and pgcrypto can't reach: self-hosted Postgres without extensions, edge SQLite on devices you don't fully control, container and serverless workloads where the volume is opaque. The framework core (@rotorsoft/act) stays unaware of cipher, key, or envelope — this is a leaf-level helper, not a port.
Nothing in the package is PII-specific. Any adapter holding a JSON-serializable column value that wants column-level encryption with an operator-controlled key can use the same primitives.
Installation
pnpm add @rotorsoft/act-cryptoQuick start
import { encrypt, decrypt, makeKeyResolver } from "@rotorsoft/act-crypto";
const resolveKey = makeKeyResolver({
keyProvider: () => Buffer.from(process.env.PII_KEY_BASE64!, "base64"),
algorithm: "aes-256-gcm",
});
const encoded = await encrypt({ email: "[email protected]" }, resolveKey);
// → base64 string; write to a jsonb / TEXT column
const payload = await decrypt(encoded, resolveKey);
// → { email: "[email protected]" }From an adapter the typical wiring is even thinner — the pii_encryption constructor option on PostgresStore / SqliteStore handles it transparently.
API
encrypt(payload, resolveKey)— encrypts a JSON-serializable value to a base64-framed string.decrypt(encoded, resolveKey)— recovers the original value; throwsDecryptionErroron framing, key, or auth-tag failures.makeKeyResolver({ keyProvider, algorithm })— caches the operator's key on first use for the resolver's lifetime; restart the adapter to rotate.DecryptionError— narrow error class with a generic message (the failure mode is intentionally not exposed).Encryption— operator-supplied configuration type.Algorithm— supported algorithm literal type (singleton today).
Full type reference: typedoc.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
| keyProvider | () => Buffer \| Promise<Buffer> | — | Returns the symmetric key; called once on first use and cached. Must return a 32-byte Buffer (AES-256 requires 256 bits). |
| algorithm | "aes-256-gcm" | — | Cipher selection. Literal type — keeps misspellings out of operator config at compile time. |
A sync provider works for env-var-backed deployments. An async provider fits KMS callbacks — AWS KMS / GCP KMS / Vault keyProvider callbacks return Promise<Buffer>.
Wire format
Base64-encoded on disk:
[version: 1 byte = 0x01][iv: 12 bytes][gcm tag: 16 bytes][ciphertext: NB]version = 0x01— AES-256-GCM. The version byte is the only markerdecryptinspects; a future algorithm bumps the byte and gains a new switch arm without breaking existing rows.iv— 12 random bytes per encryption, generated viarandomBytes(12). GCM's IV-uniqueness contract — collision-safe at any reasonable scale.tag— 16-byte GCM auth tag.decipher.final()throws on mismatch, surfaced asDecryptionError.
Common patterns
Mixed-data rollout (adopting encryption on an existing column)
The adapter's read path discriminates by type — strings get decrypted, objects pass through as plaintext. Rows written before enabling encryption keep reading cleanly; new writes land as ciphertext. No backfill needed; no migration step.
Rotation
makeKeyResolver caches the key for the resolver's lifetime. To rotate, restart the adapter with a fresh keyProvider. Mid-flight key changes are not supported — re-encrypt is an operator concern.
Defense in depth
Composes with TDE / pgcrypto / SQLite SEE / OS-level FDE. The adapter encrypts the column; the database or volume encrypts the bytes. Use both layers when the compliance regime demands it; one is fine when it doesn't.
What this package deliberately doesn't ship
- No key management — no KEK/DEK split, no rotation tooling, no audit trail.
- No KMS integration —
keyProvideris the integration point; you wire it. - No application-layer crypto for
events.data— that's whatevents.piiis for. Usesensitive(...)to declare which fields live in the PII column, then encrypt at this layer or below. - No cipher choice beyond
aes-256-gcm— authenticated encryption is non-negotiable for sensitive data, and offering options mostly invites footguns.
Compatibility
- Node: >=22.18.0
- Bundled deps: none. Pure
node:crypto. - Module formats: ESM (
import) and CJS (require). No side effects.
Stability
Public API governed by the Act Stability Charter. encrypt / decrypt are stable wire-format primitives — the version byte is the upgrade path. Charter is in effect as of 1.0.0; the milestone tracker is milestone 1.0.
Related packages
- @rotorsoft/act — the event-sourcing framework whose sensitive-data surface (
sensitive(...),.discloses(...),app.forget(...)) the encryption shell sits behind. - @rotorsoft/act-pg / @rotorsoft/act-sqlite — production
Storeadapters. Both acceptpii_encryptionto wire this package's primitives into their commit / query paths. - @rotorsoft/act-tck —
pii_isolationcapability tests. Encryption is orthogonal to the capability and not part of the TCK.
Documentation
- PII encryption at rest — operator cookbook covering the five common patterns (pgcrypto, RDS TDE, Cloud SQL TDE, SQLite SEE, adapter-layer envelope encryption via this package).
- Handling sensitive data — declarative surface (
sensitive(...),.discloses(...),app.forget(...)) the column encryption sits underneath.
License
MIT
