package main import ( "fmt" "os" "path" "path/filepath" "strings" ) // cmdRm removes an entry or (with -r) a directory. func cmdRm(args []string) error { recursive, args := popBool(args, "-r", "--recursive") force, args := popBool(args, "-f", "--force") if len(args) != 1 { return &usageError{"Usage: passv rm [-r] [-f] pass-name"} } name := strings.Trim(args[0], "/") s, err := openStore() if err != nil { return err } switch { case s.isDir(name): if !recursive { return errf("Error: %s is a directory (use -r to remove it).", name) } if !force && !confirm(fmt.Sprintf("Delete directory %s and everything in it?", name)) { return nil } if err := os.RemoveAll(filepath.Join(s.dir, name)); err != nil { return err } case s.hasEntry(name): if !force && !confirm(fmt.Sprintf("Delete %s?", name)) { return nil } if err := os.Remove(s.entryPath(name)); err != nil { return err } default: return errf("Error: %s is not in the password store.", name) } s.commit("Remove " + name + " from store.") return nil } func cmdMv(args []string) error { return relocate(args, false) } func cmdCp(args []string) error { return relocate(args, true) } // relocate moves (keep=false) or copies (keep=true) an entry or directory. // When source and destination fall under different `.gpg-id`s, affected entries // are transparently decrypted and re-encrypted to the new key. func relocate(args []string, keep bool) error { force, args := popBool(args, "-f", "--force") if len(args) != 2 { verb := "mv" if keep { verb = "cp" } return &usageError{"Usage: passv " + verb + " [-f] old-path new-path"} } src := strings.Trim(args[0], "/") dst := strings.Trim(args[1], "/") s, err := openStore() if err != nil { return err } v, err := newVault() if err != nil { return err } // If the destination is an existing directory, move into it under src's base. if s.isDir(dst) { dst = path.Join(dst, path.Base(src)) } switch { case s.hasEntry(src): if err := relocateEntry(s, v, src, dst, keep, force); err != nil { return err } case s.isDir(src): names, err := s.list(src) if err != nil { return err } for _, n := range names { rel := strings.TrimPrefix(n, src+"/") if err := relocateEntry(s, v, n, path.Join(dst, rel), keep, force); err != nil { return err } } if !keep { _ = os.RemoveAll(filepath.Join(s.dir, src)) } default: return errf("Error: %s is not in the password store.", src) } action := "Rename" if keep { action = "Copy" } s.commit(fmt.Sprintf("%s %s to %s.", action, src, dst)) return nil } func relocateEntry(s *store, v *vaultClient, src, dst string, keep, force bool) error { if s.hasEntry(dst) && !force { if !confirm(fmt.Sprintf("%s already exists. Overwrite it?", dst)) { return nil } } srcRef, err := s.vaultRef(src) if err != nil { return err } dstRef, err := s.vaultRef(dst) if err != nil { return err } if srcRef == dstRef { // Same recipient: move/copy the ciphertext verbatim, no crypto needed. ct, err := s.readCipher(src) if err != nil { return err } if err := s.writeCipher(dst, ct); err != nil { return err } } else { // Different recipient: decrypt with the old key, re-encrypt with the new. plain, err := decryptEntry(s, v, src) if err != nil { return err } if err := encryptEntry(s, v, dst, plain); err != nil { return err } } if !keep { return os.Remove(s.entryPath(src)) } return nil } // cmdGit runs git inside the store directory. func cmdGit(args []string) error { s, err := openStore() if err != nil { return err } return s.gitPassthrough(args) }