Vault
The vault provides encrypted persistence for restoration maps, enabling secure storage of PII mappings across process boundaries. It also offers in-memory encryption for sessions that need to keep restoration maps protected at rest.
Why vault?
The standard RestorationMap stores plaintext PII mappings (placeholder to original value). If an attacker gains access to memory or a serialized map, all original values are exposed. The vault encrypts these mappings using AES-256-GCM with envelope encryption, so plaintext PII never sits in memory unprotected and serialized maps are safe to persist.
VaultProvider
interface VaultProvider {
readonly name: string;
encrypt(plaintext: string): Promise<string>;
decrypt(ciphertext: string): Promise<string>;
generateDataKey(): Promise<{ plaintext: string; encrypted: string }>;
decryptDataKey(encrypted: string): Promise<string>;
}The VaultProvider interface defines four methods for envelope encryption:
generateDataKey()— Creates a random 32-byte data encryption key (DEK) and returns both its plaintext (base64) and an encrypted copy wrapped by the provider's master key.decryptDataKey()— Decrypts an encrypted DEK using the provider's master key.encrypt()— Encrypts a plaintext string (used for individual map entries).decrypt()— Decrypts a ciphertext string.
The MIT-licensed sensored package ships LocalVaultProvider. Cloud KMS providers (AWS KMS, GCP KMS, Azure Key Vault, HashiCorp Vault, WorkOS EKM) are available in the commercial @sensored/enterprise package.
LocalVaultProvider
LocalVaultProvider uses AES-256-GCM with a local master key. It supports two key modes:
Raw key mode
Provide a 32-byte Buffer as the master key:
import { LocalVaultProvider } from "sensored/vault";
import { randomBytes } from "node:crypto";
const key = randomBytes(32);
const provider = new LocalVaultProvider({ key });The key must be exactly 32 bytes. Store it securely (environment variable, secrets manager, HSM). Anyone with the key can decrypt all sealed data.
Passphrase mode
Provide a passphrase string; the provider derives a 32-byte key via HKDF-SHA256 with a random 16-byte salt:
const provider = new LocalVaultProvider({
passphrase: process.env.VAULT_PASSPHRASE!,
});The salt is generated per provider instance and embedded as a prefix in all encrypted output, so you don't need to store it separately. The same passphrase with a different salt produces a different key — this is by design.
Encrypted sessions
createEncryptedSession wraps a redaction session with in-memory encryption. The restoration map is stored as encrypted entries (AES-256-GCM with a DEK) and decrypted on demand for restore() and stream():
import { createEncryptedSession, LocalVaultProvider } from "sensored/vault";
import { randomBytes } from "node:crypto";
const provider = new LocalVaultProvider({ key: randomBytes(32) });
const session = await createEncryptedSession({
provider,
redactorConfig: {
presets: ["pii"],
rules: {},
},
});
// Redact as usual — PII is encrypted in memory
const redacted = session.redact("Email john@example.com");
// "Email [EMAIL_1]"
// Restore decrypts on demand
const restored = session.restore("Contact [EMAIL_1]");
// "Contact john@example.com"
// Stream restoration also decrypts on demand
const restorer = session.stream();
// Seal the map for persistence
const sealed = await session.seal();
// Base64-encoded sealed blob — safe to store anywhere
// Dispose to zero the DEK from memory
session.dispose();Hydration
Pass an existing RestorationMap to initialize the encrypted session with prior data. Each entry is encrypted at creation time:
const session = await createEncryptedSession({
provider,
redactorConfig: { presets: ["pii"], rules: {} },
existingMap: {
"[EMAIL_1]": "john@example.com",
},
});API
The EncryptedSession interface mirrors Session with two additions:
| Method | Description |
|---|---|
redact(text) | Redacts PII, encrypts new entries in the map |
redactMessages(messages) | Recursively redacts structured messages |
restore(text) | Restores placeholders by decrypting map entries |
stream() | Returns a StreamRestorer bound to the decrypted map |
reset() | Clears encrypted entries and reinitializes the redactor |
seal() | Returns a base64-encoded sealed blob of the map |
map | Returns the decrypted restoration map (read-only) |
dispose() | Clears entries and zeroes the DEK from memory |
detectOnly
Encrypted sessions do not support detectOnly mode. Passing a config with detectOnly: true throws a VaultError.
Seal and open
seal() and open() provide standalone envelope encryption for restoration maps, independent of session lifecycle:
import { seal, open, LocalVaultProvider } from "sensored/vault";
const provider = new LocalVaultProvider({ passphrase: "secret" });
// Seal a map into a portable base64 blob
const sealed = await seal(
{ "[EMAIL_1]": "john@example.com" },
provider,
{ conversationId: "abc-123" }, // optional metadata
);
// "eyJ2ZXJzaW9uIjoxLCJwcm92aWRlciI6ImxvY2FsIi..."
// Open it later (same provider)
const map = await open(sealed, provider);
// { "[EMAIL_1]": "john@example.com" }Sealed blob format
The sealed blob is a base64-encoded JSON object:
{
"version": 1,
"provider": "local",
"createdAt": "2026-01-15T12:00:00.000Z",
"encryptedDek": "base64...",
"encryptedMap": "base64...",
"metadata": { "conversationId": "abc-123" }
}version— Sealed blob format version (currently1).provider— Name of the provider that created the blob.open()throwsVAULT_PROVIDER_MISMATCHif the provider name doesn't match.encryptedDek— The data encryption key, encrypted by the provider's master key.encryptedMap— The JSON-serialized restoration map, encrypted by the DEK.metadata— Optional user-provided metadata (string key-value pairs).
Serialization helpers
serializeSealedBlob() and deserializeSealedBlob() are exported for low-level access:
import { serializeSealedBlob, deserializeSealedBlob } from "sensored/vault";Error handling
All vault errors extend VaultError with a code property:
import { VaultError } from "sensored/vault";
try {
await open(sealed, wrongProvider);
} catch (e) {
if (e instanceof VaultError) {
console.log(e.code); // "VAULT_PROVIDER_MISMATCH"
console.log(e.message); // "The sealed blob was created with a different vault provider."
}
}Error codes
| Code | Description |
|---|---|
VAULT_ENCRYPT_FAILED | Encryption operation failed |
VAULT_DECRYPT_FAILED | Decryption operation failed |
VAULT_KEY_DERIVATION_FAILED | HKDF key derivation from passphrase failed |
VAULT_INVALID_KEY | Key is missing or not 32 bytes |
VAULT_INVALID_SEALED_BLOB | Sealed blob is malformed or unsupported version |
VAULT_PROVIDER_MISMATCH | Sealed blob was created with a different provider |
VAULT_DEK_GENERATION_FAILED | DEK generation failed |
VAULT_DEK_DECRYPT_FAILED | DEK decryption failed |
Enterprise providers
Cloud KMS providers are available in @sensored/enterprise:
import { AwsKmsProvider } from "@sensored/enterprise";
const provider = new AwsKmsProvider({
keyId: "arn:aws:kms:us-east-1:123456789012:key/abc-def",
// Optional: override default AWS SDK credentials
region: "us-east-1",
});Supported providers:
| Provider | Class | Key manager |
|---|---|---|
| AWS KMS | AwsKmsProvider | AWS KMS |
| GCP KMS | GcpKmsProvider | Google Cloud KMS |
| Azure Key Vault | AzureKeyVaultProvider | Azure Key Vault |
| HashiCorp Vault | HashiCorpVaultProvider | HashiCorp Vault Transit Engine |
| WorkOS EKM | WorkosEkmProvider | WorkOS Enterprise Key Manager |
All cloud SDKs are lazy-loaded with no hard dependencies. Install the corresponding SDK package only when using that provider.
Platform requirements
The vault module uses node:crypto and requires Node.js 20+ or Bun. It is not available in browsers or edge runtimes.