package main import ( "fmt" "os" "os/exec" "path/filepath" "sort" "strings" ) // store is a password-store directory ($PASSWORD_STORE_DIR or ~/.password-store) // whose entries are `.gpg` binary OpenPGP files. It is layout-compatible // with GnuPG `pass`; the only difference is that `.gpg-id` holds a Vault key // reference ("/") instead of a GPG fingerprint. type store struct { dir string } func openStore() (*store, error) { dir := os.Getenv("PASSWORD_STORE_DIR") if dir == "" { home, err := os.UserHomeDir() if err != nil { return nil, err } dir = filepath.Join(home, ".password-store") } return &store{dir: dir}, nil } func (s *store) entryPath(name string) string { return filepath.Join(s.dir, name+".gpg") } func (s *store) hasEntry(name string) bool { fi, err := os.Stat(s.entryPath(name)) return err == nil && !fi.IsDir() } func (s *store) isDir(name string) bool { fi, err := os.Stat(filepath.Join(s.dir, name)) return err == nil && fi.IsDir() } // vaultRef walks up from an entry's directory to the store root and returns the // governing Vault key reference, mirroring pass's per-subtree recipient // resolution. It prefers a `.vault-id` file — which lets a dual-mode store keep // real GPG fingerprints in `.gpg-id` (for plain `pass`) and the Vault ref here — // and otherwise falls back to a `.gpg-id` that itself holds the Vault ref (a // passv-native store). func (s *store) vaultRef(name string) (string, error) { if ref, ok := s.lookupUp(name, ".vault-id"); ok { return ref, nil } if ref, ok := s.lookupUp(name, ".gpg-id"); ok { if _, _, err := splitRef(ref); err == nil { return ref, nil } return "", fmt.Errorf(".gpg-id holds a GPG id, not a Vault ref; add a .vault-id containing / to read this store with passv") } return "", fmt.Errorf("password store is not initialized (no .gpg-id / .vault-id); run `passv init /`") } // lookupUp returns the first non-empty line of the nearest `file` at or above // the entry's directory. func (s *store) lookupUp(name, file string) (string, bool) { dir := filepath.Dir(s.entryPath(name)) for { if b, err := os.ReadFile(filepath.Join(dir, file)); err == nil { for _, line := range strings.Split(string(b), "\n") { if line = strings.TrimSpace(line); line != "" { return line, true } } } if dir == s.dir { return "", false } parent := filepath.Dir(dir) if parent == dir { return "", false } dir = parent } } // readCipher returns the raw bytes of an entry's `.gpg` file. func (s *store) readCipher(name string) ([]byte, error) { return os.ReadFile(s.entryPath(name)) } // writeCipher writes an entry, creating parent directories as needed. func (s *store) writeCipher(name string, data []byte) error { p := s.entryPath(name) if err := os.MkdirAll(filepath.Dir(p), 0o700); err != nil { return err } return os.WriteFile(p, data, 0o600) } // list returns the entry names (without the .gpg suffix) under sub, recursively. func (s *store) list(sub string) ([]string, error) { root := filepath.Join(s.dir, sub) var names []string err := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } base := d.Name() if d.IsDir() { if base == ".git" || (path != root && strings.HasPrefix(base, ".")) { return filepath.SkipDir } return nil } if !strings.HasSuffix(base, ".gpg") { return nil } rel, _ := filepath.Rel(s.dir, path) names = append(names, strings.TrimSuffix(rel, ".gpg")) return nil }) sort.Strings(names) return names, err } // --- git integration (optional; the store may or may not be a git repo) --- func (s *store) isGit() bool { fi, err := os.Stat(filepath.Join(s.dir, ".git")) return err == nil && (fi.IsDir() || fi.Mode().IsRegular()) } // gitPassthrough runs `git -C ...` with inherited stdio (for `passv git`). func (s *store) gitPassthrough(args []string) error { cmd := exec.Command("git", append([]string{"-C", s.dir}, args...)...) cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr return cmd.Run() } // commit stages everything and commits, quietly, when the store is a git repo. func (s *store) commit(msg string) { if !s.isGit() { return } _ = exec.Command("git", "-C", s.dir, "add", "-A").Run() // Nothing staged (e.g. content unchanged) is not an error worth surfacing. _ = exec.Command("git", "-C", s.dir, "commit", "-q", "-m", msg).Run() }