11package secrets
22
33import (
4+ "bytes"
45 "fmt"
6+ "io"
57 "os"
68 "path/filepath"
79
810 "filippo.io/age"
11+ "gopkg.in/yaml.v3"
912)
1013
1114// Manager handles secret encryption and storage
@@ -51,57 +54,137 @@ func (m *Manager) Init() (string, error) {
5154 return identity .Recipient ().String (), nil
5255}
5356
54- // Store saves a secret value (simplified for MVP: just writing to a file)
55- // In a real implementation, this would use SOPS to encrypt a YAML file.
56- // For this MVP without the sops binary, we'll implement direct age encryption.
57+ // Set encrypts and stores a secret value
5758func (m * Manager ) Set (key , value string ) error {
58- // 1. Load recipient (public key)
59- fKeys , err := os .Open (m .keyFile )
59+ secrets , err := m .loadAll ()
60+ if err != nil && ! os .IsNotExist (err ) {
61+ return err
62+ }
63+ if secrets == nil {
64+ secrets = make (map [string ]string )
65+ }
66+
67+ secrets [key ] = value
68+ return m .saveAll (secrets )
69+ }
70+
71+ // Get retrieves and decrypts a specific secret
72+ func (m * Manager ) Get (key string ) (string , error ) {
73+ secrets , err := m .loadAll ()
6074 if err != nil {
61- return fmt . Errorf ( "failed to open key file (did you run 'secrets init'?): %w " , err )
75+ return " " , err
6276 }
63- defer fKeys .Close ()
77+ val , ok := secrets [key ]
78+ if ! ok {
79+ return "" , fmt .Errorf ("secret '%s' not found" , key )
80+ }
81+ return val , nil
82+ }
6483
65- _ , err = age .ParseIdentities (fKeys )
84+ // List returns the names of all stored secrets
85+ func (m * Manager ) List () ([]string , error ) {
86+ secrets , err := m .loadAll ()
6687 if err != nil {
67- return fmt .Errorf ("failed to parse keys: %w" , err )
68- }
69-
70- // We need the recipient, usually simpler to just parse the first line or store pubkey strictly.
71- // For MVP, simplified approach:
72- // We will assume the secrets file is a simple YAML map, encrypted as a blob.
73- // Loading, decrypting, updating, and re-encrypting.
74-
75- // For stricter MVP: just standard file for now but encrypted content?
76- // Let's implement a dummy "Encrypted" marker for now as true SOPS integration logic requires external bins.
77-
88+ if os .IsNotExist (err ) {
89+ return []string {}, nil
90+ }
91+ return nil , err
92+ }
93+
94+ keys := make ([]string , 0 , len (secrets ))
95+ for k := range secrets {
96+ keys = append (keys , k )
97+ }
98+ return keys , nil
99+ }
100+
101+ func (m * Manager ) loadAll () (map [string ]string , error ) {
78102 secretsPath := filepath .Join (m .configDir , "secrets.enc" )
79-
80- // Implementation note: Fully implementing age encryption in pure Go here is possible
81- // but might be verbose for this step. Let's create the key infrastructure and
82- // a placeholder for the actual encryption to keep it buildable.
83-
84- // We'll write to a plaintext file for now, clearly marked, to demonstrate the flow,
85- // or fail if we want to be strict.
86- // BETTER: Let's assume we proceed with the structure but warn about encryption.
87-
88- f , err := os .OpenFile (secretsPath , os .O_APPEND | os .O_CREATE | os .O_WRONLY , 0600 )
103+ if _ , err := os .Stat (secretsPath ); os .IsNotExist (err ) {
104+ return nil , err
105+ }
106+
107+ // 1. Load identity
108+ identity , err := m .getIdentity ()
89109 if err != nil {
90- return err
110+ return nil , err
111+ }
112+
113+ // 2. Read encrypted file
114+ f , err := os .Open (secretsPath )
115+ if err != nil {
116+ return nil , err
91117 }
92118 defer f .Close ()
93-
94- // Mock encryption
95- if _ , err := fmt .Fprintf (f , "%s: [ENCRYPTED]%s\n " , key , value ); err != nil {
96- return err
119+
120+ // 3. Decrypt
121+ r , err := age .Decrypt (f , identity )
122+ if err != nil {
123+ return nil , fmt .Errorf ("failed to decrypt secrets: %w" , err )
124+ }
125+
126+ data , err := io .ReadAll (r )
127+ if err != nil {
128+ return nil , err
97129 }
98-
99- return nil
130+
131+ // 4. Parse YAML
132+ var secrets map [string ]string
133+ if err := yaml .Unmarshal (data , & secrets ); err != nil {
134+ return nil , err
135+ }
136+
137+ return secrets , nil
100138}
101139
102- // GetRecipient returns the public key for the managed identity
103- func (m * Manager ) GetRecipient () (string , error ) {
104- // Read first line which should be the private key, derive public
105- // This is a simplification.
106- return "age1..." , nil
140+ func (m * Manager ) saveAll (secrets map [string ]string ) error {
141+ secretsPath := filepath .Join (m .configDir , "secrets.enc" )
142+
143+ // 1. Get recipient
144+ identity , err := m .getIdentity ()
145+ if err != nil {
146+ return err
147+ }
148+ recipient := identity .Recipient ()
149+
150+ // 2. Marshal secrets
151+ data , err := yaml .Marshal (secrets )
152+ if err != nil {
153+ return err
154+ }
155+
156+ // 3. Encrypt
157+ buf := & bytes.Buffer {}
158+ w , err := age .Encrypt (buf , recipient )
159+ if err != nil {
160+ return err
161+ }
162+ if _ , err := w .Write (data ); err != nil {
163+ return err
164+ }
165+ if err := w .Close (); err != nil {
166+ return err
167+ }
168+
169+ // 4. Write to disk
170+ return os .WriteFile (secretsPath , buf .Bytes (), 0600 )
107171}
172+
173+ func (m * Manager ) getIdentity () (* age.X25519Identity , error ) {
174+ data , err := os .ReadFile (m .keyFile )
175+ if err != nil {
176+ return nil , fmt .Errorf ("failed to read key file (run 'secrets init'): %w" , err )
177+ }
178+
179+ // Parse first non-comment line
180+ lines := bytes .Split (data , []byte ("\n " ))
181+ for _ , line := range lines {
182+ line = bytes .TrimSpace (line )
183+ if len (line ) == 0 || line [0 ] == '#' {
184+ continue
185+ }
186+ return age .ParseX25519Identity (string (line ))
187+ }
188+
189+ return nil , fmt .Errorf ("no identity found in key file" )
190+ }
0 commit comments