Merge pull request #97 from s864372002/hotfix/typo

Typo fixed.
diff --git a/.travis.yml b/.travis.yml
index bf1ac60..d1703b1 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -3,20 +3,21 @@
   sudo: required
   language: go
   dist: trusty
+  osx_image: xcode10.1
   os:
     - linux
     - osx
   notifications:
     email: false
   go:
-    - 1.8
-  install: make deps
+    - 1.12.x
   addons:
     apt:
       packages:
         - libsecret-1-dev
         - pass
   before_script:
+    - make deps
     - "export DISPLAY=:99.0"
     - if [[ "$TRAVIS_OS_NAME" == "linux"  ]]; then sh ci/before_script_linux.sh; fi
     - make validate
diff --git a/MAINTAINERS b/MAINTAINERS
index 7136d1c..5867d3a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -11,18 +11,13 @@
 [Org]
 	[Org."Core maintainers"]
 		people = [
-			"aaronlehmann",
-			"calavera",
 			"coolljt0725",
 			"cpuguy83",
 			"crosbymichael",
 			"dnephin",
-			"dongluochen",
 			"duglin",
 			"estesp",
-			"icecrime",
 			"jhowardmsft",
-			"lk4d4",
 			"mavenugo",
 			"mhbauer",
 			"n4ss",
@@ -45,16 +40,6 @@
 
 	# ADD YOURSELF HERE IN ALPHABETICAL ORDER
 
-	[people.aaronlehmann]
-	Name = "Aaron Lehmann"
-	Email = "aaron.lehmann@docker.com"
-	GitHub = "aaronlehmann"
-
-	[people.calavera]
-	Name = "David Calavera"
-	Email = "david.calavera@gmail.com"
-	GitHub = "calavera"
-
 	[people.coolljt0725]
 	Name = "Lei Jitang"
 	Email = "leijitang@huawei.com"
@@ -75,11 +60,6 @@
 	Email = "dnephin@gmail.com"
 	GitHub = "dnephin"
 
-	[people.dongluochen]
-	Name = "Dongluo Chen"
-	Email = "dongluo.chen@docker.com"
-	GitHub = "dongluochen"
-
 	[people.duglin]
 	Name = "Doug Davis"
 	Email = "dug@us.ibm.com"
@@ -90,21 +70,11 @@
 	Email = "estesp@linux.vnet.ibm.com"
 	GitHub = "estesp"
 
-	[people.icecrime]
-	Name = "Arnaud Porterie"
-	Email = "arnaud@docker.com"
-	GitHub = "icecrime"
-
 	[people.jhowardmsft]
 	Name = "John Howard"
 	Email = "jhoward@microsoft.com"
 	GitHub = "jhowardmsft"
 
-	[people.lk4d4]
-	Name = "Alexander Morozov"
-	Email = "lk4d4@docker.com"
-	GitHub = "lk4d4"
-
 	[people.mavenugo]
 	Name = "Madhu Venugopal"
 	Email = "madhu@docker.com"
diff --git a/Makefile b/Makefile
index 22bd8b0..db58f8a 100644
--- a/Makefile
+++ b/Makefile
@@ -6,7 +6,7 @@
 all: test
 
 deps:
-	go get -u github.com/golang/lint/golint
+	go get -u golang.org/x/lint/golint
 
 clean:
 	rm -rf bin
diff --git a/appveyor.yml b/appveyor.yml
index 282e1d5..04eadb0 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -2,13 +2,13 @@
 environment:
       GOPATH: c:\gopath
 
+stack: go 1.8.7
+
 clone_folder: c:\gopath\src\github.com\docker\docker-credential-helpers
 clone_depth: 10
 
 before_build:
   - set PATH=%PATH%;C:\MinGW\bin;
-  - set PATH=%PATH%;C:\go18\bin;
-  - set GOROOT=C:\go18
 
 build_script:
   - mingw32-make vet_win wincred
diff --git a/credentials/version.go b/credentials/version.go
index 033a5fe..e12d2dd 100644
--- a/credentials/version.go
+++ b/credentials/version.go
@@ -1,4 +1,4 @@
 package credentials
 
 // Version holds a string describing the current version
-const Version = "0.6.0"
+const Version = "0.6.1"
diff --git a/osxkeychain/osxkeychain_darwin.go b/osxkeychain/osxkeychain_darwin.go
index 4391267..da57d00 100644
--- a/osxkeychain/osxkeychain_darwin.go
+++ b/osxkeychain/osxkeychain_darwin.go
@@ -1,8 +1,8 @@
 package osxkeychain
 
 /*
-#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.10
-#cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.10
+#cgo CFLAGS: -x objective-c -mmacosx-version-min=10.11
+#cgo LDFLAGS: -framework Security -framework Foundation -mmacosx-version-min=10.11
 
 #include "osxkeychain_darwin.h"
 #include <stdlib.h>
@@ -10,12 +10,11 @@
 import "C"
 import (
 	"errors"
-	"net/url"
 	"strconv"
-	"strings"
 	"unsafe"
 
 	"github.com/docker/docker-credential-helpers/credentials"
+	"github.com/docker/docker-credential-helpers/registryurl"
 )
 
 // errCredentialsNotFound is the specific error message returned by OS X
@@ -113,6 +112,10 @@
 	if errMsg != nil {
 		defer C.free(unsafe.Pointer(errMsg))
 		goMsg := C.GoString(errMsg)
+		if goMsg == errCredentialsNotFound {
+			return make(map[string]string), nil
+		}
+
 		return nil, errors.New(goMsg)
 	}
 
@@ -135,7 +138,7 @@
 }
 
 func splitServer(serverURL string) (*C.struct_Server, error) {
-	u, err := parseURL(serverURL)
+	u, err := registryurl.Parse(serverURL)
 	if err != nil {
 		return nil, err
 	}
@@ -145,7 +148,7 @@
 		proto = C.kSecProtocolTypeHTTP
 	}
 	var port int
-	p := getPort(u)
+	p := registryurl.GetPort(u)
 	if p != "" {
 		port, err = strconv.Atoi(p)
 		if err != nil {
@@ -155,7 +158,7 @@
 
 	return &C.struct_Server{
 		proto: C.SecProtocolType(proto),
-		host:  C.CString(getHostname(u)),
+		host:  C.CString(registryurl.GetHostname(u)),
 		port:  C.uint(port),
 		path:  C.CString(u.Path),
 	}, nil
@@ -165,32 +168,3 @@
 	C.free(unsafe.Pointer(s.host))
 	C.free(unsafe.Pointer(s.path))
 }
-
-// parseURL parses and validates a given serverURL to an url.URL, and
-// returns an error if validation failed. Querystring parameters are
-// omitted in the resulting URL, because they are not used in the helper.
-//
-// If serverURL does not have a valid scheme, `//` is used as scheme
-// before parsing. This prevents the hostname being used as path,
-// and the credentials being stored without host.
-func parseURL(serverURL string) (*url.URL, error) {
-	// Check if serverURL has a scheme, otherwise add `//` as scheme.
-	if !strings.Contains(serverURL, "://") && !strings.HasPrefix(serverURL, "//") {
-		serverURL = "//" + serverURL
-	}
-
-	u, err := url.Parse(serverURL)
-	if err != nil {
-		return nil, err
-	}
-
-	if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
-		return nil, errors.New("unsupported scheme: " + u.Scheme)
-	}
-	if getHostname(u) == "" {
-		return nil, errors.New("no hostname in URL")
-	}
-
-	u.RawQuery = ""
-	return u, nil
-}
diff --git a/osxkeychain/osxkeychain_darwin_test.go b/osxkeychain/osxkeychain_darwin_test.go
index b74927c..27518e5 100644
--- a/osxkeychain/osxkeychain_darwin_test.go
+++ b/osxkeychain/osxkeychain_darwin_test.go
@@ -1,10 +1,10 @@
 package osxkeychain
 
 import (
-	"errors"
 	"fmt"
-	"github.com/docker/docker-credential-helpers/credentials"
 	"testing"
+
+	"github.com/docker/docker-credential-helpers/credentials"
 )
 
 func TestOSXKeychainHelper(t *testing.T) {
@@ -56,46 +56,6 @@
 	}
 }
 
-// TestOSXKeychainHelperParseURL verifies that a // "scheme" is added to URLs,
-// and that invalid URLs produce an error.
-func TestOSXKeychainHelperParseURL(t *testing.T) {
-	tests := []struct {
-		url         string
-		expectedURL string
-		err         error
-	}{
-		{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
-		{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
-		{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
-		{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
-		{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
-		{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
-		{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
-		{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
-		{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
-	}
-
-	for _, te := range tests {
-		u, err := parseURL(te.url)
-
-		if te.err == nil && err != nil {
-			t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
-			continue
-		}
-		if te.err != nil && err == nil {
-			t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
-			continue
-		}
-		if te.err != nil && err.Error() != te.err.Error() {
-			t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
-			continue
-		}
-		if u != nil && u.String() != te.expectedURL {
-			t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
-		}
-	}
-}
-
 // TestOSXKeychainHelperRetrieveAliases verifies that secrets can be accessed
 // through variations on the URL
 func TestOSXKeychainHelperRetrieveAliases(t *testing.T) {
diff --git a/osxkeychain/url_go18.go b/osxkeychain/url_go18.go
deleted file mode 100644
index 0b7297d..0000000
--- a/osxkeychain/url_go18.go
+++ /dev/null
@@ -1,13 +0,0 @@
-//+build go1.8
-
-package osxkeychain
-
-import "net/url"
-
-func getHostname(u *url.URL) string {
-	return u.Hostname()
-}
-
-func getPort(u *url.URL) string {
-	return u.Port()
-}
diff --git a/pass/pass_linux.go b/pass/pass_linux.go
index bb4de07..342e538 100644
--- a/pass/pass_linux.go
+++ b/pass/pass_linux.go
@@ -5,6 +5,7 @@
 package pass
 
 import (
+	"bytes"
 	"encoding/base64"
 	"errors"
 	"fmt"
@@ -13,139 +14,99 @@
 	"os/exec"
 	"path"
 	"strings"
+	"sync"
 
 	"github.com/docker/docker-credential-helpers/credentials"
 )
 
 const PASS_FOLDER = "docker-credential-helpers"
 
-var (
-	PassInitialized bool
-)
-
-func init() {
-	// In principle, we could just run `pass init`. However, pass has a bug
-	// where if gpg fails, it doesn't always exit 1. Additionally, pass
-	// uses gpg2, but gpg is the default, which may be confusing. So let's
-	// just explictily check that pass actually can store and retreive a
-	// password.
-	password := "pass is initialized"
-	name := path.Join(PASS_FOLDER, "docker-pass-initialized-check")
-
-	_, err := runPass(password, "insert", "-f", "-m", name)
-	if err != nil {
-		return
-	}
-
-	stored, err := runPass("", "show", name)
-	PassInitialized = err == nil && stored == password
-
-	if PassInitialized {
-		runPass("", "rm", "-rf", name)
-	}
-}
-
-func runPass(stdinContent string, args ...string) (string, error) {
-	cmd := exec.Command("pass", args...)
-
-	stdin, err := cmd.StdinPipe()
-	if err != nil {
-		return "", err
-	}
-	defer stdin.Close()
-
-	stderr, err := cmd.StderrPipe()
-	if err != nil {
-		return "", err
-	}
-	defer stderr.Close()
-
-	stdout, err := cmd.StdoutPipe()
-	if err != nil {
-		return "", err
-	}
-	defer stdout.Close()
-
-	err = cmd.Start()
-	if err != nil {
-		return "", err
-	}
-
-	_, err = stdin.Write([]byte(stdinContent))
-	if err != nil {
-		return "", err
-	}
-	stdin.Close()
-
-	errContent, err := ioutil.ReadAll(stderr)
-	if err != nil {
-		return "", fmt.Errorf("error reading stderr: %s", err)
-	}
-
-	result, err := ioutil.ReadAll(stdout)
-	if err != nil {
-		return "", fmt.Errorf("Error reading stdout: %s", err)
-	}
-
-	cmdErr := cmd.Wait()
-	if cmdErr != nil {
-		return "", fmt.Errorf("%s: %s", cmdErr, errContent)
-	}
-
-	return string(result), nil
-}
-
 // Pass handles secrets using Linux secret-service as a store.
 type Pass struct{}
 
-// Add adds new credentials to the keychain.
-func (h Pass) Add(creds *credentials.Credentials) error {
-	if !PassInitialized {
-		return errors.New("pass store is uninitialized")
+// Ideally these would be stored as members of Pass, but since all of Pass's
+// methods have value receivers, not pointer receivers, and changing that is
+// backwards incompatible, we assume that all Pass instances share the same configuration
+
+// initializationMutex is held while initializing so that only one 'pass'
+// round-tripping is done to check pass is functioning.
+var initializationMutex sync.Mutex
+var passInitialized bool
+
+// CheckInitialized checks whether the password helper can be used. It
+// internally caches and so may be safely called multiple times with no impact
+// on performance, though the first call may take longer.
+func (p Pass) CheckInitialized() bool {
+	return p.checkInitialized() == nil
+}
+
+func (p Pass) checkInitialized() error {
+	initializationMutex.Lock()
+	defer initializationMutex.Unlock()
+	if passInitialized {
+		return nil
+	}
+	// We just run a `pass ls`, if it fails then pass is not initialized.
+	_, err := p.runPassHelper("", "ls")
+	if err != nil {
+		return fmt.Errorf("pass not initialized: %v", err)
+	}
+	passInitialized = true
+	return nil
+}
+
+func (p Pass) runPass(stdinContent string, args ...string) (string, error) {
+	if err := p.checkInitialized(); err != nil {
+		return "", err
+	}
+	return p.runPassHelper(stdinContent, args...)
+}
+
+func (p Pass) runPassHelper(stdinContent string, args ...string) (string, error) {
+	var stdout, stderr bytes.Buffer
+	cmd := exec.Command("pass", args...)
+	cmd.Stdin = strings.NewReader(stdinContent)
+	cmd.Stdout = &stdout
+	cmd.Stderr = &stderr
+
+	err := cmd.Run()
+	if err != nil {
+		return "", fmt.Errorf("%s: %s", err, stderr.String())
 	}
 
+	// trim newlines; pass v1.7.1+ includes a newline at the end of `show` output
+	return strings.TrimRight(stdout.String(), "\n\r"), nil
+}
+
+// Add adds new credentials to the keychain.
+func (h Pass) Add(creds *credentials.Credentials) error {
 	if creds == nil {
 		return errors.New("missing credentials")
 	}
 
 	encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
 
-	_, err := runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
+	_, err := h.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
 	return err
 }
 
 // Delete removes credentials from the store.
 func (h Pass) Delete(serverURL string) error {
-	if !PassInitialized {
-		return errors.New("pass store is uninitialized")
-	}
-
 	if serverURL == "" {
 		return errors.New("missing server url")
 	}
 
 	encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
-	_, err := runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
+	_, err := h.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
 	return err
 }
 
 func getPassDir() string {
-	passDir := os.ExpandEnv("$HOME/.password-store")
-	for _, e := range os.Environ() {
-		parts := strings.SplitN(e, "=", 2)
-		if len(parts) < 2 {
-			continue
-		}
-
-		if parts[0] != "PASSWORD_STORE_DIR" {
-			continue
-		}
-
-		passDir = parts[1]
-		break
+	passDir := "$HOME/.password-store"
+	if envDir := os.Getenv("PASSWORD_STORE_DIR"); envDir != "" {
+		passDir = envDir
 	}
-
-	return passDir
+	return os.ExpandEnv(passDir)
 }
 
 // listPassDir lists all the contents of a directory in the password store.
@@ -168,10 +129,6 @@
 
 // Get returns the username and secret to use for a given registry server URL.
 func (h Pass) Get(serverURL string) (string, string, error) {
-	if !PassInitialized {
-		return "", "", errors.New("pass store is uninitialized")
-	}
-
 	if serverURL == "" {
 		return "", "", errors.New("missing server url")
 	}
@@ -180,7 +137,7 @@
 
 	if _, err := os.Stat(path.Join(getPassDir(), PASS_FOLDER, encoded)); err != nil {
 		if os.IsNotExist(err) {
-			return "", "", nil;
+			return "", "", nil
 		}
 
 		return "", "", err
@@ -196,16 +153,12 @@
 	}
 
 	actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
-	secret, err := runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
+	secret, err := h.runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
 	return actual, secret, err
 }
 
 // List returns the stored URLs and corresponding usernames for a given credentials label
 func (h Pass) List() (map[string]string, error) {
-	if !PassInitialized {
-		return nil, errors.New("pass store is uninitialized")
-	}
-
 	servers, err := listPassDir()
 	if err != nil {
 		return nil, err
diff --git a/registryurl/parse.go b/registryurl/parse.go
new file mode 100644
index 0000000..7617132
--- /dev/null
+++ b/registryurl/parse.go
@@ -0,0 +1,37 @@
+package registryurl
+
+import (
+	"errors"
+	"net/url"
+	"strings"
+)
+
+// Parse parses and validates a given serverURL to an url.URL, and
+// returns an error if validation failed. Querystring parameters are
+// omitted in the resulting URL, because they are not used in the helper.
+//
+// If serverURL does not have a valid scheme, `//` is used as scheme
+// before parsing. This prevents the hostname being used as path,
+// and the credentials being stored without host.
+func Parse(registryURL string) (*url.URL, error) {
+	// Check if registryURL has a scheme, otherwise add `//` as scheme.
+	if !strings.Contains(registryURL, "://") && !strings.HasPrefix(registryURL, "//") {
+		registryURL = "//" + registryURL
+	}
+
+	u, err := url.Parse(registryURL)
+	if err != nil {
+		return nil, err
+	}
+
+	if u.Scheme != "" && u.Scheme != "https" && u.Scheme != "http" {
+		return nil, errors.New("unsupported scheme: " + u.Scheme)
+	}
+
+	if GetHostname(u) == "" {
+		return nil, errors.New("no hostname in URL")
+	}
+
+	u.RawQuery = ""
+	return u, nil
+}
diff --git a/registryurl/parse_test.go b/registryurl/parse_test.go
new file mode 100644
index 0000000..89feaa7
--- /dev/null
+++ b/registryurl/parse_test.go
@@ -0,0 +1,46 @@
+package registryurl
+
+import (
+	"errors"
+	"testing"
+)
+
+// TestHelperParseURL verifies that a // "scheme" is added to URLs,
+// and that invalid URLs produce an error.
+func TestHelperParseURL(t *testing.T) {
+	tests := []struct {
+		url         string
+		expectedURL string
+		err         error
+	}{
+		{url: "foobar.docker.io", expectedURL: "//foobar.docker.io"},
+		{url: "foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
+		{url: "//foobar.docker.io:2376", expectedURL: "//foobar.docker.io:2376"},
+		{url: "http://foobar.docker.io:2376", expectedURL: "http://foobar.docker.io:2376"},
+		{url: "https://foobar.docker.io:2376", expectedURL: "https://foobar.docker.io:2376"},
+		{url: "https://foobar.docker.io:2376/some/path", expectedURL: "https://foobar.docker.io:2376/some/path"},
+		{url: "https://foobar.docker.io:2376/some/other/path?foo=bar", expectedURL: "https://foobar.docker.io:2376/some/other/path"},
+		{url: "/foobar.docker.io", err: errors.New("no hostname in URL")},
+		{url: "ftp://foobar.docker.io:2376", err: errors.New("unsupported scheme: ftp")},
+	}
+
+	for _, te := range tests {
+		u, err := Parse(te.url)
+
+		if te.err == nil && err != nil {
+			t.Errorf("Error: failed to parse URL %q: %s", te.url, err)
+			continue
+		}
+		if te.err != nil && err == nil {
+			t.Errorf("Error: expected error %q, got none when parsing URL %q", te.err, te.url)
+			continue
+		}
+		if te.err != nil && err.Error() != te.err.Error() {
+			t.Errorf("Error: expected error %q, got %q when parsing URL %q", te.err, err, te.url)
+			continue
+		}
+		if u != nil && u.String() != te.expectedURL {
+			t.Errorf("Error: expected URL: %q, but got %q for URL: %q", te.expectedURL, u.String(), te.url)
+		}
+	}
+}
diff --git a/registryurl/url_go18.go b/registryurl/url_go18.go
new file mode 100644
index 0000000..dbfbfa1
--- /dev/null
+++ b/registryurl/url_go18.go
@@ -0,0 +1,15 @@
+//+build go1.8
+
+package registryurl
+
+import (
+	url "net/url"
+)
+
+func GetHostname(u *url.URL) string {
+	return u.Hostname()
+}
+
+func GetPort(u *url.URL) string {
+	return u.Port()
+}
diff --git a/osxkeychain/url_non_go18.go b/registryurl/url_non_go18.go
similarity index 85%
rename from osxkeychain/url_non_go18.go
rename to registryurl/url_non_go18.go
index bdf9b7b..7c8a88f 100644
--- a/osxkeychain/url_non_go18.go
+++ b/registryurl/url_non_go18.go
@@ -1,17 +1,17 @@
 //+build !go1.8
 
-package osxkeychain
+package registryurl
 
 import (
-	"net/url"
+	url "net/url"
 	"strings"
 )
 
-func getHostname(u *url.URL) string {
+func GetHostname(u *url.URL) string {
 	return stripPort(u.Host)
 }
 
-func getPort(u *url.URL) string {
+func GetPort(u *url.URL) string {
 	return portOnly(u.Host)
 }
 
diff --git a/secretservice/secretservice_linux.go b/secretservice/secretservice_linux.go
index 95a1310..383b0c2 100644
--- a/secretservice/secretservice_linux.go
+++ b/secretservice/secretservice_linux.go
@@ -93,7 +93,7 @@
 	var listLenC C.uint
 	err := C.list(credsLabelC, &pathsC, &acctsC, &listLenC)
 	if err != nil {
-		defer C.free(unsafe.Pointer(err))
+		defer C.g_error_free(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)
diff --git a/wincred/wincred_windows.go b/wincred/wincred_windows.go
index dcfa123..7caac5d 100644
--- a/wincred/wincred_windows.go
+++ b/wincred/wincred_windows.go
@@ -2,10 +2,12 @@
 
 import (
 	"bytes"
+	"net/url"
 	"strings"
 
 	winc "github.com/danieljoos/wincred"
 	"github.com/docker/docker-credential-helpers/credentials"
+	"github.com/docker/docker-credential-helpers/registryurl"
 )
 
 // Wincred handles secrets using the Windows credential service.
@@ -13,11 +15,12 @@
 
 // Add adds new credentials to the windows credentials manager.
 func (h Wincred) Add(creds *credentials.Credentials) error {
+	credsLabels := []byte(credentials.CredsLabel)
 	g := winc.NewGenericCredential(creds.ServerURL)
 	g.UserName = creds.Username
 	g.CredentialBlob = []byte(creds.Secret)
 	g.Persist = winc.PersistLocalMachine
-	g.Attributes = []winc.CredentialAttribute{{"label", []byte(credentials.CredsLabel)}}
+	g.Attributes = []winc.CredentialAttribute{{Keyword: "label", Value: credsLabels}}
 
 	return g.Write()
 }
@@ -36,10 +39,18 @@
 
 // Get retrieves credentials from the windows credentials manager.
 func (h Wincred) Get(serverURL string) (string, string, error) {
-	g, _ := winc.GetGenericCredential(serverURL)
+	target, err := getTarget(serverURL)
+	if err != nil {
+		return "", "", err
+	} else if target == "" {
+		return "", "", credentials.NewErrCredentialsNotFound()
+	}
+
+	g, _ := winc.GetGenericCredential(target)
 	if g == nil {
 		return "", "", credentials.NewErrCredentialsNotFound()
 	}
+
 	for _, attr := range g.Attributes {
 		if strings.Compare(attr.Keyword, "label") == 0 &&
 			bytes.Compare(attr.Value, []byte(credentials.CredsLabel)) == 0 {
@@ -50,6 +61,71 @@
 	return "", "", credentials.NewErrCredentialsNotFound()
 }
 
+func getTarget(serverURL string) (string, error) {
+	s, err := registryurl.Parse(serverURL)
+	if err != nil {
+		return serverURL, nil
+	}
+
+	creds, err := winc.List()
+	if err != nil {
+		return "", err
+	}
+
+	var targets []string
+	for i := range creds {
+		attrs := creds[i].Attributes
+		for _, attr := range attrs {
+			if attr.Keyword == "label" && bytes.Equal(attr.Value, []byte(credentials.CredsLabel)) {
+				targets = append(targets, creds[i].TargetName)
+			}
+		}
+	}
+
+	if target, found := findMatch(s, targets, exactMatch); found {
+		return target, nil
+	}
+
+	if target, found := findMatch(s, targets, approximateMatch); found {
+		return target, nil
+	}
+
+	return "", nil
+}
+
+func findMatch(serverUrl *url.URL, targets []string, matches func(url.URL, url.URL) bool) (string, bool) {
+	for _, target := range targets {
+		tURL, err := registryurl.Parse(target)
+		if err != nil {
+			continue
+		}
+		if matches(*serverUrl, *tURL) {
+			return target, true
+		}
+	}
+	return "", false
+}
+
+func exactMatch(serverURL, target url.URL) bool {
+	return serverURL.String() == target.String()
+}
+
+func approximateMatch(serverURL, target url.URL) bool {
+	//if scheme is missing assume it is the same as target
+	if serverURL.Scheme == "" {
+		serverURL.Scheme = target.Scheme
+	}
+	//if port is missing assume it is the same as target
+	if serverURL.Port() == "" && target.Port() != "" {
+		serverURL.Host = serverURL.Host + ":" + target.Port()
+	}
+	//if path is missing assume it is the same as target
+	if serverURL.Path == "" {
+		serverURL.Path = target.Path
+	}
+	return serverURL.String() == target.String()
+}
+
 // List returns the stored URLs and corresponding usernames for a given credentials label.
 func (h Wincred) List() (map[string]string, error) {
 	creds, err := winc.List()
diff --git a/wincred/wincred_windows_test.go b/wincred/wincred_windows_test.go
index 4421fb1..68d293a 100644
--- a/wincred/wincred_windows_test.go
+++ b/wincred/wincred_windows_test.go
@@ -1,6 +1,7 @@
 package wincred
 
 import (
+	"fmt"
 	"strings"
 	"testing"
 
@@ -86,6 +87,154 @@
 	}
 }
 
+// TestWinCredHelperRetrieveAliases verifies that secrets can be accessed
+// through variations on the URL
+func TestWinCredHelperRetrieveAliases(t *testing.T) {
+	tests := []struct {
+		storeURL string
+		readURL  string
+	}{
+		// stored with port, retrieved without
+		{"https://foobar.docker.io:2376", "https://foobar.docker.io"},
+
+		// stored as https, retrieved without scheme
+		{"https://foobar.docker.io", "foobar.docker.io"},
+
+		// stored with path, retrieved without
+		{"https://foobar.docker.io/one/two", "https://foobar.docker.io"},
+	}
+
+	helper := Wincred{}
+	defer func() {
+		for _, te := range tests {
+			helper.Delete(te.storeURL)
+		}
+	}()
+
+	// Clean store before testing.
+	for _, te := range tests {
+		helper.Delete(te.storeURL)
+	}
+
+	for _, te := range tests {
+		c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
+		if err := helper.Add(c); err != nil {
+			t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
+			continue
+		}
+		if _, _, err := helper.Get(te.readURL); err != nil {
+			t.Errorf("Error: failed to read secret for URL %q using %q", te.storeURL, te.readURL)
+		}
+		helper.Delete(te.storeURL)
+	}
+}
+
+// TestWinCredHelperRetrieveStrict verifies that only matching secrets are
+// returned.
+func TestWinCredHelperRetrieveStrict(t *testing.T) {
+	tests := []struct {
+		storeURL string
+		readURL  string
+	}{
+		// stored as https, retrieved using http
+		{"https://foobar.docker.io:2376", "http://foobar.docker.io:2376"},
+
+		// stored as http, retrieved using https
+		{"http://foobar.docker.io:2376", "https://foobar.docker.io:2376"},
+
+		// same: stored as http, retrieved without a scheme specified (hence, using the default https://)
+		{"http://foobar.docker.io", "foobar.docker.io:5678"},
+
+		// non-matching ports
+		{"https://foobar.docker.io:1234", "https://foobar.docker.io:5678"},
+
+		// non-matching ports TODO is this desired behavior? The other way round does work
+		//{"https://foobar.docker.io", "https://foobar.docker.io:5678"},
+
+		// non-matching paths
+		{"https://foobar.docker.io:1234/one/two", "https://foobar.docker.io:1234/five/six"},
+	}
+
+	helper := Wincred{}
+	defer func() {
+		for _, te := range tests {
+			helper.Delete(te.storeURL)
+		}
+	}()
+
+	// Clean store before testing.
+	for _, te := range tests {
+		helper.Delete(te.storeURL)
+	}
+
+	for _, te := range tests {
+		c := &credentials.Credentials{ServerURL: te.storeURL, Username: "hello", Secret: "world"}
+		if err := helper.Add(c); err != nil {
+			t.Errorf("Error: failed to store secret for URL %q: %s", te.storeURL, err)
+			continue
+		}
+		if _, _, err := helper.Get(te.readURL); err == nil {
+			t.Errorf("Error: managed to read secret for URL %q using %q, but should not be able to", te.storeURL, te.readURL)
+		}
+		helper.Delete(te.storeURL)
+	}
+}
+
+// TestWinCredHelperStoreRetrieve verifies that secrets stored in the
+// the keychain can be read back using the URL that was used to store them.
+func TestWinCredHelperStoreRetrieve(t *testing.T) {
+	tests := []struct {
+		url string
+	}{
+		{url: "foobar.docker.io"},
+		{url: "foobar.docker.io:2376"},
+		{url: "//foobar.docker.io:2376"},
+		{url: "https://foobar.docker.io:2376"},
+		{url: "http://foobar.docker.io:2376"},
+		{url: "https://foobar.docker.io:2376/some/path"},
+		{url: "https://foobar.docker.io:2376/some/other/path"},
+		{url: "https://foobar.docker.io:2376/some/other/path?foo=bar"},
+	}
+
+	helper := Wincred{}
+	defer func() {
+		for _, te := range tests {
+			helper.Delete(te.url)
+		}
+	}()
+
+	// Clean store before testing.
+	for _, te := range tests {
+		helper.Delete(te.url)
+	}
+
+	// Note that we don't delete between individual tests here, to verify that
+	// subsequent stores/overwrites don't affect storing / retrieving secrets.
+	for i, te := range tests {
+		c := &credentials.Credentials{
+			ServerURL: te.url,
+			Username:  fmt.Sprintf("user-%d", i),
+			Secret:    fmt.Sprintf("secret-%d", i),
+		}
+
+		if err := helper.Add(c); err != nil {
+			t.Errorf("Error: failed to store secret for URL: %s: %s", te.url, err)
+			continue
+		}
+		user, secret, err := helper.Get(te.url)
+		if err != nil {
+			t.Errorf("Error: failed to read secret for URL %q: %s", te.url, err)
+			continue
+		}
+		if user != c.Username {
+			t.Errorf("Error: expected username %s, got username %s for URL: %s", c.Username, user, te.url)
+		}
+		if secret != c.Secret {
+			t.Errorf("Error: expected secret %s, got secret %s for URL: %s", c.Secret, secret, te.url)
+		}
+	}
+}
+
 func TestMissingCredentials(t *testing.T) {
 	helper := Wincred{}
 	_, _, err := helper.Get("https://adsfasdf.wrewerwer.com/asdfsdddd")