From 8d4a605b9f9945a92c780a2fc2d4e83c2f4090bc Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Wed, 1 Jul 2026 16:33:13 +0700 Subject: [PATCH] feat(crypto): AES-256-GCM password encryption --- internal/crypto/crypto.go | 47 ++++++++++++++++++++++++++++++++++ internal/crypto/crypto_test.go | 33 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 internal/crypto/crypto.go create mode 100644 internal/crypto/crypto_test.go diff --git a/internal/crypto/crypto.go b/internal/crypto/crypto.go new file mode 100644 index 0000000..789bbe8 --- /dev/null +++ b/internal/crypto/crypto.go @@ -0,0 +1,47 @@ +package crypto + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "errors" + "io" +) + +func Encrypt(key, plaintext []byte) (string, error) { + gcm, err := newGCM(key) + if err != nil { + return "", err + } + nonce := make([]byte, gcm.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return "", err + } + ct := gcm.Seal(nonce, nonce, plaintext, nil) + return base64.StdEncoding.EncodeToString(ct), nil +} + +func Decrypt(key []byte, enc string) ([]byte, error) { + gcm, err := newGCM(key) + if err != nil { + return nil, err + } + raw, err := base64.StdEncoding.DecodeString(enc) + if err != nil { + return nil, err + } + ns := gcm.NonceSize() + if len(raw) < ns { + return nil, errors.New("ciphertext too short") + } + return gcm.Open(nil, raw[:ns], raw[ns:], nil) +} + +func newGCM(key []byte) (cipher.AEAD, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return cipher.NewGCM(block) +} diff --git a/internal/crypto/crypto_test.go b/internal/crypto/crypto_test.go new file mode 100644 index 0000000..f94a6e4 --- /dev/null +++ b/internal/crypto/crypto_test.go @@ -0,0 +1,33 @@ +package crypto + +import ( + "bytes" + "testing" +) + +func TestEncryptDecryptRoundTrip(t *testing.T) { + key := make([]byte, 32) + enc, err := Encrypt(key, []byte("hunter2")) + if err != nil { + t.Fatalf("encrypt: %v", err) + } + if enc == "hunter2" { + t.Fatal("ciphertext must not equal plaintext") + } + got, err := Decrypt(key, enc) + if err != nil { + t.Fatalf("decrypt: %v", err) + } + if !bytes.Equal(got, []byte("hunter2")) { + t.Fatalf("got %q, want hunter2", got) + } +} + +func TestEncryptNonDeterministic(t *testing.T) { + key := make([]byte, 32) + a, _ := Encrypt(key, []byte("x")) + b, _ := Encrypt(key, []byte("x")) + if a == b { + t.Fatal("two encryptions must differ (random nonce)") + } +}