1
2
3
4
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
24 const PASS_FOLDER = "docker-credential-helpers"
25
26
27 type Pass struct{}
28
29
30
31
32 var (
33
34
35 initializationMutex sync.Mutex
36 passInitialized bool
37 )
38
39
40
41
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
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
81 return strings.TrimRight(stdout.String(), "\n\r"), nil
82 }
83
84
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
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
116
117
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
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
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