Files
passv/main.go
T
unkinben 06da0a668a
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
Add passv: a Vault-backed drop-in for pass
Provide a password-store CLI that keeps pass's on-disk layout (<name>.gpg
binary OpenPGP under $PASSWORD_STORE_DIR) but routes all encryption and
decryption through a vault-plugin-secrets-gpg engine mount, so the GPG private
key never lives on the client.

- Implement init/show/ls/insert/generate/edit/rm/mv/cp/find/grep/git with
  pass-compatible flags and implicit `passv <name>`=show dispatch.
- Resolve the recipient from a Vault ref: .gpg-id holds <mount>/<key> for
  passv-native stores, or a sibling .vault-id lets a store keep real GPG
  fingerprints in .gpg-id for dual gpg+Vault use.
- mv/cp re-encrypt across differing recipients; same-recipient moves copy the
  ciphertext verbatim.
- Ship the sibling build/packaging/CI: nfpm RPM to /usr/bin/passv (artifactapi
  rpm-internal on v* tag), Woodpecker PR/release pipelines, unit tests plus a
  real-Vault e2e that also proves dual gpg+Vault decryption.
- README covers store creation, migrating a GPG pass store, and dual-mode.
2026-07-16 22:25:31 +10:00

107 lines
3.3 KiB
Go

// Command passv is a Vault-backed, drop-in replacement for the standard
// password-store (`pass`). It keeps the exact on-disk layout — a tree of
// `<name>.gpg` binary OpenPGP files under $PASSWORD_STORE_DIR — but performs all
// encryption and decryption through a vault-plugin-secrets-gpg engine mount, so
// the private key never lives on the client. A store's `.gpg-id` holds a Vault
// key reference ("<mount>/<key>", e.g. "gpg/app") in place of a GPG fingerprint.
package main
import (
"errors"
"fmt"
"os"
)
var version = "dev"
// usageError is returned by commands for bad invocations (exit code 1 with the
// command's own usage line already included in the message).
type usageError struct{ msg string }
func (e *usageError) Error() string { return e.msg }
func main() {
if err := run(os.Args[1:]); err != nil {
var ue *usageError
if errors.As(err, &ue) {
fmt.Fprintln(os.Stderr, ue.msg)
} else {
fmt.Fprintln(os.Stderr, "passv: "+err.Error())
}
os.Exit(1)
}
}
func run(args []string) error {
if len(args) == 0 {
return cmdShow(nil) // default: list the whole store
}
switch args[0] {
case "init":
return cmdInit(args[1:])
case "ls", "list":
return cmdShow(args[1:]) // show handles the dir-vs-entry split
case "show", "cat":
return cmdShow(args[1:])
case "find", "search":
return cmdFind(args[1:])
case "grep":
return cmdGrep(args[1:])
case "insert", "add":
return cmdInsert(args[1:])
case "generate":
return cmdGenerate(args[1:])
case "edit":
return cmdEdit(args[1:])
case "rm", "remove", "delete":
return cmdRm(args[1:])
case "mv", "rename":
return cmdMv(args[1:])
case "cp", "copy":
return cmdCp(args[1:])
case "git":
return cmdGit(args[1:])
case "help", "-h", "--help":
usage()
return nil
case "version", "--version", "-v":
fmt.Printf("passv %s\n", version)
return nil
default:
// pass semantics: a bare name is an implicit `show`.
return cmdShow(args)
}
}
func usage() {
fmt.Print(`passv — a Vault-backed drop-in for pass
Usage:
passv [ls] [subfolder] list the store (or a subfolder)
passv [show] pass-name [-c|--clip[=N]] decrypt an entry (optionally to clipboard)
passv insert [-e|-m] [-f] pass-name add a new entry
passv generate [-n] [-c] [-i|-f] name [length]
generate and store a random password
passv edit pass-name edit an entry in $EDITOR
passv rm [-r] [-f] pass-name remove an entry or directory
passv mv|cp [-f] old new move/copy (re-encrypts across gpg-ids)
passv find term... list entries matching term
passv grep [-i] regexp search decrypted content
passv git args... run git in the store
passv init [-p subfolder] <mount>/<key>
point a (sub)store at a Vault gpg key
Environment:
PASSWORD_STORE_DIR store location (default ~/.password-store)
VAULT_ADDR, VAULT_TOKEN standard Vault connection settings
PASSWORD_STORE_CLIP_TIME seconds before the clipboard is cleared (45)
PASSWORD_STORE_GENERATED_LENGTH default generated length (25)
`)
}
// fatalf builds a plain error (no "passv:" doubling for already-formatted msgs).
func errf(format string, a ...interface{}) error {
return fmt.Errorf(format, a...)
}