diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ca8b1064..d0650e95 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,7 +104,7 @@ jobs: - name: Test run: | - make test COVERAGEDIR=${{ env.DESTDIR }} + make test COVERAGEDIR=${{ env.DESTDIR }} TEST_TAGS=skip_secretservice_tests shell: bash - name: Upload coverage diff --git a/Dockerfile b/Dockerfile index 81ff7536..932bcc69 100644 --- a/Dockerfile +++ b/Dockerfile @@ -89,7 +89,7 @@ RUN --mount=type=bind,target=. \ mkdir /out xx-go --wrap - make test COVERAGEDIR=/out + make test COVERAGEDIR=/out TEST_TAGS=skip_secretservice_tests EOT FROM scratch AS test-coverage diff --git a/Makefile b/Makefile index e413bbfb..5528a773 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,7 @@ GO_LDFLAGS = -s -w -X ${GO_PKG}/credentials.Version=${VERSION} -X ${GO_PKG}/cred BUILDX_CMD ?= docker buildx DESTDIR ?= ./bin/build COVERAGEDIR ?= ./bin/coverage +TEST_TAGS ?= # 10.11 is the minimum supported version for osxkeychain export MACOSX_DEPLOYMENT_TARGET = 10.11 @@ -59,7 +60,7 @@ release: # create release .PHONY: test test: mkdir -p $(COVERAGEDIR) - go test -short -v -coverprofile=$(COVERAGEDIR)/coverage.txt -covermode=atomic ./... + go test -tags $(TEST_TAGS) -short -v -coverprofile=$(COVERAGEDIR)/coverage.txt -covermode=atomic ./... go tool cover -func=$(COVERAGEDIR)/coverage.txt .PHONY: lint diff --git a/go.mod b/go.mod index 27b6411c..fb3ee68f 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,13 @@ go 1.21 require ( github.com/danieljoos/wincred v1.2.2 + github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a github.com/keybase/go-keychain v0.0.1 ) -require golang.org/x/sys v0.20.0 // indirect +require ( + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/sys v0.29.0 // indirect +) + +replace github.com/keybase/dbus => github.com/godbus/dbus/v5 v5.1.0 diff --git a/go.sum b/go.sum index 25ec7fd9..dca79f6f 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 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/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= @@ -10,7 +12,9 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 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/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/secretservice/secretservice.c b/secretservice/secretservice.c deleted file mode 100644 index 938bf9a3..00000000 --- a/secretservice/secretservice.c +++ /dev/null @@ -1,164 +0,0 @@ -#include -#include -#include "secretservice.h" - -const SecretSchema *docker_get_schema(void) -{ - static const SecretSchema docker_schema = { - "io.docker.Credentials", SECRET_SCHEMA_NONE, - { - { "label", SECRET_SCHEMA_ATTRIBUTE_STRING }, - { "server", SECRET_SCHEMA_ATTRIBUTE_STRING }, - { "username", SECRET_SCHEMA_ATTRIBUTE_STRING }, - { "docker_cli", SECRET_SCHEMA_ATTRIBUTE_STRING }, - { "NULL", 0 }, - } - }; - return &docker_schema; -} - -GError *add(char *label, char *server, char *username, char *secret, char *displaylabel) { - GError *err = NULL; - - secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT, - displaylabel, secret, NULL, &err, - "label", label, - "server", server, - "username", username, - "docker_cli", "1", - NULL); - return err; -} - -GError *delete(char *server) { - GError *err = NULL; - - secret_password_clear_sync(DOCKER_SCHEMA, NULL, &err, - "server", server, - "docker_cli", "1", - NULL); - if (err != NULL) - return err; - return NULL; -} - -char *get_attribute(const char *attribute, SecretItem *item) { - GHashTable *attributes; - GHashTableIter iter; - gchar *value, *key; - - attributes = secret_item_get_attributes(item); - g_hash_table_iter_init(&iter, attributes); - while (g_hash_table_iter_next(&iter, (void **)&key, (void **)&value)) { - if (strncmp(key, attribute, strlen(key)) == 0) - return (char *)value; - } - g_hash_table_unref(attributes); - return NULL; -} - -GError *get(char *server, char **username, char **secret) { - GError *err = NULL; - GHashTable *attributes; - SecretService *service; - GList *items, *l; - SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK; - SecretValue *secretValue; - gsize length; - gchar *value; - - attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); - g_hash_table_insert(attributes, g_strdup("server"), g_strdup(server)); - g_hash_table_insert(attributes, g_strdup("docker_cli"), g_strdup("1")); - - service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err); - if (err == NULL) { - items = secret_service_search_sync(service, DOCKER_SCHEMA, attributes, flags, NULL, &err); - if (err == NULL) { - for (l = items; l != NULL; l = g_list_next(l)) { - value = secret_item_get_schema_name(l->data); - if (strncmp(value, "io.docker.Credentials", strlen(value)) != 0) { - g_free(value); - continue; - } - g_free(value); - secretValue = secret_item_get_secret(l->data); - if (secretValue == NULL) { - continue; - } - if (secret != NULL) { - *secret = strdup(secret_value_get(secretValue, &length)); - secret_value_unref(secretValue); - } - *username = get_attribute("username", l->data); - } - g_list_free_full(items, g_object_unref); - } - g_object_unref(service); - } - g_hash_table_unref(attributes); - if (err != NULL) { - return err; - } - return NULL; -} - -GError *list(char *ref_label, char *** paths, char *** accts, unsigned int *list_l) { - GList *items; - GError *err = NULL; - SecretService *service; - SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK; - GHashTable *attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free); - - // List credentials with the right label only - g_hash_table_insert(attributes, g_strdup("label"), g_strdup(ref_label)); - - service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err); - if (err != NULL) { - return err; - } - - items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err); - int numKeys = g_list_length(items); - if (err != NULL) { - return err; - } - - char **tmp_paths = (char **) calloc(1,(int)sizeof(char *)*numKeys); - char **tmp_accts = (char **) calloc(1,(int)sizeof(char *)*numKeys); - - // items now contains our keys from the gnome keyring - // we will now put it in our two lists to return it to go - GList *current; - int listNumber = 0; - for(current = items; current!=NULL; current = current->next) { - char *pathTmp = secret_item_get_label(current->data); - // you cannot have a key without a label in the gnome keyring - char *acctTmp = get_attribute("username",current->data); - if (acctTmp==NULL) { - acctTmp = "account not defined"; - } - - tmp_paths[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(pathTmp)+1)); - tmp_accts[listNumber] = (char *) calloc(1, sizeof(char)*(strlen(acctTmp)+1)); - - memcpy(tmp_paths[listNumber], pathTmp, sizeof(char)*(strlen(pathTmp)+1)); - memcpy(tmp_accts[listNumber], acctTmp, sizeof(char)*(strlen(acctTmp)+1)); - - listNumber = listNumber + 1; - } - - *paths = (char **) realloc(tmp_paths, (int)sizeof(char *)*listNumber); - *accts = (char **) realloc(tmp_accts, (int)sizeof(char *)*listNumber); - - *list_l = listNumber; - - return NULL; -} - -void freeListData(char *** data, unsigned int length) { - int i; - for(i=0; i -*/ -import "C" - import ( "errors" - "unsafe" "github.com/docker/docker-credential-helpers/credentials" + "github.com/keybase/dbus" + "github.com/keybase/go-keychain/secretservice" +) + +const ( + schemaAttr = "xdg:schema" + labelAttr = "label" + serverAttr = "server" + usernameAttr = "username" + dockerCliAttr = "docker_cli" + dockerCliValue = "1" ) // Secretservice handles secrets using Linux secret-service as a store. @@ -25,23 +27,37 @@ func (h Secretservice) Add(creds *credentials.Credentials) error { if creds == nil { return errors.New("missing credentials") } - credsLabel := C.CString(credentials.CredsLabel) - defer C.free(unsafe.Pointer(credsLabel)) - server := C.CString(creds.ServerURL) - defer C.free(unsafe.Pointer(server)) - username := C.CString(creds.Username) - defer C.free(unsafe.Pointer(username)) - secret := C.CString(creds.Secret) - defer C.free(unsafe.Pointer(secret)) - displayLabel := C.CString("Registry credentials for " + creds.ServerURL) - defer C.free(unsafe.Pointer(displayLabel)) - - if err := C.add(credsLabel, server, username, secret, displayLabel); err != nil { - defer C.g_error_free(err) - errMsg := (*C.char)(unsafe.Pointer(err.message)) - return errors.New(C.GoString(errMsg)) - } - return nil + + service, session, err := getSession() + if err != nil { + return err + } + defer service.CloseSession(session) + + if err := unlock(service); err != nil { + return err + } + + secret, err := session.NewSecret([]byte(creds.Secret)) + if err != nil { + return err + } + + return handleTimeout(func() error { + _, err = service.CreateItem( + secretservice.DefaultCollection, + secretservice.NewSecretProperties("Registry credentials for "+creds.ServerURL, map[string]string{ + schemaAttr: "io.docker.Credentials", + labelAttr: credentials.CredsLabel, + serverAttr: creds.ServerURL, + usernameAttr: creds.Username, + dockerCliAttr: dockerCliValue, + }), + secret, + secretservice.ReplaceBehaviorReplace, + ) + return err + }) } // Delete removes credentials from the store. @@ -49,15 +65,26 @@ func (h Secretservice) Delete(serverURL string) error { if serverURL == "" { return errors.New("missing server url") } - server := C.CString(serverURL) - defer C.free(unsafe.Pointer(server)) - if err := C.delete(server); err != nil { - defer C.g_error_free(err) - errMsg := (*C.char)(unsafe.Pointer(err.message)) - return errors.New(C.GoString(errMsg)) + service, session, err := getSession() + if err != nil { + return err + } + defer service.CloseSession(session) + + items, err := getItems(service, map[string]string{ + serverAttr: serverURL, + dockerCliAttr: dockerCliValue, + }) + if err != nil { + return err + } else if len(items) == 0 { + return credentials.NewErrCredentialsNotFound() } - return nil + + return handleTimeout(func() error { + return service.DeleteItem(items[0]) + }) } // Get returns the username and secret to use for a given registry server URL. @@ -65,60 +92,122 @@ func (h Secretservice) Get(serverURL string) (string, string, error) { if serverURL == "" { return "", "", errors.New("missing server url") } - var username *C.char - defer C.free(unsafe.Pointer(username)) - var secret *C.char - defer C.free(unsafe.Pointer(secret)) - server := C.CString(serverURL) - defer C.free(unsafe.Pointer(server)) - err := C.get(server, &username, &secret) + service, session, err := getSession() if err != nil { - defer C.g_error_free(err) - errMsg := (*C.char)(unsafe.Pointer(err.message)) - return "", "", errors.New(C.GoString(errMsg)) + return "", "", err + } + defer service.CloseSession(session) + + if err := unlock(service); err != nil { + return "", "", err } - user := C.GoString(username) - pass := C.GoString(secret) - if pass == "" { + + items, err := getItems(service, map[string]string{ + serverAttr: serverURL, + dockerCliAttr: dockerCliValue, + }) + if err != nil { + return "", "", err + } else if len(items) == 0 { return "", "", credentials.NewErrCredentialsNotFound() } - return user, pass, nil + + attrs, err := service.GetAttributes(items[0]) + if err != nil { + return "", "", err + } + + var secret []byte + err = handleTimeout(func() error { + var err error + secret, err = service.GetSecret(items[0], *session) + return err + }) + if err != nil { + return "", "", err + } + + return attrs[usernameAttr], string(secret), nil } // List returns the stored URLs and corresponding usernames for a given credentials label func (h Secretservice) List() (map[string]string, error) { - credsLabelC := C.CString(credentials.CredsLabel) - defer C.free(unsafe.Pointer(credsLabelC)) - - var pathsC **C.char - defer C.free(unsafe.Pointer(pathsC)) - var acctsC **C.char - defer C.free(unsafe.Pointer(acctsC)) - var listLenC C.uint - err := C.list(credsLabelC, &pathsC, &acctsC, &listLenC) - defer C.freeListData(&pathsC, listLenC) - defer C.freeListData(&acctsC, listLenC) + service, session, err := getSession() if err != nil { - defer C.g_error_free(err) - errMsg := (*C.char)(unsafe.Pointer(err.message)) - return nil, errors.New(C.GoString(errMsg)) + return nil, err } + defer service.CloseSession(session) - resp := make(map[string]string) + items, err := getItems(service, map[string]string{ + dockerCliAttr: dockerCliValue, + }) + if err != nil { + return nil, err + } - listLen := int(listLenC) - if listLen == 0 { + resp := make(map[string]string) + if len(items) == 0 { return resp, nil } - // The maximum capacity of the following two slices is limited to (2^29)-1 to remain compatible - // with 32-bit platforms. The size of a `*C.char` (a pointer) is 4 Byte on a 32-bit system - // and (2^29)*4 == math.MaxInt32 + 1. -- See issue golang/go#13656 - pathTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen] - acctTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen] - for i := 0; i < listLen; i++ { - resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i]) + + for _, it := range items { + attrs, err := service.GetAttributes(it) + if err != nil { + return nil, err + } + if v, ok := attrs[usernameAttr]; !ok || v == "" { + continue + } + resp[attrs[serverAttr]] = attrs[usernameAttr] } return resp, nil } + +func getSession() (*secretservice.SecretService, *secretservice.Session, error) { + service, err := secretservice.NewService() + if err != nil { + return nil, nil, err + } + session, err := service.OpenSession(secretservice.AuthenticationDHAES) + if err != nil { + return nil, nil, err + } + return service, session, nil +} + +func unlock(service *secretservice.SecretService) error { + return handleTimeout(func() error { + return service.Unlock([]dbus.ObjectPath{secretservice.DefaultCollection}) + }) +} + +func handleTimeout(f func() error) error { + err := f() + if errors.Is(err, errors.New("prompt timed out")) { + return f() + } + return err +} + +func getItems(service *secretservice.SecretService, attributes map[string]string) ([]dbus.ObjectPath, error) { + if err := unlock(service); err != nil { + return nil, err + } + + var items []dbus.ObjectPath + err := handleTimeout(func() error { + var err error + items, err = service.SearchCollection( + secretservice.DefaultCollection, + attributes, + ) + return err + }) + if err != nil { + return nil, err + } + + return items, nil +} diff --git a/secretservice/secretservice.h b/secretservice/secretservice.h deleted file mode 100644 index 154ee8c4..00000000 --- a/secretservice/secretservice.h +++ /dev/null @@ -1,13 +0,0 @@ -#define SECRET_WITH_UNSTABLE 1 -#define SECRET_API_SUBJECT_TO_CHANGE 1 -#include - -const SecretSchema *docker_get_schema(void) G_GNUC_CONST; - -#define DOCKER_SCHEMA docker_get_schema() - -GError *add(char *label, char *server, char *username, char *secret, char *displaylabel); -GError *delete(char *server); -GError *get(char *server, char **username, char **secret); -GError *list(char *label, char *** paths, char *** accts, unsigned int *list_l); -void freeListData(char *** data, unsigned int length); diff --git a/secretservice/secretservice_test.go b/secretservice/secretservice_test.go index 21da1042..e18ddc25 100644 --- a/secretservice/secretservice_test.go +++ b/secretservice/secretservice_test.go @@ -1,4 +1,4 @@ -//go:build linux && cgo +//go:build linux && cgo && !skip_secretservice_tests package secretservice diff --git a/vendor/github.com/keybase/dbus/CONTRIBUTING.md b/vendor/github.com/keybase/dbus/CONTRIBUTING.md new file mode 100644 index 00000000..c88f9b2b --- /dev/null +++ b/vendor/github.com/keybase/dbus/CONTRIBUTING.md @@ -0,0 +1,50 @@ +# How to Contribute + +## Getting Started + +- Fork the repository on GitHub +- Read the [README](README.markdown) for build and test instructions +- Play with the project, submit bugs, submit patches! + +## Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create a topic branch from where you want to base your work (usually master). +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Make sure the tests pass, and add any new tests as appropriate. +- Submit a pull request to the original repository. + +Thanks for your contributions! + +### Format of the Commit Message + +We follow a rough convention for commit messages that is designed to answer two +questions: what changed and why. The subject line should feature the what and +the body of the commit should describe the why. + +``` +scripts: add the test-cluster command + +this uses tmux to setup a test cluster that you can easily kill and +start for debugging. + +Fixes #38 +``` + +The format can be described more formally as follows: + +``` +: + + + +