Skip to content

Commit d474f94

Browse files
authored
Merge pull request #29 from docker/filestore
feat(store): encrypted filestore
2 parents 7893b5d + 180b7a1 commit d474f94

File tree

158 files changed

+36570
-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.

158 files changed

+36570
-3
lines changed

go.work.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwm
2020
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
2121
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
2222
github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI=
23+
github.com/onsi/ginkgo v1.12.0 h1:Iw5WCbBcaAAd0fpRb1c9r5YCylv4XDoCSigm1zLevwU=
2324
github.com/opencontainers/runtime-tools v0.9.0/go.mod h1:r3f7wjNzSs2extwzU3Y+6pKfobzPh+kKFJ3ofN+3nfs=
2425
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
2526
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=

store/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Supported stores include:
88
- Linux keychain (gnome-keyring and kdewallet)
99
- macOS keychain
1010
- windows credential management API
11+
- file encryption via [age](https://github.com/filoSottile/age)
1112

1213
## Local Testing
1314

store/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ go 1.25
88
replace github.com/docker/secrets-engine/x => ../x
99

1010
require (
11+
filippo.io/age v1.2.1
1112
github.com/cenkalti/backoff/v5 v5.0.3
1213
github.com/danieljoos/wincred v1.2.2
1314
github.com/docker/secrets-engine/x v0.0.3-do.not.use
@@ -21,6 +22,7 @@ require (
2122
)
2223

2324
require (
25+
filippo.io/edwards25519 v1.1.0 // indirect
2426
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
2527
github.com/inconshreveable/mousetrap v1.1.0 // indirect
2628
github.com/kr/pretty v0.3.1 // indirect

store/go.sum

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805 h1:u2qwJeEvnypw+OCPUHmoZE3IqwfuN5kgDfo5MLzpNM0=
2+
c2sp.org/CCTV/age v0.0.0-20240306222714-3ec4d716e805/go.mod h1:FomMrUJ2Lxt5jCLmZkG3FHa72zUprnhd3v/Z18Snm4w=
3+
filippo.io/age v1.2.1 h1:X0TZjehAZylOIj4DubWYU1vWQxv9bJpo+Uu2/LGhi1o=
4+
filippo.io/age v1.2.1/go.mod h1:JL9ew2lTN+Pyft4RiNGguFfOpewKwSHm5ayKD/A4004=
5+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
6+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
17
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
28
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
39
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -38,6 +44,8 @@ golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
3844
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
3945
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
4046
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
47+
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
48+
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
4149
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
4250
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
4351
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

store/keychain/keychain.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ type keychainStore[T store.Secret] struct {
1717

1818
var _ store.Store = &keychainStore[store.Secret]{}
1919

20-
type Factory[T store.Secret] func() T
21-
2220
// New creates a new keychain store.
2321
//
2422
// It takes ServiceGroup and ServiceName and a [Factory] as input.
@@ -42,7 +40,7 @@ type Factory[T store.Secret] func() T
4240
// Changing the service name can be done, but would require migrating existing credentials.
4341
//
4442
// [Factory] is a function used to instantiate new secrets of type T.
45-
func New[T store.Secret](serviceGroup, serviceName string, factory Factory[T]) (store.Store, error) {
43+
func New[T store.Secret](serviceGroup, serviceName string, factory store.Factory[T]) (store.Store, error) {
4644
if serviceGroup == "" || serviceName == "" {
4745
return nil, errors.New("serviceGroup and serviceName are required")
4846
}

store/posixage/README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
# Store posixage
2+
3+
The posixage store is a POSIX compliant encrypted file store. It uses [age](https://github.com/FiloSottile/age)
4+
to encrypt/decrypt its files and has support for password, ssh and age keys.
5+
6+
## Quickstart
7+
8+
```go
9+
import "github.com/docker/secrets-engine/store/posixage"
10+
11+
func main() {
12+
root, err := os.OpenRoot("my/secrets/path")
13+
if err != nil {
14+
panic(err)
15+
}
16+
17+
s, err := posixage.New(root,
18+
func() *mocks.MockCredential {
19+
return &mocks.MockCredential{}
20+
},
21+
WithEncryptionCallbackFunc[EncryptionPassword](func(_ context.Context) ([]byte, error) {
22+
return []byte(masterKey), nil
23+
}),
24+
WithDecryptionCallbackFunc[DecryptionPassword](func(_ context.Context) ([]byte, error) {
25+
return []byte(masterKey), nil
26+
}),
27+
)
28+
}
29+
```
30+
31+
The store allows you to register multiple encryption and decryption callback
32+
functions. Each callback gives your application control over how to retrieve
33+
the required data — for example, from environment variables, a configuration
34+
file, or via an interactive user prompt.
35+
36+
### Features
37+
38+
- Support for multiple encryption functions
39+
- Support for multiple decryption functions
40+
41+
Callbacks are invoked in the order they are registered. For decryption, the
42+
store tries each callback in sequence, and the first one that successfully
43+
provides a valid key will return the decrypted secret.
44+
45+
Here's an example of accepting multiple passwords for encryption:
46+
47+
```go
48+
import "github.com/docker/secrets-engine/store/posixage"
49+
50+
func main() {
51+
root, err := os.OpenRoot("my/secrets/path")
52+
if err != nil {
53+
panic(err)
54+
}
55+
56+
s, err := posixage.New(root,
57+
func() *mocks.MockCredential {
58+
return &mocks.MockCredential{}
59+
},
60+
WithEncryptionCallbackFunc[EncryptionPassword](func(_ context.Context) ([]byte, error) {
61+
return []byte(masterKey), nil
62+
}),
63+
WithEncryptionCallbackFunc[EncryptionPassword](func(_ context.Context) ([]byte, error) {
64+
return []byte(bobPassword), nil
65+
}),
66+
WithEncryptionCallbackFunc[EncryptionAgeX25519](func(_ context.Context) ([]byte, error) {
67+
return []byte(identity.Recipient().String()), nil
68+
}),
69+
WithDecryptionCallbackFunc[DecryptionPassword](func(_ context.Context) ([]byte, error) {
70+
return []byte(masterKey), nil
71+
}),
72+
)
73+
}
74+
```
75+
76+
### Secrets
77+
78+
Any secret format is supported as long as it conforms to the `store.Secret` interface.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package secretfile
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"filippo.io/age"
8+
"filippo.io/age/agessh"
9+
)
10+
11+
type (
12+
// PromptFunc is a callback invoked by the store when encrypting or
13+
// decrypting a file. The function is expected to return the key material
14+
// (as a byte slice) or an error if the key cannot be obtained.
15+
PromptFunc func(context.Context) ([]byte, error)
16+
17+
// KeyType identifies the type of encryption or decryption key associated
18+
// with a secret (e.g., password, age, or SSH).
19+
KeyType string
20+
)
21+
22+
const (
23+
PasswordKeyType KeyType = "pass"
24+
AgeKeyType KeyType = "age"
25+
SSHKeyType KeyType = "ssh"
26+
)
27+
28+
func getRecipient(k KeyType, encryptionKey string) (age.Recipient, error) {
29+
var recipient age.Recipient
30+
var err error
31+
32+
switch k {
33+
case PasswordKeyType:
34+
recipient, err = age.NewScryptRecipient(encryptionKey)
35+
case AgeKeyType:
36+
recipient, err = age.ParseX25519Recipient(encryptionKey)
37+
case SSHKeyType:
38+
recipient, err = agessh.ParseRecipient(encryptionKey)
39+
default:
40+
return nil, fmt.Errorf("unsupported encryption type %T", k)
41+
}
42+
43+
if err != nil {
44+
return nil, err
45+
}
46+
47+
return recipient, nil
48+
}
49+
50+
// GetRecipients returns a slice of [age.Recipient] for the given key type and
51+
// encryption keys.
52+
//
53+
// The recipient implementation depends on the provided [KeyType]:
54+
// - passwordKeyType → [age.NewScryptRecipient]
55+
// - ageKeyType → [age.ParseX25519Recipient]
56+
// - sshKeyType → [agessh.ParseRecipient]
57+
//
58+
// An error is returned if the key cannot be parsed or the key type is
59+
// unsupported.
60+
func GetRecipients(k KeyType, encryptionKeys []string) ([]age.Recipient, error) {
61+
var recipients []age.Recipient
62+
for _, encryptionKey := range encryptionKeys {
63+
recipient, err := getRecipient(k, encryptionKey)
64+
if err != nil {
65+
return nil, err
66+
}
67+
recipients = append(recipients, recipient)
68+
}
69+
return recipients, nil
70+
}
71+
72+
// GetIdentity returns an [age.Identity] for the given key type and
73+
// decryption key.
74+
//
75+
// The identity implementation depends on the provided [KeyType]:
76+
// - PasswordKeyType → [age.NewScryptIdentity]
77+
// - AgeKeyType → [age.ParseX25519Identity]
78+
// - SSHKeyType → [agessh.ParseIdentity]
79+
//
80+
// An error is returned if the key cannot be parsed or the key type is
81+
// unsupported.
82+
func GetIdentity(k KeyType, decryptionKey string) (age.Identity, error) {
83+
var identity age.Identity
84+
var err error
85+
86+
switch k {
87+
case PasswordKeyType:
88+
identity, err = age.NewScryptIdentity(decryptionKey)
89+
case AgeKeyType:
90+
identity, err = age.ParseX25519Identity(decryptionKey)
91+
case SSHKeyType:
92+
identity, err = agessh.ParseIdentity([]byte(decryptionKey))
93+
default:
94+
return nil, fmt.Errorf("unsupported decryption type %T", k)
95+
}
96+
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
return identity, nil
102+
}

0 commit comments

Comments
 (0)