Skip to content

Commit bd02232

Browse files
amirejazclaude
andauthored
Remove in-memory cache from EncryptedManager (#4645)
The previous implementation populated a syncmap.Map at construction time and served GetSecret reads from that cache. This meant writes by other processes were never visible to a long-running process. Remove the cache entirely. GetSecret now reads and decrypts the file on every call, consistent with all other Provider implementations (environment, keyring) which already read live on every access. SetSecret, DeleteSecret, and DeleteSecrets already re-read the file inside the write lock; remove the now-unnecessary cache update calls after each write. ListSecrets and Cleanup are updated to go through readFileSecrets instead of the removed map. NewEncryptedManager validates the file is readable at startup but no longer populates a cache. Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 9f38bbe commit bd02232

1 file changed

Lines changed: 40 additions & 73 deletions

File tree

pkg/secrets/encrypted.go

Lines changed: 40 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import (
1313
"path"
1414
"strings"
1515

16-
"golang.org/x/sync/syncmap"
17-
1816
"github.com/stacklok/toolhive/pkg/fileutils"
1917
"github.com/stacklok/toolhive/pkg/secrets/aes"
2018
)
@@ -24,30 +22,32 @@ import (
2422
type EncryptedManager struct {
2523
filePath string
2624
// Key used to re-encrypt the secrets file if changes are needed.
27-
key []byte
28-
secrets syncmap.Map // Thread-safe map for storing secrets
25+
key []byte
2926
}
3027

3128
// fileStructure is the structure of the secrets file.
3229
type fileStructure struct {
3330
Secrets map[string]string `json:"secrets"`
3431
}
3532

36-
// GetSecret retrieves a secret from the in-memory cache.
37-
// It does not re-read the file; secrets written by other processes after
38-
// construction may not be visible. This is intentional: CLI invocations
39-
// create a fresh manager per call, and long-running proxies only need
40-
// their own tokens.
33+
// GetSecret retrieves a secret from the secret store.
34+
//
35+
// The file is read and decrypted on every call so that changes written by
36+
// other processes are immediately visible.
4137
func (e *EncryptedManager) GetSecret(_ context.Context, name string) (string, error) {
4238
if name == "" {
4339
return "", errors.New("secret name cannot be empty")
4440
}
4541

46-
value, ok := e.secrets.Load(name)
42+
secrets, err := e.readFileSecrets()
43+
if err != nil {
44+
return "", fmt.Errorf("reading secrets: %w", err)
45+
}
46+
value, ok := secrets[name]
4747
if !ok {
4848
return "", fmt.Errorf("%w: %s", ErrSecretNotFound, name)
4949
}
50-
return value.(string), nil
50+
return value, nil
5151
}
5252

5353
// SetSecret stores a secret in the secret store.
@@ -64,14 +64,7 @@ func (e *EncryptedManager) SetSecret(_ context.Context, name, value string) erro
6464
return err
6565
}
6666
secrets[name] = value
67-
if err := e.writeFileSecrets(secrets); err != nil {
68-
return err
69-
}
70-
// Update the in-memory cache after the disk write. There is a brief
71-
// window where a concurrent GetSecret may return a stale value; this
72-
// is acceptable because the file is the authoritative source of truth.
73-
e.secrets.Store(name, value)
74-
return nil
67+
return e.writeFileSecrets(secrets)
7568
})
7669
}
7770

@@ -89,33 +82,24 @@ func (e *EncryptedManager) DeleteSecret(_ context.Context, name string) error {
8982
return err
9083
}
9184
if _, ok := secrets[name]; !ok {
92-
// Evict stale cache entry: another process may have already
93-
// deleted this key from disk while it remained in our cache.
94-
e.secrets.Delete(name)
9585
return fmt.Errorf("cannot delete non-existent secret: %s", name)
9686
}
9787
delete(secrets, name)
98-
if err := e.writeFileSecrets(secrets); err != nil {
99-
return err
100-
}
101-
// Update the in-memory cache after the disk write. There is a brief
102-
// window where a concurrent GetSecret may return a stale value; this
103-
// is acceptable because the file is the authoritative source of truth.
104-
e.secrets.Delete(name)
105-
return nil
88+
return e.writeFileSecrets(secrets)
10689
})
10790
}
10891

