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