Skip to content

Commit 9a5d6d9

Browse files
committed
store: feat encrypted filestore
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent 5272474 commit 9a5d6d9

File tree

105 files changed

+18745
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

105 files changed

+18745
-3
lines changed

store/filestore/filestore.go

Lines changed: 350 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,350 @@
1+
package filestore
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/base64"
7+
"encoding/json"
8+
"io"
9+
"io/fs"
10+
"os"
11+
"strings"
12+
13+
"filippo.io/age"
14+
15+
"github.com/docker/secrets-engine/store"
16+
)
17+
18+
type fileStore[T store.Secret] struct {
19+
filesystem *os.Root
20+
masterKey string
21+
f store.Factory[T]
22+
}
23+
24+
var _ store.Store = &fileStore[store.Secret]{}
25+
26+
type keyStore map[string]string
27+
28+
type file struct {
29+
metadata map[string]string
30+
encryptedSecret []byte
31+
}
32+
33+
// getFileNames retrieves the base64 encoded string of the secret ID and
34+
// metadata file
35+
func getFileNames(id store.ID) (string, string) {
36+
secretFileName := base64.StdEncoding.EncodeToString([]byte(id.String()))
37+
metadataFileName := secretFileName + "-meta.json"
38+
return secretFileName, metadataFileName
39+
}
40+
41+
func decryptSecret(encrypted []byte, masterKey string) ([]byte, error) {
42+
identity, err := age.NewScryptIdentity(masterKey)
43+
if err != nil {
44+
return nil, err
45+
}
46+
r, err := age.Decrypt(bytes.NewReader(encrypted), identity)
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
data, err := io.ReadAll(r)
52+
if err != nil {
53+
return nil, err
54+
}
55+
return data, nil
56+
}
57+
58+
func getFile(ctx context.Context, filesystem *os.Root, secretFileName, metadataFileName string) (*file, error) {
59+
select {
60+
case <-ctx.Done():
61+
return nil, ctx.Err()
62+
default:
63+
}
64+
65+
metadataStore, err := filesystem.Open(metadataFileName)
66+
if err != nil {
67+
return nil, err
68+
}
69+
defer metadataStore.Close()
70+
71+
fileStore, err := filesystem.Open(secretFileName)
72+
if err != nil {
73+
return nil, err
74+
}
75+
defer fileStore.Close()
76+
77+
var metadata map[string]string
78+
b, err := io.ReadAll(metadataStore)
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
if err := json.Unmarshal(b, &metadata); err != nil {
84+
return nil, err
85+
}
86+
87+
encryptedSecret, err := io.ReadAll(fileStore)
88+
if err != nil {
89+
return nil, err
90+
}
91+
92+
return &file{
93+
metadata: metadata,
94+
encryptedSecret: encryptedSecret,
95+
}, nil
96+
}
97+
98+
func saveFile(ctx context.Context, id store.ID, f file, filesystem *os.Root) error {
99+
select {
100+
case <-ctx.Done():
101+
return ctx.Err()
102+
default:
103+
}
104+
105+
secretFileName, metadataFileName := getFileNames(id)
106+
tmpSecretFileName := secretFileName + ".tmp"
107+
tmpMetadataFileName := metadataFileName + ".tmp"
108+
109+
tmpMetadata, err := filesystem.Create(tmpMetadataFileName)
110+
if err != nil {
111+
return err
112+
}
113+
defer tmpMetadata.Close()
114+
115+
tmpSecret, err := filesystem.Create(tmpSecretFileName)
116+
if err != nil {
117+
return err
118+
}
119+
defer tmpSecret.Close()
120+
121+
enc := json.NewEncoder(tmpMetadata)
122+
if err := enc.Encode(f.metadata); err != nil {
123+
return err
124+
}
125+
if err := tmpMetadata.Sync(); err != nil {
126+
return err
127+
}
128+
if err := tmpMetadata.Close(); err != nil {
129+
return err
130+
}
131+
132+
if _, err = tmpSecret.Write(f.encryptedSecret); err != nil {
133+
return err
134+
}
135+
if err := tmpSecret.Sync(); err != nil {
136+
return err
137+
}
138+
if err := tmpSecret.Close(); err != nil {
139+
return err
140+
}
141+
142+
if err := filesystem.Rename(tmpMetadataFileName, metadataFileName); err != nil {
143+
return err
144+
}
145+
if err := filesystem.Rename(tmpSecretFileName, secretFileName); err != nil {
146+
return err
147+
}
148+
149+
return nil
150+
}
151+
152+
func (f *fileStore[T]) Delete(ctx context.Context, id store.ID) error {
153+
secretsFileName, metadataFileName := getFileNames(id)
154+
if err := f.filesystem.Remove(metadataFileName); err != nil {
155+
return err
156+
}
157+
if err := f.filesystem.Remove(secretsFileName); err != nil {
158+
return err
159+
}
160+
return nil
161+
}
162+
163+
func (f *fileStore[T]) Filter(ctx context.Context, pattern store.Pattern) (map[store.ID]store.Secret, error) {
164+
fsFiles, err := fs.ReadDir(f.filesystem.FS(), ".")
165+
if err != nil {
166+
return nil, err
167+
}
168+
if len(fsFiles) == 0 {
169+
return nil, store.ErrCredentialNotFound
170+
}
171+
172+
secrets := make(map[store.ID]store.Secret)
173+
for _, fsFile := range fsFiles {
174+
select {
175+
case <-ctx.Done():
176+
return nil, ctx.Err()
177+
default:
178+
}
179+
180+
if fsFile.IsDir() || strings.HasSuffix(fsFile.Name(), "-meta.json") {
181+
continue
182+
}
183+
184+
s, err := base64.StdEncoding.DecodeString(fsFile.Name())
185+
if err != nil {
186+
return nil, err
187+
}
188+
id, err := store.ParseID(string(s))
189+
if err != nil {
190+
continue
191+
}
192+
193+
if !pattern.Match(id) {
194+
continue
195+
}
196+
197+
secretsFileName, metadataFileName := getFileNames(id)
198+
199+
secretsFile, err := getFile(ctx, f.filesystem, secretsFileName, metadataFileName)
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
decryptedSecret, err := decryptSecret(secretsFile.encryptedSecret, f.masterKey)
205+
if err != nil {
206+
return nil, err
207+
}
208+
209+
secret := f.f()
210+
if err := secret.SetMetadata(secretsFile.metadata); err != nil {
211+
return nil, err
212+
}
213+
if err := secret.Unmarshal(decryptedSecret); err != nil {
214+
return nil, err
215+
}
216+
secrets[id] = secret
217+
}
218+
if len(secrets) == 0 {
219+
return nil, store.ErrCredentialNotFound
220+
}
221+
return secrets, nil
222+
}
223+
224+
func (f *fileStore[T]) Get(ctx context.Context, id store.ID) (store.Secret, error) {
225+
secretFileName, metadataFileName := getFileNames(id)
226+
secretFile, err := getFile(ctx, f.filesystem, secretFileName, metadataFileName)
227+
if err != nil {
228+
return nil, err
229+
}
230+
231+
decryptedSecret, err := decryptSecret(secretFile.encryptedSecret, f.masterKey)
232+
if err != nil {
233+
return nil, err
234+
}
235+
236+
secret := f.f()
237+
if err := secret.SetMetadata(secretFile.metadata); err != nil {
238+
return nil, err
239+
}
240+
if err := secret.Unmarshal(decryptedSecret); err != nil {
241+
return nil, err
242+
}
243+
return secret, nil
244+
}
245+
246+
func (f *fileStore[T]) GetAllMetadata(ctx context.Context) (map[store.ID]store.Secret, error) {
247+
fsFiles, err := fs.ReadDir(f.filesystem.FS(), ".")
248+
if err != nil {
249+
return nil, err
250+
}
251+
if len(fsFiles) == 0 {
252+
return nil, store.ErrCredentialNotFound
253+
}
254+
255+
secrets := make(map[store.ID]store.Secret)
256+
for _, fsFile := range fsFiles {
257+
if fsFile.IsDir() {
258+
continue
259+
}
260+
261+
// skip secret files, we are only interested in metadata files
262+
secretsFileName, found := strings.CutSuffix(fsFile.Name(), "-meta.json")
263+
if !found {
264+
continue
265+
}
266+
267+
s, err := base64.StdEncoding.DecodeString(secretsFileName)
268+
if err != nil {
269+
return nil, err
270+
}
271+
272+
id, err := store.ParseID(string(s))
273+
if err != nil {
274+
continue
275+
}
276+
277+
metaFile, err := f.filesystem.Open(fsFile.Name())
278+
if err != nil {
279+
return nil, err
280+
}
281+
defer metaFile.Close()
282+
283+
enc := json.NewDecoder(metaFile)
284+
var v map[string]string
285+
if err := enc.Decode(&v); err != nil {
286+
return nil, err
287+
}
288+
289+
secret := f.f()
290+
if err := secret.SetMetadata(v); err != nil {
291+
return nil, err
292+
}
293+
secrets[id] = secret
294+
}
295+
296+
if len(secrets) == 0 {
297+
return nil, store.ErrCredentialNotFound
298+
}
299+
return secrets, nil
300+
}
301+
302+
func (f *fileStore[T]) Save(ctx context.Context, id store.ID, secret store.Secret) error {
303+
var err error
304+
305+
val, err := secret.Marshal()
306+
if err != nil {
307+
return err
308+
}
309+
metadata := secret.Metadata()
310+
311+
recipient, err := age.NewScryptRecipient(f.masterKey)
312+
if err != nil {
313+
return err
314+
}
315+
316+
var encryptedSecret bytes.Buffer
317+
w, err := age.Encrypt(&encryptedSecret, recipient)
318+
if err != nil {
319+
return err
320+
}
321+
defer w.Close()
322+
323+
if _, err := w.Write(val); err != nil {
324+
return err
325+
}
326+
if err := w.Close(); err != nil {
327+
return err
328+
}
329+
330+
return saveFile(ctx, id, file{
331+
encryptedSecret: encryptedSecret.Bytes(),
332+
metadata: metadata,
333+
}, f.filesystem)
334+
}
335+
336+
// NewFileStore is a [store.Store] that manages encrypted files on disk.
337+
//
338+
// Each secret stored gets persistet to its own file and the name is a
339+
// base64 encoded string of the secret's ID. The files are stored alongside
340+
// one another in the same directory as specified by rootDir.
341+
//
342+
// The secret metadata is stored alongside the file unencrypted as
343+
// `base64(id)-meta.json`
344+
func NewFileStore[T store.Secret](rootDir *os.Root, masterKey string, f store.Factory[T]) (store.Store, error) {
345+
return &fileStore[T]{
346+
masterKey: masterKey,
347+
filesystem: rootDir,
348+
f: f,
349+
}, nil
350+
}

0 commit comments

Comments
 (0)