10992
// ListSecrets returns a list of all secret names stored in the manager.
11093
func (e *EncryptedManager) ListSecrets(_ context.Context) ([]SecretDescription, error) {
111-
var secretNames []SecretDescription
112-
113-
e.secrets.Range(func(key, _ interface{}) bool {
114-
secretNames = append(secretNames, SecretDescription{Key: key.(string)})
115-
return true
116-
})
117-
118-
return secretNames, nil
94+
secrets, err := e.readFileSecrets()
95+
if err != nil {
96+
return nil, fmt.Errorf("reading secrets: %w", err)
97+
}
98+
result := make([]SecretDescription, 0, len(secrets))
99+
for key := range secrets {
100+
result = append(result, SecretDescription{Key: key})
101+
}
102+
return result, nil
119103
}
120104

121105
// DeleteSecrets removes all named keys from the store.
@@ -130,30 +114,14 @@ func (e *EncryptedManager) DeleteSecrets(_ context.Context, keys []string) error
130114
for _, key := range keys {
131115
delete(current, key)
132116
}
133-
if err := e.writeFileSecrets(current); err != nil {
134-
return err
135-
}
136-
// Update in-memory cache after the disk write.
137-
for _, key := range keys {
138-
e.secrets.Delete(key)
139-
}
140-
return nil
117+
return e.writeFileSecrets(current)
141118
})
142119
}
143120

144121
// Cleanup removes all secrets managed by this manager.
145122
func (e *EncryptedManager) Cleanup() error {
146123
return fileutils.WithFileLock(e.filePath, func() error {
147-
empty := make(map[string]string)
148-
if err := e.writeFileSecrets(empty); err != nil {
149-
return err
150-
}
151-
// Clear the in-memory cache
152-
e.secrets.Range(func(key, _ interface{}) bool {
153-
e.secrets.Delete(key)
154-
return true
155-
})
156-
return nil
124+
return e.writeFileSecrets(make(map[string]string))
157125
})
158126
}
159127

@@ -170,7 +138,6 @@ func (*EncryptedManager) Capabilities() ProviderCapabilities {
170138

171139
// readFileSecrets reads and decrypts the secrets file, returning the current
172140
// on-disk secrets. Returns an empty map for an empty or non-existent file.
173-
// Must be called while holding the file lock.
174141
func (e *EncryptedManager) readFileSecrets() (map[string]string, error) {
175142
// #nosec G304: File path is not configurable at this time.
176143
data, err := os.ReadFile(e.filePath)
@@ -228,35 +195,35 @@ func NewEncryptedManager(filePath string, key []byte) (Provider, error) {
228195

229196
// Ensure the file exists (create if needed).
230197
// #nosec G304: File path is not configurable at this time.
231-
secretsFile, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, 0600)
198+
f, err := os.OpenFile(filePath, os.O_CREATE|os.O_RDWR, 0600)
232199
if err != nil {
233200
return nil, fmt.Errorf("failed to open secrets file: %w", err)
234201
}
235-
if err := secretsFile.Close(); err != nil {
236-
// Non-fatal: secrets file cleanup failure
202+
if err := f.Close(); err != nil {
237203
slog.Warn("Failed to close secrets file", "error", err)
238204
}
239205

240206
manager := &EncryptedManager{
241207
filePath: filePath,
242-
secrets: syncmap.Map{},
243208
key: key,
244209
}
245210

246-
// Load the initial snapshot into the in-memory cache.
247-
secrets, err := manager.readFileSecrets()
211+
// Validate the file is readable and correctly encrypted at startup.
212+
stat, err := os.Stat(filePath)
248213
if err != nil {
249-
if strings.Contains(err.Error(), "unable to decrypt") {
250-
fmt.Fprintf(os.Stderr, "\nSecrets file decryption failed: this usually means the password "+
251-
"is incorrect or the secrets file has been corrupted.\n"+
252-
"If your keyring was recently reset, try again with your original password.\n"+
253-
"If the secrets file is corrupted, delete it at %s and run 'thv secret setup' to start fresh.\n\n",
254-
filePath)
214+
return nil, fmt.Errorf("failed to stat secrets file: %w", err)
215+
}
216+
if stat.Size() > 0 {
217+
if _, err := manager.readFileSecrets(); err != nil {
218+
if strings.Contains(err.Error(), "unable to decrypt") {
219+
fmt.Fprintf(os.Stderr, "\nSecrets file decryption failed: this usually means the password "+
220+
"is incorrect or the secrets file has been corrupted.\n"+
221+
"If your keyring was recently reset, try again with your original password.\n"+
222+
"If the secrets file is corrupted, delete it at %s and run 'thv secret setup' to start fresh.\n\n",
223+
filePath)
224+
}
225+
return nil, err
255226
}
256-
return nil, err
257-
}
258-
for k, v := range secrets {
259-
manager.secrets.Store(k, v)
260227
}
261228

262229
return manager, nil

0 commit comments

Comments
 (0)