Skip to content
This repository was archived by the owner on Apr 28, 2026. It is now read-only.

Commit ac3db24

Browse files
committed
feat: add CipherStore, PrefixStore, FallbackStore
1 parent b866f7d commit ac3db24

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

pkg/secrets/extras.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package secrets
2+
3+
import (
4+
"crypto/aes"
5+
"crypto/cipher"
6+
"crypto/rand"
7+
"errors"
8+
"fmt"
9+
"io"
10+
)
11+
12+
// CipherStore wraps a Store and encrypts values at rest.
13+
type CipherStore struct {
14+
store Store
15+
key []byte
16+
}
17+
18+
// NewCipherStore creates a store that encrypts values with AES-GCM.
19+
func NewCipherStore(store Store, key []byte) (*CipherStore, error) {
20+
if len(key) != 32 {
21+
return nil, errors.New("secrets: cipher key must be 32 bytes")
22+
}
23+
return &CipherStore{store: store, key: key}, nil
24+
}
25+
26+
func (c *CipherStore) Set(key string, value []byte) error {
27+
block, err := aes.NewCipher(c.key)
28+
if err != nil {
29+
return err
30+
}
31+
gcm, err := cipher.NewGCM(block)
32+
if err != nil {
33+
return err
34+
}
35+
nonce := make([]byte, gcm.NonceSize())
36+
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
37+
return err
38+
}
39+
ciphertext := gcm.Seal(nil, nonce, value, nil)
40+
return c.store.Set(key, append(nonce, ciphertext...))
41+
}
42+
43+
func (c *CipherStore) Get(key string) ([]byte, error) {
44+
data, err := c.store.Get(key)
45+
if err != nil {
46+
return nil, err
47+
}
48+
block, err := aes.NewCipher(c.key)
49+
if err != nil {
50+
return nil, err
51+
}
52+
gcm, err := cipher.NewGCM(block)
53+
if err != nil {
54+
return nil, err
55+
}
56+
nonceSize := gcm.NonceSize()
57+
if len(data) < nonceSize {
58+
return nil, errors.New("secrets: ciphertext too short")
59+
}
60+
plaintext, err := gcm.Open(nil, data[:nonceSize], data[nonceSize:], nil)
61+
if err != nil {
62+
return nil, fmt.Errorf("secrets: decrypt failed: %w", err)
63+
}
64+
return plaintext, nil
65+
}
66+
67+
func (c *CipherStore) Delete(key string) error {
68+
return c.store.Delete(key)
69+
}
70+
71+
// PrefixStore adds a namespace prefix to keys.
72+
type PrefixStore struct {
73+
store Store
74+
prefix string
75+
}
76+
77+
func NewPrefixStore(store Store, prefix string) *PrefixStore {
78+
return &PrefixStore{store: store, prefix: prefix}
79+
}
80+
81+
func (p *PrefixStore) Set(key string, value []byte) error {
82+
return p.store.Set(p.prefix+key, value)
83+
}
84+
85+
func (p *PrefixStore) Get(key string) ([]byte, error) {
86+
return p.store.Get(p.prefix + key)
87+
}
88+
89+
func (p *PrefixStore) Delete(key string) error {
90+
return p.store.Delete(p.prefix + key)
91+
}
92+
93+
// FallbackStore tries the primary store first, then falls back to secondary.
94+
type FallbackStore struct {
95+
primary Store
96+
secondary Store
97+
}
98+
99+
func NewFallbackStore(primary, secondary Store) *FallbackStore {
100+
return &FallbackStore{primary: primary, secondary: secondary}
101+
}
102+
103+
func (f *FallbackStore) Set(key string, value []byte) error {
104+
err := f.primary.Set(key, value)
105+
if err != nil {
106+
return f.secondary.Set(key, value)
107+
}
108+
return nil
109+
}
110+
111+
func (f *FallbackStore) Get(key string) ([]byte, error) {
112+
v, err := f.primary.Get(key)
113+
if err == nil {
114+
return v, nil
115+
}
116+
return f.secondary.Get(key)
117+
}
118+
119+
func (f *FallbackStore) Delete(key string) error {
120+
err1 := f.primary.Delete(key)
121+
err2 := f.secondary.Delete(key)
122+
if err1 != nil && err2 != nil {
123+
return err1
124+
}
125+
return nil
126+
}

0 commit comments

Comments
 (0)