summaryrefslogtreecommitdiff
path: root/internal/security/credentials.go
diff options
context:
space:
mode:
authorChia <Chia@93.nz>2026-08-05 22:01:29 +1200
committerChia <Chia@93.nz>2026-08-05 22:07:50 +1200
commiteadb2ffe85c43cf6fc741c9823cd28eedb4a844c (patch)
tree1aba2536d57360da403aa35c9ced58b615c7064e /internal/security/credentials.go
parentcd0dd91ab93653631904f2ea0e574ccde6d60339 (diff)
feat: harden prepaid billing and commercial operations
Diffstat (limited to '')
-rw-r--r--internal/security/credentials.go106
1 files changed, 83 insertions, 23 deletions
diff --git a/internal/security/credentials.go b/internal/security/credentials.go
index b55fb6b..900261e 100644
--- a/internal/security/credentials.go
+++ b/internal/security/credentials.go
@@ -4,54 +4,114 @@ 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) {
- 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)
+ 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})
}
- aead, err := cipher.NewGCM(block)
- if err != nil {
- return nil, fmt.Errorf("create credential AEAD: %w", err)
+ if len(keys) == 0 {
+ return nil, errors.New("at least one credential key is required")
}
- return &CredentialCipher{aead: aead}, nil
+ 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.aead.NonceSize())
+ 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)
}
- return c.aead.Seal(nonce, nonce, []byte(plaintext), nil), nil
+ 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) < c.aead.NonceSize() {
- return "", errors.New("credential ciphertext is truncated")
+ 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")
}
- 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")
+ 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 string(plaintext), nil
+ return "", errors.New("decrypt credential: authentication failed")
}