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:
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
@@ -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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+64
-100
@@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// errCredentialsNotFound is the specific error message returned by OS X
|
// https://opensource.apple.com/source/Security/Security-55471/sec/Security/SecBase.h.auto.html
|
||||||
// when the credentials are not in the keychain.
|
const (
|
||||||
const errCredentialsNotFound = "The specified item could not be found in the keychain."
|
// errCredentialsNotFound is the specific error message returned by OS X
|
||||||
|
// when the credentials are not in the keychain.
|
||||||
// errCredentialsNotFound is the specific error message returned by OS X
|
errCredentialsNotFound = "The specified item could not be found in the keychain. (-25300)"
|
||||||
// when environment does not allow showing dialog to unlock keychain.
|
// errInteractionNotAllowed is the specific error message returned by OS X
|
||||||
const errInteractionNotAllowed = "User interaction is not allowed."
|
// 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.
|
// 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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -0,0 +1,13 @@
|
|||||||
|
linters-settings:
|
||||||
|
gocritic:
|
||||||
|
disabled-checks:
|
||||||
|
- ifElseChain
|
||||||
|
- elseif
|
||||||
|
|
||||||
|
linters:
|
||||||
|
enable:
|
||||||
|
- gofmt
|
||||||
|
- gocritic
|
||||||
|
- unconvert
|
||||||
|
- revive
|
||||||
|
- govet
|
||||||
+22
@@ -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
@@ -0,0 +1,126 @@
|
|||||||
|
# Go Keychain
|
||||||
|
|
||||||
|
[](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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
}
|
||||||
Vendored
+3
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user