425c299359
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>
100 lines
3.0 KiB
Go
100 lines
3.0 KiB
Go
package sink
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
|
|
"github.com/awnumar/memguard"
|
|
)
|
|
|
|
// OpenSSL is a non-default Sealer kept for environments that have the openssl CLI
|
|
// but not age, and for hand-restorability with a one-line openssl command.
|
|
//
|
|
// openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in backup.enc
|
|
//
|
|
// IMPORTANT — this mode is UNAUTHENTICATED. OpenSSL 3.x's `enc` command refuses
|
|
// AEAD ciphers ("AEAD ciphers not supported"), so there is no GCM/Poly1305 tag:
|
|
// a tampered or truncated bundle will decrypt to garbage with no error. Prefer the
|
|
// age sealer (see age.go), which is authenticated, for any backup you care about.
|
|
//
|
|
// The passphrase is handed to openssl over an inherited file descriptor
|
|
// (`-pass fd:3`), never via argv (visible in `ps`) or the environment (visible in
|
|
// /proc/<pid>/environ). The bytes come straight from a locked buffer.
|
|
type OpenSSL struct {
|
|
Bin string // default "openssl"
|
|
Iter int // default 600000
|
|
}
|
|
|
|
func (o *OpenSSL) bin() string {
|
|
if o.Bin == "" {
|
|
return "openssl"
|
|
}
|
|
return o.Bin
|
|
}
|
|
|
|
func (o *OpenSSL) iter() int {
|
|
if o.Iter == 0 {
|
|
return 600000
|
|
}
|
|
return o.Iter
|
|
}
|
|
|
|
func (o *OpenSSL) Name() string { return "openssl" }
|
|
|
|
func (o *OpenSSL) run(ctx context.Context, decrypt bool, in io.Reader, out io.Writer, pass *memguard.LockedBuffer) error {
|
|
args := []string{"enc", "-aes-256-cbc", "-pbkdf2", "-iter", fmt.Sprint(o.iter()), "-pass", "fd:3"}
|
|
if decrypt {
|
|
args = append(args, "-d")
|
|
} else {
|
|
args = append(args, "-salt")
|
|
}
|
|
return opensslEnc(ctx, o.bin(), args, in, out, pass)
|
|
}
|
|
|
|
func (o *OpenSSL) Seal(ctx context.Context, plaintext io.Reader, out io.Writer, pass *memguard.LockedBuffer) error {
|
|
return o.run(ctx, false, plaintext, out, pass)
|
|
}
|
|
|
|
func (o *OpenSSL) Open(ctx context.Context, in io.Reader, out io.Writer, pass *memguard.LockedBuffer) error {
|
|
return o.run(ctx, true, in, out, pass)
|
|
}
|
|
|
|
// opensslEnc runs `openssl <args...>` with stdin/stdout wired to in/out and the
|
|
// passphrase delivered over inherited fd 3. The caller must include
|
|
// `-pass fd:3` in args. The passphrase bytes come straight from a locked buffer —
|
|
// they never appear in argv (visible in `ps`) or the environment (visible in
|
|
// /proc/<pid>/environ). Shared by the OpenSSL and HMAC sealers.
|
|
func opensslEnc(ctx context.Context, bin string, args []string, in io.Reader, out io.Writer, pass *memguard.LockedBuffer) error {
|
|
pr, pw, err := os.Pipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, bin, args...)
|
|
cmd.Stdin = in
|
|
cmd.Stdout = out
|
|
cmd.ExtraFiles = []*os.File{pr} // child sees this as fd 3
|
|
var stderr bytes.Buffer
|
|
cmd.Stderr = &stderr
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
pr.Close()
|
|
pw.Close()
|
|
return err
|
|
}
|
|
pr.Close() // the child holds its own copy of the read end
|
|
|
|
// Feed the passphrase from locked memory, then close so openssl sees EOF.
|
|
_, werr := pw.Write(pass.Bytes())
|
|
pw.Close()
|
|
|
|
if err := cmd.Wait(); err != nil {
|
|
return fmt.Errorf("%s: %v: %s", bin, err, stderr.String())
|
|
}
|
|
return werr
|
|
}
|