Basis: Certvia dev@a48c5fb als Fundament für Craftvia
Unveränderter Stand von certvia/dev (a48c5fb) plus Craftvia-Spezifikation und Brandbook unter docs/craftvia/. ISMS-Module werden im Folgecommit entfernt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
// ── §9-Verschlüsselung der Tenant-Artefakte (client-seitig, vor dem Upload) ───
|
||||
//
|
||||
// Entscheidung (KONZEPT §9): client-seitige AES-256-Verschlüsselung mit EINEM
|
||||
// Schlüssel pro Umgebung — das Artefakt ist verschlüsselt, BEVOR es MinIO/S3
|
||||
// erreicht (zero-knowledge vom Speicher, at-rest + in-transit). Konsistent zur
|
||||
// bestehenden TOTP-Verschlüsselung (AES-256-GCM, src/server/secret-crypto.ts).
|
||||
//
|
||||
// Schlüssel: `BACKUP_ENC_KEY` (dediziert, pro Umgebung), Fallback `AUTH_SECRET`.
|
||||
// Der Schlüssel liegt NIE im Artefakt (Restore-Kohärenz-Regel, KONZEPT §9):
|
||||
// Pepper/MFA_ENC_KEY/BACKUP_ENC_KEY sind Umgebungs-Secrets, kein Artefakt-Inhalt.
|
||||
//
|
||||
// Blob-Layout (nach der Kompression):
|
||||
// magic "CVB1" | iv(12) | authTag(16) | ciphertext(gzip(plaintext))
|
||||
|
||||
import { createCipheriv, createDecipheriv, createHash, randomBytes } from "node:crypto";
|
||||
import { gzipSync, gunzipSync } from "node:zlib";
|
||||
|
||||
const MAGIC = Buffer.from("CVB1", "ascii");
|
||||
|
||||
function backupKey(): Buffer {
|
||||
const material = process.env.BACKUP_ENC_KEY || process.env.AUTH_SECRET;
|
||||
if (!material) {
|
||||
throw new Error(
|
||||
"BACKUP_ENC_KEY/AUTH_SECRET fehlt — Tenant-Artefakte können nicht verschlüsselt werden. " +
|
||||
"Pro Umgebung eindeutig setzen; NIEMALS im selben Bucket wie die Artefakte ablegen (KONZEPT §9).",
|
||||
);
|
||||
}
|
||||
return createHash("sha256").update(`${material}:tenant-backup`).digest(); // 32 Byte
|
||||
}
|
||||
|
||||
/** Komprimiert (gzip) und verschlüsselt (AES-256-GCM) den Klartext-Body. */
|
||||
export function sealArtifact(plaintext: string): Buffer {
|
||||
const gz = gzipSync(Buffer.from(plaintext, "utf8"));
|
||||
const iv = randomBytes(12);
|
||||
const cipher = createCipheriv("aes-256-gcm", backupKey(), iv);
|
||||
const ct = Buffer.concat([cipher.update(gz), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
return Buffer.concat([MAGIC, iv, tag, ct]);
|
||||
}
|
||||
|
||||
/** Entschlüsselt und dekomprimiert ein von `sealArtifact` erzeugtes Blob. */
|
||||
export function openArtifact(blob: Buffer): string {
|
||||
if (blob.length < MAGIC.length + 12 + 16 || !blob.subarray(0, 4).equals(MAGIC)) {
|
||||
throw new Error("Ungültiges/beschädigtes Backup-Artefakt (Magic-Byte-Prüfung fehlgeschlagen).");
|
||||
}
|
||||
let off = MAGIC.length;
|
||||
const iv = blob.subarray(off, off + 12);
|
||||
off += 12;
|
||||
const tag = blob.subarray(off, off + 16);
|
||||
off += 16;
|
||||
const ct = blob.subarray(off);
|
||||
const decipher = createDecipheriv("aes-256-gcm", backupKey(), iv);
|
||||
decipher.setAuthTag(tag);
|
||||
const gz = Buffer.concat([decipher.update(ct), decipher.final()]);
|
||||
return gunzipSync(gz).toString("utf8");
|
||||
}
|
||||
Reference in New Issue
Block a user