From 9a6e6528e8d67ba3b6ba128d4099490368ec852c Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Sat, 27 May 2023 23:18:54 +0200 Subject: [PATCH 1/6] osxkeychain: switch to github.com/keybase/go-keychain Signed-off-by: CrazyMax --- go.mod | 5 +- go.sum | 2 + osxkeychain/osxkeychain.c | 227 ------ osxkeychain/osxkeychain.go | 168 ++--- osxkeychain/osxkeychain.h | 14 - .../github.com/keybase/go-keychain/.gitignore | 26 + .../keybase/go-keychain/.golangci.yml | 11 + vendor/github.com/keybase/go-keychain/LICENSE | 22 + .../github.com/keybase/go-keychain/README.md | 124 ++++ .../keybase/go-keychain/corefoundation.go | 370 ++++++++++ .../keybase/go-keychain/datetime.go | 69 ++ vendor/github.com/keybase/go-keychain/ios.go | 23 + .../keybase/go-keychain/keychain.go | 653 ++++++++++++++++++ .../github.com/keybase/go-keychain/macos.go | 25 + vendor/github.com/keybase/go-keychain/util.go | 31 + vendor/modules.txt | 3 + 16 files changed, 1429 insertions(+), 344 deletions(-) delete mode 100644 osxkeychain/osxkeychain.c delete mode 100644 osxkeychain/osxkeychain.h create mode 100644 vendor/github.com/keybase/go-keychain/.gitignore create mode 100644 vendor/github.com/keybase/go-keychain/.golangci.yml create mode 100644 vendor/github.com/keybase/go-keychain/LICENSE create mode 100644 vendor/github.com/keybase/go-keychain/README.md create mode 100644 vendor/github.com/keybase/go-keychain/corefoundation.go create mode 100644 vendor/github.com/keybase/go-keychain/datetime.go create mode 100644 vendor/github.com/keybase/go-keychain/ios.go create mode 100644 vendor/github.com/keybase/go-keychain/keychain.go create mode 100644 vendor/github.com/keybase/go-keychain/macos.go create mode 100644 vendor/github.com/keybase/go-keychain/util.go diff --git a/go.mod b/go.mod index cf8ae320..e92b0f7d 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,9 @@ module github.com/docker/docker-credential-helpers go 1.19 -require github.com/danieljoos/wincred v1.2.0 +require ( + github.com/danieljoos/wincred v1.2.0 + github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 +) require golang.org/x/sys v0.8.0 // indirect diff --git a/go.sum b/go.sum index 3d4a7e0d..c9238edf 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,8 @@ github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM= +github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= diff --git a/osxkeychain/osxkeychain.c b/osxkeychain/osxkeychain.c deleted file mode 100644 index 840b85a5..00000000 --- a/osxkeychain/osxkeychain.c +++ /dev/null @@ -1,227 +0,0 @@ -#include "osxkeychain.h" -#include -#include -#include -#include - -char *get_error(OSStatus status) { - char *buf = malloc(128); - CFStringRef str = SecCopyErrorMessageString(status, NULL); - int success = CFStringGetCString(str, buf, 128, kCFStringEncodingUTF8); - if (!success) { - strncpy(buf, "Unknown error", 128); - } - return buf; -} - -char *keychain_add(struct Server *server, char *label, char *username, char *secret) { - SecKeychainItemRef item; - - OSStatus status = SecKeychainAddInternetPassword( - NULL, - strlen(server->host), server->host, - 0, NULL, - strlen(username), username, - strlen(server->path), server->path, - server->port, - server->proto, - kSecAuthenticationTypeDefault, - strlen(secret), secret, - &item - ); - - if (status) { - return get_error(status); - } - - SecKeychainAttribute attribute; - SecKeychainAttributeList attrs; - attribute.tag = kSecLabelItemAttr; - attribute.data = label; - attribute.length = strlen(label); - attrs.count = 1; - attrs.attr = &attribute; - - status = SecKeychainItemModifyContent(item, &attrs, 0, NULL); - - if (status) { - return get_error(status); - } - - return NULL; -} - -char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret) { - char *tmp; - SecKeychainItemRef item; - - OSStatus status = SecKeychainFindInternetPassword( - NULL, - strlen(server->host), server->host, - 0, NULL, - 0, NULL, - strlen(server->path), server->path, - server->port, - server->proto, - kSecAuthenticationTypeDefault, - secret_l, (void **)&tmp, - &item); - - if (status) { - return get_error(status); - } - - *secret = strdup(tmp); - SecKeychainItemFreeContent(NULL, tmp); - - SecKeychainAttributeList list; - SecKeychainAttribute attr; - - list.count = 1; - list.attr = &attr; - attr.tag = kSecAccountItemAttr; - - status = SecKeychainItemCopyContent(item, NULL, &list, NULL, NULL); - if (status) { - return get_error(status); - } - - *username = strdup(attr.data); - *username_l = attr.length; - SecKeychainItemFreeContent(&list, NULL); - - return NULL; -} - -char *keychain_delete(struct Server *server) { - SecKeychainItemRef item; - - OSStatus status = SecKeychainFindInternetPassword( - NULL, - strlen(server->host), server->host, - 0, NULL, - 0, NULL, - strlen(server->path), server->path, - server->port, - server->proto, - kSecAuthenticationTypeDefault, - 0, NULL, - &item); - - if (status) { - return get_error(status); - } - - status = SecKeychainItemDelete(item); - if (status) { - return get_error(status); - } - return NULL; -} - -char * CFStringToCharArr(CFStringRef aString) { - if (aString == NULL) { - return NULL; - } - CFIndex length = CFStringGetLength(aString); - CFIndex maxSize = - CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1; - char *buffer = (char *)malloc(maxSize); - if (CFStringGetCString(aString, buffer, maxSize, - kCFStringEncodingUTF8)) { - return buffer; - } - return NULL; -} - -char *keychain_list(char *credsLabel, char *** paths, char *** accts, unsigned int *list_l) { - CFStringRef credsLabelCF = CFStringCreateWithCString(NULL, credsLabel, kCFStringEncodingUTF8); - CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL); - CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword); - CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue); - CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll); - CFDictionaryAddValue(query, kSecAttrLabel, credsLabelCF); - //Use this query dictionary - CFTypeRef result= NULL; - OSStatus status = SecItemCopyMatching( - query, - &result); - - CFRelease(credsLabelCF); - - //Ran a search and store the results in result - if (status) { - return get_error(status); - } - CFIndex numKeys = CFArrayGetCount(result); - *paths = (char **) malloc((int)sizeof(char *)*numKeys); - *accts = (char **) malloc((int)sizeof(char *)*numKeys); - //result is of type CFArray - for(CFIndex i=0; i +#include +#include */ import "C" import ( "errors" "strconv" - "unsafe" "github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/registryurl" + "github.com/keybase/go-keychain" ) -// errCredentialsNotFound is the specific error message returned by OS X -// when the credentials are not in the keychain. -const errCredentialsNotFound = "The specified item could not be found in the keychain." - -// errCredentialsNotFound is the specific error message returned by OS X -// when environment does not allow showing dialog to unlock keychain. -const errInteractionNotAllowed = "User interaction is not allowed." +// https://opensource.apple.com/source/Security/Security-55471/sec/Security/SecBase.h.auto.html +const ( + // errCredentialsNotFound is the specific error message returned by OS X + // when the credentials are not in the keychain. + errCredentialsNotFound = "The specified item could not be found in the keychain. (-25300)" + // errInteractionNotAllowed is the specific error message returned by OS X + // when environment does not allow showing dialog to unlock keychain. + errInteractionNotAllowed = "User interaction is not allowed. (-25308)" +) // ErrInteractionNotAllowed is returned if keychain password prompt can not be shown. var ErrInteractionNotAllowed = errors.New(`keychain cannot be accessed because the current session does not allow user interaction. The keychain may be locked; unlock it by running "security -v unlock-keychain ~/Library/Keychains/login.keychain-db" and try again`) @@ -38,152 +39,115 @@ type Osxkeychain struct{} func (h Osxkeychain) Add(creds *credentials.Credentials) error { _ = h.Delete(creds.ServerURL) // ignore errors as existing credential may not exist. - s, err := splitServer(creds.ServerURL) - if err != nil { + item := keychain.NewItem() + item.SetSecClass(keychain.SecClassInternetPassword) + item.SetLabel(credentials.CredsLabel) + item.SetAccount(creds.Username) + item.SetData([]byte(creds.Secret)) + if err := splitServer(creds.ServerURL, item); err != nil { return err } - defer freeServer(s) - - label := C.CString(credentials.CredsLabel) - defer C.free(unsafe.Pointer(label)) - username := C.CString(creds.Username) - defer C.free(unsafe.Pointer(username)) - secret := C.CString(creds.Secret) - defer C.free(unsafe.Pointer(secret)) - - errMsg := C.keychain_add(s, label, username, secret) - if errMsg != nil { - defer C.free(unsafe.Pointer(errMsg)) - return errors.New(C.GoString(errMsg)) - } - return nil + return keychain.AddItem(item) } // Delete removes credentials from the keychain. func (h Osxkeychain) Delete(serverURL string) error { - s, err := splitServer(serverURL) - if err != nil { + item := keychain.NewItem() + item.SetSecClass(keychain.SecClassInternetPassword) + if err := splitServer(serverURL, item); err != nil { return err } - defer freeServer(s) - - if errMsg := C.keychain_delete(s); errMsg != nil { - defer C.free(unsafe.Pointer(errMsg)) - switch goMsg := C.GoString(errMsg); goMsg { + if err := keychain.DeleteItem(item); err != nil { + switch err.Error() { case errCredentialsNotFound: return credentials.NewErrCredentialsNotFound() case errInteractionNotAllowed: return ErrInteractionNotAllowed default: - return errors.New(goMsg) + return err } } - return nil } // Get returns the username and secret to use for a given registry server URL. func (h Osxkeychain) Get(serverURL string) (string, string, error) { - s, err := splitServer(serverURL) - if err != nil { + item := keychain.NewItem() + item.SetSecClass(keychain.SecClassInternetPassword) + item.SetMatchLimit(keychain.MatchLimitOne) + item.SetReturnAttributes(true) + item.SetReturnData(true) + if err := splitServer(serverURL, item); err != nil { return "", "", err } - defer freeServer(s) - - var usernameLen C.uint - var username *C.char - var secretLen C.uint - var secret *C.char - defer C.free(unsafe.Pointer(username)) - defer C.free(unsafe.Pointer(secret)) - - errMsg := C.keychain_get(s, &usernameLen, &username, &secretLen, &secret) - if errMsg != nil { - defer C.free(unsafe.Pointer(errMsg)) - switch goMsg := C.GoString(errMsg); goMsg { + + res, err := keychain.QueryItem(item) + if err != nil { + switch err.Error() { case errCredentialsNotFound: return "", "", credentials.NewErrCredentialsNotFound() case errInteractionNotAllowed: return "", "", ErrInteractionNotAllowed default: - return "", "", errors.New(goMsg) + return "", "", err } + } else if len(res) == 0 { + return "", "", credentials.NewErrCredentialsNotFound() } - user := C.GoStringN(username, C.int(usernameLen)) - pass := C.GoStringN(secret, C.int(secretLen)) - return user, pass, nil + return res[0].Account, string(res[0].Data), nil } // List returns the stored URLs and corresponding usernames. func (h Osxkeychain) 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 - errMsg := C.keychain_list(credsLabelC, &pathsC, &acctsC, &listLenC) - defer C.freeListData(&pathsC, listLenC) - defer C.freeListData(&acctsC, listLenC) - if errMsg != nil { - defer C.free(unsafe.Pointer(errMsg)) - switch goMsg := C.GoString(errMsg); goMsg { + item := keychain.NewItem() + item.SetSecClass(keychain.SecClassInternetPassword) + item.SetMatchLimit(keychain.MatchLimitAll) + item.SetReturnAttributes(true) + item.SetLabel(credentials.CredsLabel) + + res, err := keychain.QueryItem(item) + if err != nil { + switch err.Error() { case errCredentialsNotFound: return make(map[string]string), nil case errInteractionNotAllowed: return nil, ErrInteractionNotAllowed default: - return nil, errors.New(goMsg) + return nil, err } + } else if len(res) == 0 { + return nil, credentials.NewErrCredentialsNotFound() } - var listLen int - listLen = int(listLenC) - pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen] - acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen] - // taking the array of c strings into go while ignoring all the stuff irrelevant to credentials-helper resp := make(map[string]string) - for i := 0; i < listLen; i++ { - if C.GoString(pathTmp[i]) == "0" { + for _, r := range res { + if r.Path == "" { continue } - resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i]) + resp[r.Path] = r.Account } return resp, nil } -func splitServer(serverURL string) (*C.struct_Server, error) { +func splitServer(serverURL string, item keychain.Item) error { u, err := registryurl.Parse(serverURL) if err != nil { - return nil, err + return err } - - proto := C.kSecProtocolTypeHTTPS + item.SetProtocol("https") if u.Scheme == "http" { - proto = C.kSecProtocolTypeHTTP + item.SetProtocol("http") } - var port int - p := u.Port() - if p != "" { - port, err = strconv.Atoi(p) + item.SetServer(u.Hostname()) + if p := u.Port(); p != "" { + port, err := strconv.Atoi(p) if err != nil { - return nil, err + return err } + item.SetPort(int32(port)) } - - return &C.struct_Server{ - proto: C.SecProtocolType(proto), - host: C.CString(u.Hostname()), - port: C.uint(port), - path: C.CString(u.Path), - }, nil -} - -func freeServer(s *C.struct_Server) { - C.free(unsafe.Pointer(s.host)) - C.free(unsafe.Pointer(s.path)) + item.SetPath(u.Path) + return nil } diff --git a/osxkeychain/osxkeychain.h b/osxkeychain/osxkeychain.h deleted file mode 100644 index c54e7d72..00000000 --- a/osxkeychain/osxkeychain.h +++ /dev/null @@ -1,14 +0,0 @@ -#include - -struct Server { - SecProtocolType proto; - char *host; - char *path; - unsigned int port; -}; - -char *keychain_add(struct Server *server, char *label, char *username, char *secret); -char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, char **secret); -char *keychain_delete(struct Server *server); -char *keychain_list(char *credsLabel, char *** data, char *** accts, unsigned int *list_l); -void freeListData(char *** data, unsigned int length); \ No newline at end of file diff --git a/vendor/github.com/keybase/go-keychain/.gitignore b/vendor/github.com/keybase/go-keychain/.gitignore new file mode 100644 index 00000000..3142232b --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/.gitignore @@ -0,0 +1,26 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +vendor diff --git a/vendor/github.com/keybase/go-keychain/.golangci.yml b/vendor/github.com/keybase/go-keychain/.golangci.yml new file mode 100644 index 00000000..23aaf432 --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/.golangci.yml @@ -0,0 +1,11 @@ +linters-settings: + gocritic: + disabled-checks: + - ifElseChain + - elseif + +linters: + enable: + - gofmt + - gocritic + - unconvert diff --git a/vendor/github.com/keybase/go-keychain/LICENSE b/vendor/github.com/keybase/go-keychain/LICENSE new file mode 100644 index 00000000..2d54c656 --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Keybase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/vendor/github.com/keybase/go-keychain/README.md b/vendor/github.com/keybase/go-keychain/README.md new file mode 100644 index 00000000..86f1ad8c --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/README.md @@ -0,0 +1,124 @@ +# Go Keychain + +[![Build Status](https://github.com/keybase/go-keychain/actions/workflows/ci.yml/badge.svg)](https://github.com/keybase/go-keychain/actions) + +A library for accessing the Keychain for macOS, iOS, and Linux in Go (golang). + +Requires macOS 10.9 or greater and iOS 8 or greater. On Linux, communicates to +a provider of the DBUS SecretService spec like gnome-keyring or ksecretservice. + +```go +import "github.com/keybase/go-keychain" +``` + +## Mac/iOS Usage + +The API is meant to mirror the macOS/iOS Keychain API and is not necessarily idiomatic go. + +#### Add Item + +```go +item := keychain.NewItem() +item.SetSecClass(keychain.SecClassGenericPassword) +item.SetService("MyService") +item.SetAccount("gabriel") +item.SetLabel("A label") +item.SetAccessGroup("A123456789.group.com.mycorp") +item.SetData([]byte("toomanysecrets")) +item.SetSynchronizable(keychain.SynchronizableNo) +item.SetAccessible(keychain.AccessibleWhenUnlocked) +err := keychain.AddItem(item) + +if err == keychain.ErrorDuplicateItem { + // Duplicate +} +``` + +#### Query Item + +Query for multiple results, returning attributes: + +```go +query := keychain.NewItem() +query.SetSecClass(keychain.SecClassGenericPassword) +query.SetService(service) +query.SetAccount(account) +query.SetAccessGroup(accessGroup) +query.SetMatchLimit(keychain.MatchLimitAll) +query.SetReturnAttributes(true) +results, err := keychain.QueryItem(query) +if err != nil { + // Error +} else { + for _, r := range results { + fmt.Printf("%#v\n", r) + } +} +``` + +Query for a single result, returning data: + +```go +query := keychain.NewItem() +query.SetSecClass(keychain.SecClassGenericPassword) +query.SetService(service) +query.SetAccount(account) +query.SetAccessGroup(accessGroup) +query.SetMatchLimit(keychain.MatchLimitOne) +query.SetReturnData(true) +results, err := keychain.QueryItem(query) +if err != nil { + // Error +} else if len(results) != 1 { + // Not found +} else { + password := string(results[0].Data) +} +``` + +#### Delete Item + +Delete a generic password item with service and account: + +```go +item := keychain.NewItem() +item.SetSecClass(keychain.SecClassGenericPassword) +item.SetService(service) +item.SetAccount(account) +err := keychain.DeleteItem(item) +``` + +### Other + +There are some convenience methods for generic password: + +```go +// Create generic password item with service, account, label, password, access group +item := keychain.NewGenericPassword("MyService", "gabriel", "A label", []byte("toomanysecrets"), "A123456789.group.com.mycorp") +item.SetSynchronizable(keychain.SynchronizableNo) +item.SetAccessible(keychain.AccessibleWhenUnlocked) +err := keychain.AddItem(item) +if err == keychain.ErrorDuplicateItem { + // Duplicate +} + +password, err := keychain.GetGenericPassword("MyService", "gabriel", "A label", "A123456789.group.com.mycorp") + +accounts, err := keychain.GetGenericPasswordAccounts("MyService") +// Should have 1 account == "gabriel" + +err := keychain.DeleteGenericPasswordItem("MyService", "gabriel") +if err == keychain.ErrorItemNotFound { + // Not found +} +``` + +## iOS + +Bindable package in `bind`. iOS project in `ios`. Run that project to test iOS. + +To re-generate framework: + +``` +(cd bind && gomobile bind -target=ios -tags=ios -o ../ios/bind.framework) +``` diff --git a/vendor/github.com/keybase/go-keychain/corefoundation.go b/vendor/github.com/keybase/go-keychain/corefoundation.go new file mode 100644 index 00000000..b7ee544e --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/corefoundation.go @@ -0,0 +1,370 @@ +//go:build darwin || ios +// +build darwin ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation + +#include + +// Can't cast a *uintptr to *unsafe.Pointer in Go, and casting +// C.CFTypeRef to unsafe.Pointer is unsafe in Go, so have shim functions to +// do the casting in C (where it's safe). + +// We add a suffix to the C functions below, because we copied this +// file from go-kext, which means that any project that depends on this +// package and go-kext would run into duplicate symbol errors otherwise. +// +// TODO: Move this file into its own package depended on by go-kext +// and this package. + +CFDictionaryRef CFDictionaryCreateSafe2(CFAllocatorRef allocator, const uintptr_t *keys, const uintptr_t *values, CFIndex numValues, const CFDictionaryKeyCallBacks *keyCallBacks, const CFDictionaryValueCallBacks *valueCallBacks) { + return CFDictionaryCreate(allocator, (const void **)keys, (const void **)values, numValues, keyCallBacks, valueCallBacks); +} + +CFArrayRef CFArrayCreateSafe2(CFAllocatorRef allocator, const uintptr_t *values, CFIndex numValues, const CFArrayCallBacks *callBacks) { + return CFArrayCreate(allocator, (const void **)values, numValues, callBacks); +} +*/ +import "C" +import ( + "errors" + "fmt" + "math" + "reflect" + "unicode/utf8" + "unsafe" +) + +// Release releases memory pointed to by a CFTypeRef. +func Release(ref C.CFTypeRef) { + C.CFRelease(ref) +} + +// BytesToCFData will return a CFDataRef and if non-nil, must be released with +// Release(ref). +func BytesToCFData(b []byte) (C.CFDataRef, error) { + if uint64(len(b)) > math.MaxUint32 { + return 0, errors.New("Data is too large") + } + var p *C.UInt8 + if len(b) > 0 { + p = (*C.UInt8)(&b[0]) + } + cfData := C.CFDataCreate(C.kCFAllocatorDefault, p, C.CFIndex(len(b))) + if cfData == 0 { + return 0, fmt.Errorf("CFDataCreate failed") + } + return cfData, nil +} + +// CFDataToBytes converts CFData to bytes. +func CFDataToBytes(cfData C.CFDataRef) ([]byte, error) { + return C.GoBytes(unsafe.Pointer(C.CFDataGetBytePtr(cfData)), C.int(C.CFDataGetLength(cfData))), nil +} + +// MapToCFDictionary will return a CFDictionaryRef and if non-nil, must be +// released with Release(ref). +func MapToCFDictionary(m map[C.CFTypeRef]C.CFTypeRef) (C.CFDictionaryRef, error) { + var keys, values []C.uintptr_t + for key, value := range m { + keys = append(keys, C.uintptr_t(key)) + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var keysPointer, valuesPointer *C.uintptr_t + if numValues > 0 { + keysPointer = &keys[0] + valuesPointer = &values[0] + } + cfDict := C.CFDictionaryCreateSafe2(C.kCFAllocatorDefault, keysPointer, valuesPointer, C.CFIndex(numValues), + &C.kCFTypeDictionaryKeyCallBacks, &C.kCFTypeDictionaryValueCallBacks) //nolint + if cfDict == 0 { + return 0, fmt.Errorf("CFDictionaryCreate failed") + } + return cfDict, nil +} + +// CFDictionaryToMap converts CFDictionaryRef to a map. +func CFDictionaryToMap(cfDict C.CFDictionaryRef) (m map[C.CFTypeRef]C.CFTypeRef) { + count := C.CFDictionaryGetCount(cfDict) + if count > 0 { + keys := make([]C.CFTypeRef, count) + values := make([]C.CFTypeRef, count) + C.CFDictionaryGetKeysAndValues(cfDict, (*unsafe.Pointer)(unsafe.Pointer(&keys[0])), (*unsafe.Pointer)(unsafe.Pointer(&values[0]))) + m = make(map[C.CFTypeRef]C.CFTypeRef, count) + for i := C.CFIndex(0); i < count; i++ { + m[keys[i]] = values[i] + } + } + return +} + +// Int32ToCFNumber will return a CFNumberRef, must be released with Release(ref). +func Int32ToCFNumber(u int32) C.CFNumberRef { + sint := C.SInt32(u) + p := unsafe.Pointer(&sint) + return C.CFNumberCreate(C.kCFAllocatorDefault, C.kCFNumberSInt32Type, p) +} + +// StringToCFString will return a CFStringRef and if non-nil, must be released with +// Release(ref). +func StringToCFString(s string) (C.CFStringRef, error) { + if !utf8.ValidString(s) { + return 0, errors.New("Invalid UTF-8 string") + } + if uint64(len(s)) > math.MaxUint32 { + return 0, errors.New("String is too large") + } + + bytes := []byte(s) + var p *C.UInt8 + if len(bytes) > 0 { + p = (*C.UInt8)(&bytes[0]) + } + return C.CFStringCreateWithBytes(C.kCFAllocatorDefault, p, C.CFIndex(len(s)), C.kCFStringEncodingUTF8, C.false), nil +} + +// CFStringToString converts a CFStringRef to a string. +func CFStringToString(s C.CFStringRef) string { + p := C.CFStringGetCStringPtr(s, C.kCFStringEncodingUTF8) + if p != nil { + return C.GoString(p) + } + length := C.CFStringGetLength(s) + if length == 0 { + return "" + } + maxBufLen := C.CFStringGetMaximumSizeForEncoding(length, C.kCFStringEncodingUTF8) + if maxBufLen == 0 { + return "" + } + buf := make([]byte, maxBufLen) + var usedBufLen C.CFIndex + _ = C.CFStringGetBytes(s, C.CFRange{0, length}, C.kCFStringEncodingUTF8, C.UInt8(0), C.false, (*C.UInt8)(&buf[0]), maxBufLen, &usedBufLen) + return string(buf[:usedBufLen]) +} + +// ArrayToCFArray will return a CFArrayRef and if non-nil, must be released with +// Release(ref). +func ArrayToCFArray(a []C.CFTypeRef) C.CFArrayRef { + var values []C.uintptr_t + for _, value := range a { + values = append(values, C.uintptr_t(value)) + } + numValues := len(values) + var valuesPointer *C.uintptr_t + if numValues > 0 { + valuesPointer = &values[0] + } + return C.CFArrayCreateSafe2(C.kCFAllocatorDefault, valuesPointer, C.CFIndex(numValues), &C.kCFTypeArrayCallBacks) //nolint +} + +// CFArrayToArray converts a CFArrayRef to an array of CFTypes. +func CFArrayToArray(cfArray C.CFArrayRef) (a []C.CFTypeRef) { + count := C.CFArrayGetCount(cfArray) + if count > 0 { + a = make([]C.CFTypeRef, count) + C.CFArrayGetValues(cfArray, C.CFRange{0, count}, (*unsafe.Pointer)(unsafe.Pointer(&a[0]))) + } + return +} + +// Convertable knows how to convert an instance to a CFTypeRef. +type Convertable interface { + Convert() (C.CFTypeRef, error) +} + +// ConvertMapToCFDictionary converts a map to a CFDictionary and if non-nil, +// must be released with Release(ref). +func ConvertMapToCFDictionary(attr map[string]interface{}) (C.CFDictionaryRef, error) { + m := make(map[C.CFTypeRef]C.CFTypeRef) + for key, i := range attr { + var valueRef C.CFTypeRef + switch val := i.(type) { + default: + return 0, fmt.Errorf("Unsupported value type: %v", reflect.TypeOf(i)) + case C.CFTypeRef: + valueRef = val + case bool: + if val { + valueRef = C.CFTypeRef(C.kCFBooleanTrue) + } else { + valueRef = C.CFTypeRef(C.kCFBooleanFalse) + } + case int32: + valueRef = C.CFTypeRef(Int32ToCFNumber(val)) + defer Release(valueRef) + case []byte: + bytesRef, err := BytesToCFData(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(bytesRef) + defer Release(valueRef) + case string: + stringRef, err := StringToCFString(val) + if err != nil { + return 0, err + } + valueRef = C.CFTypeRef(stringRef) + defer Release(valueRef) + case Convertable: + convertedRef, err := val.Convert() + if err != nil { + return 0, err + } + valueRef = convertedRef + defer Release(valueRef) + } + keyRef, err := StringToCFString(key) + if err != nil { + return 0, err + } + m[C.CFTypeRef(keyRef)] = valueRef + defer Release(C.CFTypeRef(keyRef)) + } + + cfDict, err := MapToCFDictionary(m) + if err != nil { + return 0, err + } + return cfDict, nil +} + +// CFTypeDescription returns type string for CFTypeRef. +func CFTypeDescription(ref C.CFTypeRef) string { + typeID := C.CFGetTypeID(ref) + typeDesc := C.CFCopyTypeIDDescription(typeID) + defer Release(C.CFTypeRef(typeDesc)) + return CFStringToString(typeDesc) +} + +// Convert converts a CFTypeRef to a go instance. +func Convert(ref C.CFTypeRef) (interface{}, error) { + typeID := C.CFGetTypeID(ref) + if typeID == C.CFStringGetTypeID() { + return CFStringToString(C.CFStringRef(ref)), nil + } else if typeID == C.CFDictionaryGetTypeID() { + return ConvertCFDictionary(C.CFDictionaryRef(ref)) + } else if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(ref)) + results := make([]interface{}, 0, len(arr)) + for _, ref := range arr { + v, err := Convert(ref) + if err != nil { + return nil, err + } + results = append(results, v) + } + return results, nil + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(ref)) + if err != nil { + return nil, err + } + return b, nil + } else if typeID == C.CFNumberGetTypeID() { + return CFNumberToInterface(C.CFNumberRef(ref)), nil + } else if typeID == C.CFBooleanGetTypeID() { + if C.CFBooleanGetValue(C.CFBooleanRef(ref)) != 0 { + return true, nil + } + return false, nil + } + + return nil, fmt.Errorf("Invalid type: %s", CFTypeDescription(ref)) +} + +// ConvertCFDictionary converts a CFDictionary to map (deep). +func ConvertCFDictionary(d C.CFDictionaryRef) (map[interface{}]interface{}, error) { + m := CFDictionaryToMap(d) + result := make(map[interface{}]interface{}) + + for k, v := range m { + gk, err := Convert(k) + if err != nil { + return nil, err + } + gv, err := Convert(v) + if err != nil { + return nil, err + } + result[gk] = gv + } + return result, nil +} + +// CFNumberToInterface converts the CFNumberRef to the most appropriate numeric +// type. +// This code is from github.com/kballard/go-osx-plist. +func CFNumberToInterface(cfNumber C.CFNumberRef) interface{} { + typ := C.CFNumberGetType(cfNumber) + switch typ { + case C.kCFNumberSInt8Type: + var sint C.SInt8 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int8(sint) + case C.kCFNumberSInt16Type: + var sint C.SInt16 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int16(sint) + case C.kCFNumberSInt32Type: + var sint C.SInt32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int32(sint) + case C.kCFNumberSInt64Type: + var sint C.SInt64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&sint)) //nolint + return int64(sint) + case C.kCFNumberFloat32Type: + var float C.Float32 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float32(float) + case C.kCFNumberFloat64Type: + var float C.Float64 + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float64(float) + case C.kCFNumberCharType: + var char C.char + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&char)) //nolint + return byte(char) + case C.kCFNumberShortType: + var short C.short + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&short)) //nolint + return int16(short) + case C.kCFNumberIntType: + var i C.int + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&i)) //nolint + return int32(i) + case C.kCFNumberLongType: + var long C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&long)) //nolint + return int(long) + case C.kCFNumberLongLongType: + // This is the only type that may actually overflow us + var longlong C.longlong + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&longlong)) //nolint + return int64(longlong) + case C.kCFNumberFloatType: + var float C.float + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&float)) //nolint + return float32(float) + case C.kCFNumberDoubleType: + var double C.double + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&double)) //nolint + return float64(double) + case C.kCFNumberCFIndexType: + // CFIndex is a long + var index C.CFIndex + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&index)) //nolint + return int(index) + case C.kCFNumberNSIntegerType: + // We don't have a definition of NSInteger, but we know it's either an int or a long + var nsInt C.long + C.CFNumberGetValue(cfNumber, typ, unsafe.Pointer(&nsInt)) //nolint + return int(nsInt) + } + panic("Unknown CFNumber type") +} diff --git a/vendor/github.com/keybase/go-keychain/datetime.go b/vendor/github.com/keybase/go-keychain/datetime.go new file mode 100644 index 00000000..2daca4ae --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/datetime.go @@ -0,0 +1,69 @@ +//go:build darwin || ios +// +build darwin ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation + +#include +*/ +import "C" +import ( + "math" + "time" +) + +const nsPerSec = 1000 * 1000 * 1000 + +// absoluteTimeIntervalSince1970() returns the number of seconds from +// the Unix epoch (1970-01-01T00:00:00+00:00) to the Core Foundation +// absolute reference date (2001-01-01T00:00:00+00:00). It should be +// exactly 978307200. +func absoluteTimeIntervalSince1970() int64 { + return int64(C.kCFAbsoluteTimeIntervalSince1970) +} + +func unixToAbsoluteTime(s int64, ns int64) C.CFAbsoluteTime { + // Subtract as int64s first before converting to floating + // point to minimize precision loss (assuming the given time + // isn't much earlier than the Core Foundation absolute + // reference date). + abs := s - absoluteTimeIntervalSince1970() + return C.CFAbsoluteTime(abs) + C.CFTimeInterval(ns)/nsPerSec +} + +func absoluteTimeToUnix(abs C.CFAbsoluteTime) (int64, int64) { + int, frac := math.Modf(float64(abs)) + return int64(int) + absoluteTimeIntervalSince1970(), int64(frac * nsPerSec) +} + +// TimeToCFDate will convert the given time.Time to a CFDateRef, which +// must be released with Release(ref). +func TimeToCFDate(t time.Time) C.CFDateRef { + s := t.Unix() + ns := int64(t.Nanosecond()) + abs := unixToAbsoluteTime(s, ns) + return C.CFDateCreate(C.kCFAllocatorDefault, abs) +} + +// CFDateToTime will convert the given CFDateRef to a time.Time. +func CFDateToTime(d C.CFDateRef) time.Time { + abs := C.CFDateGetAbsoluteTime(d) + s, ns := absoluteTimeToUnix(abs) + return time.Unix(s, ns) +} + +// Wrappers around C functions for testing. + +func cfDateToAbsoluteTime(d C.CFDateRef) C.CFAbsoluteTime { + return C.CFDateGetAbsoluteTime(d) +} + +func absoluteTimeToCFDate(abs C.CFAbsoluteTime) C.CFDateRef { + return C.CFDateCreate(C.kCFAllocatorDefault, abs) +} + +func releaseCFDate(d C.CFDateRef) { + Release(C.CFTypeRef(d)) +} diff --git a/vendor/github.com/keybase/go-keychain/ios.go b/vendor/github.com/keybase/go-keychain/ios.go new file mode 100644 index 00000000..02c9b872 --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/ios.go @@ -0,0 +1,23 @@ +//go:build darwin && ios +// +build darwin,ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include +*/ +import "C" + +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), +} diff --git a/vendor/github.com/keybase/go-keychain/keychain.go b/vendor/github.com/keybase/go-keychain/keychain.go new file mode 100644 index 00000000..7d0a1ac3 --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/keychain.go @@ -0,0 +1,653 @@ +//go:build darwin +// +build darwin + +package keychain + +// See https://developer.apple.com/library/ios/documentation/Security/Reference/keychainservices/index.html for the APIs used below. + +// Also see https://developer.apple.com/library/ios/documentation/Security/Conceptual/keychainServConcepts/01introduction/introduction.html . + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security + +#include +#include +*/ +import "C" +import ( + "fmt" + "time" +) + +// Error defines keychain errors +type Error int + +var ( + // ErrorUnimplemented corresponds to errSecUnimplemented result code + ErrorUnimplemented = Error(C.errSecUnimplemented) + // ErrorParam corresponds to errSecParam result code + ErrorParam = Error(C.errSecParam) + // ErrorAllocate corresponds to errSecAllocate result code + ErrorAllocate = Error(C.errSecAllocate) + // ErrorNotAvailable corresponds to errSecNotAvailable result code + ErrorNotAvailable = Error(C.errSecNotAvailable) + // ErrorAuthFailed corresponds to errSecAuthFailed result code + ErrorAuthFailed = Error(C.errSecAuthFailed) + // ErrorDuplicateItem corresponds to errSecDuplicateItem result code + ErrorDuplicateItem = Error(C.errSecDuplicateItem) + // ErrorItemNotFound corresponds to errSecItemNotFound result code + ErrorItemNotFound = Error(C.errSecItemNotFound) + // ErrorInteractionNotAllowed corresponds to errSecInteractionNotAllowed result code + ErrorInteractionNotAllowed = Error(C.errSecInteractionNotAllowed) + // ErrorDecode corresponds to errSecDecode result code + ErrorDecode = Error(C.errSecDecode) + // ErrorNoSuchKeychain corresponds to errSecNoSuchKeychain result code + ErrorNoSuchKeychain = Error(C.errSecNoSuchKeychain) + // ErrorNoAccessForItem corresponds to errSecNoAccessForItem result code + ErrorNoAccessForItem = Error(C.errSecNoAccessForItem) + // ErrorReadOnly corresponds to errSecReadOnly result code + ErrorReadOnly = Error(C.errSecReadOnly) + // ErrorInvalidKeychain corresponds to errSecInvalidKeychain result code + ErrorInvalidKeychain = Error(C.errSecInvalidKeychain) + // ErrorDuplicateKeyChain corresponds to errSecDuplicateKeychain result code + ErrorDuplicateKeyChain = Error(C.errSecDuplicateKeychain) + // ErrorWrongVersion corresponds to errSecWrongSecVersion result code + ErrorWrongVersion = Error(C.errSecWrongSecVersion) + // ErrorReadonlyAttribute corresponds to errSecReadOnlyAttr result code + ErrorReadonlyAttribute = Error(C.errSecReadOnlyAttr) + // ErrorInvalidSearchRef corresponds to errSecInvalidSearchRef result code + ErrorInvalidSearchRef = Error(C.errSecInvalidSearchRef) + // ErrorInvalidItemRef corresponds to errSecInvalidItemRef result code + ErrorInvalidItemRef = Error(C.errSecInvalidItemRef) + // ErrorDataNotAvailable corresponds to errSecDataNotAvailable result code + ErrorDataNotAvailable = Error(C.errSecDataNotAvailable) + // ErrorDataNotModifiable corresponds to errSecDataNotModifiable result code + ErrorDataNotModifiable = Error(C.errSecDataNotModifiable) + // ErrorInvalidOwnerEdit corresponds to errSecInvalidOwnerEdit result code + ErrorInvalidOwnerEdit = Error(C.errSecInvalidOwnerEdit) + // ErrorUserCanceled corresponds to errSecUserCanceled result code + ErrorUserCanceled = Error(C.errSecUserCanceled) +) + +func checkError(errCode C.OSStatus) error { + if errCode == C.errSecSuccess { + return nil + } + return Error(errCode) +} + +func (k Error) Error() (msg string) { + // SecCopyErrorMessageString is only available on OSX, so derive manually. + // Messages derived from `$ security error $errcode`. + switch k { + case ErrorUnimplemented: + msg = "Function or operation not implemented." + case ErrorParam: + msg = "One or more parameters passed to the function were not valid." + case ErrorAllocate: + msg = "Failed to allocate memory." + case ErrorNotAvailable: + msg = "No keychain is available. You may need to restart your computer." + case ErrorAuthFailed: + msg = "The user name or passphrase you entered is not correct." + case ErrorDuplicateItem: + msg = "The specified item already exists in the keychain." + case ErrorItemNotFound: + msg = "The specified item could not be found in the keychain." + case ErrorInteractionNotAllowed: + msg = "User interaction is not allowed." + case ErrorDecode: + msg = "Unable to decode the provided data." + case ErrorNoSuchKeychain: + msg = "The specified keychain could not be found." + case ErrorNoAccessForItem: + msg = "The specified item has no access control." + case ErrorReadOnly: + msg = "Read-only error." + case ErrorReadonlyAttribute: + msg = "The attribute is read-only." + case ErrorInvalidKeychain: + msg = "The keychain is not valid." + case ErrorDuplicateKeyChain: + msg = "A keychain with the same name already exists." + case ErrorWrongVersion: + msg = "The version is incorrect." + case ErrorInvalidItemRef: + msg = "The item reference is invalid." + case ErrorInvalidSearchRef: + msg = "The search reference is invalid." + case ErrorDataNotAvailable: + msg = "The data is not available." + case ErrorDataNotModifiable: + msg = "The data is not modifiable." + case ErrorInvalidOwnerEdit: + msg = "An invalid attempt to change the owner of an item." + case ErrorUserCanceled: + msg = "User canceled the operation." + default: + msg = "Keychain Error." + } + return fmt.Sprintf("%s (%d)", msg, k) +} + +// SecClass is the items class code +type SecClass int + +// Keychain Item Classes +var ( + /* + kSecClassGenericPassword item attributes: + kSecAttrAccess (OS X only) + kSecAttrAccessGroup (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccessible (iOS; also OS X if kSecAttrSynchronizable specified) + kSecAttrAccount + kSecAttrService + */ + SecClassGenericPassword SecClass = 1 + SecClassInternetPassword SecClass = 2 +) + +// SecClassKey is the key type for SecClass +var SecClassKey = attrKey(C.CFTypeRef(C.kSecClass)) +var secClassTypeRef = map[SecClass]C.CFTypeRef{ + SecClassGenericPassword: C.CFTypeRef(C.kSecClassGenericPassword), + SecClassInternetPassword: C.CFTypeRef(C.kSecClassInternetPassword), +} + +var ( + // ServiceKey is for kSecAttrService + ServiceKey = attrKey(C.CFTypeRef(C.kSecAttrService)) + + // ServerKey is for kSecAttrServer + ServerKey = attrKey(C.CFTypeRef(C.kSecAttrServer)) + // ProtocolKey is for kSecAttrProtocol + ProtocolKey = attrKey(C.CFTypeRef(C.kSecAttrProtocol)) + // AuthenticationTypeKey is for kSecAttrAuthenticationType + AuthenticationTypeKey = attrKey(C.CFTypeRef(C.kSecAttrAuthenticationType)) + // PortKey is for kSecAttrPort + PortKey = attrKey(C.CFTypeRef(C.kSecAttrPort)) + // PathKey is for kSecAttrPath + PathKey = attrKey(C.CFTypeRef(C.kSecAttrPath)) + + // LabelKey is for kSecAttrLabel + LabelKey = attrKey(C.CFTypeRef(C.kSecAttrLabel)) + // AccountKey is for kSecAttrAccount + AccountKey = attrKey(C.CFTypeRef(C.kSecAttrAccount)) + // AccessGroupKey is for kSecAttrAccessGroup + AccessGroupKey = attrKey(C.CFTypeRef(C.kSecAttrAccessGroup)) + // DataKey is for kSecValueData + DataKey = attrKey(C.CFTypeRef(C.kSecValueData)) + // DescriptionKey is for kSecAttrDescription + DescriptionKey = attrKey(C.CFTypeRef(C.kSecAttrDescription)) + // CommentKey is for kSecAttrComment + CommentKey = attrKey(C.CFTypeRef(C.kSecAttrComment)) + // CreationDateKey is for kSecAttrCreationDate + CreationDateKey = attrKey(C.CFTypeRef(C.kSecAttrCreationDate)) + // ModificationDateKey is for kSecAttrModificationDate + ModificationDateKey = attrKey(C.CFTypeRef(C.kSecAttrModificationDate)) +) + +// Synchronizable is the items synchronizable status +type Synchronizable int + +const ( + // SynchronizableDefault is the default setting + SynchronizableDefault Synchronizable = 0 + // SynchronizableAny is for kSecAttrSynchronizableAny + SynchronizableAny = 1 + // SynchronizableYes enables synchronization + SynchronizableYes = 2 + // SynchronizableNo disables synchronization + SynchronizableNo = 3 +) + +// SynchronizableKey is the key type for Synchronizable +var SynchronizableKey = attrKey(C.CFTypeRef(C.kSecAttrSynchronizable)) +var syncTypeRef = map[Synchronizable]C.CFTypeRef{ + SynchronizableAny: C.CFTypeRef(C.kSecAttrSynchronizableAny), + SynchronizableYes: C.CFTypeRef(C.kCFBooleanTrue), + SynchronizableNo: C.CFTypeRef(C.kCFBooleanFalse), +} + +// Accessible is the items accessibility +type Accessible int + +const ( + // AccessibleDefault is the default + AccessibleDefault Accessible = 0 + // AccessibleWhenUnlocked is when unlocked + AccessibleWhenUnlocked = 1 + // AccessibleAfterFirstUnlock is after first unlock + AccessibleAfterFirstUnlock = 2 + // AccessibleAlways is always + AccessibleAlways = 3 + // AccessibleWhenPasscodeSetThisDeviceOnly is when passcode is set + AccessibleWhenPasscodeSetThisDeviceOnly = 4 + // AccessibleWhenUnlockedThisDeviceOnly is when unlocked for this device only + AccessibleWhenUnlockedThisDeviceOnly = 5 + // AccessibleAfterFirstUnlockThisDeviceOnly is after first unlock for this device only + AccessibleAfterFirstUnlockThisDeviceOnly = 6 + // AccessibleAccessibleAlwaysThisDeviceOnly is always for this device only + AccessibleAccessibleAlwaysThisDeviceOnly = 7 +) + +// MatchLimit is whether to limit results on query +type MatchLimit int + +const ( + // MatchLimitDefault is the default + MatchLimitDefault MatchLimit = 0 + // MatchLimitOne limits to one result + MatchLimitOne = 1 + // MatchLimitAll is no limit + MatchLimitAll = 2 +) + +// MatchLimitKey is key type for MatchLimit +var MatchLimitKey = attrKey(C.CFTypeRef(C.kSecMatchLimit)) +var matchTypeRef = map[MatchLimit]C.CFTypeRef{ + MatchLimitOne: C.CFTypeRef(C.kSecMatchLimitOne), + MatchLimitAll: C.CFTypeRef(C.kSecMatchLimitAll), +} + +// ReturnAttributesKey is key type for kSecReturnAttributes +var ReturnAttributesKey = attrKey(C.CFTypeRef(C.kSecReturnAttributes)) + +// ReturnDataKey is key type for kSecReturnData +var ReturnDataKey = attrKey(C.CFTypeRef(C.kSecReturnData)) + +// ReturnRefKey is key type for kSecReturnRef +var ReturnRefKey = attrKey(C.CFTypeRef(C.kSecReturnRef)) + +// Item for adding, querying or deleting. +type Item struct { + // Values can be string, []byte, Convertable or CFTypeRef (constant). + attr map[string]interface{} +} + +// SetSecClass sets the security class +func (k *Item) SetSecClass(sc SecClass) { + k.attr[SecClassKey] = secClassTypeRef[sc] +} + +// SetInt32 sets an int32 attribute for a string key +func (k *Item) SetInt32(key string, v int32) { + if v != 0 { + k.attr[key] = v + } else { + delete(k.attr, key) + } +} + +// SetString sets a string attibute for a string key +func (k *Item) SetString(key string, s string) { + if s != "" { + k.attr[key] = s + } else { + delete(k.attr, key) + } +} + +// SetService sets the service attribute (for generic application items) +func (k *Item) SetService(s string) { + k.SetString(ServiceKey, s) +} + +// SetServer sets the server attribute (for internet password items) +func (k *Item) SetServer(s string) { + k.SetString(ServerKey, s) +} + +// SetProtocol sets the protocol attribute (for internet password items) +// Example values are: "htps", "http", "smb " +func (k *Item) SetProtocol(s string) { + k.SetString(ProtocolKey, s) +} + +// SetAuthenticationType sets the authentication type attribute (for internet password items) +func (k *Item) SetAuthenticationType(s string) { + k.SetString(AuthenticationTypeKey, s) +} + +// SetPort sets the port attribute (for internet password items) +func (k *Item) SetPort(v int32) { + k.SetInt32(PortKey, v) +} + +// SetPath sets the path attribute (for internet password items) +func (k *Item) SetPath(s string) { + k.SetString(PathKey, s) +} + +// SetAccount sets the account attribute +func (k *Item) SetAccount(a string) { + k.SetString(AccountKey, a) +} + +// SetLabel sets the label attribute +func (k *Item) SetLabel(l string) { + k.SetString(LabelKey, l) +} + +// SetDescription sets the description attribute +func (k *Item) SetDescription(s string) { + k.SetString(DescriptionKey, s) +} + +// SetComment sets the comment attribute +func (k *Item) SetComment(s string) { + k.SetString(CommentKey, s) +} + +// SetData sets the data attribute +func (k *Item) SetData(b []byte) { + if b != nil { + k.attr[DataKey] = b + } else { + delete(k.attr, DataKey) + } +} + +// SetAccessGroup sets the access group attribute +func (k *Item) SetAccessGroup(ag string) { + k.SetString(AccessGroupKey, ag) +} + +// SetSynchronizable sets the synchronizable attribute +func (k *Item) SetSynchronizable(sync Synchronizable) { + if sync != SynchronizableDefault { + k.attr[SynchronizableKey] = syncTypeRef[sync] + } else { + delete(k.attr, SynchronizableKey) + } +} + +// SetAccessible sets the accessible attribute +func (k *Item) SetAccessible(accessible Accessible) { + if accessible != AccessibleDefault { + k.attr[AccessibleKey] = accessibleTypeRef[accessible] + } else { + delete(k.attr, AccessibleKey) + } +} + +// SetMatchLimit sets the match limit +func (k *Item) SetMatchLimit(matchLimit MatchLimit) { + if matchLimit != MatchLimitDefault { + k.attr[MatchLimitKey] = matchTypeRef[matchLimit] + } else { + delete(k.attr, MatchLimitKey) + } +} + +// SetReturnAttributes sets the return value type on query +func (k *Item) SetReturnAttributes(b bool) { + k.attr[ReturnAttributesKey] = b +} + +// SetReturnData enables returning data on query +func (k *Item) SetReturnData(b bool) { + k.attr[ReturnDataKey] = b +} + +// SetReturnRef enables returning references on query +func (k *Item) SetReturnRef(b bool) { + k.attr[ReturnRefKey] = b +} + +// NewItem is a new empty keychain item +func NewItem() Item { + return Item{make(map[string]interface{})} +} + +// NewGenericPassword creates a generic password item with the default keychain. This is a convenience method. +func NewGenericPassword(service string, account string, label string, data []byte, accessGroup string) Item { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + item.SetLabel(label) + item.SetData(data) + item.SetAccessGroup(accessGroup) + return item +} + +// AddItem adds a Item to a Keychain +func AddItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemAdd(cfDict, nil) + err = checkError(errCode) + return err +} + +// UpdateItem updates the queryItem with the parameters from updateItem +func UpdateItem(queryItem Item, updateItem Item) error { + cfDict, err := ConvertMapToCFDictionary(queryItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + cfDictUpdate, err := ConvertMapToCFDictionary(updateItem.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDictUpdate)) + errCode := C.SecItemUpdate(cfDict, cfDictUpdate) + err = checkError(errCode) + return err +} + +// QueryResult stores all possible results from queries. +// Not all fields are applicable all the time. Results depend on query. +type QueryResult struct { + // For generic application items + Service string + + // For internet password items + Server string + Protocol string + AuthenticationType string + Port int32 + Path string + + Account string + AccessGroup string + Label string + Description string + Comment string + Data []byte + CreationDate time.Time + ModificationDate time.Time +} + +// QueryItemRef returns query result as CFTypeRef. You must release it when you are done. +func QueryItemRef(item Item) (C.CFTypeRef, error) { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return 0, err + } + defer Release(C.CFTypeRef(cfDict)) + + var resultsRef C.CFTypeRef + errCode := C.SecItemCopyMatching(cfDict, &resultsRef) //nolint + if Error(errCode) == ErrorItemNotFound { + return 0, nil + } + err = checkError(errCode) + if err != nil { + return 0, err + } + return resultsRef, nil +} + +// QueryItem returns a list of query results. +func QueryItem(item Item) ([]QueryResult, error) { + resultsRef, err := QueryItemRef(item) + if err != nil { + return nil, err + } + if resultsRef == 0 { + return nil, nil + } + defer Release(resultsRef) + + results := make([]QueryResult, 0, 1) + + typeID := C.CFGetTypeID(resultsRef) + if typeID == C.CFArrayGetTypeID() { + arr := CFArrayToArray(C.CFArrayRef(resultsRef)) + for _, ref := range arr { + elementTypeID := C.CFGetTypeID(ref) + if elementTypeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(ref)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else { + return nil, fmt.Errorf("invalid result type (If you SetReturnRef(true) you should use QueryItemRef directly)") + } + } + } else if typeID == C.CFDictionaryGetTypeID() { + item, err := convertResult(C.CFDictionaryRef(resultsRef)) + if err != nil { + return nil, err + } + results = append(results, *item) + } else if typeID == C.CFDataGetTypeID() { + b, err := CFDataToBytes(C.CFDataRef(resultsRef)) + if err != nil { + return nil, err + } + item := QueryResult{Data: b} + results = append(results, item) + } else { + return nil, fmt.Errorf("Invalid result type: %s", CFTypeDescription(resultsRef)) + } + + return results, nil +} + +func attrKey(ref C.CFTypeRef) string { + return CFStringToString(C.CFStringRef(ref)) +} + +func convertResult(d C.CFDictionaryRef) (*QueryResult, error) { + m := CFDictionaryToMap(d) + result := QueryResult{} + for k, v := range m { + switch attrKey(k) { + case ServiceKey: + result.Service = CFStringToString(C.CFStringRef(v)) + case ServerKey: + result.Server = CFStringToString(C.CFStringRef(v)) + case ProtocolKey: + result.Protocol = CFStringToString(C.CFStringRef(v)) + case AuthenticationTypeKey: + result.AuthenticationType = CFStringToString(C.CFStringRef(v)) + case PortKey: + val := CFNumberToInterface(C.CFNumberRef(v)) + result.Port = val.(int32) + case PathKey: + result.Path = CFStringToString(C.CFStringRef(v)) + case AccountKey: + result.Account = CFStringToString(C.CFStringRef(v)) + case AccessGroupKey: + result.AccessGroup = CFStringToString(C.CFStringRef(v)) + case LabelKey: + result.Label = CFStringToString(C.CFStringRef(v)) + case DescriptionKey: + result.Description = CFStringToString(C.CFStringRef(v)) + case CommentKey: + result.Comment = CFStringToString(C.CFStringRef(v)) + case DataKey: + b, err := CFDataToBytes(C.CFDataRef(v)) + if err != nil { + return nil, err + } + result.Data = b + case CreationDateKey: + result.CreationDate = CFDateToTime(C.CFDateRef(v)) + case ModificationDateKey: + result.ModificationDate = CFDateToTime(C.CFDateRef(v)) + // default: + // fmt.Printf("Unhandled key in conversion: %v = %v\n", cfTypeValue(k), cfTypeValue(v)) + } + } + return &result, nil +} + +// DeleteGenericPasswordItem removes a generic password item. +func DeleteGenericPasswordItem(service string, account string) error { + item := NewItem() + item.SetSecClass(SecClassGenericPassword) + item.SetService(service) + item.SetAccount(account) + return DeleteItem(item) +} + +// DeleteItem removes a Item +func DeleteItem(item Item) error { + cfDict, err := ConvertMapToCFDictionary(item.attr) + if err != nil { + return err + } + defer Release(C.CFTypeRef(cfDict)) + + errCode := C.SecItemDelete(cfDict) + return checkError(errCode) +} + +// GetAccountsForService is deprecated +func GetAccountsForService(service string) ([]string, error) { + return GetGenericPasswordAccounts(service) +} + +// GetGenericPasswordAccounts returns generic password accounts for service. This is a convenience method. +func GetGenericPasswordAccounts(service string) ([]string, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetMatchLimit(MatchLimitAll) + query.SetReturnAttributes(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + + accounts := make([]string, 0, len(results)) + for _, r := range results { + accounts = append(accounts, r.Account) + } + + return accounts, nil +} + +// GetGenericPassword returns password data for service and account. This is a convenience method. +// If item is not found returns nil, nil. +func GetGenericPassword(service string, account string, label string, accessGroup string) ([]byte, error) { + query := NewItem() + query.SetSecClass(SecClassGenericPassword) + query.SetService(service) + query.SetAccount(account) + query.SetLabel(label) + query.SetAccessGroup(accessGroup) + query.SetMatchLimit(MatchLimitOne) + query.SetReturnData(true) + results, err := QueryItem(query) + if err != nil { + return nil, err + } + if len(results) > 1 { + return nil, fmt.Errorf("Too many results") + } + if len(results) == 1 { + return results[0].Data, nil + } + return nil, nil +} diff --git a/vendor/github.com/keybase/go-keychain/macos.go b/vendor/github.com/keybase/go-keychain/macos.go new file mode 100644 index 00000000..366cc42a --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/macos.go @@ -0,0 +1,25 @@ +//go:build darwin && !ios +// +build darwin,!ios + +package keychain + +/* +#cgo LDFLAGS: -framework CoreFoundation -framework Security +#include +#include +*/ +import "C" + +// AccessibleKey is key for kSecAttrAccessible +var AccessibleKey = attrKey(C.CFTypeRef(C.kSecAttrAccessible)) +var accessibleTypeRef = map[Accessible]C.CFTypeRef{ + AccessibleWhenUnlocked: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlocked), + AccessibleAfterFirstUnlock: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlock), + AccessibleAlways: C.CFTypeRef(C.kSecAttrAccessibleAlways), + AccessibleWhenUnlockedThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenUnlockedThisDeviceOnly), + AccessibleAfterFirstUnlockThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly), + AccessibleAccessibleAlwaysThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleAlwaysThisDeviceOnly), + + // Only available in 10.10 + //AccessibleWhenPasscodeSetThisDeviceOnly: C.CFTypeRef(C.kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly), +} diff --git a/vendor/github.com/keybase/go-keychain/util.go b/vendor/github.com/keybase/go-keychain/util.go new file mode 100644 index 00000000..8e3119d1 --- /dev/null +++ b/vendor/github.com/keybase/go-keychain/util.go @@ -0,0 +1,31 @@ +package keychain + +import ( + "crypto/rand" + "encoding/base32" + "strings" +) + +var randRead = rand.Read + +// RandomID returns random ID (base32) string with prefix, using 256 bits as +// recommended by tptacek: https://gist.github.com/tqbf/be58d2d39690c3b366ad +func RandomID(prefix string) (string, error) { + buf, err := RandBytes(32) + if err != nil { + return "", err + } + str := base32.StdEncoding.EncodeToString(buf) + str = strings.ReplaceAll(str, "=", "") + str = prefix + str + return str, nil +} + +// RandBytes returns random bytes of length +func RandBytes(length int) ([]byte, error) { + buf := make([]byte, length) + if _, err := randRead(buf); err != nil { + return nil, err + } + return buf, nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 1606e1c0..b636863c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,6 +1,9 @@ # github.com/danieljoos/wincred v1.2.0 ## explicit; go 1.18 github.com/danieljoos/wincred +# github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 +## explicit; go 1.19 +github.com/keybase/go-keychain # golang.org/x/sys v0.8.0 ## explicit; go 1.17 golang.org/x/sys/internal/unsafeheader From 4e7f40f515aee2a349fa9bf102664f2ca13a6eee Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Sun, 28 May 2023 00:23:39 +0200 Subject: [PATCH 2/6] osxkeychain: TestOSXKeychainHelperRetrieveAliases print err Signed-off-by: CrazyMax --- osxkeychain/osxkeychain_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osxkeychain/osxkeychain_test.go b/osxkeychain/osxkeychain_test.go index 447c1452..5dcf64b0 100644 --- a/osxkeychain/osxkeychain_test.go +++ b/osxkeychain/osxkeychain_test.go @@ -107,7 +107,7 @@ func TestOSXKeychainHelperRetrieveAliases(t *testing.T) { t.Fatalf("Error: failed to store secret for URL %q: %s", tc.storeURL, err) } if _, _, err := helper.Get(tc.readURL); err != nil { - t.Errorf("Error: failed to read secret for URL %q using %q", tc.storeURL, tc.readURL) + t.Errorf("Error: failed to read secret for URL %q using %q: %s", tc.storeURL, tc.readURL, err) } if err := helper.Delete(tc.storeURL); err != nil { t.Error(err) From e61a226170b201cdf23049c131acb2c181416323 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Sun, 28 May 2023 16:59:59 +0200 Subject: [PATCH 3/6] ci: add macOS-12 to test matrix Signed-off-by: CrazyMax --- .github/workflows/build.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 57683336..afb0d820 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -46,6 +46,7 @@ jobs: os: - ubuntu-22.04 - ubuntu-20.04 + - macOS-12 - macOS-11 - windows-2022 steps: From 3011b3faedae965c28076a916e3e9a2a1a4276d5 Mon Sep 17 00:00:00 2001 From: CrazyMax Date: Mon, 29 May 2023 01:34:21 +0200 Subject: [PATCH 4/6] secretservice: switch to github.com/keybase/go-keychain Signed-off-by: CrazyMax --- go.mod | 7 +- go.sum | 6 + secretservice/secretservice.c | 161 --- secretservice/secretservice.go | 229 ++-- secretservice/secretservice.h | 13 - secretservice/secretservice_test.go | 29 +- .../github.com/keybase/dbus/CONTRIBUTING.md | 50 + vendor/github.com/keybase/dbus/LICENSE | 25 + vendor/github.com/keybase/dbus/MAINTAINERS | 3 + vendor/github.com/keybase/dbus/README.md | 47 + vendor/github.com/keybase/dbus/auth.go | 257 +++++ .../github.com/keybase/dbus/auth_anonymous.go | 16 + .../github.com/keybase/dbus/auth_external.go | 26 + vendor/github.com/keybase/dbus/auth_sha1.go | 102 ++ vendor/github.com/keybase/dbus/call.go | 66 ++ vendor/github.com/keybase/dbus/conn.go | 987 ++++++++++++++++++ vendor/github.com/keybase/dbus/conn_darwin.go | 36 + vendor/github.com/keybase/dbus/conn_other.go | 90 ++ vendor/github.com/keybase/dbus/conn_unix.go | 18 + .../github.com/keybase/dbus/conn_windows.go | 13 + vendor/github.com/keybase/dbus/dbus.go | 427 ++++++++ vendor/github.com/keybase/dbus/decoder.go | 292 ++++++ .../keybase/dbus/default_handler.go | 338 ++++++ vendor/github.com/keybase/dbus/doc.go | 71 ++ vendor/github.com/keybase/dbus/encoder.go | 235 +++++ vendor/github.com/keybase/dbus/escape.go | 84 ++ vendor/github.com/keybase/dbus/export.go | 463 ++++++++ vendor/github.com/keybase/dbus/homedir.go | 25 + vendor/github.com/keybase/dbus/match.go | 89 ++ vendor/github.com/keybase/dbus/message.go | 394 +++++++ vendor/github.com/keybase/dbus/object.go | 174 +++ vendor/github.com/keybase/dbus/sequence.go | 24 + .../keybase/dbus/sequential_handler.go | 125 +++ .../keybase/dbus/server_interfaces.go | 107 ++ vendor/github.com/keybase/dbus/sig.go | 296 ++++++ .../keybase/dbus/transport_darwin.go | 6 + .../keybase/dbus/transport_generic.go | 52 + .../keybase/dbus/transport_nonce_tcp.go | 40 + .../github.com/keybase/dbus/transport_tcp.go | 41 + .../github.com/keybase/dbus/transport_unix.go | 220 ++++ .../dbus/transport_unixcred_dragonfly.go | 95 ++ .../dbus/transport_unixcred_freebsd.go | 92 ++ .../keybase/dbus/transport_unixcred_linux.go | 25 + .../keybase/dbus/transport_unixcred_netbsd.go | 14 + .../dbus/transport_unixcred_openbsd.go | 14 + .../github.com/keybase/dbus/transport_zos.go | 6 + vendor/github.com/keybase/dbus/variant.go | 150 +++ .../github.com/keybase/dbus/variant_lexer.go | 284 +++++ .../github.com/keybase/dbus/variant_parser.go | 815 +++++++++++++++ .../dh_ietf1024_sha256_aes128_cbc_pkcs7.go | 147 +++ .../secretservice/secretservice.go | 406 +++++++ vendor/github.com/pkg/errors/.gitignore | 24 + vendor/github.com/pkg/errors/.travis.yml | 10 + vendor/github.com/pkg/errors/LICENSE | 23 + vendor/github.com/pkg/errors/Makefile | 44 + vendor/github.com/pkg/errors/README.md | 59 ++ vendor/github.com/pkg/errors/appveyor.yml | 32 + vendor/github.com/pkg/errors/errors.go | 288 +++++ vendor/github.com/pkg/errors/go113.go | 38 + vendor/github.com/pkg/errors/stack.go | 177 ++++ vendor/golang.org/x/crypto/LICENSE | 27 + vendor/golang.org/x/crypto/PATENTS | 22 + vendor/golang.org/x/crypto/hkdf/hkdf.go | 93 ++ vendor/modules.txt | 10 + 64 files changed, 8318 insertions(+), 261 deletions(-) delete mode 100644 secretservice/secretservice.c delete mode 100644 secretservice/secretservice.h create mode 100644 vendor/github.com/keybase/dbus/CONTRIBUTING.md create mode 100644 vendor/github.com/keybase/dbus/LICENSE create mode 100644 vendor/github.com/keybase/dbus/MAINTAINERS create mode 100644 vendor/github.com/keybase/dbus/README.md create mode 100644 vendor/github.com/keybase/dbus/auth.go create mode 100644 vendor/github.com/keybase/dbus/auth_anonymous.go create mode 100644 vendor/github.com/keybase/dbus/auth_external.go create mode 100644 vendor/github.com/keybase/dbus/auth_sha1.go create mode 100644 vendor/github.com/keybase/dbus/call.go create mode 100644 vendor/github.com/keybase/dbus/conn.go create mode 100644 vendor/github.com/keybase/dbus/conn_darwin.go create mode 100644 vendor/github.com/keybase/dbus/conn_other.go create mode 100644 vendor/github.com/keybase/dbus/conn_unix.go create mode 100644 vendor/github.com/keybase/dbus/conn_windows.go create mode 100644 vendor/github.com/keybase/dbus/dbus.go create mode 100644 vendor/github.com/keybase/dbus/decoder.go create mode 100644 vendor/github.com/keybase/dbus/default_handler.go create mode 100644 vendor/github.com/keybase/dbus/doc.go create mode 100644 vendor/github.com/keybase/dbus/encoder.go create mode 100644 vendor/github.com/keybase/dbus/escape.go create mode 100644 vendor/github.com/keybase/dbus/export.go create mode 100644 vendor/github.com/keybase/dbus/homedir.go create mode 100644 vendor/github.com/keybase/dbus/match.go create mode 100644 vendor/github.com/keybase/dbus/message.go create mode 100644 vendor/github.com/keybase/dbus/object.go create mode 100644 vendor/github.com/keybase/dbus/sequence.go create mode 100644 vendor/github.com/keybase/dbus/sequential_handler.go create mode 100644 vendor/github.com/keybase/dbus/server_interfaces.go create mode 100644 vendor/github.com/keybase/dbus/sig.go create mode 100644 vendor/github.com/keybase/dbus/transport_darwin.go create mode 100644 vendor/github.com/keybase/dbus/transport_generic.go create mode 100644 vendor/github.com/keybase/dbus/transport_nonce_tcp.go create mode 100644 vendor/github.com/keybase/dbus/transport_tcp.go create mode 100644 vendor/github.com/keybase/dbus/transport_unix.go create mode 100644 vendor/github.com/keybase/dbus/transport_unixcred_dragonfly.go create mode 100644 vendor/github.com/keybase/dbus/transport_unixcred_freebsd.go create mode 100644 vendor/github.com/keybase/dbus/transport_unixcred_linux.go create mode 100644 vendor/github.com/keybase/dbus/transport_unixcred_netbsd.go create mode 100644 vendor/github.com/keybase/dbus/transport_unixcred_openbsd.go create mode 100644 vendor/github.com/keybase/dbus/transport_zos.go create mode 100644 vendor/github.com/keybase/dbus/variant.go create mode 100644 vendor/github.com/keybase/dbus/variant_lexer.go create mode 100644 vendor/github.com/keybase/dbus/variant_parser.go create mode 100644 vendor/github.com/keybase/go-keychain/secretservice/dh_ietf1024_sha256_aes128_cbc_pkcs7.go create mode 100644 vendor/github.com/keybase/go-keychain/secretservice/secretservice.go create mode 100644 vendor/github.com/pkg/errors/.gitignore create mode 100644 vendor/github.com/pkg/errors/.travis.yml create mode 100644 vendor/github.com/pkg/errors/LICENSE create mode 100644 vendor/github.com/pkg/errors/Makefile create mode 100644 vendor/github.com/pkg/errors/README.md create mode 100644 vendor/github.com/pkg/errors/appveyor.yml create mode 100644 vendor/github.com/pkg/errors/errors.go create mode 100644 vendor/github.com/pkg/errors/go113.go create mode 100644 vendor/github.com/pkg/errors/stack.go create mode 100644 vendor/golang.org/x/crypto/LICENSE create mode 100644 vendor/golang.org/x/crypto/PATENTS create mode 100644 vendor/golang.org/x/crypto/hkdf/hkdf.go diff --git a/go.mod b/go.mod index e92b0f7d..865dda39 100644 --- a/go.mod +++ b/go.mod @@ -7,4 +7,9 @@ require ( github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100 ) -require golang.org/x/sys v0.8.0 // indirect +require ( + github.com/keybase/dbus v0.0.0-20220506165403-5aa21ea2c23a // indirect + github.com/pkg/errors v0.9.1 // indirect + golang.org/x/crypto v0.1.0 // indirect + golang.org/x/sys v0.8.0 // indirect +) diff --git a/go.sum b/go.sum index c9238edf..b149008a 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,17 @@ github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE= github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +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.0-20230523030712-b5615109f100 h1:rG3VnJUnAWyiv7qYmmdOdSapzz6HM+zb9/uRFr0T5EM= github.com/keybase/go-keychain v0.0.0-20230523030712-b5615109f100/go.mod h1:qDHUvIjGZJUtdPtuP4WMu5/U4aVWbFw1MhlkJqCGmCQ= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/secretservice/secretservice.c b/secretservice/secretservice.c deleted file mode 100644 index 676cddb2..00000000 --- a/secretservice/secretservice.c +++ /dev/null @@ -1,161 +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) { - GError *err = NULL; - - secret_password_store_sync (DOCKER_SCHEMA, SECRET_COLLECTION_DEFAULT, - server, 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 (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,21 +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)) - - if err := C.add(credsLabel, server, username, secret); 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(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. @@ -47,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. @@ -63,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 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 a28179db..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); -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 7609d656..74aa03ad 100644 --- a/secretservice/secretservice_test.go +++ b/secretservice/secretservice_test.go @@ -10,8 +10,6 @@ import ( ) func TestSecretServiceHelper(t *testing.T) { - t.Skip("test requires gnome-keyring but travis CI doesn't have it") - creds := &credentials.Credentials{ ServerURL: "https://foobar.docker.io:2376/v1", Username: "foobar", @@ -23,7 +21,7 @@ func TestSecretServiceHelper(t *testing.T) { // Check how many docker credentials we have when starting the test oldAuths, err := helper.List() if err != nil { - t.Fatal(err) + t.Error(err) } // If any docker credentials with the tests values we are providing, we @@ -31,7 +29,7 @@ func TestSecretServiceHelper(t *testing.T) { for k, v := range oldAuths { if strings.Compare(k, creds.ServerURL) == 0 && strings.Compare(v, creds.Username) == 0 { if err := helper.Delete(creds.ServerURL); err != nil { - t.Fatal(err) + t.Error(err) } } } @@ -39,53 +37,50 @@ func TestSecretServiceHelper(t *testing.T) { // Check again how many docker credentials we have when starting the test oldAuths, err = helper.List() if err != nil { - t.Fatal(err) + t.Error(err) } // Add new credentials if err := helper.Add(creds); err != nil { - t.Fatal(err) + t.Error(err) } // Verify that it is inside the secret service store username, secret, err := helper.Get(creds.ServerURL) if err != nil { - t.Fatal(err) + t.Error(err) } if username != "foobar" { - t.Fatalf("expected %s, got %s\n", "foobar", username) + t.Errorf("expected %s, got %s\n", "foobar", username) } if secret != "foobarbaz" { - t.Fatalf("expected %s, got %s\n", "foobarbaz", secret) + t.Errorf("expected %s, got %s\n", "foobarbaz", secret) } // We should have one more credential than before adding newAuths, err := helper.List() if err != nil || (len(newAuths)-len(oldAuths) != 1) { - t.Fatal(err) + t.Error(err) } oldAuths = newAuths // Deleting the credentials associated to current server url should succeed if err := helper.Delete(creds.ServerURL); err != nil { - t.Fatal(err) + t.Error(err) } // We should have one less credential than before deleting newAuths, err = helper.List() if err != nil || (len(oldAuths)-len(newAuths) != 1) { - t.Fatal(err) + t.Error(err) } } func TestMissingCredentials(t *testing.T) { - t.Skip("test requires gnome-keyring but travis CI doesn't have it") - helper := Secretservice{} - _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd") - if !credentials.IsErrCredentialsNotFound(err) { - t.Fatalf("expected ErrCredentialsNotFound, got %v", err) + if _, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd"); !credentials.IsErrCredentialsNotFound(err) { + t.Errorf("expected ErrCredentialsNotFound, got %v", err) } } 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: + +``` +: + + + +