diff --git a/cli/config/credentials/default_store.go b/cli/config/credentials/default_store.go index b2cc4df8bc..7a760f1a97 100644 --- a/cli/config/credentials/default_store.go +++ b/cli/config/credentials/default_store.go @@ -7,13 +7,15 @@ import ( // DetectDefaultStore return the default credentials store for the platform if // the store executable is available. func DetectDefaultStore(store string) string { + platformDefault := defaultCredentialsStore() + // user defined or no default for platform - if store != "" || defaultCredentialsStore == "" { + if store != "" || platformDefault == "" { return store } - if _, err := exec.LookPath(remoteCredentialsPrefix + defaultCredentialsStore); err == nil { - return defaultCredentialsStore + if _, err := exec.LookPath(remoteCredentialsPrefix + platformDefault); err == nil { + return platformDefault } return "" } diff --git a/cli/config/credentials/default_store_darwin.go b/cli/config/credentials/default_store_darwin.go index 63e8ed4010..5d42dec622 100644 --- a/cli/config/credentials/default_store_darwin.go +++ b/cli/config/credentials/default_store_darwin.go @@ -1,3 +1,5 @@ package credentials -const defaultCredentialsStore = "osxkeychain" +func defaultCredentialsStore() string { + return "osxkeychain" +} diff --git a/cli/config/credentials/default_store_linux.go b/cli/config/credentials/default_store_linux.go index 864c540f6c..9b619c0279 100644 --- a/cli/config/credentials/default_store_linux.go +++ b/cli/config/credentials/default_store_linux.go @@ -1,3 +1,13 @@ package credentials -const defaultCredentialsStore = "secretservice" +import ( + "github.com/docker/docker-credential-helpers/pass" +) + +func defaultCredentialsStore() string { + if pass.PassInitialized { + return "pass" + } + + return "secretservice" +} diff --git a/cli/config/credentials/default_store_windows.go b/cli/config/credentials/default_store_windows.go index fb6a9745cf..bb799ca61b 100644 --- a/cli/config/credentials/default_store_windows.go +++ b/cli/config/credentials/default_store_windows.go @@ -1,3 +1,5 @@ package credentials -const defaultCredentialsStore = "wincred" +func defaultCredentialsStore() string { + return "wincred" +} diff --git a/docs/reference/commandline/login.md b/docs/reference/commandline/login.md index 49ca5428ec..f1088c6987 100644 --- a/docs/reference/commandline/login.md +++ b/docs/reference/commandline/login.md @@ -63,8 +63,9 @@ $ cat ~/my_password.txt | docker login --username foo --password-stdin 2. user is added to the `docker` group. This will impact the security of your system; the `docker` group is `root` equivalent. See [Docker Daemon Attack Surface](https://docs.docker.com/security/security/#docker-daemon-attack-surface) for details. You can log into any public or private repository for which you have -credentials. When you log in, the command stores encoded credentials in -`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on Windows. +credentials. When you log in, the command stores credentials in +`$HOME/.docker/config.json` on Linux or `%USERPROFILE%/.docker/config.json` on +Windows, via the procedure described below. ### Credentials store @@ -82,6 +83,7 @@ you can download them from: - D-Bus Secret Service: https://github.com/docker/docker-credential-helpers/releases - Apple macOS keychain: https://github.com/docker/docker-credential-helpers/releases - Microsoft Windows Credential Manager: https://github.com/docker/docker-credential-helpers/releases +- [pass](https://www.passwordstore.org/): https://github.com/docker/docker-credential-helpers/releases You need to specify the credentials store in `$HOME/.docker/config.json` to tell the docker engine to use it. The value of the config property should be @@ -97,6 +99,15 @@ For example, to use `docker-credential-osxkeychain`: If you are currently logged in, run `docker logout` to remove the credentials from the file and run `docker login` again. +### Default behavior + +By default, Docker looks for the native binary on each of the platforms, i.e. +"osxkeychain" on macOS, "wincred" on windows, and "pass" on Linux. A special +case is that on Linux, Docker will fall back to the "secretservice" binary if +it cannot find the "pass" binary. If none of these binaries are present, it +stores the credentials (i.e. password) in base64 encoding in the config files +described above. + ### Credential helper protocol Credential helpers can be any program or script that follows a very simple protocol. diff --git a/vendor.conf b/vendor.conf index 5d45797fa6..c5784f35fd 100755 --- a/vendor.conf +++ b/vendor.conf @@ -5,7 +5,7 @@ github.com/cpuguy83/go-md2man a65d4d2de4d5f7c74868dfa9b202a3c8be315aaa github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 github.com/docker/distribution edc3ab29cdff8694dd6feb85cfeb4b5f1b38ed9c github.com/docker/docker 84144a8c66c1bb2af8fa997288f51ef2719971b4 -github.com/docker/docker-credential-helpers v0.5.1 +github.com/docker/docker-credential-helpers 3c90bd29a46b943b2a9842987b58fb91a7c1819b # the docker/go package contains a customized version of canonical/json # and is used by Notary. The package is periodically rebased on current Go versions. diff --git a/vendor/github.com/docker/docker-credential-helpers/client/command.go b/vendor/github.com/docker/docker-credential-helpers/client/command.go index a144d5ac18..8da3343065 100644 --- a/vendor/github.com/docker/docker-credential-helpers/client/command.go +++ b/vendor/github.com/docker/docker-credential-helpers/client/command.go @@ -1,6 +1,7 @@ package client import ( + "fmt" "io" "os" "os/exec" @@ -17,15 +18,26 @@ type ProgramFunc func(args ...string) Program // NewShellProgramFunc creates programs that are executed in a Shell. func NewShellProgramFunc(name string) ProgramFunc { + return NewShellProgramFuncWithEnv(name, nil) +} + +// NewShellProgramFuncWithEnv creates programs that are executed in a Shell with environment variables +func NewShellProgramFuncWithEnv(name string, env *map[string]string) ProgramFunc { return func(args ...string) Program { - return &Shell{cmd: newCmdRedirectErr(name, args)} + return &Shell{cmd: createProgramCmdRedirectErr(name, args, env)} } } -func newCmdRedirectErr(name string, args []string) *exec.Cmd { - newCmd := exec.Command(name, args...) - newCmd.Stderr = os.Stderr - return newCmd +func createProgramCmdRedirectErr(commandName string, args []string, env *map[string]string) *exec.Cmd { + programCmd := exec.Command(commandName, args...) + programCmd.Env = os.Environ() + if env != nil { + for k, v := range *env { + programCmd.Env = append(programCmd.Env, fmt.Sprintf("%s=%s", k, v)) + } + } + programCmd.Stderr = os.Stderr + return programCmd } // Shell invokes shell commands to talk with a remote credentials helper. diff --git a/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go b/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go index 544ab3c4e1..da8b594e7f 100644 --- a/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go +++ b/vendor/github.com/docker/docker-credential-helpers/credentials/credentials.go @@ -33,11 +33,12 @@ func (c *Credentials) isValid() (bool, error) { return true, nil } -// Docker credentials should be labeled as such in credentials stores that allow labelling. +// CredsLabel holds the way Docker credentials should be labeled as such in credentials stores that allow labelling. // That label allows to filter out non-Docker credentials too at lookup/search in macOS keychain, // Windows credentials manager and Linux libsecret. Default value is "Docker Credentials" var CredsLabel = "Docker Credentials" +// SetCredsLabel is a simple setter for CredsLabel func SetCredsLabel(label string) { CredsLabel = label } @@ -50,7 +51,7 @@ func SetCredsLabel(label string) { func Serve(helper Helper) { var err error if len(os.Args) != 2 { - err = fmt.Errorf("Usage: %s ", os.Args[0]) + err = fmt.Errorf("Usage: %s ", os.Args[0]) } if err == nil { @@ -74,6 +75,8 @@ func HandleCommand(helper Helper, key string, in io.Reader, out io.Writer) error return Erase(helper, in) case "list": return List(helper, out) + case "version": + return PrintVersion(out) } return fmt.Errorf("Unknown credential action `%s`", key) } @@ -131,8 +134,8 @@ func Get(helper Helper, reader io.Reader, writer io.Writer) error { resp := Credentials{ ServerURL: serverURL, - Username: username, - Secret: secret, + Username: username, + Secret: secret, } buffer.Reset() @@ -175,3 +178,9 @@ func List(helper Helper, writer io.Writer) error { } return json.NewEncoder(writer).Encode(accts) } + +//PrintVersion outputs the current version. +func PrintVersion(writer io.Writer) error { + fmt.Fprintln(writer, Version) + return nil +} diff --git a/vendor/github.com/docker/docker-credential-helpers/credentials/error.go b/vendor/github.com/docker/docker-credential-helpers/credentials/error.go index 588d4a83d7..fe6a5aef45 100644 --- a/vendor/github.com/docker/docker-credential-helpers/credentials/error.go +++ b/vendor/github.com/docker/docker-credential-helpers/credentials/error.go @@ -8,7 +8,7 @@ const ( // ErrCredentialsMissingServerURL and ErrCredentialsMissingUsername standardize // invalid credentials or credentials management operations errCredentialsMissingServerURLMessage = "no credentials server URL" - errCredentialsMissingUsernameMessage = "no credentials username" + errCredentialsMissingUsernameMessage = "no credentials username" ) // errCredentialsNotFound represents an error @@ -43,7 +43,6 @@ func IsErrCredentialsNotFoundMessage(err string) bool { return err == errCredentialsNotFoundMessage } - // errCredentialsMissingServerURL represents an error raised // when the credentials object has no server URL or when no // server URL is provided to a credentials operation requiring @@ -64,7 +63,6 @@ func (errCredentialsMissingUsername) Error() string { return errCredentialsMissingUsernameMessage } - // NewErrCredentialsMissingServerURL creates a new error for // errCredentialsMissingServerURL. func NewErrCredentialsMissingServerURL() error { @@ -77,7 +75,6 @@ func NewErrCredentialsMissingUsername() error { return errCredentialsMissingUsername{} } - // IsCredentialsMissingServerURL returns true if the error // was an errCredentialsMissingServerURL. func IsCredentialsMissingServerURL(err error) bool { diff --git a/vendor/github.com/docker/docker-credential-helpers/credentials/version.go b/vendor/github.com/docker/docker-credential-helpers/credentials/version.go new file mode 100644 index 0000000000..a4834dd41a --- /dev/null +++ b/vendor/github.com/docker/docker-credential-helpers/credentials/version.go @@ -0,0 +1,4 @@ +package credentials + +// Version holds a string describing the current version +const Version = "0.5.2" diff --git a/vendor/github.com/docker/docker-credential-helpers/osxkeychain/osxkeychain_darwin.go b/vendor/github.com/docker/docker-credential-helpers/osxkeychain/osxkeychain_darwin.go index a3f5da84b6..439126761e 100644 --- a/vendor/github.com/docker/docker-credential-helpers/osxkeychain/osxkeychain_darwin.go +++ b/vendor/github.com/docker/docker-credential-helpers/osxkeychain/osxkeychain_darwin.go @@ -135,30 +135,27 @@ func (h Osxkeychain) List() (map[string]string, error) { } func splitServer(serverURL string) (*C.struct_Server, error) { - u, err := url.Parse(serverURL) + u, err := parseURL(serverURL) if err != nil { return nil, err } - hostAndPort := strings.Split(u.Host, ":") - host := hostAndPort[0] + proto := C.kSecProtocolTypeHTTPS + if u.Scheme == "http" { + proto = C.kSecProtocolTypeHTTP + } var port int - if len(hostAndPort) == 2 { - p, err := strconv.Atoi(hostAndPort[1]) + p := getPort(u) + if p != "" { + port, err = strconv.Atoi(p) if err != nil { return nil, err } - port = p - } - - proto := C.kSecProtocolTypeHTTPS - if u.Scheme != "https" { - proto = C.kSecProtocolTypeHTTP } return &C.struct_Server{ proto: C.SecProtocolType(proto), - host: C.CString(host), + host: C.CString(getHostname(u)), port: C.uint(port), path: C.CString(u.Path), }, nil @@ -168,3 +165,32 @@ func freeServer(s *C.struct_Server) { 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/vendor/github.com/docker/docker-credential-helpers/osxkeychain/url_go18.go b/vendor/github.com/docker/docker-credential-helpers/osxkeychain/url_go18.go new file mode 100644 index 0000000000..0b7297d2fc --- /dev/null +++ b/vendor/github.com/docker/docker-credential-helpers/osxkeychain/url_go18.go @@ -0,0 +1,13 @@ +//+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/vendor/github.com/docker/docker-credential-helpers/osxkeychain/url_non_go18.go b/vendor/github.com/docker/docker-credential-helpers/osxkeychain/url_non_go18.go new file mode 100644 index 0000000000..bdf9b7b006 --- /dev/null +++ b/vendor/github.com/docker/docker-credential-helpers/osxkeychain/url_non_go18.go @@ -0,0 +1,41 @@ +//+build !go1.8 + +package osxkeychain + +import ( + "net/url" + "strings" +) + +func getHostname(u *url.URL) string { + return stripPort(u.Host) +} + +func getPort(u *url.URL) string { + return portOnly(u.Host) +} + +func stripPort(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return hostport + } + if i := strings.IndexByte(hostport, ']'); i != -1 { + return strings.TrimPrefix(hostport[:i], "[") + } + return hostport[:colon] +} + +func portOnly(hostport string) string { + colon := strings.IndexByte(hostport, ':') + if colon == -1 { + return "" + } + if i := strings.Index(hostport, "]:"); i != -1 { + return hostport[i+len("]:"):] + } + if strings.Contains(hostport, "]") { + return "" + } + return hostport[colon+len(":"):] +} diff --git a/vendor/github.com/docker/docker-credential-helpers/pass/pass_linux.go b/vendor/github.com/docker/docker-credential-helpers/pass/pass_linux.go new file mode 100644 index 0000000000..2ddfa4a6ec --- /dev/null +++ b/vendor/github.com/docker/docker-credential-helpers/pass/pass_linux.go @@ -0,0 +1,208 @@ +// A `pass` based credential helper. Passwords are stored as arguments to pass +// of the form: "$PASS_FOLDER/base64-url(serverURL)/username". We base64-url +// encode the serverURL, because under the hood pass uses files and folders, so +// /s will get translated into additional folders. +package pass + +import ( + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path" + "strings" + + "github.com/docker/docker-credential-helpers/credentials" +) + +const PASS_FOLDER = "docker-credential-helpers" + +var ( + PassInitialized bool +) + +func init() { + PassInitialized = exec.Command("pass").Run() == nil +} + +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") + } + + 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)) + 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)) + return err +} + +// listPassDir lists all the contents of a directory in the password store. +// Pass uses fancy unicode to emit stuff to stdout, so rather than try +// and parse this, let's just look at the directory structure instead. +func listPassDir(args ...string) ([]os.FileInfo, error) { + 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 + } + + p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...) + contents, err := ioutil.ReadDir(p) + if err != nil { + if os.IsNotExist(err) { + return []os.FileInfo{}, nil + } + + return nil, err + } + + return contents, nil +} + +// 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") + } + + encoded := base64.URLEncoding.EncodeToString([]byte(serverURL)) + + usernames, err := listPassDir(encoded) + if err != nil { + return "", "", err + } + + if len(usernames) < 1 { + return "", "", fmt.Errorf("no usernames for %s", serverURL) + } + + actual := strings.TrimSuffix(usernames[0].Name(), ".gpg") + secret, err := 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 + } + + resp := map[string]string{} + + for _, server := range servers { + if !server.IsDir() { + continue + } + + serverURL, err := base64.URLEncoding.DecodeString(server.Name()) + if err != nil { + return nil, err + } + + usernames, err := listPassDir(server.Name()) + if err != nil { + return nil, err + } + + if len(usernames) < 1 { + return nil, fmt.Errorf("no usernames for %s", serverURL) + } + + resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg") + } + + return resp, nil +} diff --git a/vendor/github.com/docker/docker-credential-helpers/secretservice/secretservice_linux.go b/vendor/github.com/docker/docker-credential-helpers/secretservice/secretservice_linux.go index ec1c5d3af9..95a1310b65 100644 --- a/vendor/github.com/docker/docker-credential-helpers/secretservice/secretservice_linux.go +++ b/vendor/github.com/docker/docker-credential-helpers/secretservice/secretservice_linux.go @@ -105,8 +105,11 @@ func (h Secretservice) List() (map[string]string, error) { if listLen == 0 { return resp, nil } - pathTmp := (*[1 << 30]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen] - acctTmp := (*[1 << 30]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen] + // The maximum capacity of the following two slices is limited to (2^29)-1 to remain compatible + // with 32-bit platforms. The size of a `*C.char` (a pointer) is 4 Byte on a 32-bit system + // and (2^29)*4 == math.MaxInt32 + 1. -- See issue golang/go#13656 + pathTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(pathsC))[:listLen:listLen] + acctTmp := (*[(1 << 29) - 1]*C.char)(unsafe.Pointer(acctsC))[:listLen:listLen] for i := 0; i < listLen; i++ { resp[C.GoString(pathTmp[i])] = C.GoString(acctTmp[i]) }