@@ -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 (
2422type 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.
3229type 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.
4137func (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.
11093func (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.
145122func (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.
174141func (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 , "\n Secrets 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 , "\n Secrets 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