Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions keychain/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Secrets Engine Keychain

Keychain is a standalone library for use to store secrets in a standardized
format to the OS keychain.
119 changes: 119 additions & 0 deletions keychain/cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"context"
"fmt"
"log"
"path"

"github.com/docker/secrets-engine/keychain"
"github.com/docker/secrets-engine/keychain/mocks"
"github.com/spf13/cobra"
)

func NewCommand() (*cobra.Command, error) {
kc, err := keychain.New(
func() *mocks.MockCredential {
return &mocks.MockCredential{}
},
keychain.WithKeyPrefix[*mocks.MockCredential]("cli"),
)
if err != nil {
return nil, err
}
list := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
RunE: func(cmd *cobra.Command, args []string) error {
secrets, err := kc.GetAll(cmd.Context())
if err != nil {
return err
}
if len(secrets) == 0 {
fmt.Println("No Secrets found")
return nil
}
for k, v := range secrets {
vv, err := v.Marshal()
if err != nil {
return err
}
fmt.Printf("\nID: %s\nValues: %s\n", k, vv)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to Password
flows to a logging call.

Copilot Autofix

AI 9 months ago

To fix the issue, the sensitive information (password) should be excluded from the logs or obfuscated before being logged. The Marshal method in MockCredential should be updated to ensure that sensitive data is not exposed. Additionally, the logging statement in keychain/cmd/main.go should be modified to avoid printing the password.

Steps to fix:

  1. Update the Marshal method in keychain/mocks/mock_credential.go to obfuscate the password (e.g., replace it with a placeholder like ***).
  2. Modify the logging statement in keychain/cmd/main.go to exclude sensitive data or use the updated Marshal method that obfuscates the password.

Suggested changeset 2
keychain/cmd/main.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/keychain/cmd/main.go b/keychain/cmd/main.go
--- a/keychain/cmd/main.go
+++ b/keychain/cmd/main.go
@@ -40,3 +40,3 @@
 				}
-				fmt.Printf("\nID: %s\nValues: %s\n", k, vv)
+				fmt.Printf("\nID: %s\nValues: %s\n", k, vv) // Password obfuscated in Marshal method
 			}
EOF
@@ -40,3 +40,3 @@
}
fmt.Printf("\nID: %s\nValues: %s\n", k, vv)
fmt.Printf("\nID: %s\nValues: %s\n", k, vv) // Password obfuscated in Marshal method
}
keychain/mocks/mock_credential.go
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/keychain/mocks/mock_credential.go b/keychain/mocks/mock_credential.go
--- a/keychain/mocks/mock_credential.go
+++ b/keychain/mocks/mock_credential.go
@@ -18,3 +18,3 @@
 func (m *MockCredential) Marshal() ([]byte, error) {
-	return []byte(m.Username + ":" + m.Password), nil
+	return []byte(m.Username + ":***"), nil // Obfuscate the password
 }
EOF
@@ -18,3 +18,3 @@
func (m *MockCredential) Marshal() ([]byte, error) {
return []byte(m.Username + ":" + m.Password), nil
return []byte(m.Username + ":***"), nil // Obfuscate the password
}
Copilot is powered by AI and may make mistakes. Always verify output.
}
return nil
},
}

var (
username string
password string
)
store := &cobra.Command{
Use: "store",
Aliases: []string{"set"},
RunE: func(cmd *cobra.Command, args []string) error {
id, err := keychain.ParseID(path.Join("keystore-cli", username))
if err != nil {
return err
}
creds := &mocks.MockCredential{
Username: username,
Password: password,
}
return kc.Store(cmd.Context(), id, creds)
},
}
store.PersistentFlags().StringVar(&username, "username", "", "The secret key")
store.PersistentFlags().StringVar(&password, "password", "", "The secret value")
store.MarkFlagsRequiredTogether("username", "password")

