diff --git a/cli/config/credentials/default_store_linux.go b/cli/config/credentials/default_store_linux.go index 9b619c0279..94a501d78a 100644 --- a/cli/config/credentials/default_store_linux.go +++ b/cli/config/credentials/default_store_linux.go @@ -5,7 +5,8 @@ import ( ) func defaultCredentialsStore() string { - if pass.PassInitialized { + passStore := pass.Pass{} + if passStore.CheckInitialized() { return "pass" } diff --git a/vendor.conf b/vendor.conf index e26a66e351..4f743eaa48 100755 --- a/vendor.conf +++ b/vendor.conf @@ -7,7 +7,7 @@ github.com/cpuguy83/go-md2man v1.0.8 github.com/davecgh/go-spew 346938d642f2ec3594ed81d874461961cd0faa76 github.com/docker/distribution 83389a148052d74ac602f5f1d62f86ff2f3c4aa5 github.com/docker/docker c752b0991e31ba9869ab6a0661af57e9423874fb -github.com/docker/docker-credential-helpers 3c90bd29a46b943b2a9842987b58fb91a7c1819b +github.com/docker/docker-credential-helpers 5241b46610f2491efdf9d1c85f1ddf5b02f6d962 # 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. github.com/docker/go d30aec9fd63c35133f8f79c3412ad91a3b08be06 diff --git a/vendor/github.com/docker/docker-credential-helpers/README.md b/vendor/github.com/docker/docker-credential-helpers/README.md index e795f7c5e4..f9cbc3fb5f 100644 --- a/vendor/github.com/docker/docker-credential-helpers/README.md +++ b/vendor/github.com/docker/docker-credential-helpers/README.md @@ -55,6 +55,12 @@ You can see examples of each function in the [client](https://godoc.org/github.c 1. osxkeychain: Provides a helper to use the OS X keychain as credentials store. 2. secretservice: Provides a helper to use the D-Bus secret service as credentials store. 3. wincred: Provides a helper to use Windows credentials manager as store. +4. pass: Provides a helper to use `pass` as credentials store. + +#### Note + +`pass` needs to be configured for `docker-credential-pass` to work properly. +It must be initialized with a `gpg2` key ID. Make sure your GPG key exists is in `gpg2` keyring as `pass` uses `gpg2` instead of the regular `gpg`. ## Development diff --git a/vendor/github.com/docker/docker-credential-helpers/credentials/version.go b/vendor/github.com/docker/docker-credential-helpers/credentials/version.go index a4834dd41a..033a5fee55 100644 --- a/vendor/github.com/docker/docker-credential-helpers/credentials/version.go +++ b/vendor/github.com/docker/docker-credential-helpers/credentials/version.go @@ -1,4 +1,4 @@ package credentials // Version holds a string describing the current version -const Version = "0.5.2" +const Version = "0.6.0" 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 index 2ddfa4a6ec..66f372bedb 100644 --- a/vendor/github.com/docker/docker-credential-helpers/pass/pass_linux.go +++ b/vendor/github.com/docker/docker-credential-helpers/pass/pass_linux.go @@ -5,6 +5,7 @@ package pass import ( + "bytes" "encoding/base64" "errors" "fmt" @@ -13,123 +14,121 @@ import ( "os/exec" "path" "strings" + "sync" "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") +// 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 + } + // 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(getPassDir(), "docker-pass-initialized-check") + + _, err := p.runPassHelper(password, "insert", "-f", "-m", name) + if err != nil { + return fmt.Errorf("error initializing pass: %v", err) } + stored, err := p.runPassHelper("", "show", name) + if err != nil { + return fmt.Errorf("error fetching password during initialization: %v", err) + } + if stored != password { + return fmt.Errorf("error round-tripping password during initialization: %q != %q", password, stored) + } + 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 := "$HOME/.password-store" + if envDir := os.Getenv("PASSWORD_STORE_DIR"); envDir != "" { + passDir = envDir + } + return os.ExpandEnv(passDir) +} + // 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 - } - + passDir := getPassDir() p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...) contents, err := ioutil.ReadDir(p) if err != nil { @@ -145,16 +144,20 @@ func listPassDir(args ...string) ([]os.FileInfo, error) { // 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)) + if _, err := os.Stat(path.Join(getPassDir(), PASS_FOLDER, encoded)); err != nil { + if os.IsNotExist(err) { + return "", "", nil + } + + return "", "", err + } + usernames, err := listPassDir(encoded) if err != nil { return "", "", err @@ -165,16 +168,12 @@ func (h Pass) Get(serverURL string) (string, string, error) { } 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