1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-28 07:11:36 +05:30

Compare commits

...

31 Commits

Author SHA1 Message Date
Vincent Demeester 2514d5e8b2 Merge pull request #39 from docker/release-v0.4.0
Prepare v0.4.0 release
2017-02-16 15:55:09 +01:00
Vincent Demeester 01ed4b811b Merge pull request #26 from danieljoos/master
Updated wincred vendor & fixed Windows build
2017-02-16 15:52:37 +01:00
Vincent Demeester 9b6be7c243 Merge pull request #28 from KingEmet/patch-1
Update README.md
2017-02-16 11:41:56 +01:00
Vincent Demeester f853612e07 Prepare v0.4.0 release
Signed-off-by: Vincent Demeester <vincent@sbr.pm>
2017-02-16 11:19:45 +01:00
Brian Goff 69c9d2eab8 Merge pull request #38 from ebriney/mac-delete-before-adding
mac: delete credentials before adding them
2017-02-15 08:44:08 -05:00
ebriney eecc09c974 mac: delete credentials before adding them to avoid already exist error (fixes #37)
Signed-off-by: ebriney <emmanuel.briney@docker.com>
2017-02-14 22:50:44 +01:00
KingEmet 48079a964a add 'list' to API list
Signed-off-by: KingEmet <jsand@google.com>
2016-09-07 15:59:21 -07:00
Daniel Joos cfbce1c845 Removed unnecessary lines in wincred_windows.go
Those lines seem to be left from the last merge.
They cause build errors on Windows.

Signed-off-by: Daniel Joos <daniel@joosweb.de>
2016-09-07 09:59:03 +02:00
Daniel Joos a994ca1d54 Updated vendor pkg: github.com/danieljoos/wincred
This includes the following changes:
- Removed need for `C`
- Added some null checks to avoid possible panics
- `List` returns an empty list instead of an error, in case no credentials
  are installed on the system

Signed-off-by: Daniel Joos <daniel@joosweb.de>
2016-09-07 09:58:33 +02:00
Antonio Murdaca f72c04f1d8 Merge pull request #25 from KingEmet/master
Implement client.List, change list API
2016-09-06 18:43:16 +02:00
Jake Sanders c45d9e9e28 Implement client.List, change list API
[]string, []string -> map[string]string because the other APIs assume a
1:1 correspondence

Signed-off-by: Jake Sanders <jsand@google.com>
2016-09-02 15:15:12 -07:00
David Calavera 80833adff5 Merge pull request #18 from avaid96/listing
Implementing listing functionality across OSX, Linux and Windows
2016-07-20 13:55:38 -07:00
avaid96 f1498a0524 go fmt is all osx files and general files
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:19:47 -07:00
Avi Vaid c2abee4c0c go fmt in windows files
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-16 12:18:03 -07:00
avaid96 9e96a4905d fixed issue with wincred
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:09:31 -07:00
avaid96 cf9b6df432 fixed issue with wincred
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:08:55 -07:00
avaid96 b0c64357eb fixed issue with the windows test
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 12:07:50 -07:00
avaid96 5b764cc13a changes to wincred, completes list functionality in windows
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-16 11:59:43 -07:00
avaid96 b63a32e7a2 fixed another bug in the test for osx
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-15 17:46:46 -07:00
avaid96 173fe2dbc2 fixed a bug in the test for osx
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-15 17:43:52 -07:00
avaid96 b3ebaa455d trying to the OSX test to pass on Travis, it passes locally
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-15 17:32:47 -07:00
avaid96 029e094488 removed a semicolon and the placeholder for the error which we used to locate it. Seems like SecItemCopyMatching is giving an error for some reason- no error on local machine
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-13 14:36:14 -07:00
avaid96 8fa18eb16c implementation on client side as well, complete with tests
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-13 10:20:51 -07:00
avaid96 887a66459a I think OSX tests were failing because it was a list against an empty keychain, checking whether that is the case by filling it first
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-12 10:52:06 -07:00
Avi Vaid d2e6ed779a moved loop variable declaration to comply with c mode in travis
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-12 12:51:30 -04:00
avaid96 9557dc3c3f fixing a vet issue
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-12 00:04:34 -07:00
Avi Vaid 59b3d54595 added better test for list in linux
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-12 02:52:44 -04:00
avaid96 205e3b3056 cleaned up some of the osx code, added a better test for list
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-11 23:34:20 -07:00
Avi Vaid 7566a1e399 implemented freeing memory in secretservice and made minor edits to osxkeychain
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-11 19:45:37 -04:00
Avi Vaid 72661b3103 Implemented list functionality for secretservice- linux
Signed-off-by: Avi Vaid <avaid1996@gmail.com>
2016-07-11 19:33:35 -04:00
avaid96 5a8fb214ed Full implementation for OSX ready
Signed-off-by: avaid96 <avaid1996@gmail.com>
2016-07-11 10:50:44 -07:00
20 changed files with 410 additions and 16 deletions
+8
View File
@@ -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.
+2 -1
View File
@@ -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
View File
@@ -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
}
+15
View File
@@ -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)
}
}
+13 -1
View File
@@ -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)
}
+19
View File
@@ -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)
}
}
+2
View File
@@ -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)
} }
+77
View File
@@ -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);
}
+34 -1
View File
@@ -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 {
+2
View File
@@ -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);
+21 -3
View File
@@ -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)
} }
+50
View File
@@ -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);
}
+26
View File
@@ -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
}
+2
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}
+14
View File
@@ -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
}
+24
View File
@@ -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)
} }