forked from gopasspw/gopass
-
Notifications
You must be signed in to change notification settings - Fork 0
/
recipients.go
286 lines (246 loc) · 7.02 KB
/
recipients.go
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
package sub
import (
"bufio"
"bytes"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/justwatchcom/gopass/store"
"github.com/justwatchcom/gopass/utils/out"
"github.com/pkg/errors"
)
const (
keyDir = ".gpg-keys"
fileMode = 0600
dirMode = 0700
)
// Recipients returns the list of recipients of this store
func (s *Store) Recipients(ctx context.Context) []string {
rs, err := s.GetRecipients(ctx, "")
if err != nil {
out.Red(ctx, "failed to read recipient list: %s", err)
}
return rs
}
// AddRecipient adds a new recipient to the list
func (s *Store) AddRecipient(ctx context.Context, id string) error {
rs, err := s.GetRecipients(ctx, "")
if err != nil {
return errors.Wrapf(err, "failed to read recipient list")
}
for _, k := range rs {
if k == id {
return errors.Errorf("Recipient already in store")
}
}
rs = append(rs, id)
if err := s.saveRecipients(ctx, rs, "Added Recipient "+id, true); err != nil {
return errors.Wrapf(err, "failed to save recipients")
}
out.Cyan(ctx, "Reencrypting existing secrets. This may take some time ...")
return s.reencrypt(WithReason(ctx, "Added Recipient "+id))
}
// SaveRecipients persists the current recipients on disk
func (s *Store) SaveRecipients(ctx context.Context) error {
rs, err := s.GetRecipients(ctx, "")
if err != nil {
return errors.Wrapf(err, "failed get recipients")
}
return s.saveRecipients(ctx, rs, "Save Recipients", true)
}
// RemoveRecipient will remove the given recipient from the store
// but if this key is not available on this machine we
// just try to remove it literally
func (s *Store) RemoveRecipient(ctx context.Context, id string) error {
keys, err := s.gpg.FindPublicKeys(ctx, id)
if err != nil {
out.Cyan(ctx, "Warning: Failed to get GPG Key Info for %s: %s", id, err)
}
rs, err := s.GetRecipients(ctx, "")
if err != nil {
return errors.Wrapf(err, "failed to read recipient list")
}
nk := make([]string, 0, len(rs)-1)
RECIPIENTS:
for _, k := range rs {
if k == id {
continue RECIPIENTS
}
// if the key is available locally we can also match the id against
// the fingerprint
for _, key := range keys {
if strings.HasSuffix(key.Fingerprint, k) {
continue RECIPIENTS
}
}
nk = append(nk, k)
}
if len(rs) == len(nk) {
return errors.Errorf("recipient not in store")
}
if err := s.saveRecipients(ctx, nk, "Removed Recipient "+id, true); err != nil {
return errors.Wrapf(err, "failed to save recipients")
}
return s.reencrypt(WithReason(ctx, "Removed Recipient "+id))
}
// OurKeyID returns the key fingprint this user can use to access the store
// (if any)
func (s *Store) OurKeyID(ctx context.Context) string {
for _, r := range s.Recipients(ctx) {
kl, err := s.gpg.FindPrivateKeys(ctx, r)
if err != nil || len(kl) < 1 {
continue
}
return kl[0].Fingerprint
}
return ""
}
// GetRecipients will load all Recipients from the .gpg-id file for the given
// secret path
func (s *Store) GetRecipients(ctx context.Context, name string) ([]string, error) {
idf := s.idFile(name)
out.Debug(ctx, "GetRecipients(%s) - idfile: %s", name, idf)
// open recipient list (store/.gpg-id)
f, err := os.Open(idf)
if err != nil {
return []string{}, err
}
defer func() {
if err := f.Close(); err != nil {
out.Red(ctx, "Failed to close %s: %s", idf, err)
}
}()
return unmarshalRecipients(f), nil
}
// ExportMissingPublicKeys will export any possibly missing public keys to the
// stores .gpg-keys directory
func (s *Store) ExportMissingPublicKeys(ctx context.Context, rs []string) (bool, error) {
ok := true
exported := false
for _, r := range rs {
path, err := s.exportPublicKey(ctx, r)
if err != nil {
ok = false
out.Red(ctx, "failed to export public key for '%s': %s", r, err)
continue
}
if path == "" {
continue
}
// at least one key has been exported
exported = true
if err := s.git.Add(ctx, path); err != nil {
if errors.Cause(err) == store.ErrGitNotInit {
continue
}
ok = false
out.Red(ctx, "failed to add public key for '%s' to git: %s", r, err)
continue
}
if err := s.git.Commit(ctx, fmt.Sprintf("Exported Public Keys %s", r)); err != nil && err != store.ErrGitNothingToCommit {
ok = false
out.Red(ctx, "Failed to git commit: %s", err)
continue
}
}
if !ok {
return exported, errors.New("some keys failed")
}
return exported, nil
}
// Save all Recipients in memory to the .gpg-id file on disk.
func (s *Store) saveRecipients(ctx context.Context, rs []string, msg string, exportKeys bool) error {
if len(rs) < 1 {
return errors.New("can not remove all recipients")
}
idf := s.idFile("")
// filepath.Dir(s.idFile()) should equal s.path, but better safe than sorry
if err := os.MkdirAll(filepath.Dir(idf), dirMode); err != nil {
return errors.Wrapf(err, "failed to create directory for recipients")
}
// save recipients to store/.gpg-id
if err := ioutil.WriteFile(idf, marshalRecipients(rs), fileMode); err != nil {
return errors.Wrapf(err, "failed to write recipients file")
}
if err := s.git.Add(ctx, idf); err != nil {
if err != store.ErrGitNotInit {
return errors.Wrapf(err, "failed to add file '%s' to git", idf)
}
}
if err := s.git.Commit(ctx, msg); err != nil {
if err != store.ErrGitNotInit && err != store.ErrGitNothingToCommit {
return errors.Wrapf(err, "failed to commit changes to git")
}
}
// save recipients' public keys
if err := os.MkdirAll(filepath.Join(s.path, keyDir), dirMode); err != nil {
return errors.Wrapf(err, "failed to create key dir '%s'", keyDir)
}
// save all recipients public keys to the repo
if exportKeys {
if _, err := s.ExportMissingPublicKeys(ctx, rs); err != nil {
out.Red(ctx, "Failed to export missing public keys: %s", err)
}
}
// push to remote repo
if err := s.git.Push(ctx, "", ""); err != nil {
if errors.Cause(err) == store.ErrGitNotInit {
return nil
}
if errors.Cause(err) == store.ErrGitNoRemote {
msg := "Warning: git has no remote. Ignoring auto-push option\n" +
"Run: gopass git remote add origin ..."
out.Yellow(ctx, msg)
return nil
}
return errors.Wrapf(err, "failed to push changes to git")
}
return nil
}
// marshal all in memory Recipients line by line to []byte.
func marshalRecipients(r []string) []byte {
if len(r) == 0 {
return []byte("\n")
}
// deduplicate
m := make(map[string]struct{}, len(r))
for _, k := range r {
m[k] = struct{}{}
}
// sort
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
out := bytes.Buffer{}
for _, k := range keys {
_, _ = out.WriteString(k)
_, _ = out.WriteString("\n")
}
return out.Bytes()
}
// unmarshal Recipients line by line from a io.Reader.
func unmarshalRecipients(reader io.Reader) []string {
m := make(map[string]struct{}, 5)
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line != "" {
// deduplicate
m[line] = struct{}{}
}
}
lst := make([]string, 0, len(m))
for k := range m {
lst = append(lst, k)
}
// sort
sort.Strings(lst)
return lst
}