Envelope-Encryption: DEK (32 Byte) frisch je Objekt, AES-256-GCM fuer Objektinhalt, DEK selbst mit Tenant-KEK verpackt (WrapDEK/UnwrapDEK). KEK kommt ausschliesslich ueber HTTPKEKProvider von Core API-10 (internal/kek.Handler.TenantKEKHandler-Vertrag, Service-Credential- authentifiziert), wird nie persistiert - jeder Seal/Open-Aufruf bezieht ihn frisch. RewrapDEK fuer KEK-Rotation ohne Neuverschluesselung der Objekte (nur der DEK-Wrapper wird neu verpackt, Chiffretext bleibt unveraendert). Auf 192.168.1.131 verifiziert: Round-Trip Encrypt/Decrypt, manipulierter Chiffretext UND falscher Schluessel werden beide ueber GCM-Auth-Tag abgelehnt, KEK-Rotation real getestet (RewrapDEK, altes Objekt danach mit neuem KEK weiterhin lesbar, alter KEK funktioniert nicht mehr). Befund dokumentiert: Cores internal/kek.Handler (Gegenstelle fuer den KEK-Bezug) ist noch in keinem cmd/*/main.go verdrahtet, dieselbe Fehlerklasse wie FDN-03/QA-05/AUD-06. HTTPKEKProvider daher gegen den dokumentierten Vertrag getestet, nicht gegen eine laufende Core-Instanz. Siehe dms/docs/FDN-09-PRUEFPROTOKOLL.md. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HhgFcLS8tYMhDJpP74C6AQ
141 lines
4.7 KiB
Go
141 lines
4.7 KiB
Go
// Package crypto implementiert FDN-09: Envelope-Encryption fuer Objekte
|
|
// at rest. Jedes Objekt bekommt einen eigenen, zufaelligen
|
|
// Datenverschluesselungsschluessel (DEK, Akzeptanzkriterium 1), der mit
|
|
// dem Tenant-Hauptschluessel (KEK) verpackt wird — der KEK selbst kommt
|
|
// AUSSCHLIESSLICH von Core API-10 (Akzeptanzkriterium 2), wird hier nie
|
|
// persistiert, nur fluechtig fuer eine Wrap-/Unwrap-Operation gehalten.
|
|
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
// DEKSize/KEKSize sind die geforderten Laengen fuer AES-256-GCM.
|
|
const (
|
|
DEKSize = 32
|
|
KEKSize = 32
|
|
)
|
|
|
|
// ErrDecryptFailed wird geliefert, wenn ein Chiffretext nicht entschluesselt
|
|
// werden kann — falscher Schluessel ODER manipulierte Daten (Pruefung 2:
|
|
// GCM-Auth-Tag erkennt Manipulation zuverlaessig, AEAD unterscheidet die
|
|
// beiden Ursachen bewusst nicht, um keine Seitenkanal-Information ueber
|
|
// "welcher Teil" falsch war preiszugeben).
|
|
var ErrDecryptFailed = errors.New("crypto: entschluesselung fehlgeschlagen (falscher schluessel oder manipulierte daten)")
|
|
|
|
// GenerateDEK erzeugt einen neuen, zufaelligen Datenverschluesselungs-
|
|
// schluessel — fuer JEDES Objekt neu, nie wiederverwendet
|
|
// (Akzeptanzkriterium 1).
|
|
func GenerateDEK() ([]byte, error) {
|
|
dek := make([]byte, DEKSize)
|
|
if _, err := rand.Read(dek); err != nil {
|
|
return nil, fmt.Errorf("crypto: dek erzeugen: %w", err)
|
|
}
|
|
return dek, nil
|
|
}
|
|
|
|
func seal(key, plaintext []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: aes-cipher erstellen: %w", err)
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: gcm erstellen: %w", err)
|
|
}
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := rand.Read(nonce); err != nil {
|
|
return nil, fmt.Errorf("crypto: nonce erzeugen: %w", err)
|
|
}
|
|
return gcm.Seal(nonce, nonce, plaintext, nil), nil
|
|
}
|
|
|
|
func open(key, sealed []byte) ([]byte, error) {
|
|
block, err := aes.NewCipher(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: aes-cipher erstellen: %w", err)
|
|
}
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: gcm erstellen: %w", err)
|
|
}
|
|
if len(sealed) < gcm.NonceSize() {
|
|
return nil, ErrDecryptFailed
|
|
}
|
|
nonce, ciphertext := sealed[:gcm.NonceSize()], sealed[gcm.NonceSize():]
|
|
plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
|
|
if err != nil {
|
|
return nil, ErrDecryptFailed
|
|
}
|
|
return plaintext, nil
|
|
}
|
|
|
|
// WrapDEK verpackt einen DEK mit dem Tenant-KEK (Envelope-Verfahren).
|
|
func WrapDEK(kek, dek []byte) ([]byte, error) {
|
|
wrapped, err := seal(kek, dek)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: dek verpacken: %w", err)
|
|
}
|
|
return wrapped, nil
|
|
}
|
|
|
|
// UnwrapDEK entpackt einen zuvor mit WrapDEK verpackten DEK.
|
|
func UnwrapDEK(kek, wrappedDEK []byte) ([]byte, error) {
|
|
return open(kek, wrappedDEK)
|
|
}
|
|
|
|
// RewrapDEK verpackt einen bereits vorhandenen DEK-Wrapper von oldKEK auf
|
|
// newKEK um, OHNE den DEK selbst oder den Objekt-Chiffretext zu veraendern
|
|
// (Akzeptanzkriterium 3: KEK-Rotation ohne Neuverschluesselung aller
|
|
// Objekte — nur der DEK-Wrapper wird neu verpackt).
|
|
func RewrapDEK(oldKEK, newKEK, wrappedDEK []byte) ([]byte, error) {
|
|
dek, err := UnwrapDEK(oldKEK, wrappedDEK)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: dek mit altem kek entpacken: %w", err)
|
|
}
|
|
rewrapped, err := WrapDEK(newKEK, dek)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: dek mit neuem kek verpacken: %w", err)
|
|
}
|
|
return rewrapped, nil
|
|
}
|
|
|
|
// EncryptStream verschluesselt den gesamten Inhalt von r mit dek
|
|
// (AES-256-GCM) und liefert den Chiffretext als Stream. Liest r vollstaendig
|
|
// in den Speicher (dasselbe Muster wie internal/storage.S3Driver.Put aus
|
|
// FDN-03, das S3-PutObject ebenfalls vollstaendig puffert) — fuer sehr
|
|
// grosse Dateien waere ein segmentiertes AEAD-Verfahren noetig, das ist
|
|
// bewusst nicht Teil der "kleinsten Loesung" dieser Kachel.
|
|
func EncryptStream(dek []byte, r io.Reader) (io.Reader, error) {
|
|
plaintext, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: klartext lesen: %w", err)
|
|
}
|
|
ciphertext, err := seal(dek, plaintext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: verschluesseln: %w", err)
|
|
}
|
|
return bytes.NewReader(ciphertext), nil
|
|
}
|
|
|
|
// DecryptStream entschluesselt einen zuvor mit EncryptStream erzeugten
|
|
// Chiffretext-Stream. Liefert ErrDecryptFailed bei manipuliertem Chiffretext
|
|
// (Pruefung 2, GCM-Auth-Tag).
|
|
func DecryptStream(dek []byte, r io.Reader) (io.Reader, error) {
|
|
ciphertext, err := io.ReadAll(r)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("crypto: chiffretext lesen: %w", err)
|
|
}
|
|
plaintext, err := open(dek, ciphertext)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return bytes.NewReader(plaintext), nil
|
|
}
|