package passdriver import ( "bytes" "context" "errors" "fmt" "io" "io/ioutil" "os" "os/exec" "path/filepath" "sort" "strings" ) var ( // errNoSecretData indicates that there is not data associated with an id errNoSecretData = errors.New("no secret data with ID") // errNoSecretData indicates that there is secret data already associated with an id errSecretIDExists = errors.New("secret data with ID already exists") // errInvalidKey indicates that something about your key is wrong errInvalidKey = errors.New("invalid key") ) type driverConfig struct { // Root contains the root directory where the secrets are stored Root string // KeyID contains the key id that will be used for encryption (i.e. user@domain.tld) KeyID string // GPGHomedir is the homedir where the GPG keys are stored GPGHomedir string } func (cfg *driverConfig) ParseOpts(opts map[string]string) { if val, ok := opts["root"]; ok { cfg.Root = val cfg.findGpgID() // try to find a .gpg-id in the parent directories of Root } if val, ok := opts["key"]; ok { cfg.KeyID = val } if val, ok := opts["gpghomedir"]; ok { cfg.GPGHomedir = val } } func defaultDriverConfig() *driverConfig { cfg := &driverConfig{} if home, err := os.UserHomeDir(); err == nil { defaultLocations := []string{ filepath.Join(home, ".password-store"), filepath.Join(home, ".local/share/gopass/stores/root"), } for _, path := range defaultLocations { if stat, err := os.Stat(path); err != nil || !stat.IsDir() { continue } cfg.Root = path bs, err := ioutil.ReadFile(filepath.Join(path, ".gpg-id")) if err != nil { continue } cfg.KeyID = string(bytes.Trim(bs, "\r\n")) break } } return cfg } func (cfg *driverConfig) findGpgID() { path := cfg.Root for len(path) > 1 { if _, err := os.Stat(filepath.Join(path, ".gpg-id")); err == nil { bs, err := ioutil.ReadFile(filepath.Join(path, ".gpg-id")) if err != nil { continue } cfg.KeyID = string(bytes.Trim(bs, "\r\n")) break } path = filepath.Dir(path) } } // Driver is the passdriver object type Driver struct { driverConfig } // NewDriver creates a new secret driver. func NewDriver(opts map[string]string) (*Driver, error) { cfg := defaultDriverConfig() cfg.ParseOpts(opts) driver := &Driver{ driverConfig: *cfg, } return driver, nil } // List returns all secret IDs func (d *Driver) List() (secrets []string, err error) { files, err := ioutil.ReadDir(d.Root) if err != nil { return nil, fmt.Errorf("failed to read secret directory: %w", err) } for _, f := range files { fileName := f.Name() withoutSuffix := fileName[:len(fileName)-len(".gpg")] secrets = append(secrets, withoutSuffix) } sort.Strings(secrets) return secrets, nil } // Lookup returns the bytes associated with a secret ID func (d *Driver) Lookup(id string) ([]byte, error) { out := &bytes.Buffer{} key, err := d.getPath(id) if err != nil { return nil, err } if err := d.gpg(context.TODO(), nil, out, "--decrypt", key); err != nil { return nil, fmt.Errorf("%s: %w", id, errNoSecretData) } if out.Len() == 0 { return nil, fmt.Errorf("%s: %w", id, errNoSecretData) } return out.Bytes(), nil } // Store saves the bytes associated with an ID. An error is returned if the ID already exists func (d *Driver) Store(id string, data []byte) error { if _, err := d.Lookup(id); err == nil { return fmt.Errorf("%s: %w", id, errSecretIDExists) } in := bytes.NewReader(data) key, err := d.getPath(id) if err != nil { return err } return d.gpg(context.TODO(), in, nil, "--encrypt", "-r", d.KeyID, "-o", key) } // Delete removes the secret associated with the specified ID. An error is returned if no matching secret is found. func (d *Driver) Delete(id string) error { key, err := d.getPath(id) if err != nil { return err } if err := os.Remove(key); err != nil { return fmt.Errorf("%s: %w", id, errNoSecretData) } return nil } func (d *Driver) gpg(ctx context.Context, in io.Reader, out io.Writer, args ...string) error { if d.GPGHomedir != "" { args = append([]string{"--homedir", d.GPGHomedir}, args...) } cmd := exec.CommandContext(ctx, "gpg", args...) cmd.Env = os.Environ() cmd.Stdin = in cmd.Stdout = out cmd.Stderr = ioutil.Discard return cmd.Run() } func (d *Driver) getPath(id string) (string, error) { path, err := filepath.Abs(filepath.Join(d.Root, id)) if err != nil { return "", errInvalidKey } if !strings.HasPrefix(path, d.Root) { return "", errInvalidKey } return path + ".gpg", nil }