Encryption
Это содержимое пока не доступно на вашем языке.
Object-storage payloads support client-side AES-GCM encryption — the framework encrypts before put, decrypts on get, keys managed as a single master key or a versioned key ring for rotation.
import { ObjectStorageDurableStateStore, ObjectStorageDurableStateStoreOptions, S3ObjectStorageBackend, S3ObjectStorageOptions,} from 'actor-ts';
const masterKey = Buffer.from(process.env.MASTER_KEY_V1!, 'base64'); // 32 bytes
const objectStorageDurableStateStoreOptions = ObjectStorageDurableStateStoreOptions.create() .withBackend(new S3ObjectStorageBackend(S3ObjectStorageOptions.create() /* .withRegion(...).withBucket(...) */)) .withEncryption({ mode: 'client-aes256-gcm', masterKey, info: 'acme/prod/durable-state/v1' });const store = new ObjectStorageDurableStateStore(objectStorageDurableStateStoreOptions);Now every persisted state is AES-GCM-encrypted before upload —
under a per-persistenceId subkey derived from the master key.
Reads transparently decrypt.
info is required and deployment-specific. It is not
decoration — see Choosing info below.
Why client-side encryption
Section titled “Why client-side encryption”Object stores (S3, GCS, Azure Blob) offer server-side encryption built-in. Why also encrypt client-side?
| Threat | Server-side | Client-side |
|---|---|---|
| Storage compromise (someone reads from disk) | ✓ | ✓ |
| Account compromise (someone has S3 credentials) | ✗ | ✓ |
| Cloud provider compromise | ✗ | ✓ |
| Audit / compliance requiring “we hold the keys” | ✗ | ✓ |
Client-side encryption protects against more threats but costs more (CPU per op, key-management overhead). Most apps should use both: server-side as baseline + client-side for sensitive payloads.
Configuration
Section titled “Configuration”type EncryptionConfig = | { mode: 'none' } | { mode: 'sse-s3' } // server-side, S3-managed | { mode: 'sse-kms'; kmsKeyId: string } // server-side, KMS-managed | { mode: 'client-aes256-gcm'; masterKey: Uint8Array; info: string } | { mode: 'client-aes256-gcm'; masterKeys: MasterKeyRing; info: string };
type MasterKeyRing = { active: MasterKeyRingEntry; // new writes encrypt under this retired?: MasterKeyRingEntry[]; // older keys, kept for decryption};type MasterKeyRingEntry = { version: number; // 0..255, embedded in the body manifest key: Uint8Array; // 32 bytes (AES-256)};Client-side encryption uses mode: 'client-aes256-gcm' with
either a single masterKey (32 bytes) or a masterKeys
ring. The ring carries:
active— the key new writes encrypt under.retired— older keys, kept to decrypt historical blobs.
Each entry’s version byte travels in the body manifest so
decrypt can pick the matching key. Carrying the active key plus
retired keys is the foundation of
key rotation.
Choosing info
Section titled “Choosing info”info is HKDF’s context-binding input (RFC 5869 §3.2). The
subkey for a blob is derived from three things: the master key,
the persistenceId (as HKDF salt), and info. Change any one
and you get an unrelated key.
It is required, and there is no default, deliberately. A shared
default would mean any two deployments holding the same master
key derive byte-for-byte the same subkey for the same
persistenceId — so a staging environment restored from a
production dump, or a DR region, could read production’s blobs
and nothing in the config would say so. That is a decision only
the operator can make, so the framework insists you make it.
Encode environment + purpose + version, most specific first:
'acme/prod/snapshot/v1''acme/staging/snapshot/v1''acme/prod/durable-state/v1'- Different environments MUST differ, even on the same master key. This is the whole point.
- Different payload kinds SHOULD differ (snapshots vs. durable state), so one compromised derivation context does not extend to the other.
- A trailing version gives a future context rotation somewhere to go.
info is not recorded on the wire. Unlike the key version,
no manifest byte records which info a blob was written under.
Changing it makes every existing blob undecryptable until a sweep
rewrites them — see
rotating the context.
Pick the value before the first write.
How it works
Section titled “How it works”On put: serialize value → compress → derive per-pid subkey (HKDF from active key) → AES-GCM(bytes, subkey, iv) → ciphertext → body manifest "ATS1" { flags, keyVersion, iv, ciphertext } → S3.put(body) // no key-id metadata header
On get: S3.get → body manifest { flags, keyVersion, iv, ciphertext } → pick master key by keyVersion (active or a retired entry) → derive per-pid subkey (HKDF) → AES-GCM decrypt → decompress → deserializeThe key version is embedded in the body manifest — every blob records which key version it was encrypted under. This lets the framework decrypt old payloads using the right key even after the active key has been rotated (retired keys stay in the ring).
What it encrypts
Section titled “What it encrypts”- The body — serialized state / event / snapshot.
- Not — the object key, object metadata headers, the bucket name.
For object metadata that shouldn’t leak (sensitive persistenceIds), use a separate naming scheme (hash IDs before they become object keys).
Key sources
Section titled “Key sources”The master key bytes come from somewhere. Common patterns:
Env vars
Section titled “Env vars”const masterKey = Buffer.from(process.env.MASTER_KEY_V1!, 'base64'); // 32 bytes// → .withEncryption({ mode: 'client-aes256-gcm', masterKey, info: 'acme/prod/snapshot/v1' })Simplest. Each key is a 32-byte buffer (for AES-256-GCM), base64-encoded in the env.
Risk: env vars are visible to anything that can read the process environment. Use only when the env is itself secured (K8s secrets, etc.).
KMS-on-load
Section titled “KMS-on-load”import { KMS } from '@aws-sdk/client-kms';
const kms = new KMS();const decrypted = await kms.decrypt({ KeyId: 'alias/master', CiphertextBlob: Buffer.from(process.env.WRAPPED_KEY!, 'base64'),});
const masterKey = decrypted.Plaintext!; // 32 bytes, held in memory// → .withEncryption({ mode: 'client-aes256-gcm', masterKey, info: 'acme/prod/snapshot/v1' })Master key is stored encrypted under a cloud KMS key. The app fetches it on startup, decrypts via KMS, holds in memory.
Better than raw env vars — only KMS access is required to recover keys.
HashiCorp Vault
Section titled “HashiCorp Vault”Similar pattern: pull master keys from Vault at startup.
CPU cost
Section titled “CPU cost”AES-GCM is fast — modern CPUs have hardware support.
Per 100 KB encrypt + decrypt:
- ~0.5-1 ms on modern x86 / Apple Silicon.
- Effectively free on small objects.
For most workloads, encryption is invisible in profiles.
Encryption + compression
Section titled “Encryption + compression”encrypt → compress → S3.put # ✗ no compression benefit on ciphertextcompress → encrypt → S3.put # ✓ this is what the framework doesThe framework’s order is compress first, then encrypt — compressed bytes are still compressible (not random); after encryption, they’re effectively random and uncompressable.
If you set both compression and encryption, you get this order automatically.
Reading old payloads
Section titled “Reading old payloads”After enabling encryption on a previously-unencrypted bucket:
state/cart-42 ← old, plaintext (encrypted flag unset in the manifest)state/cart-43 ← new, encrypted (encrypted flag set, key version 0)The framework detects each per-payload from the body manifest:
- Encrypted flag unset → plaintext path.
- Encrypted flag set → decrypt using the key version the manifest records.
Means you can enable encryption gradually — new writes get encrypted, old reads still work, and a background re-encryption sweep can migrate the rest.
See key rotation for the rotation flow.
Where to next
Section titled “Where to next”- Object storage overview — the bigger picture.
- Key rotation — the online rotation flow.
- Master key rotation (operations) — the operational side.
- Per-actor policies — per-actor encryption configuration.
