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>
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user