package crypto import ( "crypto/aes" "crypto/cipher" "crypto/rand" "encoding/base64" "errors" "fmt" "strings" ) // SecretStore verschlüsselt und entschlüsselt vertrauliche Werte. // // Die Schnittstelle ist bewusst schmal gehalten, damit die lokale Umsetzung // später gegen ein KMS, ein HSM oder einen externen Secret Store getauscht // werden kann, ohne aufrufenden Code zu ändern (PROMPT.md §12). type SecretStore interface { // Encrypt verschlüsselt einen Klartext und meldet die verwendete Schlüsselversion. Encrypt(plaintext []byte) (ciphertext []byte, keyVersion string, encryptError error) // Decrypt entschlüsselt einen Geheimtext der angegebenen Schlüsselversion. Decrypt(ciphertext []byte, keyVersion string) ([]byte, error) // CurrentKeyVersion liefert die Version, mit der neu verschlüsselt wird. CurrentKeyVersion() string } // masterKeyLength ist die geforderte Länge des Hauptschlüssels (AES-256). const masterKeyLength = 32 // ErrUnknownKeyVersion meldet einen Geheimtext, dessen Schlüssel nicht vorliegt. // // Das ist ein Datenverlustrisiko und niemals ein stillschweigend zu // überspringender Fall (PROMPT.md §142). var ErrUnknownKeyVersion = errors.New("der zur entschlüsselung nötige schlüssel ist nicht verfügbar") // ErrSecretTampered meldet einen veränderten oder beschädigten Geheimtext. var ErrSecretTampered = errors.New("der geheimtext ist beschädigt oder wurde verändert") // LocalSecretStore verschlüsselt Secrets mit AES-256-GCM und lokal gehaltenen Schlüsseln. // // GCM ist authentifiziert: eine nachträgliche Veränderung des Geheimtextes wird // beim Entschlüsseln erkannt und nicht etwa als gültiger Wert ausgeliefert. type LocalSecretStore struct { // keysByVersion hält alle bekannten Schlüssel. Alte Versionen bleiben // erhalten, damit früher verschlüsselte Werte lesbar bleiben (PROMPT.md §143). keysByVersion map[string]cipher.AEAD // currentKeyVersion ist die Version, mit der neu verschlüsselt wird. currentKeyVersion string } // NewLocalSecretStore baut einen Secret Store aus den übergebenen Schlüsseln. // // keysByVersion bildet Versionsnamen auf 32 Byte lange Schlüssel ab. // currentKeyVersion benennt den Schlüssel für neue Verschlüsselungen. func NewLocalSecretStore(keysByVersion map[string][]byte, currentKeyVersion string) (*LocalSecretStore, error) { if len(keysByVersion) == 0 { return nil, errors.New("es wurde kein verschlüsselungsschlüssel übergeben") } if _, hasCurrentKey := keysByVersion[currentKeyVersion]; !hasCurrentKey { return nil, fmt.Errorf("der aktuelle schlüssel %q ist nicht in der schlüsselliste enthalten", currentKeyVersion) } preparedKeys := make(map[string]cipher.AEAD, len(keysByVersion)) for keyVersion, keyMaterial := range keysByVersion { // Ein zu kurzer Schlüssel würde die Verschlüsselung wirkungslos machen. if len(keyMaterial) != masterKeyLength { return nil, fmt.Errorf("der schlüssel %q hat %d byte, erforderlich sind %d", keyVersion, len(keyMaterial), masterKeyLength) } blockCipher, cipherError := aes.NewCipher(keyMaterial) if cipherError != nil { return nil, fmt.Errorf("der schlüssel %q konnte nicht verwendet werden: %w", keyVersion, cipherError) } authenticatedCipher, gcmError := cipher.NewGCM(blockCipher) if gcmError != nil { return nil, fmt.Errorf("der schlüssel %q konnte nicht für GCM verwendet werden: %w", keyVersion, gcmError) } preparedKeys[keyVersion] = authenticatedCipher } return &LocalSecretStore{keysByVersion: preparedKeys, currentKeyVersion: currentKeyVersion}, nil } // CurrentKeyVersion liefert die Version, mit der neu verschlüsselt wird. func (secretStore *LocalSecretStore) CurrentKeyVersion() string { return secretStore.currentKeyVersion } // Encrypt verschlüsselt einen Klartext mit dem aktuellen Schlüssel. // // Der Rückgabewert enthält die Nonce vorangestellt, damit der Geheimtext ohne // zusätzliche Ablage entschlüsselt werden kann. func (secretStore *LocalSecretStore) Encrypt(plaintext []byte) ([]byte, string, error) { authenticatedCipher := secretStore.keysByVersion[secretStore.currentKeyVersion] // Die Nonce muss je Schlüssel einmalig sein; sie wird deshalb zufällig erzeugt. messageNonce := make([]byte, authenticatedCipher.NonceSize()) if _, randomError := rand.Read(messageNonce); randomError != nil { return nil, "", fmt.Errorf("es konnte keine sichere nonce erzeugt werden: %w", randomError) } // Seal hängt den Geheimtext an die Nonce an, sodass beides zusammen bleibt. sealedSecret := authenticatedCipher.Seal(messageNonce, messageNonce, plaintext, nil) return sealedSecret, secretStore.currentKeyVersion, nil } // Decrypt entschlüsselt einen Geheimtext der angegebenen Schlüsselversion. func (secretStore *LocalSecretStore) Decrypt(ciphertext []byte, keyVersion string) ([]byte, error) { authenticatedCipher, hasKey := secretStore.keysByVersion[keyVersion] if !hasKey { return nil, fmt.Errorf("%w (version %q)", ErrUnknownKeyVersion, keyVersion) } nonceSize := authenticatedCipher.NonceSize() if len(ciphertext) < nonceSize { return nil, ErrSecretTampered } messageNonce := ciphertext[:nonceSize] sealedPayload := ciphertext[nonceSize:] plaintext, openError := authenticatedCipher.Open(nil, messageNonce, sealedPayload, nil) if openError != nil { // GCM meldet hier jede Veränderung des Geheimtextes. Der Fehler wird // bewusst nicht durchgereicht, da er keine verwertbare Information trägt. return nil, ErrSecretTampered } return plaintext, nil } // ParseKeySet liest eine Schlüsselliste aus ihrer Konfigurationsdarstellung. // // Erwartet wird eine kommaseparierte Liste aus Version und base64-kodiertem // Schlüssel, jüngste Version zuerst: // // v1:,v2: func ParseKeySet(encodedKeySet string) (map[string][]byte, error) { if strings.TrimSpace(encodedKeySet) == "" { return nil, errors.New("die schlüsselliste ist leer") } parsedKeys := make(map[string][]byte) for _, keyEntry := range strings.Split(encodedKeySet, ",") { trimmedEntry := strings.TrimSpace(keyEntry) if trimmedEntry == "" { continue } keyVersion, encodedKey, hasSeparator := strings.Cut(trimmedEntry, ":") if !hasSeparator { return nil, errors.New("ein eintrag der schlüsselliste hat nicht die form version:base64schlüssel") } keyVersion = strings.TrimSpace(keyVersion) if keyVersion == "" { return nil, errors.New("ein eintrag der schlüsselliste hat keine version") } keyMaterial, decodeError := base64.StdEncoding.DecodeString(strings.TrimSpace(encodedKey)) if decodeError != nil { // Der fehlerhafte Wert selbst wird nicht ausgegeben: er ist ein Geheimnis. return nil, fmt.Errorf("der schlüssel der version %q ist kein gültiges base64", keyVersion) } if len(keyMaterial) != masterKeyLength { return nil, fmt.Errorf("der schlüssel der version %q hat %d byte, erforderlich sind %d", keyVersion, len(keyMaterial), masterKeyLength) } parsedKeys[keyVersion] = keyMaterial } if len(parsedKeys) == 0 { return nil, errors.New("die schlüsselliste enthält keinen gültigen schlüssel") } return parsedKeys, nil } // GenerateMasterKey erzeugt einen neuen zufälligen Hauptschlüssel in base64. // // Die Funktion dient der Erstinbetriebnahme; erzeugte Schlüssel müssen sicher // verwahrt werden, da ohne sie keine verschlüsselten Werte lesbar sind. func GenerateMasterKey() (string, error) { keyMaterial := make([]byte, masterKeyLength) if _, randomError := rand.Read(keyMaterial); randomError != nil { return "", fmt.Errorf("es konnte kein sicherer schlüssel erzeugt werden: %w", randomError) } return base64.StdEncoding.EncodeToString(keyMaterial), nil }