1package action
2
3import (
4	"context"
5	"fmt"
6
7	"github.com/gopasspw/gopass/internal/tree"
8
9	"github.com/gopasspw/gopass/internal/cui"
10	"github.com/gopasspw/gopass/internal/out"
11	"github.com/gopasspw/gopass/pkg/ctxutil"
12	"github.com/gopasspw/gopass/pkg/debug"
13	"github.com/gopasspw/gopass/pkg/termio"
14
15	"github.com/urfave/cli/v2"
16)
17
18var (
19	removalWarning = `
20
21
22@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
23@   WARNING: REMOVING A USER WILL NOT REVOKE ACCESS FROM OLD REVISONS!   @
24@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
25THE USER %s WILL STILL BE ABLE TO ACCESS ANY OLD COPY OF THE STORE AND
26ANY OLD REVISION THEY HAD ACCESS TO.
27
28ANY CREDENTIALS THIS USER HAD ACCESS TO NEED TO BE CONSIDERED COMPROMISED
29AND SHOULD BE REVOKED.
30
31This feature is only meant for revoking access to any added or changed
32credentials.
33
34`
35)
36
37// RecipientsPrint prints all recipients per store
38func (s *Action) RecipientsPrint(c *cli.Context) error {
39	ctx := ctxutil.WithGlobalFlags(c)
40	out.Printf(ctx, "Hint: run 'gopass sync' to import any missing public keys")
41
42	t, err := s.Store.RecipientsTree(ctx, true)
43	if err != nil {
44		return ExitError(ExitList, err, "failed to list recipients: %s", err)
45	}
46
47	fmt.Fprintln(stdout, t.Format(tree.INF))
48	return nil
49}
50
51func (s *Action) recipientsList(ctx context.Context) []string {
52	t, err := s.Store.RecipientsTree(ctxutil.WithHidden(ctx, true), false)
53	if err != nil {
54		debug.Log("failed to list recipients: %s", err)
55		return nil
56	}
57
58	return t.List(tree.INF)
59}
60
61// RecipientsComplete will print a list of recipients for bash
62// completion
63func (s *Action) RecipientsComplete(c *cli.Context) {
64	ctx := ctxutil.WithGlobalFlags(c)
65	for _, v := range s.recipientsList(ctx) {
66		fmt.Fprintln(stdout, v)
67	}
68}
69
70// RecipientsAdd adds new recipients
71func (s *Action) RecipientsAdd(c *cli.Context) error {
72	ctx := ctxutil.WithGlobalFlags(c)
73	store := c.String("store")
74	force := c.Bool("force")
75	added := 0
76
77	// select store
78	if store == "" {
79		store = cui.AskForStore(ctx, s.Store)
80	}
81
82	crypto := s.Store.Crypto(ctx, store)
83
84	// select recipient
85	recipients := []string(c.Args().Slice())
86	if len(recipients) < 1 {
87		debug.Log("no recipients given, asking for selection")
88		r, err := s.recipientsSelectForAdd(ctx, store)
89		if err != nil {
90			return err
91		}
92		recipients = r
93	}
94
95	debug.Log("adding recipients: %+v", recipients)
96	for _, r := range recipients {
97		keys, err := crypto.FindRecipients(ctx, r)
98		if err != nil {
99			out.Printf(ctx, "WARNING: Failed to list public key %q: %s", r, err)
100			if !force {
101				continue
102			}
103			keys = []string{r}
104		}
105		if len(keys) < 1 && !force && crypto.Name() == "gpgcli" {
106			out.Printf(ctx, "Warning: No matching valid key found. If the key is in your keyring you may need to validate it.")
107			out.Printf(ctx, "If this is your key: gpg --edit-key %s; trust (set to ultimate); quit", r)
108			out.Printf(ctx, "If this is not your key: gpg --edit-key %s; lsign; trust; save; quit", r)
109			out.Printf(ctx, "You may need to run 'gpg --update-trustdb' afterwards")
110			continue
111		}
112
113		recp := r
114		debug.Log("found recipients for %q: %+v", r, keys)
115
116		if !termio.AskForConfirmation(ctx, fmt.Sprintf("Do you want to add %q (key %q) as a recipient to the store %q?", crypto.FormatKey(ctx, recp, ""), recp, store)) {
117			continue
118		}
119
120		if err := s.Store.AddRecipient(ctx, store, recp); err != nil {
121			return ExitError(ExitRecipients, err, "failed to add recipient %q: %s", r, err)
122		}
123		added++
124	}
125	if added < 1 {
126		return ExitError(ExitUnknown, nil, "no key added")
127	}
128
129	out.Printf(ctx, "\nAdded %d recipients", added)
130	out.Printf(ctx, "You need to run 'gopass sync' to push these changes")
131	return nil
132}
133
134// RecipientsRemove removes recipients
135func (s *Action) RecipientsRemove(c *cli.Context) error {
136	ctx := ctxutil.WithGlobalFlags(c)
137	store := c.String("store")
138	force := c.Bool("force")
139	removed := 0
140
141	// select store
142	if store == "" {
143		store = cui.AskForStore(ctx, s.Store)
144	}
145
146	crypto := s.Store.Crypto(ctx, store)
147
148	// select recipient
149	recipients := []string(c.Args().Slice())
150	if len(recipients) < 1 {
151		rs, err := s.recipientsSelectForRemoval(ctx, store)
152		if err != nil {
153			return err
154		}
155		recipients = rs
156	}
157
158	for _, r := range recipients {
159		kl, err := crypto.FindIdentities(ctx, r)
160		if err == nil {
161			if len(kl) > 0 {
162				if !termio.AskForConfirmation(ctx, fmt.Sprintf("Do you want to remove yourself (%s) from the recipients?", r)) {
163					continue
164				}
165			}
166		}
167
168		keys, err := crypto.FindRecipients(ctx, r)
169		if err != nil {
170			out.Printf(ctx, "WARNING: Failed to list public key %q: %s", r, err)
171			out.Printf(ctx, "Hint: You can use `--force` to remove unknown keys.")
172			if !force {
173				continue
174			}
175			keys = []string{r}
176		}
177		if len(keys) < 1 && !force {
178			out.Printf(ctx, "Warning: No matching valid key found. If the key is in your keyring you may need to validate it.")
179			out.Printf(ctx, "If this is your key: gpg --edit-key %s; trust (set to ultimate); quit", r)
180			out.Printf(ctx, "If this is not your key: gpg --edit-key %s; lsign; trust; save; quit", r)
181			out.Printf(ctx, "You may need to run 'gpg --update-trustdb' afterwards")
182			continue
183		}
184
185		recp := r
186		if len(keys) > 0 {
187			recp = crypto.Fingerprint(ctx, keys[0])
188		}
189
190		if err := s.Store.RemoveRecipient(ctx, store, recp); err != nil {
191			return ExitError(ExitRecipients, err, "failed to remove recipient %q: %s", recp, err)
192		}
193		fmt.Fprintf(stdout, removalWarning, r)
194		removed++
195	}
196	if removed < 1 {
197		return ExitError(ExitUnknown, nil, "no key removed")
198	}
199
200	out.Printf(ctx, "\nRemoved %d recipients", removed)
201	out.Printf(ctx, "You need to run 'gopass sync' to push these changes")
202	return nil
203}
204
205func (s *Action) recipientsSelectForRemoval(ctx context.Context, store string) ([]string, error) {
206	crypto := s.Store.Crypto(ctx, store)
207
208	ids := s.Store.ListRecipients(ctx, store)
209	choices := make([]string, 0, len(ids))
210	for _, id := range ids {
211		choices = append(choices, crypto.FormatKey(ctx, id, ""))
212	}
213	if len(choices) < 1 {
214		return nil, nil
215	}
216
217	act, sel := cui.GetSelection(ctx, "Remove recipient -", choices)
218	switch act {
219	case "default":
220		fallthrough
221	case "show":
222		return []string{ids[sel]}, nil
223	default:
224		return nil, ExitError(ExitAborted, nil, "user aborted")
225	}
226}
227
228func (s *Action) recipientsSelectForAdd(ctx context.Context, store string) ([]string, error) {
229	crypto := s.Store.Crypto(ctx, store)
230
231	choices := []string{}
232	kl, _ := crypto.FindRecipients(ctx)
233	for _, key := range kl {
234		choices = append(choices, crypto.FormatKey(ctx, key, ""))
235	}
236	if len(choices) < 1 {
237		return nil, nil
238	}
239
240	act, sel := cui.GetSelection(ctx, "Add Recipient -", choices)
241	switch act {
242	case "default":
243		fallthrough
244	case "show":
245		return []string{kl[sel]}, nil
246	default:
247		return nil, ExitError(ExitAborted, nil, "user aborted")
248	}
249}
250