Skip to content

Commit 96f475c

Browse files
Refactor secret management with pluggable storage providers
Introduce a modular secret storage architecture that supports multiple backends (keyring and file-based storage). This enables better flexibility for different deployment scenarios while maintaining consistent encryption. Key changes: • Rename Client to Encryption for clarity and rename associated files • Extract secret storage into a Provider interface with two implementations: - KeyringProvider: Uses system keyring for credential storage - FileProvider: Uses encrypted files for secret persistence • Consolidate error types into errors.go for better maintainability • Update ModelProviderSecret to ModelProviderAssociated for semantic clarity • Refactor daemon_run to support configurable secret providers via config • Add comprehensive provider tests for both keyring and file backends The provider can be selected via the 'secret.provider' configuration (defaults to keyring, can be set to 'file' for file-based storage). Co-authored-by: construct-agent <noreply@construct.sh>
1 parent 2f4583b commit 96f475c

14 files changed

Lines changed: 332 additions & 117 deletions

File tree

backend/agent/client.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ import (
1313
)
1414

1515
type ModelProviderFactory struct {
16-
encryption *secret.Client
16+
encryption *secret.Encryption
1717
memory *memory.Client
1818
}
1919

20-
func NewModelProviderFactory(encryption *secret.Client, memory *memory.Client) *ModelProviderFactory {
20+
func NewModelProviderFactory(encryption *secret.Encryption, memory *memory.Client) *ModelProviderFactory {
2121
return &ModelProviderFactory{
2222
encryption: encryption,
2323
memory: memory,
@@ -33,7 +33,7 @@ func (f *ModelProviderFactory) CreateClient(
3333
return nil, fmt.Errorf("failed to fetch model provider: %w", err)
3434
}
3535

36-
providerAuth, err := f.encryption.Decrypt(provider.Secret, []byte(secret.ModelProviderSecret(provider.ID)))
36+
providerAuth, err := f.encryption.Decrypt(provider.Secret, []byte(secret.ModelProviderAssociated(provider.ID)))
3737
if err != nil {
3838
return nil, fmt.Errorf("failed to decrypt model provider secret: %w", err)
3939
}

backend/agent/runtime.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ func WithAnalytics(analytics analytics.Client) RuntimeOption {
6161
type Runtime struct {
6262
api *api.Server
6363
memory *memory.Client
64-
encryption *secret.Client
64+
encryption *secret.Encryption
6565
eventHub *event.MessageHub
6666
bus *event.Bus
6767
taskReconciler *TaskReconciler
@@ -71,7 +71,7 @@ type Runtime struct {
7171
metrics *prometheus.Registry
7272
}
7373

74-
func NewRuntime(memory *memory.Client, encryption *secret.Client, listener net.Listener, opts ...RuntimeOption) (*Runtime, error) {
74+
func NewRuntime(memory *memory.Client, encryption *secret.Encryption, listener net.Listener, opts ...RuntimeOption) (*Runtime, error) {
7575
options := DefaultRuntimeOptions()
7676
for _, opt := range opts {
7777
opt(options)
@@ -154,7 +154,7 @@ func (rt *Runtime) Run(ctx context.Context) error {
154154
}
155155
}
156156

157-
func (rt *Runtime) Encryption() *secret.Client {
157+
func (rt *Runtime) Encryption() *secret.Encryption {
158158
return rt.encryption
159159
}
160160

backend/api/api_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,7 @@ func (m *MockAgentRuntime) Memory() *memory.Client {
298298
return nil
299299
}
300300

301-
func (m *MockAgentRuntime) Encryption() *secret.Client {
301+
func (m *MockAgentRuntime) Encryption() *secret.Encryption {
302302
return nil
303303
}
304304

backend/api/model_provider.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import (
2222

2323
var _ v1connect.ModelProviderServiceHandler = (*ModelProviderHandler)(nil)
2424

25-
func NewModelProviderHandler(db *memory.Client, encryption *secret.Client) *ModelProviderHandler {
25+
func NewModelProviderHandler(db *memory.Client, encryption *secret.Encryption) *ModelProviderHandler {
2626
return &ModelProviderHandler{
2727
db: db,
2828
encryption: encryption,
@@ -31,7 +31,7 @@ func NewModelProviderHandler(db *memory.Client, encryption *secret.Client) *Mode
3131

3232
type ModelProviderHandler struct {
3333
db *memory.Client
34-
encryption *secret.Client
34+
encryption *secret.Encryption
3535
v1connect.UnimplementedModelProviderServiceHandler
3636
}
3737

@@ -47,7 +47,7 @@ func (h *ModelProviderHandler) CreateModelProvider(ctx context.Context, req *con
4747
}
4848

4949
modelProviderID := uuid.New()
50-
encryptedSecret, err := h.encryption.Encrypt(jsonSecret, []byte(secret.ModelProviderSecret(modelProviderID)))
50+
encryptedSecret, err := h.encryption.Encrypt(jsonSecret, []byte(secret.ModelProviderAssociated(modelProviderID)))
5151
if err != nil {
5252
return nil, apiError(fmt.Errorf("failed to encrypt API key"))
5353
}
@@ -211,7 +211,7 @@ func (h *ModelProviderHandler) UpdateModelProvider(ctx context.Context, req *con
211211
return nil, apiError(fmt.Errorf("failed to marshal API key: %w", err))
212212
}
213213

214-
encryptedSecret, err := h.encryption.Encrypt(jsonSecret, []byte(secret.ModelProviderSecret(id)))
214+
encryptedSecret, err := h.encryption.Encrypt(jsonSecret, []byte(secret.ModelProviderAssociated(id)))
215215
if err != nil {
216216
return nil, apiError(fmt.Errorf("failed to encrypt API key: %w", err))
217217
}
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,24 +14,24 @@ func GenerateKeyset() (*keyset.Handle, error) {
1414
return keyset.NewHandle(aead.AES256GCMKeyTemplate())
1515
}
1616

17-
type Client struct {
17+
type Encryption struct {
1818
keyset *keyset.Handle
1919
aead tink.AEAD
2020
}
2121

22-
func NewClient(keysetHandle *keyset.Handle) (*Client, error) {
22+
func NewClient(keysetHandle *keyset.Handle) (*Encryption, error) {
2323
aeadPrimitive, err := aead.New(keysetHandle)
2424
if err != nil {
2525
return nil, fmt.Errorf("aead.New failed: %v", err)
2626
}
2727

28-
return &Client{
28+
return &Encryption{
2929
keyset: keysetHandle,
3030
aead: aeadPrimitive,
3131
}, nil
3232
}
3333

34-
func (c *Client) Encrypt(plaintext []byte, associatedData []byte) ([]byte, error) {
34+
func (c *Encryption) Encrypt(plaintext []byte, associatedData []byte) ([]byte, error) {
3535
if plaintext == nil {
3636
return nil, fmt.Errorf("plaintext cannot be nil")
3737
}
@@ -44,7 +44,7 @@ func (c *Client) Encrypt(plaintext []byte, associatedData []byte) ([]byte, error
4444
return ciphertext, nil
4545
}
4646

47-
func (c *Client) Decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
47+
func (c *Encryption) Decrypt(ciphertext []byte, associatedData []byte) ([]byte, error) {
4848
if ciphertext == nil {
4949
return nil, fmt.Errorf("ciphertext cannot be nil")
5050
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,4 @@ func TestKeysetSerialization(t *testing.T) {
8585
if !bytes.Equal(decrypted, plaintext) {
8686
t.Fatal("Decrypted data doesn't match original plaintext")
8787
}
88-
}
88+
}

backend/secret/errors.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package secret
2+
3+
import "fmt"
4+
5+
type ErrSecretNotFound struct {
6+
Key string
7+
Err error
8+
}
9+
10+
func (e *ErrSecretNotFound) Error() string {
11+
return fmt.Sprintf("key %s not found: %s", e.Key, e.Err)
12+
}
13+
14+
func (e *ErrSecretNotFound) Is(target error) bool {
15+
_, ok := target.(*ErrSecretNotFound)
16+
return ok
17+
}
18+
19+
type ErrSecretMarshal struct {
20+
Key string
21+
Err error
22+
}
23+
24+
func (e *ErrSecretMarshal) Error() string {
25+
return fmt.Sprintf("failed to marshal secret %s: %s", e.Key, e.Err)
26+
}
27+
28+
type ErrSecretTooLarge struct {
29+
Key string
30+
Err error
31+
}
32+
33+
func (e *ErrSecretTooLarge) Error() string {
34+
return fmt.Sprintf("secret %s is too large: %s", e.Key, e.Err)
35+
}

backend/secret/file_provider.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package secret
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
8+
"github.com/spf13/afero"
9+
)
10+
11+
type FileProvider struct {
12+
basePath string
13+
fs afero.Fs
14+
}
15+
16+
func NewFileProvider(basePath string, fs afero.Fs) (*FileProvider, error) {
17+
if err := fs.MkdirAll(basePath, 0700); err != nil {
18+
return nil, fmt.Errorf("failed to create secret directory: %w", err)
19+
}
20+
21+
return &FileProvider{
22+
basePath: basePath,
23+
fs: fs,
24+
}, nil
25+
}
26+
27+
func (fp *FileProvider) Get(key string) (string, error) {
28+
filePath := filepath.Join(fp.basePath, key)
29+
30+
data, err := afero.ReadFile(fp.fs, filePath)
31+
if err != nil {
32+
if os.IsNotExist(err) {
33+
return "", &ErrSecretNotFound{Key: key, Err: err}
34+
}
35+
return "", fmt.Errorf("failed to read secret file: %w", err)
36+
}
37+
38+
return string(data), nil
39+
}
40+
41+
func (fp *FileProvider) Set(key string, value string) error {
42+
filePath := filepath.Join(fp.basePath, key)
43+
44+
if err := afero.WriteFile(fp.fs, filePath, []byte(value), 0600); err != nil {
45+
return fmt.Errorf("failed to write secret file: %w", err)
46+
}
47+
48+
return nil
49+
}
50+
51+
func (fp *FileProvider) Delete(key string) error {
52+
filePath := filepath.Join(fp.basePath, key)
53+
54+
if err := fp.fs.Remove(filePath); err != nil && !os.IsNotExist(err) {
55+
return fmt.Errorf("failed to delete secret file: %w", err)
56+
}
57+
58+
return nil
59+
}

backend/secret/keyring.go

Lines changed: 0 additions & 95 deletions
This file was deleted.

backend/secret/keyring_provider.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package secret
2+
3+
import (
4+
"github.com/zalando/go-keyring"
5+
)
6+
7+
const keychainService = "construct"
8+
9+
type KeyringProvider struct{}
10+
11+
func NewKeyringProvider() *KeyringProvider {
12+
return &KeyringProvider{}
13+
}
14+
15+
func (k *KeyringProvider) Get(key string) (string, error) {
16+
secret, err := keyring.Get(keychainService, key)
17+
if err != nil {
18+
return "", toError(key, err)
19+
}
20+
return secret, nil
21+
}
22+
23+
func (k *KeyringProvider) Set(key string, value string) error {
24+
err := keyring.Set(keychainService, key, value)
25+
if err != nil {
26+
return toError(key, err)
27+
}
28+
return nil
29+
}
30+
31+
func (k *KeyringProvider) Delete(key string) error {
32+
err := keyring.Delete(keychainService, key)
33+
if err != nil {
34+
return toError(key, err)
35+
}
36+
return nil
37+
}
38+
39+
func toError(key string, err error) error {
40+
if err == keyring.ErrNotFound {
41+
return &ErrSecretNotFound{Key: key, Err: err}
42+
}
43+
44+
if err == keyring.ErrSetDataTooBig {
45+
return &ErrSecretTooLarge{Key: key, Err: err}
46+
}
47+
48+
return err
49+
}

0 commit comments

Comments
 (0)