1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-13 16:01:28 +05:30

osxkeychain: switch to github.com/keybase/go-keychain

Signed-off-by: CrazyMax <crazy-max@users.noreply.github.com>
This commit is contained in:
CrazyMax
2023-05-27 23:18:54 +02:00
committed by CrazyMax
parent 8438667191
commit 4cdcdc29eb
16 changed files with 1438 additions and 344 deletions
+5 -2
View File
@@ -1,7 +1,10 @@
module github.com/docker/docker-credential-helpers module github.com/docker/docker-credential-helpers
go 1.19 go 1.21
require github.com/danieljoos/wincred v1.2.2 require (
github.com/danieljoos/wincred v1.2.2
github.com/keybase/go-keychain v0.0.0-20250124001843-7f41edfa9689
)
require golang.org/x/sys v0.20.0 // indirect require golang.org/x/sys v0.20.0 // indirect
+8 -1
View File
@@ -1,9 +1,16 @@
github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0=
github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/keybase/go-keychain v0.0.0-20250124001843-7f41edfa9689 h1:V4Li6BEtsuk3kXc4V5KWG9+xrfmqJhlTz3WFJZSurCQ=
github.com/keybase/go-keychain v0.0.0-20250124001843-7f41edfa9689/go.mod h1:5IIEcwnPTxFTdFEYdyIjJvRuZnTkf+R4nTxSXRrKDT4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 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 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-227
View File
@@ -1,227 +0,0 @@
#include "osxkeychain.h"
#include <CoreFoundation/CoreFoundation.h>
#include <Foundation/NSValue.h>
#include <stdio.h>
#include <string.h>
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<numKeys; i++) {
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
CFStringRef protocolTmp = CFDictionaryGetValue(currKey, CFSTR("ptcl"));
if (protocolTmp != NULL) {
CFStringRef protocolStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), protocolTmp);
if (CFStringCompare(protocolStr, CFSTR("htps"), 0) == kCFCompareEqualTo) {
protocolTmp = CFSTR("https://");
}
else {
protocolTmp = CFSTR("http://");
}
CFRelease(protocolStr);
}
else {
char * path = "0";
char * acct = "0";
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)));
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)));
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)));
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)));
continue;
}
CFMutableStringRef str = CFStringCreateMutableCopy(NULL, 0, protocolTmp);
CFStringRef serverTmp = CFDictionaryGetValue(currKey, CFSTR("srvr"));
if (serverTmp != NULL) {
CFStringAppend(str, serverTmp);
}
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path"));
if (pathTmp != NULL) {
CFStringAppend(str, pathTmp);
}
const NSNumber * portTmp = CFDictionaryGetValue(currKey, CFSTR("port"));
if (portTmp != NULL && portTmp.integerValue != 0) {
CFStringRef portStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("%@"), portTmp);
CFStringAppend(str, CFSTR(":"));
CFStringAppend(str, portStr);
CFRelease(portStr);
}
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct"));
if (acctTmp == NULL) {
acctTmp = CFSTR("account not defined");
}
char * path = CFStringToCharArr(str);
char * acct = CFStringToCharArr(acctTmp);
//We now have all we need, username and servername. Now export this to .go
(*paths)[i] = (char *) malloc(sizeof(char)*(strlen(path)+1));
memcpy((*paths)[i], path, sizeof(char)*(strlen(path)+1));
(*accts)[i] = (char *) malloc(sizeof(char)*(strlen(acct)+1));
memcpy((*accts)[i], acct, sizeof(char)*(strlen(acct)+1));
CFRelease(str);
}
*list_l = (int)numKeys;
return NULL;
}
void freeListData(char *** data, unsigned int length) {
for(int i=0; i<length; i++) {
free((*data)[i]);
}
}
+61 -97
View File
@@ -3,30 +3,31 @@
package osxkeychain package osxkeychain
/* /*
#cgo CFLAGS: -x objective-c #cgo LDFLAGS: -framework Security -framework CoreFoundation
#cgo LDFLAGS: -framework Security -framework Foundation
#include "osxkeychain.h" #include <CoreFoundation/CoreFoundation.h>
#include <stdlib.h> #include <Security/Security.h>
*/ */
import "C" import "C"
import ( import (
"errors" "errors"
"strconv" "strconv"
"unsafe"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
"github.com/docker/docker-credential-helpers/registryurl" "github.com/docker/docker-credential-helpers/registryurl"
"github.com/keybase/go-keychain"
) )
// 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 // errCredentialsNotFound is the specific error message returned by OS X
// when the credentials are not in the keychain. // when the credentials are not in the keychain.
const errCredentialsNotFound = "The specified item could not be found 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
// errCredentialsNotFound is the specific error message returned by OS X
// when environment does not allow showing dialog to unlock keychain. // when environment does not allow showing dialog to unlock keychain.
const errInteractionNotAllowed = "User interaction is not allowed." errInteractionNotAllowed = "User interaction is not allowed. (-25308)"
)
// ErrInteractionNotAllowed is returned if keychain password prompt can not be shown. // 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`) 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 { func (h Osxkeychain) Add(creds *credentials.Credentials) error {
_ = h.Delete(creds.ServerURL) // ignore errors as existing credential may not exist. _ = h.Delete(creds.ServerURL) // ignore errors as existing credential may not exist.
s, err := splitServer(creds.ServerURL) item := keychain.NewItem()
if err != nil { 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 return err
} }
defer freeServer(s)
label := C.CString(credentials.CredsLabel) return keychain.AddItem(item)
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
} }
// Delete removes credentials from the keychain. // Delete removes credentials from the keychain.
func (h Osxkeychain) Delete(serverURL string) error { func (h Osxkeychain) Delete(serverURL string) error {
s, err := splitServer(serverURL) item := keychain.NewItem()
if err != nil { item.SetSecClass(keychain.SecClassInternetPassword)
if err := splitServer(serverURL, item); err != nil {
return err return err
} }
defer freeServer(s) if err := keychain.DeleteItem(item); err != nil {
switch err.Error() {
if errMsg := C.keychain_delete(s); errMsg != nil {
defer C.free(unsafe.Pointer(errMsg))
switch goMsg := C.GoString(errMsg); goMsg {
case errCredentialsNotFound: case errCredentialsNotFound:
return credentials.NewErrCredentialsNotFound() return credentials.NewErrCredentialsNotFound()
case errInteractionNotAllowed: case errInteractionNotAllowed:
return ErrInteractionNotAllowed return ErrInteractionNotAllowed
default: default:
return errors.New(goMsg) return err
} }
} }
return nil return nil
} }
// Get returns the username and secret to use for a given registry server URL. // Get returns the username and secret to use for a given registry server URL.
func (h Osxkeychain) Get(serverURL string) (string, string, error) { func (h Osxkeychain) Get(serverURL string) (string, string, error) {
s, err := splitServer(serverURL) item := keychain.NewItem()
if err != nil { item.SetSecClass(keychain.SecClassInternetPassword)
item.SetMatchLimit(keychain.MatchLimitOne)
item.SetReturnAttributes(true)
item.SetReturnData(true)
if err := splitServer(serverURL, item); err != nil {
return "", "", err return "", "", err
} }
defer freeServer(s)
var usernameLen C.uint res, err := keychain.QueryItem(item)
var username *C.char if err != nil {
var secretLen C.uint switch err.Error() {
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 {
case errCredentialsNotFound: case errCredentialsNotFound:
return "", "", credentials.NewErrCredentialsNotFound() return "", "", credentials.NewErrCredentialsNotFound()
case errInteractionNotAllowed: case errInteractionNotAllowed:
return "", "", ErrInteractionNotAllowed return "", "", ErrInteractionNotAllowed
default: default:
return "", "", errors.New(goMsg) return "", "", err
} }
} else if len(res) == 0 {
return "", "", credentials.NewErrCredentialsNotFound()
} }
user := C.GoStringN(username, C.int(usernameLen)) return res[0].Account, string(res[0].Data), nil
pass := C.GoStringN(secret, C.int(secretLen))
return user, pass, nil
} }
// List returns the stored URLs and corresponding usernames. // List returns the stored URLs and corresponding usernames.
func (h Osxkeychain) List() (map[string]string, error) { func (h Osxkeychain) List() (map[string]string, error) {
credsLabelC := C.CString(credentials.CredsLabel) item := keychain.NewItem()
defer C.free(unsafe.Pointer(credsLabelC)) item.SetSecClass(keychain.SecClassInternetPassword)
item.SetMatchLimit(keychain.MatchLimitAll)
item.SetReturnAttributes(true)
item.SetLabel(credentials.CredsLabel)
var pathsC **C.char res, err := keychain.QueryItem(item)
defer C.free(unsafe.Pointer(pathsC)) if err != nil {
var acctsC **C.char switch err.Error() {
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 {
case errCredentialsNotFound: case errCredentialsNotFound:
return make(map[string]string), nil return make(map[string]string), nil
case errInteractionNotAllowed: case errInteractionNotAllowed:
return nil, ErrInteractionNotAllowed return nil, ErrInteractionNotAllowed
default: 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) resp := make(map[string]string)
for i := 0; i < listLen; i++ { for _, r := range res {
if C.GoString(pathTmp[i]) == "0" { if r.Path == "" {
continue continue
} }
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i]) resp[r.Path] = r.Account
} }
return resp, nil return resp, nil
} }
func splitServer(serverURL string) (*C.struct_Server, error) { func splitServer(serverURL string, item keychain.Item) error {
u, err := registryurl.Parse(serverURL) u, err := registryurl.Parse(serverURL)
if err != nil { if err != nil {
return nil, err return err
} }
item.SetProtocol("https")
proto := C.kSecProtocolTypeHTTPS
if u.Scheme == "http" { if u.Scheme == "http" {
proto = C.kSecProtocolTypeHTTP item.SetProtocol("http")
} }
var port int item.SetServer(u.Hostname())
p := u.Port() if p := u.Port(); p != "" {
if p != "" { port, err := strconv.Atoi(p)
port, err = strconv.Atoi(p)
if err != nil { if err != nil {
return nil, err return err
} }
item.SetPort(int32(port))
} }
item.SetPath(u.Path)
return &C.struct_Server{ return nil
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))
} }
-14
View File
@@ -1,14 +0,0 @@
#include <Security/Security.h>
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);
+26
View File
@@ -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
+13
View File
@@ -0,0 +1,13 @@
linters-settings:
gocritic:
disabled-checks:
- ifElseChain
- elseif
linters:
enable:
- gofmt
- gocritic
- unconvert
- revive
- govet
+22
View File
@@ -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.
+126
View File
@@ -0,0 +1,126 @@
# 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)
```
Post issues to: https://github.com/keybase/keybase-issues
+370
View File
@@ -0,0 +1,370 @@
//go:build darwin || ios
// +build darwin ios
package keychain
/*
#cgo LDFLAGS: -framework CoreFoundation
#include <CoreFoundation/CoreFoundation.h>
// 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")
}
+69
View File
@@ -0,0 +1,69 @@
//go:build darwin || ios
// +build darwin ios
package keychain
/*
#cgo LDFLAGS: -framework CoreFoundation
#include <CoreFoundation/CoreFoundation.h>
*/
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) {
i, frac := math.Modf(float64(abs))
return int64(i) + 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))
}
+23
View File
@@ -0,0 +1,23 @@
//go:build darwin && ios
// +build darwin,ios
package keychain
/*
#cgo LDFLAGS: -framework CoreFoundation -framework Security
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
*/
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),
}
+653
View File
@@ -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 <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
*/
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
}
+25
View File
@@ -0,0 +1,25 @@
//go:build darwin && !ios
// +build darwin,!ios
package keychain
/*
#cgo LDFLAGS: -framework CoreFoundation -framework Security
#include <CoreFoundation/CoreFoundation.h>
#include <Security/Security.h>
*/
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),
}
+31
View File
@@ -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
}
+3
View File
@@ -1,6 +1,9 @@
# github.com/danieljoos/wincred v1.2.2 # github.com/danieljoos/wincred v1.2.2
## explicit; go 1.18 ## explicit; go 1.18
github.com/danieljoos/wincred github.com/danieljoos/wincred
# github.com/keybase/go-keychain v0.0.0-20250124001843-7f41edfa9689
## explicit; go 1.21
github.com/keybase/go-keychain
# golang.org/x/sys v0.20.0 # golang.org/x/sys v0.20.0
## explicit; go 1.18 ## explicit; go 1.18
golang.org/x/sys/windows golang.org/x/sys/windows