06da0a668a
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.
107 lines
3.3 KiB
Go
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...)
|
|
}
|