1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
|
// Package pass implements 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 (
"bytes"
"encoding/base64"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path"
"strings"
"sync"
"github.com/docker/docker-credential-helpers/credentials"
)
// PASS_FOLDER contains the directory where credentials are stored
const PASS_FOLDER = "docker-credential-helpers" //nolint: golint
// Pass handles secrets using Linux secret-service as a store.
type Pass struct{}
// 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 (p Pass) Add(creds *credentials.Credentials) error {
if creds == nil {
return errors.New("missing credentials")
}
encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
_, err := p.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
return err
}
// Delete removes credentials from the store.
func (p Pass) Delete(serverURL string) error {
if serverURL == "" {
return errors.New("missing server url")
}
encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
_, err := p.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 := getPassDir()
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 (p Pass) Get(serverURL string) (string, string, error) {
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
}
if len(usernames) < 1 {
return "", "", fmt.Errorf("no usernames for %s", serverURL)
}
actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
secret, err := p.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 (p Pass) List() (map[string]string, error) {
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
}
|