package secrets import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "fmt" "io" "strings" ) const AlgorithmAES256GCM = "AES-256-GCM" var ( ErrSecretEncryptionKeyMissing = errors.New("secret encryption key is not configured") ErrSecretPayloadInvalid = errors.New("secret payload must be a json object") ) type Encryptor struct { aead cipher.AEAD keyID string } type EncryptedPayload struct { Algorithm string KeyID string Nonce []byte Ciphertext []byte PayloadSHA256 string } func NewEncryptor(masterKeyBase64, keyID string) (*Encryptor, error) { masterKeyBase64 = strings.TrimSpace(masterKeyBase64) if masterKeyBase64 == "" { return nil, ErrSecretEncryptionKeyMissing } key, err := base64.StdEncoding.DecodeString(masterKeyBase64) if err != nil { if rawKey, rawErr := base64.RawStdEncoding.DecodeString(masterKeyBase64); rawErr == nil { key = rawKey } else { return nil, fmt.Errorf("decode secret encryption key: %w", err) } } if len(key) != 32 { return nil, fmt.Errorf("secret encryption key must decode to 32 bytes") } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("create secret cipher: %w", err) } aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("create secret gcm: %w", err) } if strings.TrimSpace(keyID) == "" { keyID = "local-v1" } return &Encryptor{aead: aead, keyID: keyID}, nil } func (e *Encryptor) KeyID() string { if e == nil { return "" } return e.keyID } func (e *Encryptor) Encrypt(plaintext, aad []byte) (EncryptedPayload, error) { if e == nil { return EncryptedPayload{}, ErrSecretEncryptionKeyMissing } nonce := make([]byte, e.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return EncryptedPayload{}, fmt.Errorf("generate secret nonce: %w", err) } hash := sha256.Sum256(plaintext) return EncryptedPayload{ Algorithm: AlgorithmAES256GCM, KeyID: e.keyID, Nonce: nonce, Ciphertext: e.aead.Seal(nil, nonce, plaintext, aad), PayloadSHA256: hex.EncodeToString(hash[:]), }, nil } func (e *Encryptor) Decrypt(payload EncryptedPayload, aad []byte) ([]byte, error) { if e == nil { return nil, ErrSecretEncryptionKeyMissing } if payload.Algorithm != "" && payload.Algorithm != AlgorithmAES256GCM { return nil, fmt.Errorf("unsupported secret algorithm %q", payload.Algorithm) } plaintext, err := e.aead.Open(nil, payload.Nonce, payload.Ciphertext, aad) if err != nil { return nil, fmt.Errorf("decrypt secret payload: %w", err) } return plaintext, nil } func ResourceSecretAAD(organizationID, resourceID, secretRef, protocol string) []byte { return []byte(strings.Join([]string{ "rap-resource-secret-v1", strings.TrimSpace(organizationID), strings.TrimSpace(resourceID), strings.TrimSpace(secretRef), strings.ToLower(strings.TrimSpace(protocol)), }, "|")) }