Zum Inhalt springen
Deutsch

Verschlüsselung

Object-Storage-Payloads unterstützen Client-Side-AES-GCM-Verschlüsselung — das Framework verschlüsselt vor dem Put, entschlüsselt beim Get, Schlüssel werden über einen MasterKeyRing verwaltet.

import {
ObjectStorageDurableStateStore,
S3ObjectStorageBackend,
MasterKeyRing,
} from 'actor-ts';
const keyRing = new MasterKeyRing({
keys: { 'k1': process.env.MASTER_KEY_V1! },
currentKeyId: 'k1',
});
const store = new ObjectStorageDurableStateStore({
backend: new S3ObjectStorageBackend({ /* ... */ }),
encryption: { keyRing },
});

Jetzt wird jeder persistierte State vor dem Upload mit AES-GCM unter k1 verschlüsselt. Reads entschlüsseln transparent.

Object Stores (S3, GCS, Azure Blob) bieten eingebaute Server-Side-Encryption. Warum auch Client-Side verschlüsseln?

BedrohungServer-SideClient-Side
Storage-Kompromittierung (jemand liest von der Disk)
Account-Kompromittierung (jemand hat S3-Credentials)
Cloud-Provider-Kompromittierung
Audit / Compliance, die “wir halten die Schlüssel” verlangt

Client-Side-Verschlüsselung schützt vor mehr Bedrohungen, kostet aber mehr (CPU pro Op, Schlüsselverwaltungs-Overhead). Die meisten Apps sollten beides verwenden: Server-Side als Baseline + Client-Side für sensible Payloads.

interface EncryptionConfig {
keyRing: MasterKeyRing;
/** Den Algorithmus überschreiben; Default 'AES-GCM'. */
algorithm?: 'AES-GCM';
}

Der MasterKeyRing trägt:

  • keys — keyId → Schlüssel-Bytes-Map.
  • currentKeyId — welchen Schlüssel neue Writes verwenden.

Mehrere Schlüssel im Ring lassen dich ältere Payloads (unter älteren Schlüsseln verschlüsselt) lesen, während neue Payloads den aktuellen Schlüssel verwenden. Das ist das Fundament der Schlüsselrotation.

Beim Put:
Wert serialisieren → Bytes → AES-GCM(bytes, currentKey) → Ciphertext
→ Metadaten { keyId: currentKeyId, iv, tag }
→ S3.put(ciphertext, x-amz-meta-key-id: currentKeyId, etc.)
Beim Get:
S3.get → Ciphertext + Metadaten{ keyId, iv, tag }
→ keyRing.get(keyId) → decrypt(ciphertext, key, iv, tag)
→ Bytes → deserialisieren

Die Key-ID wird neben dem Ciphertext gespeichert — jeder Blob weiß, mit welchem Schlüssel er verschlüsselt wurde. Das lässt das Framework alte Payloads mit dem richtigen Schlüssel entschlüsseln, auch nachdem der aktuelle Schlüssel rotiert wurde.

  • Den Body — serialisierter State / Event / Snapshot.
  • Nicht — den Object-Key, Metadaten-Header (außer der Key-ID), den Bucket-Namen.

Für Object-Metadaten, die nicht durchsickern sollten (sensible persistenceIds), verwende ein separates Naming-Schema (hashe IDs, bevor sie zu Object-Keys werden).

Die Master-Key-Bytes kommen irgendwoher. Häufige Muster:

new MasterKeyRing({
keys: {
'k1': Buffer.from(process.env.MASTER_KEY_V1!, 'base64'),
},
currentKeyId: 'k1',
});

Am einfachsten. Jeder Schlüssel ist ein 32-Byte-Buffer (für AES-256-GCM), base64-kodiert in der Env.

Risiko: Env-Vars sind für alles sichtbar, das die Prozess-Umgebung lesen kann. Verwende nur, wenn die Env selbst gesichert ist (K8s-Secrets, etc.).

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 keyRing = new MasterKeyRing({
keys: { 'k1': decrypted.Plaintext! },
currentKeyId: 'k1',
});

Der Master-Key wird verschlüsselt unter einem Cloud-KMS-Key gespeichert. Die App holt ihn beim Start, entschlüsselt über KMS, hält ihn im Speicher.

Besser als rohe Env-Vars — nur KMS-Zugriff ist erforderlich, um Schlüssel wiederherzustellen.

Ähnliches Muster: Master-Keys beim Start aus Vault ziehen.

AES-GCM ist schnell — moderne CPUs haben Hardware-Unterstützung.

Pro 100 KB Verschlüsseln + Entschlüsseln:

  • ~0,5-1 ms auf modernem x86 / Apple Silicon.
  • Auf kleinen Objekten effektiv kostenlos.

Für die meisten Workloads ist Verschlüsselung in Profilen unsichtbar.

verschlüsseln → komprimieren → S3.put # ✗ kein Kompressionsnutzen auf Ciphertext
komprimieren → verschlüsseln → S3.put # ✓ das macht das Framework

Die Reihenfolge des Frameworks ist zuerst komprimieren, dann verschlüsseln — komprimierte Bytes sind immer noch komprimierbar (nicht zufällig); nach der Verschlüsselung sind sie effektiv zufällig und unkomprimierbar.

Wenn du sowohl Kompression als auch Verschlüsselung setzt, bekommst du diese Reihenfolge automatisch.

Nachdem du Verschlüsselung auf einem zuvor unverschlüsselten Bucket aktiviert hast:

state/cart-42 ← alt, Plaintext, keine Key-ID-Metadaten
state/cart-43 ← neu, verschlüsselt, Key-ID: k1

Das Framework erkennt jedes Pro-Payload über die Metadaten:

  • Keine Key-ID-Metadaten → Plaintext-Pfad.
  • Key-ID vorhanden → über den keyRing entschlüsseln.

Bedeutet, du kannst Verschlüsselung schrittweise aktivieren — neue Writes werden verschlüsselt, alte Reads funktionieren immer noch, und ein Hintergrund-Re-Encryption-Sweep kann den Rest migrieren.

Siehe Schlüsselrotation für den Rotation-Flow.