package security import ( "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "io" "strings" ) type CredentialCipher struct { current credentialKey keys []credentialKey } type credentialKey struct { id []byte aead cipher.AEAD } var credentialEnvelopeMagic = []byte("AGK1") func NewCredentialCipher(encodedKey string) (*CredentialCipher, error) { return NewCredentialKeyring(encodedKey, nil) } func NewCredentialKeyring(encodedKey string, previous []string) (*CredentialCipher, error) { values := append([]string{encodedKey}, previous...) keys := make([]credentialKey, 0, len(values)) seen := map[string]struct{}{} for _, value := range values { value = strings.TrimSpace(value) if value == "" { continue } key, err := base64.StdEncoding.DecodeString(value) if err != nil { return nil, fmt.Errorf("decode credential key: %w", err) } if len(key) != 32 { return nil, errors.New("credential key must be a base64-encoded 32-byte key") } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("create credential cipher: %w", err) } aead, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("create credential AEAD: %w", err) } fingerprint := sha256.Sum256(key) id := base64.RawURLEncoding.EncodeToString(fingerprint[:6]) if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} keys = append(keys, credentialKey{id: []byte(id), aead: aead}) } if len(keys) == 0 { return nil, errors.New("at least one credential key is required") } return &CredentialCipher{current: keys[0], keys: keys}, nil } func (c *CredentialCipher) Encrypt(plaintext string) ([]byte, error) { if plaintext == "" { return nil, errors.New("credential cannot be empty") } nonce := make([]byte, c.current.aead.NonceSize()) if _, err := io.ReadFull(rand.Reader, nonce); err != nil { return nil, fmt.Errorf("generate credential nonce: %w", err) } prefix := append(append([]byte{}, credentialEnvelopeMagic...), c.current.id...) sealed := c.current.aead.Seal(nil, nonce, []byte(plaintext), prefix) return append(append(prefix, nonce...), sealed...), nil } func (c *CredentialCipher) Decrypt(ciphertext []byte) (string, error) { if len(ciphertext) >= len(credentialEnvelopeMagic) && string(ciphertext[:len(credentialEnvelopeMagic)]) == string(credentialEnvelopeMagic) { idStart := len(credentialEnvelopeMagic) idEnd := idStart + 8 if len(ciphertext) < idEnd { return "", errors.New("credential ciphertext is truncated") } prefix := ciphertext[:idEnd] for _, key := range c.keys { if string(key.id) != string(ciphertext[idStart:idEnd]) { continue } nonceEnd := idEnd + key.aead.NonceSize() if len(ciphertext) < nonceEnd { return "", errors.New("credential ciphertext is truncated") } plaintext, err := key.aead.Open(nil, ciphertext[idEnd:nonceEnd], ciphertext[nonceEnd:], prefix) if err != nil { return "", errors.New("decrypt credential: authentication failed") } return string(plaintext), nil } return "", errors.New("credential key is not present in the configured keyring") } for _, key := range c.keys { if len(ciphertext) < key.aead.NonceSize() { continue } nonce := ciphertext[:key.aead.NonceSize()] plaintext, err := key.aead.Open(nil, nonce, ciphertext[key.aead.NonceSize():], nil) if err == nil { return string(plaintext), nil } } return "", errors.New("decrypt credential: authentication failed") }