Files
incredigo/internal/sink/gopass.go
T
leetcrypt 425c299359 Initial commit: Incredigo — local-first RAM-only credential custody
v1: discover → migrate → expiry tracking → sealed export/import.

- vault: memguard mlock/DONTDUMP arena, handle indirection, zeroize-on-drop
- discover: registry + 8 read-only scanners (aws, env, netrc, ssh, docker,
  kube, git) and a file/dir harvester (--path)
- sink: gopass streaming insert; length-prefixed bundle framing; Sealer
  interface with three impls — age (default, authenticated), hmac
  (authenticated, openssl-only encrypt-then-MAC), openssl (CBC fallback,
  unauthenticated; OpenSSL 3.x enc refuses AEAD)
- policy: local expiry engine, 60d/8w threshold parser
- audit: redacted append-only JSONL, injectable clock
- export/import: passphrase from no-echo prompt or $INCREDIGO_PASSPHRASE into
  locked memory; secrets stream gopass<->Sealer as bytes, never Go strings
- tests: scanner leak-checks, vault zeroize, bundle round-trip via fake gopass;
  go test ./... green, -race clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 07:57:34 -07:00

96 lines
2.6 KiB
Go

// Package sink writes secrets out of the RAM vault. gopass is the live store
// (GPG); openssl produces portable sealed backups. Plaintext flows vault → pipe
// → external tool's stdin, and never touches the filesystem in cleartext.
package sink
import (
"bytes"
"context"
"fmt"
"os/exec"
"path"
"strings"
"incredigo/internal/discover"
"incredigo/internal/vault"
)
// Gopass streams secrets into `gopass insert`. The binary path is configurable
// for testing; defaults to "gopass" on PATH.
type Gopass struct {
Bin string // default "gopass"
Prefix string // default "imported/"
}
func (g *Gopass) bin() string {
if g.Bin == "" {
return "gopass"
}
return g.Bin
}
// Available reports whether gopass is usable.
func (g *Gopass) Available() bool {
_, err := exec.LookPath(g.bin())
return err == nil
}
// StorePath is the gopass entry path for a credential: <prefix>/<source>/<slug>.
func (g *Gopass) StorePath(c discover.Credential) string {
prefix := g.Prefix
if prefix == "" {
prefix = "imported/"
}
return path.Join(prefix, c.Source, slug(c.Identity))
}
// Insert writes one credential's secret into gopass. The plaintext is taken from
// the locked vault buffer and piped directly to gopass stdin; we never build a
// plaintext string or temp file. `-f` forces overwrite; caller decides dedupe.
func (g *Gopass) Insert(ctx context.Context, v *vault.Vault, c discover.Credential, force bool) error {
buf, err := v.Open(c.Secret)
if err != nil {
return err
}
args := []string{"insert", "--multiline=false"}
if force {
args = append(args, "-f")
}
args = append(args, g.StorePath(c))
cmd := exec.CommandContext(ctx, g.bin(), args...)
// buf.Bytes() is the locked memory; gopass reads it from the pipe and GPG-
// encrypts. The bytes are never copied into a managed Go string.
cmd.Stdin = bytes.NewReader(buf.Bytes())
var stderr bytes.Buffer
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("gopass insert %q: %v: %s", g.StorePath(c), err, stderr.String())
}
// Shrink the in-memory window: this secret is now safely in gopass.
v.Forget(c.Secret)
return nil
}
// Exists reports whether an entry already exists (for --dedupe).
func (g *Gopass) Exists(ctx context.Context, storePath string) bool {
cmd := exec.CommandContext(ctx, g.bin(), "ls", "--flat")
out, err := cmd.Output()
if err != nil {
return false
}
for _, line := range strings.Split(string(out), "\n") {
if strings.TrimSpace(line) == storePath {
return true
}
}
return false
}
func slug(s string) string {
s = strings.ToLower(strings.TrimSpace(s))
repl := strings.NewReplacer(" / ", "/", " ", "-", "*", "")
return repl.Replace(s)
}