Skip to content

Commit 55488f6

Browse files
committed
store/keychain: windows support
Signed-off-by: Alano Terblanche <18033717+Benehiko@users.noreply.github.com>
1 parent f6646b9 commit 55488f6

14 files changed

Lines changed: 864 additions & 0 deletions

File tree

store/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ go 1.24.3
55
replace github.com/docker/secrets-engine => ../
66

77
require (
8+
github.com/danieljoos/wincred v1.2.2
89
github.com/docker/secrets-engine v0.0.0-00010101000000-000000000000
910
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a
1011
github.com/keybase/go-keychain v0.0.1
@@ -20,6 +21,7 @@ require (
2021
github.com/pmezard/go-difflib v1.0.0 // indirect
2122
github.com/spf13/pflag v1.0.6 // indirect
2223
golang.org/x/crypto v0.32.0 // indirect
24+
golang.org/x/sys v0.29.0 // indirect
2325
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
2426
gopkg.in/yaml.v3 v3.0.1 // indirect
2527
)

store/go.sum

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
2+
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
3+
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
4+
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8=
25
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
36
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
47
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -7,20 +10,28 @@ github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrm
710
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
811
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
912
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
13+
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
14+
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
1015
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
16+
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
1117
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
18+
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
1219
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
1320
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
1421
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
1522
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
1623
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
1724
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
1825
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
26+
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
1927
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
2028
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
2129
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
2230
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
31+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
32+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
2333
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2434
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
35+
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
2536
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
2637
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

store/keychain/keychain_windows.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package keychain
2+
3+
import (
4+
"context"
5+
"errors"
6+
"iter"
7+
"strings"
8+
9+
"github.com/danieljoos/wincred"
10+
"github.com/docker/secrets-engine/store"
11+
)
12+
13+
var (
14+
ErrCredentialBadUsername = errors.New("credential username is invalid")
15+
ErrInvalidCredentialFlags = errors.New("an invalid flag was specified for the flags parameter")
16+
ErrInvalidCredentialParameter = errors.New("protected field does not match provided value for an existing credential")
17+
ErrNoLogonSession = errors.New("logon session does not exist or there is no credential set associated with this logon session")
18+
)
19+
20+
func (k *keychainStore[T]) Delete(ctx context.Context, id store.ID) error {
21+
if err := id.Valid(); err != nil {
22+
return err
23+
}
24+
25+
g, err := wincred.GetGenericCredential(k.itemLabel(id))
26+
if err != nil && !errors.Is(err, wincred.ErrElementNotFound) {
27+
return mapWindowsCredentialError(err)
28+
}
29+
if g == nil {
30+
return nil
31+
}
32+
33+
err = g.Delete()
34+
if err != nil && !errors.Is(err, wincred.ErrElementNotFound) {
35+
return err
36+
}
37+
return nil
38+
}
39+
40+
func (k *keychainStore[T]) Get(ctx context.Context, id store.ID) (store.Secret, error) {
41+
if err := id.Valid(); err != nil {
42+
return nil, err
43+
}
44+
45+
g, err := wincred.GetGenericCredential(k.itemLabel(id))
46+
if err != nil {
47+
return nil, mapWindowsCredentialError(err)
48+
}
49+
50+
secret := k.factory()
51+
if err := secret.Unmarshal(g.CredentialBlob); err != nil {
52+
return nil, err
53+
}
54+
55+
return secret, nil
56+
}
57+
58+
// isServiceCredential checks if a credential attribute contains the
59+
// `service:group` and `service:name` attribute.
60+
//
61+
// The keychainStore serviceGroup and serviceName should match what is stored
62+
// in the attributes.
63+
func isServiceCredential[T store.Secret](k *keychainStore[T], attrs []wincred.CredentialAttribute) bool {
64+
// must have both serviceGroup and serviceName
65+
var (
66+
serviceName string
67+
serviceGroup string
68+
)
69+
for _, attr := range attrs {
70+
switch attr.Keyword {
71+
case "service:group":
72+
serviceGroup = string(attr.Value)
73+
case "service:name":
74+
serviceName = string(attr.Value)
75+
}
76+
}
77+
return strings.EqualFold(serviceGroup, k.serviceGroup) && strings.EqualFold(serviceName, k.serviceName)
78+
}
79+
80+
// findServiceCredentials is an iterator that yields credentials that match the
81+
// service group and service name.
82+
func findServiceCredentials[T store.Secret](k *keychainStore[T], credentials []*wincred.Credential) iter.Seq[*wincred.Credential] {
83+
return func(yield func(cred *wincred.Credential) bool) {
84+
for _, c := range credentials {
85+
if isServiceCredential(k, c.Attributes) {
86+
if !yield(c) {
87+
return
88+
}
89+
}
90+
}
91+
}
92+
}
93+
94+
func (k *keychainStore[T]) GetAll(ctx context.Context) (map[store.ID]store.Secret, error) {
95+
credentials, err := wincred.List()
96+
if err != nil {
97+
return nil, mapWindowsCredentialError(err)
98+
}
99+
100+
onlyLabelPrefix := k.itemLabel(store.ID(""))
101+
102+
secrets := make(map[store.ID]store.Secret, len(credentials))
103+
for cred := range findServiceCredentials(k, credentials) {
104+
secret := k.factory()
105+
id, err := store.ParseID(strings.ReplaceAll(cred.TargetName, onlyLabelPrefix, ""))
106+
if err != nil {
107+
return nil, err
108+
}
109+
110+
gc, err := wincred.GetGenericCredential(cred.TargetName)
111+
if err != nil {
112+
return nil, mapWindowsCredentialError(err)
113+
}
114+
if err := secret.Unmarshal(gc.CredentialBlob); err != nil {
115+
return nil, err
116+
}
117+
secrets[id] = secret
118+
}
119+
120+
return secrets, nil
121+
}
122+
123+
func (k *keychainStore[T]) Save(ctx context.Context, id store.ID, secret store.Secret) error {
124+
if err := id.Valid(); err != nil {
125+
return err
126+
}
127+
128+
data, err := secret.Marshal()
129+
if err != nil {
130+
return err
131+
}
132+
133+
g := wincred.NewGenericCredential(k.itemLabel(id))
134+
g.UserName = id.String()
135+
g.CredentialBlob = data
136+
g.Persist = wincred.PersistLocalMachine
137+
g.Attributes = []wincred.CredentialAttribute{
138+
{
139+
Keyword: "service:group",
140+
Value: []byte(k.serviceGroup),
141+
},
142+
{
143+
Keyword: "service:name",
144+
Value: []byte(k.serviceName),
145+
},
146+
}
147+
return mapWindowsCredentialError(g.Write())
148+
}
149+
150+
func mapWindowsCredentialError(err error) error {
151+
switch err {
152+
case wincred.ErrElementNotFound:
153+
return store.ErrCredentialNotFound
154+
case wincred.ErrBadUsername:
155+
return ErrCredentialBadUsername
156+
case wincred.ErrInvalidParameter:
157+
return ErrInvalidCredentialParameter
158+
}
159+
return nil
160+
}

vendor/github.com/danieljoos/wincred/.gitattributes

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/danieljoos/wincred/.gitignore

Lines changed: 25 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/danieljoos/wincred/LICENSE

Lines changed: 21 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)