From 5b651488b081b65fda8a323f228e139adb79a35d Mon Sep 17 00:00:00 2001 From: Chia Date: Tue, 4 Aug 2026 19:58:52 +1200 Subject: Build AI gateway control plane and admin UI --- internal/security/credentials.go | 57 +++++++++++++++++++++++++++++++++++ internal/security/credentials_test.go | 30 ++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 internal/security/credentials.go create mode 100644 internal/security/credentials_test.go (limited to 'internal/security') diff --git a/internal/security/credentials.go b/internal/security/credentials.go new file mode 100644 index 0000000..b55fb6b --- /dev/null +++ b/internal/security/credentials.go @@ -0,0 +1,57 @@ +package security + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "fmt" + "io" +) + +type CredentialCipher struct { + aead cipher.AEAD +} + +func NewCredentialCipher(encodedKey string) (*CredentialCipher, error) { + key, err := base64.StdEncoding.DecodeString(encodedKey) + 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) + } + return &CredentialCipher{aead: aead}, nil +} + +func (c *CredentialCipher) Encrypt(plaintext string) ([]byte, error) { + if plaintext == "" { + return nil, errors.New("credential cannot be empty") + } + nonce := make([]byte, c.aead.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, fmt.Errorf("generate credential nonce: %w", err) + } + return c.aead.Seal(nonce, nonce, []byte(plaintext), nil), nil +} + +func (c *CredentialCipher) Decrypt(ciphertext []byte) (string, error) { + if len(ciphertext) < c.aead.NonceSize() { + return "", errors.New("credential ciphertext is truncated") + } + nonce := ciphertext[:c.aead.NonceSize()] + plaintext, err := c.aead.Open(nil, nonce, ciphertext[c.aead.NonceSize():], nil) + if err != nil { + return "", errors.New("decrypt credential: authentication failed") + } + return string(plaintext), nil +} diff --git a/internal/security/credentials_test.go b/internal/security/credentials_test.go new file mode 100644 index 0000000..07fa59e --- /dev/null +++ b/internal/security/credentials_test.go @@ -0,0 +1,30 @@ +package security + +import ( + "encoding/base64" + "strings" + "testing" +) + +func TestCredentialCipherRoundTrip(t *testing.T) { + key := base64.StdEncoding.EncodeToString([]byte(strings.Repeat("k", 32))) + cipher, err := NewCredentialCipher(key) + if err != nil { + t.Fatal(err) + } + ciphertext, err := cipher.Encrypt("upstream-secret") + if err != nil { + t.Fatal(err) + } + plaintext, err := cipher.Decrypt(ciphertext) + if err != nil { + t.Fatal(err) + } + if plaintext != "upstream-secret" { + t.Fatalf("unexpected plaintext: %q", plaintext) + } + ciphertext[len(ciphertext)-1] ^= 1 + if _, err := cipher.Decrypt(ciphertext); err == nil { + t.Fatal("expected authentication failure for modified ciphertext") + } +} -- cgit v1.2.3