...

Source file src/github.com/docker/docker-credential-helpers/pass/pass.go

Documentation: github.com/docker/docker-credential-helpers/pass

     1  // Package pass implements a `pass` based credential helper. Passwords are stored
     2  // as arguments to pass of the form: "$PASS_FOLDER/base64-url(serverURL)/username".
     3  // We base64-url encode the serverURL, because under the hood pass uses files and
     4  // folders, so /s will get translated into additional folders.
     5  package pass
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/base64"
    10  	"errors"
    11  	"fmt"
    12  	"io/fs"
    13  	"os"
    14  	"os/exec"
    15  	"path"
    16  	"path/filepath"
    17  	"strings"
    18  	"sync"
    19  
    20  	"github.com/docker/docker-credential-helpers/credentials"
    21  )
    22  
    23  // PASS_FOLDER contains the directory where credentials are stored
    24  const PASS_FOLDER = "docker-credential-helpers" //nolint:revive
    25  
    26  // Pass handles secrets using pass as a store.
    27  type Pass struct{}
    28  
    29  // Ideally these would be stored as members of Pass, but since all of Pass's
    30  // methods have value receivers, not pointer receivers, and changing that is
    31  // backwards incompatible, we assume that all Pass instances share the same configuration
    32  var (
    33  	// initializationMutex is held while initializing so that only one 'pass'
    34  	// round-tripping is done to check pass is functioning.
    35  	initializationMutex sync.Mutex
    36  	passInitialized     bool
    37  )
    38  
    39  // CheckInitialized checks whether the password helper can be used. It
    40  // internally caches and so may be safely called multiple times with no impact
    41  // on performance, though the first call may take longer.
    42  func (p Pass) CheckInitialized() bool {
    43  	return p.checkInitialized() == nil
    44  }
    45  
    46  func (p Pass) checkInitialized() error {
    47  	initializationMutex.Lock()
    48  	defer initializationMutex.Unlock()
    49  	if passInitialized {
    50  		return nil
    51  	}
    52  	// We just run a `pass ls`, if it fails then pass is not initialized.
    53  	_, err := p.runPassHelper("", "ls")
    54  	if err != nil {
    55  		return fmt.Errorf("pass not initialized: %v", err)
    56  	}
    57  	passInitialized = true
    58  	return nil
    59  }
    60  
    61  func (p Pass) runPass(stdinContent string, args ...string) (string, error) {
    62  	if err := p.checkInitialized(); err != nil {
    63  		return "", err
    64  	}
    65  	return p.runPassHelper(stdinContent, args...)
    66  }
    67  
    68  func (p Pass) runPassHelper(stdinContent string, args ...string) (string, error) {
    69  	var stdout, stderr bytes.Buffer
    70  	cmd := exec.Command("pass", args...)
    71  	cmd.Stdin = strings.NewReader(stdinContent)
    72  	cmd.Stdout = &stdout
    73  	cmd.Stderr = &stderr
    74  
    75  	err := cmd.Run()
    76  	if err != nil {
    77  		return "", fmt.Errorf("%s: %s", err, stderr.String())
    78  	}
    79  
    80  	// trim newlines; pass v1.7.1+ includes a newline at the end of `show` output
    81  	return strings.TrimRight(stdout.String(), "\n\r"), nil
    82  }
    83  
    84  // Add adds new credentials to the keychain.
    85  func (p Pass) Add(creds *credentials.Credentials) error {
    86  	if creds == nil {
    87  		return errors.New("missing credentials")
    88  	}
    89  
    90  	encoded := base64.URLEncoding.EncodeToString([]byte(creds.ServerURL))
    91  
    92  	_, err := p.runPass(creds.Secret, "insert", "-f", "-m", path.Join(PASS_FOLDER, encoded, creds.Username))
    93  	return err
    94  }
    95  
    96  // Delete removes credentials from the store.
    97  func (p Pass) Delete(serverURL string) error {
    98  	if serverURL == "" {
    99  		return errors.New("missing server url")
   100  	}
   101  
   102  	encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
   103  	_, err := p.runPass("", "rm", "-rf", path.Join(PASS_FOLDER, encoded))
   104  	return err
   105  }
   106  
   107  func getPassDir() string {
   108  	if passDir := os.Getenv("PASSWORD_STORE_DIR"); passDir != "" {
   109  		return passDir
   110  	}
   111  	home, _ := os.UserHomeDir()
   112  	return filepath.Join(home, ".password-store")
   113  }
   114  
   115  // listPassDir lists all the contents of a directory in the password store.
   116  // Pass uses fancy unicode to emit stuff to stdout, so rather than try
   117  // and parse this, let's just look at the directory structure instead.
   118  func listPassDir(args ...string) ([]os.FileInfo, error) {
   119  	passDir := getPassDir()
   120  	p := path.Join(append([]string{passDir, PASS_FOLDER}, args...)...)
   121  	entries, err := os.ReadDir(p)
   122  	if err != nil {
   123  		if os.IsNotExist(err) {
   124  			return []os.FileInfo{}, nil
   125  		}
   126  		return nil, err
   127  	}
   128  	infos := make([]fs.FileInfo, 0, len(entries))
   129  	for _, entry := range entries {
   130  		info, err := entry.Info()
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  		infos = append(infos, info)
   135  	}
   136  	return infos, nil
   137  }
   138  
   139  // Get returns the username and secret to use for a given registry server URL.
   140  func (p Pass) Get(serverURL string) (string, string, error) {
   141  	if serverURL == "" {
   142  		return "", "", errors.New("missing server url")
   143  	}
   144  
   145  	encoded := base64.URLEncoding.EncodeToString([]byte(serverURL))
   146  
   147  	if _, err := os.Stat(path.Join(getPassDir(), PASS_FOLDER, encoded)); err != nil {
   148  		if os.IsNotExist(err) {
   149  			return "", "", credentials.NewErrCredentialsNotFound()
   150  		}
   151  
   152  		return "", "", err
   153  	}
   154  
   155  	usernames, err := listPassDir(encoded)
   156  	if err != nil {
   157  		return "", "", err
   158  	}
   159  
   160  	if len(usernames) < 1 {
   161  		return "", "", fmt.Errorf("no usernames for %s", serverURL)
   162  	}
   163  
   164  	actual := strings.TrimSuffix(usernames[0].Name(), ".gpg")
   165  	secret, err := p.runPass("", "show", path.Join(PASS_FOLDER, encoded, actual))
   166  	return actual, secret, err
   167  }
   168  
   169  // List returns the stored URLs and corresponding usernames for a given credentials label
   170  func (p Pass) List() (map[string]string, error) {
   171  	servers, err := listPassDir()
   172  	if err != nil {
   173  		return nil, err
   174  	}
   175  
   176  	resp := map[string]string{}
   177  
   178  	for _, server := range servers {
   179  		if !server.IsDir() {
   180  			continue
   181  		}
   182  
   183  		serverURL, err := base64.URLEncoding.DecodeString(server.Name())
   184  		if err != nil {
   185  			return nil, err
   186  		}
   187  
   188  		usernames, err := listPassDir(server.Name())
   189  		if err != nil {
   190  			return nil, err
   191  		}
   192  
   193  		if len(usernames) < 1 {
   194  			return nil, fmt.Errorf("no usernames for %s", serverURL)
   195  		}
   196  
   197  		resp[string(serverURL)] = strings.TrimSuffix(usernames[0].Name(), ".gpg")
   198  	}
   199  
   200  	return resp, nil
   201  }
   202  

View as plain text