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.
This commit is contained in:
+133
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// cmdShow decrypts an entry, or lists a directory (pass's implicit behaviour:
|
||||
// no name, or a name that is a directory, lists; otherwise show).
|
||||
func cmdShow(args []string) error {
|
||||
clip, line, rest := parseClip(args)
|
||||
|
||||
name := ""
|
||||
if len(rest) > 0 {
|
||||
name = strings.Trim(rest[0], "/")
|
||||
}
|
||||
|
||||
s, err := openStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if name == "" || s.isDir(name) {
|
||||
return printTree(s, name)
|
||||
}
|
||||
if !s.hasEntry(name) {
|
||||
return errf("Error: %s is not in the password store.", name)
|
||||
}
|
||||
|
||||
v, err := newVault()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
plain, err := decryptEntry(s, v, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !clip {
|
||||
os.Stdout.Write(plain)
|
||||
if len(plain) == 0 || plain[len(plain)-1] != '\n' {
|
||||
fmt.Println()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
lines := strings.Split(string(plain), "\n")
|
||||
if line-1 >= len(lines) {
|
||||
return errf("There is no password on line %d of %s.", line, name)
|
||||
}
|
||||
if err := copyClip(lines[line-1]); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "Copied %s to clipboard. Will clear in %d seconds.\n", name, clipTime())
|
||||
return nil
|
||||
}
|
||||
|
||||
// cmdFind lists entries whose path contains any of the given terms.
|
||||
func cmdFind(args []string) error {
|
||||
if len(args) == 0 {
|
||||
return &usageError{"Usage: passv find term..."}
|
||||
}
|
||||
s, err := openStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names, err := s.list("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println("Search Terms: " + strings.Join(args, ", "))
|
||||
for _, n := range names {
|
||||
for _, t := range args {
|
||||
if strings.Contains(n, t) {
|
||||
fmt.Println(n)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cmdGrep decrypts every entry and prints lines matching the pattern.
|
||||
func cmdGrep(args []string) error {
|
||||
ignoreCase, args := popBool(args, "-i", "--ignore-case")
|
||||
if len(args) != 1 {
|
||||
return &usageError{"Usage: passv grep [-i] search-regexp"}
|
||||
}
|
||||
pattern := args[0]
|
||||
if ignoreCase {
|
||||
pattern = "(?i)" + pattern
|
||||
}
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid pattern: %w", err)
|
||||
}
|
||||
|
||||
s, err := openStore()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v, err := newVault()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
names, err := s.list("")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, n := range names {
|
||||
plain, err := decryptEntry(s, v, n)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "passv: skipping %s: %v\n", n, err)
|
||||
continue
|
||||
}
|
||||
var hits []string
|
||||
for _, ln := range strings.Split(string(plain), "\n") {
|
||||
if re.MatchString(ln) {
|
||||
hits = append(hits, ln)
|
||||
}
|
||||
}
|
||||
if len(hits) > 0 {
|
||||
fmt.Printf("%s:\n", n)
|
||||
for _, h := range hits {
|
||||
fmt.Println(h)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user