Per-tenant key derivation (HKDF)
Per-tenant key derivation (HKDF) means generating each tenant's encryption key from one master secret on demand, never storing a distinct key per tenant. Caisson's field-crypto module runs HKDF-SHA256 over the master key, binding tenant id and key version into HKDF's info parameter so every derivation is deterministic, tenant-isolated, and rotation-aware without a key-storage surface.
In code
export function deriveTenantKey(
masterKey: Buffer,
salt: Buffer,
keyVersion: number,
tenantId: string,
): Buffer {
if (masterKey.length !== TENANT_KEY_BYTES) {
throw new ValidationError(
`field-crypto: MASTER_FIELD_KEY must be ${TENANT_KEY_BYTES} bytes, got ${masterKey.length}`,
);
}
if (salt.length !== TENANT_KEY_BYTES) {
throw new ValidationError(
`field-crypto: FIELD_CRYPTO_SALT must be ${TENANT_KEY_BYTES} bytes, got ${salt.length}`,
);
}
const info = deriveInfo(keyVersion, tenantId);
// hkdfSync returns an ArrayBuffer; wrap as a Buffer for the cipher key.
return Buffer.from(
hkdfSync("sha256", masterKey, salt, info, TENANT_KEY_BYTES),
);
}How it holds
No per-tenant key is ever stored, only derived
deriveTenantKey recomputes the identical 32-byte key every time from one master secret; there is no per-tenant key table to provision, rotate credentials for, back up, or exfiltrate. The master key itself is read once from the validated env and is never logged.
Tenant isolation lives in HKDF's info parameter, not the salt
deriveInfo binds the tenant id into the string caisson-field-crypto:v<keyVersion>:<tenantId>, exactly the domain-separation input HKDF's info parameter is defined for. Two tenants sharing the identical master key and salt still derive cryptographically independent keys (ADR-0043 Fork 3 confirmed the split).
Fail-closed validation before any derivation runs
deriveTenantKey asserts the master key and salt are each exactly 32 bytes, and deriveInfo bounds keyVersion to the integer range [1, 65535] and rejects an empty tenantId. Malformed input throws a ValidationError before hkdfSync is ever called, never a silently truncated or padded key.
Rotation is a version bump, not a re-encryption migration
keyVersion is baked into the same info string a key derives from, so incrementing it changes every newly derived key while records encrypted under an older version still decrypt correctly by re-deriving with the version recorded on them. Rotating forward never touches stored ciphertext.