package main import ( "bytes" "fmt" "io" "os" "os/exec" "path/filepath" "strings" ) // cmdInsert adds a new entry, prompting for the secret (or reading it from stdin // with -e/--echo, or reading multiline content with -m/--multiline). func cmdInsert(args []string) error { echo, args := popBool(args, "-e", "--echo") multiline, args := popBool(args, "-m", "--multiline") force, args := popBool(args, "-f", "--force") if len(args) != 1 { return &usageError{"Usage: passv insert [-e|-m] [-f] pass-name"} } name := strings.Trim(args[0], "/") s, err := openStore() if err != nil { return err } if s.hasEntry(name) && !force { if !confirm(fmt.Sprintf("An entry already exists for %s. Overwrite it?", name)) { return nil } } var content []byte if multiline { fmt.Fprintf(os.Stderr, "Enter contents of %s and press Ctrl+D when finished:\n\n", name) if content, err = io.ReadAll(os.Stdin); err != nil { return err } } else { if content, err = readSecret(name, echo); err != nil { return err } } v, err := newVault() if err != nil { return err } if err := encryptEntry(s, v, name, content); err != nil { return err } s.commit("Add given password for " + name + " to store.") return nil } // cmdGenerate creates a random password, stores it, and prints or copies it. func cmdGenerate(args []string) error { noSymbols, args := popBool(args, "-n", "--no-symbols") clip, args := popBool(args, "-c", "--clip") inPlace, args := popBool(args, "-i", "--in-place") force, args := popBool(args, "-f", "--force") if len(args) < 1 || len(args) > 2 { return &usageError{"Usage: passv generate [-n] [-c] [-i|-f] pass-name [length]"} } name := strings.Trim(args[0], "/") length := defaultLength() if len(args) == 2 { length = atoiDefault(args[1], length) } s, err := openStore() if err != nil { return err } v, err := newVault() if err != nil { return err } pw, err := genPassword(length, !noSymbols) if err != nil { return err } var content []byte switch { case inPlace && s.hasEntry(name): cur, err := decryptEntry(s, v, name) if err != nil { return err } lines := strings.Split(string(cur), "\n") lines[0] = pw content = []byte(strings.Join(lines, "\n")) default: if s.hasEntry(name) && !force { if !confirm(fmt.Sprintf("An entry already exists for %s. Overwrite it?", name)) { return nil } } content = []byte(pw) } if err := encryptEntry(s, v, name, content); err != nil { return err } s.commit("Add generated password for " + name + " to store.") if clip { if err := copyClip(pw); err != nil { return err } fmt.Fprintf(os.Stderr, "Copied %s to clipboard. Will clear in %d seconds.\n", name, clipTime()) return nil } fmt.Printf("The generated password for %s is:\n%s\n", name, pw) return nil } // cmdEdit decrypts an entry into a temp file, opens $EDITOR, and re-encrypts. func cmdEdit(args []string) error { if len(args) != 1 { return &usageError{"Usage: passv edit pass-name"} } name := strings.Trim(args[0], "/") s, err := openStore() if err != nil { return err } v, err := newVault() if err != nil { return err } var current []byte if s.hasEntry(name) { if current, err = decryptEntry(s, v, name); err != nil { return err } } tmp, err := secureTempFile(filepath.Base(name)) if err != nil { return err } defer os.Remove(tmp) if err := os.WriteFile(tmp, current, 0o600); err != nil { return err } if err := runEditor(tmp); err != nil { return err } edited, err := os.ReadFile(tmp) if err != nil { return err } if bytes.Equal(edited, current) { fmt.Fprintf(os.Stderr, "Password unchanged for %s.\n", name) return nil } if err := encryptEntry(s, v, name, edited); err != nil { return err } s.commit("Edit password for " + name + ".") return nil } // secureTempFile prefers /dev/shm (tmpfs, never hits disk) for the cleartext // editor buffer, falling back to the OS temp dir. func secureTempFile(hint string) (string, error) { dir := os.TempDir() if fi, err := os.Stat("/dev/shm"); err == nil && fi.IsDir() { dir = "/dev/shm" } f, err := os.CreateTemp(dir, "passv-"+hint+"-*.txt") if err != nil { return "", err } name := f.Name() f.Close() return name, nil } func runEditor(path string) error { editor := os.Getenv("EDITOR") if editor == "" { editor = "vi" } cmd := exec.Command("sh", "-c", editor+" \""+path+"\"") cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr return cmd.Run() }