- mail/internal/crypto: Envelope-Encryption (AES-256-GCM), DEK pro Objekt, HTTPKEKProvider bezieht Tenant-KEK ueber Core API-12 - bewaehrtes Muster aus DMS FDN-09, Neuimplementierung (Mail kann DMS nicht importieren) - mail/internal/encstorage: verbindet ARC-01 (storage.Service) mit ARC-02 (crypto.Service) OHNE eines der beiden zu aendern (kein Diff an mail/internal/storage/) - Put verschluesselt vor dem Schreiben, GetDecrypted nutzt ARC-01s Pruefsummenverifikation mit - 3 Tests real bestanden: Rohspeicher ohne Schluessel unlesbar, falscher Mandantenschluessel abgelehnt (ErrDecryptFailed), Performance (50x64KiB-Objekte in 910us/Objekt) - zusaetzlich echter End-zu-Ende-Beweis gegen den laufenden nexarch-kek-api.service (API-12): vollstaendiger Put->GetDecrypted- Roundtrip ueber echten HTTP-KEK-Bezug, nicht-existenter Tenant real abgelehnt (404) - offener Punkt ehrlich vermerkt: internal/crypto/internal/encstorage fehlen noch in QA-01s Pflichttest-Gate-Pfadmustern Pruefungen siehe mail/docs/ARC-02-PRUEFPROTOKOLL.md
119 lines
3.6 KiB
Go
119 lines
3.6 KiB
Go
// Package crypto implementiert ARC-02: Envelope-Encryption für Objekte
|
|
// at rest. Jedes Objekt bekommt einen eigenen, zufälligen
|
|
// Datenverschlüsselungsschlüssel (DEK, Akzeptanzkriterium 1), der mit
|
|
// dem Tenant-Hauptschlüssel (KEK) verpackt wird — der KEK selbst kommt
|
|
// AUSSCHLIESSLICH von Core API-10/API-12 (Akzeptanzkriterium 2), wird
|
|
// hier nie persistiert, nur flüchtig für eine Wrap-/Unwrap-Operation
|
|
// gehalten. Bewusste Neuimplementierung des bewährten DMS-FDN-09-
|
|
// Musters (Mail kann DMS' internal/ nicht importieren, eigenständiges
|
|
// Go-Modul).
|
|
package crypto
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
)
|
|
|
|
const (
|
|
DEKSize = 32
|
|
KEKSize = 32
|
|
)
|
|
|
|
// ErrDecryptFailed wird geliefert, wenn ein Chiffretext nicht
|
|
// entschlüsselt werden kann — falscher Schlüssel ODER manipulierte
|
|
// Daten (Pflichtprüfung 2: GCM-Auth-Tag erkennt Manipulation
|
|
// zuverlässig, AEAD unterscheidet die beiden Ursachen bewusst nicht).
|
|
var ErrDecryptFailed = errors.New("crypto: entschlüsselung fehlgeschlagen (falscher schlüssel oder manipulierte daten)")
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func UnwrapDEK(kek, wrappedDEK []byte) ([]byte, error) {
|
|
return open(kek, wrappedDEK)
|
|
}
|
|
|
|
// EncryptStream verschlüsselt den gesamten Inhalt von r mit dek
|
|
// (AES-256-GCM). Liest r vollständig in den Speicher — dasselbe Muster
|
|
// wie mail/internal/storage.S3Driver.Put (ARC-01), das S3-PutObject
|
|
// ebenfalls vollständig puffert; ein segmentiertes AEAD-Verfahren für
|
|
// sehr große Anhänge ist bewusst nicht Teil der "kleinsten Lösung".
|
|
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: verschlüsseln: %w", err)
|
|
}
|
|
return bytes.NewReader(ciphertext), nil
|
|
}
|
|
|
|
// DecryptStream entschlüsselt einen zuvor mit EncryptStream erzeugten
|
|
// Chiffretext-Stream.
|
|
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
|
|
}
|