mirror of
https://github.com/docker/docker-credential-helpers.git
synced 2026-06-28 07:11:36 +05:30
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7c53e02cd | |||
| 94963d0da8 | |||
| 2514d5e8b2 | |||
| 01ed4b811b | |||
| 9b6be7c243 | |||
| f853612e07 | |||
| 69c9d2eab8 | |||
| eecc09c974 | |||
| 48079a964a | |||
| cfbce1c845 | |||
| a994ca1d54 | |||
| f72c04f1d8 | |||
| c45d9e9e28 | |||
| 80833adff5 | |||
| f1498a0524 | |||
| c2abee4c0c | |||
| 9e96a4905d | |||
| cf9b6df432 | |||
| b0c64357eb | |||
| 5b764cc13a | |||
| b63a32e7a2 | |||
| 173fe2dbc2 | |||
| b3ebaa455d | |||
| 029e094488 | |||
| 8fa18eb16c | |||
| 887a66459a | |||
| d2e6ed779a | |||
| 9557dc3c3f | |||
| 59b3d54595 | |||
| 205e3b3056 | |||
| 7566a1e399 | |||
| 72661b3103 | |||
| 5a8fb214ed |
@@ -4,6 +4,14 @@ This changelog tracks the releases of docker-credential-helpers.
|
|||||||
This project includes different binaries per platform.
|
This project includes different binaries per platform.
|
||||||
The platform released is identified after the tag name.
|
The platform released is identified after the tag name.
|
||||||
|
|
||||||
|
## v0.4.0 (Go client, Mac OS X, Windows, Linux)
|
||||||
|
|
||||||
|
- Full implementation for OSX ready
|
||||||
|
- Fix some windows issues
|
||||||
|
- Implement client.List, change list API
|
||||||
|
- mac: delete credentials before adding them to avoid already exist error (fixes #37)
|
||||||
|
|
||||||
|
|
||||||
## v0.3.0 (Go client)
|
## v0.3.0 (Go client)
|
||||||
|
|
||||||
- Add Go client library to talk with the native programs.
|
- Add Go client library to talk with the native programs.
|
||||||
|
|||||||
@@ -58,11 +58,12 @@ You can see examples of each function in the [client](https://godoc.org/github.c
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are three valid values:
|
A credential helper can be any program that can read values from the standard input. We use the first argument in the command line to differentiate the kind of command to execute. There are four valid values:
|
||||||
|
|
||||||
- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Secret`.
|
- `store`: Adds credentials to the keychain. The payload in the standard input is a JSON document with `ServerURL`, `Username` and `Secret`.
|
||||||
- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
- `get`: Retrieves credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
||||||
- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
- `erase`: Removes credentials from the keychain. The payload in the standard input is the raw value for the `ServerURL`.
|
||||||
|
- `list`: Lists stored credentials. There is no standard input payload.
|
||||||
|
|
||||||
This repository also includes libraries to implement new credentials programs in Go. Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory.
|
This repository also includes libraries to implement new credentials programs in Go. Adding a new helper program is pretty easy. You can see how the OS X keychain helper works in the [osxkeychain](osxkeychain) directory.
|
||||||
|
|
||||||
|
|||||||
+19
-2
@@ -55,11 +55,10 @@ func Get(program ProgramFunc, serverURL string) (*credentials.Credentials, error
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Erase executes a program to remove the server credentails from the native store.
|
// Erase executes a program to remove the server credentials from the native store.
|
||||||
func Erase(program ProgramFunc, serverURL string) error {
|
func Erase(program ProgramFunc, serverURL string) error {
|
||||||
cmd := program("erase")
|
cmd := program("erase")
|
||||||
cmd.Input(strings.NewReader(serverURL))
|
cmd.Input(strings.NewReader(serverURL))
|
||||||
|
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t := strings.TrimSpace(string(out))
|
t := strings.TrimSpace(string(out))
|
||||||
@@ -68,3 +67,21 @@ func Erase(program ProgramFunc, serverURL string) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List executes a program to list server credentials in the native store.
|
||||||
|
func List(program ProgramFunc) (map[string]string, error) {
|
||||||
|
cmd := program("list")
|
||||||
|
cmd.Input(strings.NewReader("unused"))
|
||||||
|
out, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
t := strings.TrimSpace(string(out))
|
||||||
|
return nil, fmt.Errorf("error listing credentials - err: %v, out: `%s`", err, t)
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp map[string]string
|
||||||
|
if err = json.NewDecoder(bytes.NewReader(out)).Decode(&resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
validServerAddress = "https://index.docker.io/v1"
|
validServerAddress = "https://index.docker.io/v1"
|
||||||
|
validUsername = "linus"
|
||||||
validServerAddress2 = "https://example.com:5002"
|
validServerAddress2 = "https://example.com:5002"
|
||||||
invalidServerAddress = "https://foobar.example.com"
|
invalidServerAddress = "https://foobar.example.com"
|
||||||
missingCredsAddress = "https://missing.docker.io/v1"
|
missingCredsAddress = "https://missing.docker.io/v1"
|
||||||
@@ -70,6 +71,9 @@ func (m *mockProgram) Output() ([]byte, error) {
|
|||||||
default:
|
default:
|
||||||
return []byte("error storing credentials"), errProgramExited
|
return []byte("error storing credentials"), errProgramExited
|
||||||
}
|
}
|
||||||
|
case "list":
|
||||||
|
return []byte(fmt.Sprintf(`{"%s": "%s"}`, validServerAddress, validUsername)), nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
|
return []byte(fmt.Sprintf("unknown argument %q with %q", m.arg, inS)), errProgramExited
|
||||||
@@ -190,3 +194,14 @@ func TestErase(t *testing.T) {
|
|||||||
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
|
t.Fatalf("Expected error for server %s, got nil", invalidServerAddress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
auths, err := List(mockProgramFn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if username, exists := auths[validServerAddress]; !exists || username != validUsername {
|
||||||
|
t.Fatalf("auths[%s] returned %s, %t; expected %s, %t", validServerAddress, username, exists, validUsername, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type Credentials struct {
|
|||||||
func Serve(helper Helper) {
|
func Serve(helper Helper) {
|
||||||
var err error
|
var err error
|
||||||
if len(os.Args) != 2 {
|
if len(os.Args) != 2 {
|
||||||
err = fmt.Errorf("Usage: %s <store|get|erase>", os.Args[0])
|
err = fmt.Errorf("Usage: %s <store|get|erase|list>", os.Args[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@@ -47,6 +47,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error
|
|||||||
return Get(helper, in, out)
|
return Get(helper, in, out)
|
||||||
case "erase":
|
case "erase":
|
||||||
return Erase(helper, in)
|
return Erase(helper, in)
|
||||||
|
case "list":
|
||||||
|
return List(helper, out)
|
||||||
}
|
}
|
||||||
return fmt.Errorf("Unknown credential action `%s`", key)
|
return fmt.Errorf("Unknown credential action `%s`", key)
|
||||||
}
|
}
|
||||||
@@ -127,3 +129,13 @@ func Erase(helper Helper, reader io.Reader) error {
|
|||||||
|
|
||||||
return helper.Delete(serverURL)
|
return helper.Delete(serverURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//List returns all the serverURLs of keys in
|
||||||
|
//the OS store as a list of strings
|
||||||
|
func List(helper Helper, writer io.Writer) error {
|
||||||
|
accts, err := helper.List()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return json.NewEncoder(writer).Encode(accts)
|
||||||
|
}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ func (m *memoryStore) Get(serverURL string) (string, string, error) {
|
|||||||
return c.Username, c.Secret, nil
|
return c.Username, c.Secret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *memoryStore) List() (map[string]string, error) {
|
||||||
|
//Simply a placeholder to let memoryStore be a valid implementation of Helper interface
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
func TestStore(t *testing.T) {
|
func TestStore(t *testing.T) {
|
||||||
serverURL := "https://index.docker.io/v1/"
|
serverURL := "https://index.docker.io/v1/"
|
||||||
creds := &Credentials{
|
creds := &Credentials{
|
||||||
@@ -138,3 +143,17 @@ func TestErase(t *testing.T) {
|
|||||||
t.Fatal("expected error getting missing creds, got empty")
|
t.Fatal("expected error getting missing creds, got empty")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestList(t *testing.T) {
|
||||||
|
//This tests that there is proper input an output into the byte stream
|
||||||
|
//Individual stores are very OS specific and have been tested in osxkeychain and secretservice respectively
|
||||||
|
out := new(bytes.Buffer)
|
||||||
|
h := newMemoryStore()
|
||||||
|
if err := List(h, out); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
//testing that there is an output
|
||||||
|
if out.Len() == 0 {
|
||||||
|
t.Fatalf("expected output in the writer, got %d", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,4 +9,6 @@ type Helper interface {
|
|||||||
// Get retrieves credentials from the store.
|
// Get retrieves credentials from the store.
|
||||||
// It returns username and secret as strings.
|
// It returns username and secret as strings.
|
||||||
Get(serverURL string) (string, string, error)
|
Get(serverURL string) (string, string, error)
|
||||||
|
// List returns the stored serverURLs and their associated usernames.
|
||||||
|
List() (map[string]string, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
#include "osxkeychain_darwin.h"
|
#include "osxkeychain_darwin.h"
|
||||||
|
#include <CoreFoundation/CoreFoundation.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
char *get_error(OSStatus status) {
|
char *get_error(OSStatus status) {
|
||||||
char *buf = malloc(128);
|
char *buf = malloc(128);
|
||||||
@@ -96,3 +99,77 @@ char *keychain_delete(struct Server *server) {
|
|||||||
}
|
}
|
||||||
return NULL;
|
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 *** paths, char *** accts, unsigned int *list_l) {
|
||||||
|
CFMutableDictionaryRef query = CFDictionaryCreateMutable (NULL, 1, NULL, NULL);
|
||||||
|
CFDictionaryAddValue(query, kSecClass, kSecClassInternetPassword);
|
||||||
|
CFDictionaryAddValue(query, kSecReturnAttributes, kCFBooleanTrue);
|
||||||
|
CFDictionaryAddValue(query, kSecMatchLimit, kSecMatchLimitAll);
|
||||||
|
//Use this query dictionary
|
||||||
|
CFTypeRef result= NULL;
|
||||||
|
OSStatus status = SecItemCopyMatching(
|
||||||
|
query,
|
||||||
|
&result);
|
||||||
|
//Ran a search and store the results in result
|
||||||
|
if (status) {
|
||||||
|
return get_error(status);
|
||||||
|
}
|
||||||
|
int numKeys = CFArrayGetCount(result);
|
||||||
|
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
//result is of type CFArray
|
||||||
|
for(int i=0; i<numKeys; i++) {
|
||||||
|
CFDictionaryRef currKey = CFArrayGetValueAtIndex(result,i);
|
||||||
|
if (CFDictionaryContainsKey(currKey, CFSTR("path"))) {
|
||||||
|
//Even if a key is stored without an account, Apple defaults it to null so these arrays will be of the same length
|
||||||
|
CFStringRef pathTmp = CFDictionaryGetValue(currKey, CFSTR("path"));
|
||||||
|
CFStringRef acctTmp = CFDictionaryGetValue(currKey, CFSTR("acct"));
|
||||||
|
if (acctTmp == NULL) {
|
||||||
|
acctTmp = CFSTR("account not defined");
|
||||||
|
}
|
||||||
|
char * path = (char *) malloc(CFStringGetLength(pathTmp)+1);
|
||||||
|
path = CFStringToCharArr(pathTmp);
|
||||||
|
path[strlen(path)] = '\0';
|
||||||
|
char * acct = (char *) malloc(CFStringGetLength(acctTmp)+1);
|
||||||
|
acct = CFStringToCharArr(acctTmp);
|
||||||
|
acct[strlen(acct)] = '\0';
|
||||||
|
//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));
|
||||||
|
}
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*list_l = numKeys;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeListData(char *** data, unsigned int length) {
|
||||||
|
for(int i=0; i<length; i++) {
|
||||||
|
free((*data)[i]);
|
||||||
|
}
|
||||||
|
free(*data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package osxkeychain
|
package osxkeychain
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#cgo CFLAGS: -x objective-c
|
#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.10
|
||||||
#cgo LDFLAGS: -framework Security -framework Foundation
|
#cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.10
|
||||||
|
|
||||||
#include "osxkeychain_darwin.h"
|
#include "osxkeychain_darwin.h"
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -27,6 +27,8 @@ type Osxkeychain struct{}
|
|||||||
|
|
||||||
// Add adds new credentials to the keychain.
|
// Add adds new credentials to the keychain.
|
||||||
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
|
func (h Osxkeychain) Add(creds *credentials.Credentials) error {
|
||||||
|
h.Delete(creds.ServerURL)
|
||||||
|
|
||||||
s, err := splitServer(creds.ServerURL)
|
s, err := splitServer(creds.ServerURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -83,7 +85,6 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
|
|||||||
if errMsg != nil {
|
if errMsg != nil {
|
||||||
defer C.free(unsafe.Pointer(errMsg))
|
defer C.free(unsafe.Pointer(errMsg))
|
||||||
goMsg := C.GoString(errMsg)
|
goMsg := C.GoString(errMsg)
|
||||||
|
|
||||||
if goMsg == errCredentialsNotFound {
|
if goMsg == errCredentialsNotFound {
|
||||||
return "", "", credentials.NewErrCredentialsNotFound()
|
return "", "", credentials.NewErrCredentialsNotFound()
|
||||||
}
|
}
|
||||||
@@ -96,6 +97,38 @@ func (h Osxkeychain) Get(serverURL string) (string, string, error) {
|
|||||||
return user, pass, nil
|
return user, pass, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames.
|
||||||
|
func (h Osxkeychain) List() (map[string]string, error) {
|
||||||
|
var pathsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(pathsC))
|
||||||
|
var acctsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(acctsC))
|
||||||
|
var listLenC C.uint
|
||||||
|
errMsg := C.keychain_list(&pathsC, &acctsC, &listLenC)
|
||||||
|
if errMsg != nil {
|
||||||
|
defer C.free(unsafe.Pointer(errMsg))
|
||||||
|
goMsg := C.GoString(errMsg)
|
||||||
|
return nil, errors.New(goMsg)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer C.freeListData(&pathsC, listLenC)
|
||||||
|
defer C.freeListData(&acctsC, listLenC)
|
||||||
|
|
||||||
|
var listLen int
|
||||||
|
listLen = int(listLenC)
|
||||||
|
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||||
|
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||||
|
//taking the array of c strings into go while ignoring all the stuff irrelevant to credentials-helper
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := 0; i < listLen; i++ {
|
||||||
|
if C.GoString(pathTmp[i]) == "0" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func splitServer(serverURL string) (*C.struct_Server, error) {
|
func splitServer(serverURL string) (*C.struct_Server, error) {
|
||||||
u, err := url.Parse(serverURL)
|
u, err := url.Parse(serverURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -10,3 +10,5 @@ struct Server {
|
|||||||
char *keychain_add(struct Server *server, char *username, char *secret);
|
char *keychain_add(struct Server *server, char *username, char *secret);
|
||||||
char *keychain_get(struct Server *server, unsigned int *username_l, char **username, unsigned int *secret_l, 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_delete(struct Server *server);
|
||||||
|
char *keychain_list(char *** data, char *** accts, unsigned int *list_l);
|
||||||
|
void freeListData(char *** data, unsigned int length);
|
||||||
@@ -1,9 +1,8 @@
|
|||||||
package osxkeychain
|
package osxkeychain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/docker/docker-credential-helpers/credentials"
|
"github.com/docker/docker-credential-helpers/credentials"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestOSXKeychainHelper(t *testing.T) {
|
func TestOSXKeychainHelper(t *testing.T) {
|
||||||
@@ -12,7 +11,11 @@ func TestOSXKeychainHelper(t *testing.T) {
|
|||||||
Username: "foobar",
|
Username: "foobar",
|
||||||
Secret: "foobarbaz",
|
Secret: "foobarbaz",
|
||||||
}
|
}
|
||||||
|
creds1 := &credentials.Credentials{
|
||||||
|
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||||
|
Username: "foobarbaz",
|
||||||
|
Secret: "foobar",
|
||||||
|
}
|
||||||
helper := Osxkeychain{}
|
helper := Osxkeychain{}
|
||||||
if err := helper.Add(creds); err != nil {
|
if err := helper.Add(creds); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -31,6 +34,21 @@ func TestOSXKeychainHelper(t *testing.T) {
|
|||||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.Add(creds1)
|
||||||
|
defer helper.Delete(creds1.ServerURL)
|
||||||
|
newauths, err := helper.List()
|
||||||
|
if len(newauths)-len(auths) != 1 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||||
|
}
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
#include "secretservice_linux.h"
|
#include "secretservice_linux.h"
|
||||||
|
|
||||||
const SecretSchema *docker_get_schema(void)
|
const SecretSchema *docker_get_schema(void)
|
||||||
@@ -96,3 +97,52 @@ GError *get(char *server, char **username, char **secret) {
|
|||||||
}
|
}
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GError *list(char *** paths, char *** accts, unsigned int *list_l) {
|
||||||
|
GList *items;
|
||||||
|
GError *err = NULL;
|
||||||
|
SecretService *service;
|
||||||
|
SecretSearchFlags flags = SECRET_SEARCH_LOAD_SECRETS | SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK;
|
||||||
|
GHashTable *attributes;
|
||||||
|
g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||||
|
attributes = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
|
||||||
|
service = secret_service_get_sync(SECRET_SERVICE_NONE, NULL, &err);
|
||||||
|
items = secret_service_search_sync(service, NULL, attributes, flags, NULL, &err);
|
||||||
|
int numKeys = g_list_length(items);
|
||||||
|
if (err != NULL) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
*paths = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
*accts = (char **) malloc((int)sizeof(char *)*numKeys);
|
||||||
|
// items now contains our keys from the gnome keyring
|
||||||
|
// we will now put it in our two lists to return it to go
|
||||||
|
GList *current;
|
||||||
|
int listNumber = 0;
|
||||||
|
for(current = items; current!=NULL; current = current->next) {
|
||||||
|
char *pathTmp = secret_item_get_label(current->data);
|
||||||
|
// you cannot have a key without a label in the gnome keyring
|
||||||
|
char *acctTmp = get_username(current->data);
|
||||||
|
if (acctTmp==NULL) {
|
||||||
|
acctTmp = "account not defined";
|
||||||
|
}
|
||||||
|
char *path = (char *) malloc(strlen(pathTmp));
|
||||||
|
char *acct = (char *) malloc(strlen(acctTmp));
|
||||||
|
path = pathTmp;
|
||||||
|
acct = acctTmp;
|
||||||
|
(*paths)[listNumber] = (char *) malloc(sizeof(char)*(strlen(path)));
|
||||||
|
memcpy((*paths)[listNumber], path, sizeof(char)*(strlen(path)));
|
||||||
|
(*accts)[listNumber] = (char *) malloc(sizeof(char)*(strlen(acct)));
|
||||||
|
memcpy((*accts)[listNumber], acct, sizeof(char)*(strlen(acct)));
|
||||||
|
listNumber = listNumber + 1;
|
||||||
|
}
|
||||||
|
*list_l = numKeys;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void freeListData(char *** data, unsigned int length) {
|
||||||
|
int i;
|
||||||
|
for(i=0; i<length; i++) {
|
||||||
|
free((*data)[i]);
|
||||||
|
}
|
||||||
|
free(*data);
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,3 +78,29 @@ func (h Secretservice) Get(serverURL string) (string, string, error) {
|
|||||||
}
|
}
|
||||||
return user, pass, nil
|
return user, pass, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames.
|
||||||
|
func (h Secretservice) List() (map[string]string, error) {
|
||||||
|
var pathsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(pathsC))
|
||||||
|
var acctsC **C.char
|
||||||
|
defer C.free(unsafe.Pointer(acctsC))
|
||||||
|
var listLenC C.uint
|
||||||
|
err := C.list(&pathsC, &acctsC, &listLenC)
|
||||||
|
if err != nil {
|
||||||
|
defer C.free(unsafe.Pointer(err))
|
||||||
|
return nil, errors.New("Error from list function in secretservice_linux.c likely due to error in secretservice library")
|
||||||
|
}
|
||||||
|
defer C.freeListData(&pathsC, listLenC)
|
||||||
|
defer C.freeListData(&acctsC, listLenC)
|
||||||
|
|
||||||
|
listLen := int(listLenC)
|
||||||
|
pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen]
|
||||||
|
acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen]
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := 0; i < listLen; i++ {
|
||||||
|
resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,3 +9,5 @@ const SecretSchema *docker_get_schema(void) G_GNUC_CONST;
|
|||||||
GError *add(char *server, char *username, char *secret);
|
GError *add(char *server, char *username, char *secret);
|
||||||
GError *delete(char *server);
|
GError *delete(char *server);
|
||||||
GError *get(char *server, char **username, char **secret);
|
GError *get(char *server, char **username, char **secret);
|
||||||
|
GError *list(char *** paths, char *** accts, unsigned int *list_l);
|
||||||
|
void freeListData(char *** data, unsigned int length);
|
||||||
|
|||||||
@@ -36,6 +36,14 @@ func TestSecretServiceHelper(t *testing.T) {
|
|||||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
helper.Add(creds)
|
||||||
|
if newauths, err := helper.List(); (len(newauths) - len(auths)) != 1 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMissingCredentials(t *testing.T) {
|
func TestMissingCredentials(t *testing.T) {
|
||||||
|
|||||||
+21
-4
@@ -1,7 +1,6 @@
|
|||||||
package wincred
|
package wincred
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"C"
|
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"reflect"
|
"reflect"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -10,6 +9,8 @@ import (
|
|||||||
"unsafe"
|
"unsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var nullPointer = unsafe.Pointer(uintptr(0))
|
||||||
|
|
||||||
// Create a Go string using a pointer to a zero-terminated UTF 16 encoded string.
|
// Create a Go string using a pointer to a zero-terminated UTF 16 encoded string.
|
||||||
// See github.com/AllenDang/w32
|
// See github.com/AllenDang/w32
|
||||||
func utf16PtrToString(wstr *uint16) string {
|
func utf16PtrToString(wstr *uint16) string {
|
||||||
@@ -36,8 +37,22 @@ func utf16ToByte(wstr []uint16) (result []byte) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copies the given C byte array to a Go byte array (see `C.GoBytes`)
|
||||||
|
func goBytes(src unsafe.Pointer, len uint32) []byte {
|
||||||
|
if src == nullPointer {
|
||||||
|
return []byte{}
|
||||||
|
}
|
||||||
|
slice := (*[1 << 30]byte)(src)[0:len]
|
||||||
|
rv := make([]byte, len)
|
||||||
|
copy(rv, slice)
|
||||||
|
return rv[:]
|
||||||
|
}
|
||||||
|
|
||||||
// Convert the given CREDENTIAL struct to a more usable structure
|
// Convert the given CREDENTIAL struct to a more usable structure
|
||||||
func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
||||||
|
if unsafe.Pointer(cred) == nullPointer {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result = new(Credential)
|
result = new(Credential)
|
||||||
result.Comment = utf16PtrToString(cred.Comment)
|
result.Comment = utf16PtrToString(cred.Comment)
|
||||||
result.TargetName = utf16PtrToString(cred.TargetName)
|
result.TargetName = utf16PtrToString(cred.TargetName)
|
||||||
@@ -45,7 +60,7 @@ func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
|||||||
result.UserName = utf16PtrToString(cred.UserName)
|
result.UserName = utf16PtrToString(cred.UserName)
|
||||||
result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds())
|
result.LastWritten = time.Unix(0, cred.LastWritten.Nanoseconds())
|
||||||
result.Persist = CredentialPersistence(cred.Persist)
|
result.Persist = CredentialPersistence(cred.Persist)
|
||||||
result.CredentialBlob = C.GoBytes(unsafe.Pointer(cred.CredentialBlob), C.int(cred.CredentialBlobSize))
|
result.CredentialBlob = goBytes(unsafe.Pointer(cred.CredentialBlob), cred.CredentialBlobSize)
|
||||||
result.Attributes = make([]CredentialAttribute, cred.AttributeCount)
|
result.Attributes = make([]CredentialAttribute, cred.AttributeCount)
|
||||||
attrSliceHeader := reflect.SliceHeader{
|
attrSliceHeader := reflect.SliceHeader{
|
||||||
Data: cred.Attributes,
|
Data: cred.Attributes,
|
||||||
@@ -56,15 +71,17 @@ func nativeToCredential(cred *nativeCREDENTIAL) (result *Credential) {
|
|||||||
for i, attr := range attrSlice {
|
for i, attr := range attrSlice {
|
||||||
resultAttr := &result.Attributes[i]
|
resultAttr := &result.Attributes[i]
|
||||||
resultAttr.Keyword = utf16PtrToString(attr.Keyword)
|
resultAttr.Keyword = utf16PtrToString(attr.Keyword)
|
||||||
resultAttr.Value = C.GoBytes(unsafe.Pointer(attr.Value), C.int(attr.ValueSize))
|
resultAttr.Value = goBytes(unsafe.Pointer(attr.Value), attr.ValueSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the
|
// Convert the given Credential object back to a CREDENTIAL struct, which can be used for calling the
|
||||||
// Windows APIs
|
// Windows APIs
|
||||||
func nativeFromCredential(cred *Credential) (result *nativeCREDENTIAL) {
|
func nativeFromCredential(cred *Credential) (result *nativeCREDENTIAL) {
|
||||||
|
if cred == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
result = new(nativeCREDENTIAL)
|
result = new(nativeCREDENTIAL)
|
||||||
result.Flags = 0
|
result.Flags = 0
|
||||||
result.Type = 0
|
result.Type = 0
|
||||||
|
|||||||
+42
-4
@@ -8,12 +8,18 @@ import (
|
|||||||
var (
|
var (
|
||||||
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
|
modadvapi32 = syscall.NewLazyDLL("advapi32.dll")
|
||||||
|
|
||||||
procCredRead = modadvapi32.NewProc("CredReadW")
|
procCredRead proc = modadvapi32.NewProc("CredReadW")
|
||||||
procCredWrite = modadvapi32.NewProc("CredWriteW")
|
procCredWrite proc = modadvapi32.NewProc("CredWriteW")
|
||||||
procCredDelete = modadvapi32.NewProc("CredDeleteW")
|
procCredDelete proc = modadvapi32.NewProc("CredDeleteW")
|
||||||
procCredFree = modadvapi32.NewProc("CredFree")
|
procCredFree proc = modadvapi32.NewProc("CredFree")
|
||||||
|
procCredEnumerate proc = modadvapi32.NewProc("CredEnumerateW")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Interface for syscall.Proc: helps testing
|
||||||
|
type proc interface {
|
||||||
|
Call(a ...uintptr) (r1, r2 uintptr, lastErr error)
|
||||||
|
}
|
||||||
|
|
||||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
|
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374788(v=vs.85).aspx
|
||||||
type nativeCREDENTIAL struct {
|
type nativeCREDENTIAL struct {
|
||||||
Flags uint32
|
Flags uint32
|
||||||
@@ -48,6 +54,8 @@ const (
|
|||||||
naCRED_TYPE_DOMAIN_VISIBLE_PASSWORD nativeCRED_TYPE = 0x4
|
naCRED_TYPE_DOMAIN_VISIBLE_PASSWORD nativeCRED_TYPE = 0x4
|
||||||
naCRED_TYPE_GENERIC_CERTIFICATE nativeCRED_TYPE = 0x5
|
naCRED_TYPE_GENERIC_CERTIFICATE nativeCRED_TYPE = 0x5
|
||||||
naCRED_TYPE_DOMAIN_EXTENDED nativeCRED_TYPE = 0x6
|
naCRED_TYPE_DOMAIN_EXTENDED nativeCRED_TYPE = 0x6
|
||||||
|
|
||||||
|
naERROR_NOT_FOUND = "Element not found."
|
||||||
)
|
)
|
||||||
|
|
||||||
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374804(v=vs.85).aspx
|
// http://msdn.microsoft.com/en-us/library/windows/desktop/aa374804(v=vs.85).aspx
|
||||||
@@ -97,3 +105,33 @@ func nativeCredDelete(cred *Credential, typ nativeCRED_TYPE) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa374794(v=vs.85).aspx
|
||||||
|
func nativeCredEnumerate(filter string, all bool) ([]*Credential, error) {
|
||||||
|
var count int
|
||||||
|
var pcreds uintptr
|
||||||
|
var filterPtr uintptr
|
||||||
|
if !all {
|
||||||
|
filterUtf16Ptr, _ := syscall.UTF16PtrFromString(filter)
|
||||||
|
filterPtr = uintptr(unsafe.Pointer(filterUtf16Ptr))
|
||||||
|
} else {
|
||||||
|
filterPtr = 0
|
||||||
|
}
|
||||||
|
ret, _, err := procCredEnumerate.Call(
|
||||||
|
filterPtr,
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(&count)),
|
||||||
|
uintptr(unsafe.Pointer(&pcreds)),
|
||||||
|
)
|
||||||
|
if ret == 0 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer procCredFree.Call(pcreds)
|
||||||
|
pcredsSlice := (*[1 << 30]uintptr)(unsafe.Pointer(pcreds))[:count:count]
|
||||||
|
creds := make([]*Credential, count)
|
||||||
|
for i := range creds {
|
||||||
|
creds[i] = nativeToCredential((*nativeCREDENTIAL)(unsafe.Pointer(pcredsSlice[i])))
|
||||||
|
}
|
||||||
|
|
||||||
|
return creds, nil
|
||||||
|
}
|
||||||
|
|||||||
+11
@@ -67,3 +67,14 @@ func (t *DomainPassword) Delete() (err error) {
|
|||||||
func (t *DomainPassword) SetPassword(pw string) {
|
func (t *DomainPassword) SetPassword(pw string) {
|
||||||
t.CredentialBlob = utf16ToByte(syscall.StringToUTF16(pw))
|
t.CredentialBlob = utf16ToByte(syscall.StringToUTF16(pw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List the contents of the Credentials store
|
||||||
|
func List() ([]*Credential, error) {
|
||||||
|
creds, err := nativeCredEnumerate("", true)
|
||||||
|
if err != nil && err.Error() == naERROR_NOT_FOUND {
|
||||||
|
// Ignore ERROR_NOT_FOUND and return an empty list instead
|
||||||
|
creds = []*Credential{}
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return creds, err
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,3 +37,17 @@ func (h Wincred) Get(serverURL string) (string, string, error) {
|
|||||||
}
|
}
|
||||||
return g.UserName, string(g.CredentialBlob), nil
|
return g.UserName, string(g.CredentialBlob), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List returns the stored URLs and corresponding usernames.
|
||||||
|
func (h Wincred) List() (map[string]string, error) {
|
||||||
|
creds, err := winc.List()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := make(map[string]string)
|
||||||
|
for i := range creds {
|
||||||
|
resp[creds[i].TargetName] = creds[i].UserName
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,11 @@ func TestWinCredHelper(t *testing.T) {
|
|||||||
Username: "foobar",
|
Username: "foobar",
|
||||||
Secret: "foobarbaz",
|
Secret: "foobarbaz",
|
||||||
}
|
}
|
||||||
|
creds1 := &credentials.Credentials{
|
||||||
|
ServerURL: "https://foobar.docker.io:2376/v2",
|
||||||
|
Username: "foobarbaz",
|
||||||
|
Secret: "foobar",
|
||||||
|
}
|
||||||
|
|
||||||
helper := Wincred{}
|
helper := Wincred{}
|
||||||
if err := helper.Add(creds); err != nil {
|
if err := helper.Add(creds); err != nil {
|
||||||
@@ -31,6 +36,25 @@ func TestWinCredHelper(t *testing.T) {
|
|||||||
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
t.Fatalf("expected %s, got %s\n", "foobarbaz", secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auths, err := helper.List()
|
||||||
|
if err != nil || len(auths) == 0 {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.Add(creds1)
|
||||||
|
defer helper.Delete(creds1.ServerURL)
|
||||||
|
newauths, err := helper.List()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(newauths)-len(auths) != 1 {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d", len(newauths), len(auths))
|
||||||
|
}
|
||||||
|
t.Fatalf("Error: len(newauths): %d, len(auths): %d\n Error= %v", len(newauths), len(auths), err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := helper.Delete(creds.ServerURL); err != nil {
|
if err := helper.Delete(creds.ServerURL); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user