summaryrefslogtreecommitdiff
path: root/internal/security/credentials.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--internal/security/credentials.go57
1 files changed, 57 insertions, 0 deletions
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
+}