1package action
2
3import (
4	"context"
5	"fmt"
6	"net/url"
7	"strconv"
8	"strings"
9
10	"github.com/fatih/color"
11	"github.com/gopasspw/gopass/internal/cui"
12	"github.com/gopasspw/gopass/internal/out"
13	"github.com/gopasspw/gopass/pkg/clipboard"
14	"github.com/gopasspw/gopass/pkg/ctxutil"
15	"github.com/gopasspw/gopass/pkg/debug"
16	"github.com/gopasspw/gopass/pkg/fsutil"
17	"github.com/gopasspw/gopass/pkg/gopass/secrets"
18	"github.com/gopasspw/gopass/pkg/pwgen"
19	"github.com/gopasspw/gopass/pkg/pwgen/pwrules"
20	"github.com/gopasspw/gopass/pkg/termio"
21	"github.com/martinhoefling/goxkcdpwgen/xkcdpwgen"
22	"github.com/urfave/cli/v2"
23)
24
25func fmtfn(d int, n string, t string) string {
26	strlen := 40 - d
27	// indent - [N] - text (trailing spaces)
28	fmtStr := "%" + strconv.Itoa(d) + "s%s %-" + strconv.Itoa(strlen) + "s"
29	debug.Log("d: %d, n: %q, t: %q, strlen: %d, fmtStr: %q", d, n, t, strlen, fmtStr)
30	return fmt.Sprintf(fmtStr, "", color.GreenString("["+n+"]"), t)
31}
32
33// Create displays the password creation wizard
34func (s *Action) Create(c *cli.Context) error {
35	ctx := ctxutil.WithGlobalFlags(c)
36
37	out.Printf(ctx, "�� Welcome to the secret creation wizard (gopass create)!")
38	out.Printf(ctx, "�� Hint: Use 'gopass edit -c' for more control!")
39
40	acts := make(cui.Actions, 0, 5)
41	acts = append(acts, cui.Action{Name: "Website Login", Fn: s.createWebsite})
42	acts = append(acts, cui.Action{Name: "PIN Code (numerical)", Fn: s.createPIN})
43	acts = append(acts, cui.Action{Name: "Generic", Fn: s.createGeneric})
44
45	act, sel := cui.GetSelection(ctx, "Please select the type of secret you would like to create", acts.Selection())
46	switch act {
47	case "default":
48		fallthrough
49	case "show":
50		return acts.Run(ctx, c, sel)
51	default:
52		return ExitError(ExitAborted, nil, "user aborted")
53	}
54}
55
56// extractHostname tries to extract the hostname from a URL in a filepath-safe
57// way for use in the name of a secret
58func extractHostname(in string) string {
59	if in == "" {
60		return ""
61	}
62	// help url.Parse by adding a scheme if one is missing. This should still
63	// allow for any scheme, but by default we assume http (only for parsing)
64	urlStr := in
65	if !strings.Contains(urlStr, "://") {
66		urlStr = "http://" + urlStr
67	}
68	u, err := url.Parse(urlStr)
69	if err == nil {
70		if ch := fsutil.CleanFilename(u.Hostname()); ch != "" {
71			return ch
72		}
73	}
74	return fsutil.CleanFilename(in)
75}
76
77// createWebsite walks through the website credential creation wizard
78func (s *Action) createWebsite(ctx context.Context, c *cli.Context) error {
79	name := c.Args().First()
80	store := c.String("store")
81	force := c.Bool("force")
82
83	out.Print(ctx, "�� Creating Website login")
84	urlStr, err := termio.AskForString(ctx, fmtfn(2, "1", "URL"), "")
85	if err != nil {
86		return err
87	}
88	// the hostname is used as part of the name
89	hostname := extractHostname(urlStr)
90	if hostname == "" {
91		return ExitError(ExitUnknown, err, "Can not parse URL %q. Please use 'gopass edit' to manually create the secret", urlStr)
92	}
93
94	username, err := termio.AskForString(ctx, fmtfn(2, "2", "Login"), "")
95	if err != nil {
96		return err
97	}
98
99	genPw, err := termio.AskForBool(ctx, fmtfn(2, "3", "Generate Password?"), true)
100	if err != nil {
101		return err
102	}
103
104	var password string
105	if genPw {
106		password, err = s.createGeneratePassword(ctx, hostname)
107		if err != nil {
108			return err
109		}
110	} else {
111		password, err = termio.AskForPassword(ctx, fmt.Sprintf("password for %s", username), true)
112		if err != nil {
113			return err
114		}
115	}
116
117	comment, err := termio.AskForString(ctx, fmtfn(2, "4", "Comments"), "")
118	if err != nil {
119		debug.Log("failed to read comment input: %s", err)
120		// ignore the error, comments are considered optional
121	}
122
123	// select store
124	if store == "" {
125		store = cui.AskForStore(ctx, s.Store)
126	}
127
128	// generate name, ask for override if already taken
129	if store != "" {
130		store += "/"
131	}
132
133	// by default create will generate a name for the secret based on the user
134	// input. Only when the force flag is given it will accept a secrets path
135	// as the first argument.
136	if name == "" || !force {
137		name = fmt.Sprintf("%swebsites/%s/%s", store, fsutil.CleanFilename(hostname), fsutil.CleanFilename(username))
138	}
139	if force && !strings.HasPrefix(name, store) {
140		out.Warningf(ctx, "User supplied secret name %q does not match requested mount %q. Ignoring store flag.", name, store)
141	}
142
143	// force will also override the check for existing entries
144	if s.Store.Exists(ctx, name) && !force {
145		name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists. Choose another path or enter to overwrite"), name)
146		if err != nil {
147			return err
148		}
149	}
150
151	// populate a new secret with the gathered information
152	sec := secrets.New()
153	sec.SetPassword(password)
154	sec.Set("url", urlStr)
155	sec.Set("username", username)
156	sec.Set("comment", comment)
157	if u := pwrules.LookupChangeURL(hostname); u != "" {
158		sec.Set("password-change-url", u)
159	}
160	if err := s.Store.Set(ctxutil.WithCommitMessage(ctx, "Created new entry"), name, sec); err != nil {
161		return ExitError(ExitEncrypt, err, "failed to set %q: %s", name, err)
162	}
163	out.OKf(ctx, "Credentials saved to %q", name)
164
165	return s.createPrintOrCopy(ctx, c, name, password, genPw)
166}
167
168// createPrintOrCopy will display the created password (or copy to clipboard)
169func (s *Action) createPrintOrCopy(ctx context.Context, c *cli.Context, name, password string, genPw bool) error {
170	if !genPw {
171		return nil
172	}
173
174	if c.Bool("print") {
175		fmt.Fprintf(out.Stdout, "The generated password for %s is:\n%s\n", name, password)
176		return nil
177	}
178
179	if err := clipboard.CopyTo(ctx, name, []byte(password), s.cfg.ClipTimeout); err != nil {
180		return ExitError(ExitIO, err, "failed to copy to clipboard: %s", err)
181	}
182	return nil
183}
184
185// createPIN will walk through the numerical password (PIN) wizard
186func (s *Action) createPIN(ctx context.Context, c *cli.Context) error {
187	name := c.Args().First()
188	store := c.String("store")
189	force := c.Bool("force")
190
191	out.Printf(ctx, "�� Creating numerical PIN ...")
192	authority, err := termio.AskForString(ctx, fmtfn(2, "1", "Authority"), "")
193	if err != nil {
194		return err
195	}
196	if authority == "" {
197		return ExitError(ExitUnknown, nil, "Authority must not be empty")
198	}
199
200	application, err := termio.AskForString(ctx, fmtfn(2, "2", "Entity"), "")
201	if err != nil {
202		return err
203	}
204	if application == "" {
205		return ExitError(ExitUnknown, nil, "Application must not be empty")
206	}
207
208	genPw, err := termio.AskForBool(ctx, fmtfn(2, "3", "Generate PIN?"), false)
209	if err != nil {
210		return err
211	}
212
213	var password string
214	if genPw {
215		password, err = s.createGeneratePIN(ctx)
216		if err != nil {
217			return err
218		}
219	} else {
220		password, err = termio.AskForPassword(ctx, "PIN", true)
221		if err != nil {
222			return err
223		}
224	}
225
226	comment, err := termio.AskForString(ctx, fmtfn(2, "4", "Comments"), "")
227	if err != nil {
228		debug.Log("failed to read comment input: %s", err)
229		// ignore the error, comments are considered optional
230	}
231
232	// select store
233	if store == "" {
234		store = cui.AskForStore(ctx, s.Store)
235	}
236
237	// generate name, ask for override if already taken
238	if store != "" {
239		store += "/"
240	}
241
242	// by default create will generate a name for the secret based on the user
243	// input. Only when the force flag is given it will accept a secrets path
244	// as the first argument.
245	if name == "" || !force {
246		name = fmt.Sprintf("%spins/%s/%s", store, fsutil.CleanFilename(authority), fsutil.CleanFilename(application))
247	}
248	if force && !strings.HasPrefix(name, store) {
249		out.Warningf(ctx, "User supplied secret name %q does not match requested mount %q. Ignoring store flag.", name, store)
250	}
251
252	// force will also override the check for existing entries
253	if s.Store.Exists(ctx, name) && !force {
254		name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists. Choose another path or enter to overwrite"), name)
255		if err != nil {
256			return err
257		}
258	}
259
260	sec := secrets.New()
261	sec.SetPassword(password)
262	sec.Set("application", application)
263	sec.Set("comment", comment)
264	if err := s.Store.Set(ctxutil.WithCommitMessage(ctx, "Created new entry"), name, sec); err != nil {
265		return ExitError(ExitEncrypt, err, "failed to set %q: %s", name, err)
266	}
267	out.OKf(ctx, "Credentials saved to %q", name)
268
269	return s.createPrintOrCopy(ctx, c, name, password, genPw)
270}
271
272// createGeneric will walk through the generic secret wizard
273func (s *Action) createGeneric(ctx context.Context, c *cli.Context) error {
274	name := c.Args().Get(0)
275	store := c.String("store")
276	force := c.Bool("force")
277
278	out.Printf(ctx, "�� Creating generic secret ...")
279	shortname, err := termio.AskForString(ctx, fmtfn(2, "1", "Name"), "")
280	if err != nil {
281		return err
282	}
283	if shortname == "" {
284		return ExitError(ExitUnknown, nil, "Name must not be empty")
285	}
286
287	genPw, err := termio.AskForBool(ctx, fmtfn(2, "2", "Generate password?"), true)
288	if err != nil {
289		return err
290	}
291
292	var password string
293	if genPw {
294		password, err = s.createGeneratePassword(ctx, "")
295		if err != nil {
296			return err
297		}
298	} else {
299		password, err = termio.AskForPassword(ctx, fmt.Sprintf("password for %s", shortname), true)
300		if err != nil {
301			return err
302		}
303	}
304
305	// select store
306	if store == "" {
307		store = cui.AskForStore(ctx, s.Store)
308	}
309
310	// generate name, ask for override if already taken
311	if store != "" {
312		store += "/"
313	}
314
315	// by default create will generate a name for the secret based on the user
316	// input. Only when the force flag is given it will accept a secrets path
317	// as the first argument.
318	if name == "" || !force {
319		name = fmt.Sprintf("%smisc/%s", store, fsutil.CleanFilename(shortname))
320	}
321	if force && !strings.HasPrefix(name, store) {
322		out.Warningf(ctx, "User supplied secret name %q does not match requested mount %q. Ignoring store flag.", name, store)
323	}
324
325	// force will also override the check for existing entries
326	if s.Store.Exists(ctx, name) && !force {
327		name, err = termio.AskForString(ctx, fmtfn(2, "5", "Secret already exists. Choose another path or enter to overwrite"), name)
328		if err != nil {
329			return err
330		}
331	}
332
333	sec := secrets.New()
334	sec.SetPassword(password)
335	out.Printf(ctx, fmtfn(2, "3", "Enter zero or more key value pairs for this secret:"))
336	for {
337		key, err := termio.AskForString(ctx, fmtfn(4, "a", "Name (enter to quit)"), "")
338		if err != nil {
339			return err
340		}
341		if key == "" {
342			break
343		}
344		val, err := termio.AskForString(ctx, fmtfn(4, "b", "Value for Key '"+key+"'"), "")
345		if err != nil {
346			return err
347		}
348		sec.Set(key, val)
349	}
350	if err := s.Store.Set(ctxutil.WithCommitMessage(ctx, "Created new entry"), name, sec); err != nil {
351		return ExitError(ExitEncrypt, err, "failed to set %q: %s", name, err)
352	}
353	out.OKf(ctx, "Credentials saved to %q", name)
354
355	return s.createPrintOrCopy(ctx, c, name, password, genPw)
356}
357
358// createGeneratePasssword will walk through the password generation steps
359func (s *Action) createGeneratePassword(ctx context.Context, hostname string) (string, error) {
360	if _, found := pwrules.LookupRule(hostname); found {
361		out.Noticef(ctx, "Using password rules for %s ...", hostname)
362		length, err := termio.AskForInt(ctx, fmtfn(4, "b", "How long?"), defaultLength)
363		if err != nil {
364			return "", err
365		}
366		return pwgen.NewCrypticForDomain(length, hostname).Password(), nil
367	}
368	xkcd, err := termio.AskForBool(ctx, fmtfn(4, "a", "Human-pronounceable passphrase?"), false)
369	if err != nil {
370		return "", err
371	}
372	if xkcd {
373		length, err := termio.AskForInt(ctx, fmtfn(4, "b", "How many words?"), 4)
374		if err != nil {
375			return "", err
376		}
377		g := xkcdpwgen.NewGenerator()
378		g.SetNumWords(length)
379		g.SetDelimiter(" ")
380		g.SetCapitalize(true)
381		return string(g.GeneratePassword()), nil
382	}
383
384	length, err := termio.AskForInt(ctx, fmtfn(4, "b", "How long?"), defaultLength)
385	if err != nil {
386		return "", err
387	}
388
389	symbols, err := termio.AskForBool(ctx, fmtfn(4, "c", "Include symbols?"), false)
390	if err != nil {
391		return "", err
392	}
393
394	corp, err := termio.AskForBool(ctx, fmtfn(4, "d", "Strict rules?"), false)
395	if err != nil {
396		return "", err
397	}
398	if corp {
399		return pwgen.GeneratePasswordWithAllClasses(length, symbols)
400	}
401
402	return pwgen.GeneratePassword(length, symbols), nil
403}
404
405// createGeneratePIN will walk through the PIN generation steps
406func (s *Action) createGeneratePIN(ctx context.Context) (string, error) {
407	length, err := termio.AskForInt(ctx, fmtfn(4, "a", "How long?"), 4)
408	if err != nil {
409		return "", err
410	}
411
412	return pwgen.GeneratePasswordCharset(length, "0123456789"), nil
413}
414