retrieve := &cobra.Command{
Use: "get",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
id, err := keychain.ParseID(path.Join("keystore-cli", args[0]))
if err != nil {
return err
}
secret, err := kc.Get(cmd.Context(), id)
if err != nil {
return err
}
val, err := secret.Marshal()
if err != nil {
return err
}
fmt.Printf("Secret:\nID:%s\nValues:%s\n", id.String(), val)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to Password
flows to a logging call.

Copilot Autofix

AI 9 months ago

To fix the issue, we need to ensure that sensitive information (e.g., passwords) is not logged in clear text. Instead of logging the entire output of the Marshal method, we should log only non-sensitive information, such as the Username or a sanitized version of the data.

In this case, we will modify the fmt.Printf statement on line 86 in keychain/cmd/main.go to exclude the password from the logged output. Additionally, we will update the Marshal method in keychain/mocks/mock_credential.go to provide a sanitized version of the data for logging purposes.


Suggested changeset 2
keychain/cmd/main.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/keychain/cmd/main.go b/keychain/cmd/main.go
--- a/keychain/cmd/main.go
+++ b/keychain/cmd/main.go
@@ -85,3 +85,3 @@
 			}
-			fmt.Printf("Secret:\nID:%s\nValues:%s\n", id.String(), val)
+			fmt.Printf("Secret:\nID:%s\nValues:%s\n", id.String(), sanitizeSecret(val))
 			return nil
EOF
@@ -85,3 +85,3 @@
}
fmt.Printf("Secret:\nID:%s\nValues:%s\n", id.String(), val)
fmt.Printf("Secret:\nID:%s\nValues:%s\n", id.String(), sanitizeSecret(val))
return nil
keychain/mocks/mock_credential.go
Outside changed files

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/keychain/mocks/mock_credential.go b/keychain/mocks/mock_credential.go
--- a/keychain/mocks/mock_credential.go
+++ b/keychain/mocks/mock_credential.go
@@ -31 +31,10 @@
 }
+
+// sanitizeSecret redacts sensitive information (e.g., passwords) from the secret data.
+func sanitizeSecret(data []byte) string {
+	parts := bytes.Split(data, []byte(":"))
+	if len(parts) != 2 {
+		return "Invalid secret format"
+	}
+	return string(parts[0]) + ":<redacted>"
+}
EOF
@@ -31 +31,10 @@
}

// sanitizeSecret redacts sensitive information (e.g., passwords) from the secret data.
func sanitizeSecret(data []byte) string {
parts := bytes.Split(data, []byte(":"))
if len(parts) != 2 {
return "Invalid secret format"
}
return string(parts[0]) + ":<redacted>"
}
Copilot is powered by AI and may make mistakes. Always verify output.
return nil
},
}

erase := &cobra.Command{
Use: "erase",
Args: cobra.ExactArgs(1),
Aliases: []string{"rm", "remove"},
RunE: func(cmd *cobra.Command, args []string) error {
id, err := keychain.ParseID(path.Join("keystore-cli", args[0]))
if err != nil {
return err
}
return kc.Erase(cmd.Context(), id)
},
}
root := &cobra.Command{}
root.AddCommand(list, store, retrieve, erase)

return root, nil
}

func main() {
ctx := context.Background()
cmd, err := NewCommand()
if err != nil {
log.Fatalf("could not create CLI: %v", err)
}
cmd.SetContext(ctx)
if err := cmd.Execute(); err != nil {
log.Fatal(err)
}
}
19 changes: 19 additions & 0 deletions keychain/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module github.com/docker/secrets-engine/keychain

go 1.24.3

replace github.com/docker/secrets-engine => ../

require (
github.com/docker/secrets-engine v0.0.0-00010101000000-000000000000
github.com/godbus/dbus/v5 v5.1.0
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a
github.com/keybase/go-keychain v0.0.1
github.com/spf13/cobra v1.9.1
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
golang.org/x/crypto v0.32.0 // indirect
)
25 changes: 25 additions & 0 deletions keychain/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a h1:K0EAzgzEQHW4Y5lxrmvPMltmlRDzlhLfGmots9EHUTI=
github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a/go.mod h1:YPNKjjE7Ubp9dTbnWvsP3HT+hYnY6TfXzubYTBeUxc8=
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
66 changes: 66 additions & 0 deletions keychain/keychain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package keychain

import (
"errors"

"github.com/docker/secrets-engine/pkg/secrets"
)

// We depend on the secrets engine to define how secrets should look like
// but the caller of keychain does not necessarily need to import secrets engine
type (
Secret = secrets.Secret
Store = secrets.Store
ID = secrets.ID
)

var ParseID = secrets.ParseID

var (
ErrCredentialNotFound = secrets.ErrNotFound
ErrCollectionPathInvalid = errors.New("keychain collection path is invalid")
)

const (
// the docker label is the default prefix on all keys stored by the keychain
// e.g. io.docker.Secrets:encoded(realm/app/username)
dockerSecretsLabel = "io.docker.Secrets"
)

