1
0
mirror of https://github.com/docker/docker-credential-helpers.git synced 2026-06-13 16:01:28 +05:30

pass: only init on run, and do so lazily

This also fixes the following issues:

1. Safe for concurrent initialization still (it was before in 'init',
   but the alternative to this PR is not)
2. Uses the same password directory during init as it does during
   runtime (the change to getPassDir in initialization logic.
3. Prints significantly better errors if initialization fails
4. Has slightly cleaner abstractions by hiding the initialization check
   in 'runPass'

The 4th item there does mean there are a few cases where more work is
done before erroring, but that amount of work is trivial and my manual
audit didn't reveal anything that seemed worrying.

Fixes #96, alternative to #106

Signed-off-by: Euan Kemp <euank@euank.com>
This commit is contained in:
Euan Kemp
2018-06-20 18:24:45 -07:00
committed by Vincent Demeester
parent 26deb2937d
commit 5da09fd251
+40 -37
View File
@@ -5,7 +5,6 @@
package pass package pass
import ( import (
"bytes"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
@@ -14,40 +13,63 @@ import (
"os/exec" "os/exec"
"path" "path"
"strings" "strings"
"sync"
"github.com/docker/docker-credential-helpers/credentials" "github.com/docker/docker-credential-helpers/credentials"
) )
const PASS_FOLDER = "docker-credential-helpers" const PASS_FOLDER = "docker-credential-helpers"
var ( // Pass handles secrets using Linux secret-service as a store.
PassInitialized bool type Pass struct{}
)
func init() { // 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
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 // 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 // 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 // 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 // just explictily check that pass actually can store and retreive a
// password. // password.
password := "pass is initialized" password := "pass is initialized"
name := path.Join(PASS_FOLDER, "docker-pass-initialized-check") name := path.Join(getPassDir(), "docker-pass-initialized-check")
_, err := runPass(password, "insert", "-f", "-m", name) _, err := p.runPassHelper(password, "insert", "-f", "-m", name)
if err != nil { if err != nil {
return return fmt.Errorf("error initializing pass: %v", err)
} }
stored, err := runPass("", "show", name) stored, err := p.runPassHelper("", "show", name)
PassInitialized = err == nil && stored == password if err != nil {
return fmt.Errorf("error fetching password during initialization: %v", err)
if PassInitialized {
runPass("", "rm", "-rf", name)
} }
if stored != password {
return fmt.Errorf("error round-tripping password during initialization: %q != %q", password, stored)
}
passInitialized = true
return nil
} }
func runPass(stdin string, args ...string) (string, error) { func (p Pass) runPass(stdinContent string, args ...string) (string, error) {
var stdout, stderr bytes.Buffer if err := p.checkInitialized(); err != nil {
return "", err
}
return p.runPassHelper(stdinContent, args...)
}
func (p Pass) runPassHelper(stdinContent string, args ...string) (string, error) {
cmd := exec.Command("pass", args...) cmd := exec.Command("pass", args...)
cmd.Stdin = strings.NewReader(stdin) cmd.Stdin = strings.NewReader(stdin)
cmd.Stdout = &stdout cmd.Stdout = &stdout
@@ -61,37 +83,26 @@ func runPass(stdin string, args ...string) (string, error) {
return stdout.String(), nil return stdout.String(), nil
} }
// Pass handles secrets using Linux secret-service as a store.
type Pass struct{}
// Add adds new credentials to the keychain. // Add adds new credentials to the keychain.
func (h Pass) Add(creds *credentials.Credentials) error { func (h Pass) Add(creds *credentials.Credentials) error {
if !PassInitialized {
return errors.New("pass store is uninitialized")
}
if creds == nil { if creds == nil {
return errors.New("missing credentials") return errors.New("missing credentials")
} }
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL)) 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 return err
} }
// Delete removes credentials from the store. // Delete removes credentials from the store.
func (h Pass) Delete(serverURL string) error { func (h Pass) Delete(serverURL string) error {
if !PassInitialized {
return errors.New("pass store is uninitialized")
}
if serverURL == "" { if serverURL == "" {
return errors.New("missing server url") return errors.New("missing server url")
} }
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL)) 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 return err
} }
@@ -123,10 +134,6 @@ func listPassDir(args ...string) ([]os.FileInfo, error) {
// Get returns the username and secret to use for a given registry server URL. // Get returns the username and secret to use for a given registry server URL.
func (h Pass) Get(serverURL string) (string, string, error) { func (h Pass) Get(serverURL string) (string, string, error) {
if !PassInitialized {
return "", "", errors.New("pass store is uninitialized")
}
if serverURL == "" { if serverURL == "" {
return "", "", errors.New("missing server url") return "", "", errors.New("missing server url")
} }
@@ -151,16 +158,12 @@ func (h Pass) Get(serverURL string) (string, string, error) {
} }
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg") 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 return actual, secret, err
} }
// List returns the stored URLs and corresponding usernames for a given credentials label // List returns the stored URLs and corresponding usernames for a given credentials label
func (h Pass) List() (map[string]string, error) { func (h Pass) List() (map[string]string, error) {
if !PassInitialized {
return nil, errors.New("pass store is uninitialized")
}
servers, err := listPassDir() servers, err := listPassDir()
if err != nil { if err != nil {
return nil, err return nil, err