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
|
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
}
|