type keychainStore[T Secret] struct {
keyPrefix string
factory func() T
}

var _ Store = &keychainStore[Secret]{}

type Factory[T secrets.Secret] func() T

type Options[T Secret] func(*keychainStore[T]) error

func WithKeyPrefix[T Secret](prefix string) Options[T] {
return func(ks *keychainStore[T]) error {
if prefix == "" {
return errors.New("the prefix cannot be empty")
}
ks.keyPrefix = prefix
return nil
}
}

// New creates a new keychain store
//
// collectionID is a singular noun indicating the collection name, e.g. "docker"
// factory is a function used to instantiate new secrets of type T.
func New[T Secret](factory Factory[T], opts ...Options[T]) (Store, error) {
k := &keychainStore[T]{
factory: factory,
keyPrefix: dockerSecretsLabel,
}
for _, o := range opts {
if err := o(k); err != nil {
return nil, err
}
}
return k, nil
}
92 changes: 92 additions & 0 deletions keychain/keychain_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//go:build darwin && cgo

package keychain

import (
"context"

kc "github.com/keybase/go-keychain"
)

const (
ServiceName = "DockerAuth"
ServiceGroup = "com.docker.auth"
)

func getItem(id ID) kc.Item {
item := kc.NewItem()
item.SetSecClass(kc.SecClassGenericPassword)
item.SetService(ServiceName)
item.SetAccessGroup(ServiceGroup)
item.SetMatchLimit(kc.MatchLimitOne)
item.SetAccessible(kc.AccessibleAfterFirstUnlock)
item.SetReturnData(true)
item.SetReturnAttributes(true)
item.SetLabel(id.String())
item.SetAccount(id.String())
return item
}

func (k *keychainStore[T]) Erase(ctx context.Context, id ID) error {
return mapError(kc.DeleteItem(getItem(id)))
}

func (k *keychainStore[T]) Get(ctx context.Context, id ID) (Secret, error) {
results, err := kc.QueryItem(getItem(id))
if err != nil {
return nil, mapError(err)
}
if len(results) == 0 {
return nil, ErrCredentialsNotFound
}

secret := k.factory()
if err := secret.Unmarshal(results[0].Data); err != nil {
return nil, err
}
return secret, nil
}

func (k *keychainStore[T]) GetAll(ctx context.Context) (map[ID]Secret, error) {
item := getItem(ID(""))
item.SetMatchLimit(kc.MatchLimitAll)
results, err := kc.QueryItem(getItem(ID("")))
if err != nil {
return nil, mapError(err)
}
creds := make(map[ID]Secret, len(results))
for _, result := range results {
secret := k.factory()
if err := secret.Unmarshal(result.Data); err != nil {
return nil, err
}
id := ID(result.Label)
creds[id] = secret
}
return creds, nil
}

func (k *keychainStore[T]) Store(ctx context.Context, id ID, secret Secret) error {
data, err := secret.Marshal()
if err != nil {
return err
}
item := getItem(id)
item.SetData(data)
return kc.AddItem(item)
}

func mapError(err error) error {
if err == nil {
return nil
}
switch err.Error() {
case kc.ErrorInteractionNotAllowed.Error():
return ErrInteractionNotAllowed
case kc.ErrorItemNotFound.Error():
return ErrCredentialsNotFound
case kc.ErrorAuthFailed.Error():
return ErrAuthFailed
}
return err
}
29 changes: 29 additions & 0 deletions keychain/keychain_dumb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package keychain

import (
"context"

"github.com/docker/secrets-engine/pkg/secrets"
)

var _ Store = &keychainStore[Secret]{}

// Erase implements secrets.Store.
func (k *keychainStore[T]) Erase(ctx context.Context, id secrets.ID) error {
panic("unimplemented")
}

// Get implements secrets.Store.
func (k *keychainStore[T]) Get(ctx context.Context, id secrets.ID) (secrets.Secret, error) {
panic("unimplemented")
}

// GetAll implements secrets.Store.
func (k *keychainStore[T]) GetAll(ctx context.Context) (map[secrets.ID]secrets.Secret, error) {
panic("unimplemented")
}

// Store implements secrets.Store.
func (k *keychainStore[T]) Store(ctx context.Context, id secrets.ID, secret secrets.Secret) error {
panic("unimplemented")
}
Loading