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,86 @@
|
||||
# Adding a scanner
|
||||
|
||||
A scanner teaches incredigo to discover credentials from one location/format. Each
|
||||
is a single self-contained file in `internal/discover/` that implements the
|
||||
`Scanner` interface and registers itself in `init()`.
|
||||
|
||||
## The contract
|
||||
|
||||
```go
|
||||
type Scanner interface {
|
||||
Name() string
|
||||
Available() bool
|
||||
Scan(ctx context.Context, v *vault.Vault) ([]Credential, error)
|
||||
}
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. **Read-only, always.** Open files with `os.Open` (O_RDONLY). Never write,
|
||||
rename, or truncate a source file.
|
||||
2. **Never build plaintext into a long-lived value.** Read the secret into a
|
||||
`[]byte`, hand it to `v.Store(secret)`, and put the returned `*vault.Handle` in
|
||||
`Credential.Secret`. `Store` wipes your slice for you.
|
||||
3. **Pre-redact identities.** `Identity` and any `Meta` values are printed and
|
||||
logged. Use `redactMiddle(...)` (see `aws.go`) for key ids etc. Never put the
|
||||
secret in `Identity`/`Meta`.
|
||||
4. **Set `Modified`** from the file mtime — it feeds the expiry policy.
|
||||
5. **`Available()` should be cheap** (a `stat`), and return false when the source
|
||||
is absent so the run skips it cleanly.
|
||||
|
||||
## Template
|
||||
|
||||
```go
|
||||
package discover
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"incredigo/internal/vault"
|
||||
)
|
||||
|
||||
func init() { Register(&fooScanner{}) }
|
||||
|
||||
type fooScanner struct{}
|
||||
|
||||
func (f *fooScanner) Name() string { return "foo" }
|
||||
|
||||
func (f *fooScanner) path() string {
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".foo", "credentials")
|
||||
}
|
||||
|
||||
func (f *fooScanner) Available() bool {
|
||||
_, err := os.Stat(f.path())
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (f *fooScanner) Scan(ctx context.Context, v *vault.Vault) ([]Credential, error) {
|
||||
p := f.path()
|
||||
b, err := os.ReadFile(p) // read-only
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, _ := os.Stat(p)
|
||||
|
||||
secret := parseSecret(b) // your format logic
|
||||
return []Credential{{
|
||||
Source: f.Name(),
|
||||
Kind: KindToken,
|
||||
Identity: "foo / " + redactMiddle(/* a non-secret id */),
|
||||
Location: p,
|
||||
Modified: fi.ModTime(),
|
||||
Secret: v.Store([]byte(secret)),
|
||||
}}, nil
|
||||
}
|
||||
```
|
||||
|
||||
That's it — the registry picks it up automatically, and `scan`/`migrate`/`status`
|
||||
all gain the new source with no other changes.
|
||||
|
||||
## Tests
|
||||
|
||||
Add `foo_test.go` with a fixture file in a temp dir; assert that `Scan` returns
|
||||
the expected metadata and that `Identity`/`Meta` contain no secret substrings.
|
||||
@@ -0,0 +1,61 @@
|
||||
# Security model
|
||||
|
||||
incredigo's job is to *minimize the footprint and lifetime* of plaintext secrets.
|
||||
It is not a substitute for a trustworthy machine.
|
||||
|
||||
## Guarantees
|
||||
|
||||
| Threat | Mitigation |
|
||||
|---|---|
|
||||
| Plaintext leaks to disk | Never written. gopass GPG blobs or sealed bundles (age/hmac/openssl) only; no temp files. |
|
||||
| Plaintext leaks via swap | `mlock` on every secret buffer (memguard). |
|
||||
| Plaintext via core dump / process snapshot | `MADV_DONTDUMP`, guard pages, zeroize on drop; `memguard.CatchInterrupt`. |
|
||||
| Source files corrupted by the tool | Scanners are strictly read-only (`O_RDONLY`). |
|
||||
| Old cred destroyed before replacement verified | v1 deletes nothing. (v2 rotation verifies-new before revoke-old.) |
|
||||
| Secrets in logs | Audit log records only redacted metadata; identities are pre-redacted by scanners. |
|
||||
| Secrets in `ps`/argv | Secrets are piped via stdin (gopass); the backup passphrase reaches openssl over an inherited fd (`-pass fd:3`). Never argv, never env. |
|
||||
| Network exfiltration | No network calls in v1. |
|
||||
|
||||
## Honest limits
|
||||
|
||||
- incredigo cannot protect plaintext from a **compromised OS/kernel** or a malicious
|
||||
**gpg-agent** while it runs — those already see your secrets in normal use.
|
||||
- The sealed bundle's strength is its **passphrase**. Use a strong one; the KDF is
|
||||
scrypt (`age`, default) or PBKDF2 @ 600k iterations (`hmac`/`openssl`), but a weak
|
||||
passphrase is still a weak passphrase.
|
||||
- Go's runtime can in principle copy bytes during GC compaction. memguard buffers
|
||||
live outside the Go heap to avoid this; do not copy secret bytes into Go
|
||||
`string`s or heap `[]byte` that outlive immediate use. One documented exception:
|
||||
the `age` sealer's library API takes the passphrase as a `string` (the `hmac`
|
||||
sealer does not). The vaulted secrets themselves never become strings.
|
||||
|
||||
## Restoring a bundle by hand
|
||||
|
||||
If `incredigo` itself is unavailable, a bundle is recoverable with standard tools.
|
||||
The decrypted stream is the length-prefixed record format in
|
||||
`internal/sink/bundle.go` (`uint16 pathLen | path | uint32 chunkLen | chunk | …`).
|
||||
|
||||
**age (default):**
|
||||
|
||||
```sh
|
||||
age -d backup.incredigo.age > bundle.bin # prompts for the passphrase
|
||||
```
|
||||
|
||||
**hmac (`--sealer hmac`)** — verify integrity *before* decrypting:
|
||||
|
||||
```sh
|
||||
head -c 32 backup.incredigo.age > mac.bin # bundle = HMAC-SHA256(32) || ciphertext
|
||||
tail -c +33 backup.incredigo.age > ct.bin
|
||||
openssl dgst -sha256 -hmac "$PASS" -binary ct.bin | cmp - mac.bin # must match
|
||||
openssl enc -d -aes-256-ctr -pbkdf2 -iter 600000 -pass pass:"$PASS" -in ct.bin > bundle.bin
|
||||
```
|
||||
|
||||
**openssl (`--sealer openssl`, unauthenticated):**
|
||||
|
||||
```sh
|
||||
openssl enc -d -aes-256-cbc -pbkdf2 -iter 600000 -in backup.incredigo.age > bundle.bin
|
||||
```
|
||||
|
||||
## Reporting
|
||||
|
||||
Report vulnerabilities privately to the maintainer (do not open a public issue).
|
||||
@@ -0,0 +1,91 @@
|
||||
<!doctype html><html><head><meta charset="utf-8"><style>
|
||||
html,body{margin:0;background:#060504;font-family:'JetBrainsMono Nerd Font','JetBrains Mono',monospace}
|
||||
.grid{display:flex;gap:0}
|
||||
.card{width:300px;height:360px;display:flex;flex-direction:column;align-items:center;
|
||||
justify-content:center;gap:24px;border-right:1px solid #1c1714}
|
||||
.card:last-child{border:none}
|
||||
.lbl{color:#F7B968;font-size:15px;font-weight:700;letter-spacing:1px}
|
||||
.sub{color:#8a7c72;font-size:11px;margin-top:-16px}
|
||||
/* shared font for the in-svg wordmarks */
|
||||
.mono{font-family:'JetBrainsMono Nerd Font','JetBrainsMonoNL Nerd Font','JetBrains Mono','Fira Code',monospace;font-weight:700}
|
||||
</style></head><body>
|
||||
<div class="grid">
|
||||
|
||||
<!-- ===== A: dark tile, igo, diamond i-dot (hero) ===== -->
|
||||
<div class="card">
|
||||
<svg width="180" height="180" viewBox="0 0 200 200">
|
||||
<defs>
|
||||
<linearGradient id="ta" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#1A1410"/><stop offset="1" stop-color="#0B0806"/></linearGradient>
|
||||
<linearGradient id="la" x1="40" y1="80" x2="160" y2="150" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F7B968"/><stop offset="0.6" stop-color="#E8772E"/><stop offset="1" stop-color="#C2531A"/></linearGradient>
|
||||
</defs>
|
||||
<rect width="200" height="200" rx="44" fill="url(#ta)" stroke="#2a201a" stroke-width="2"/>
|
||||
<!-- custom i stem -->
|
||||
<rect x="44" y="92" width="15" height="54" rx="6" fill="url(#la)"/>
|
||||
<!-- diamond i-dot (nested strata) -->
|
||||
<path d="M51.5 60 L66 74 L51.5 88 L37 74 Z" fill="none" stroke="url(#la)" stroke-width="4"/>
|
||||
<path d="M51.5 68 L58 74 L51.5 80 L45 74 Z" fill="#F7B968"/>
|
||||
<!-- g o -->
|
||||
<text x="128" y="146" text-anchor="middle" class="mono" font-size="96" fill="url(#la)">go</text>
|
||||
</svg>
|
||||
<div class="lbl">A · DIAMOND-DOT</div><div class="sub">dark tile · i-dot = Strata core</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== B: orange tile, knockout igo ===== -->
|
||||
<div class="card">
|
||||
<svg width="180" height="180" viewBox="0 0 200 200">
|
||||
<defs>
|
||||
<linearGradient id="tb" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F8A23E"/><stop offset="1" stop-color="#C2531A"/></linearGradient>
|
||||
</defs>
|
||||
<rect width="200" height="200" rx="44" fill="url(#tb)"/>
|
||||
<rect x="44" y="92" width="15" height="54" rx="6" fill="#1A0E06"/>
|
||||
<path d="M51.5 60 L66 74 L51.5 88 L37 74 Z" fill="#1A0E06"/>
|
||||
<text x="128" y="146" text-anchor="middle" class="mono" font-size="96" fill="#1A0E06">go</text>
|
||||
</svg>
|
||||
<div class="lbl">B · KNOCKOUT</div><div class="sub">orange tile · bold solid</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== C: Strata mark + igo lockup ===== -->
|
||||
<div class="card">
|
||||
<svg width="180" height="180" viewBox="0 0 200 200">
|
||||
<defs>
|
||||
<linearGradient id="tc" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#1A1410"/><stop offset="1" stop-color="#0B0806"/></linearGradient>
|
||||
<linearGradient id="lc" x1="40" y1="20" x2="160" y2="170" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F7B968"/><stop offset="0.6" stop-color="#E8772E"/><stop offset="1" stop-color="#C2531A"/></linearGradient>
|
||||
</defs>
|
||||
<rect width="200" height="200" rx="44" fill="url(#tc)" stroke="#2a201a" stroke-width="2"/>
|
||||
<g fill="none" stroke="url(#lc)" stroke-linejoin="round" transform="translate(100,78)">
|
||||
<path d="M0 -46 L46 0 L0 46 L-46 0 Z" stroke-width="8" opacity="0.45"/>
|
||||
<path d="M0 -26 L26 0 L0 26 L-26 0 Z" stroke-width="8"/>
|
||||
</g>
|
||||
<path d="M100 70 L108 78 L100 86 L92 78 Z" fill="#F7B968" transform="translate(0,0)"/>
|
||||
<text x="100" y="168" text-anchor="middle" class="mono" font-size="46" letter-spacing="4" fill="url(#lc)">igo</text>
|
||||
</svg>
|
||||
<div class="lbl">C · STACK</div><div class="sub">Strata mark over igo</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== D: ig + diamond-o (o = vault) ===== -->
|
||||
<div class="card">
|
||||
<svg width="180" height="180" viewBox="0 0 200 200">
|
||||
<defs>
|
||||
<linearGradient id="td" x1="0" y1="0" x2="200" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#1A1410"/><stop offset="1" stop-color="#0B0806"/></linearGradient>
|
||||
<linearGradient id="ld" x1="30" y1="80" x2="170" y2="150" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F7B968"/><stop offset="0.6" stop-color="#E8772E"/><stop offset="1" stop-color="#C2531A"/></linearGradient>
|
||||
</defs>
|
||||
<rect width="200" height="200" rx="44" fill="url(#td)" stroke="#2a201a" stroke-width="2"/>
|
||||
<text x="62" y="146" text-anchor="middle" class="mono" font-size="96" fill="url(#ld)">ig</text>
|
||||
<!-- diamond-o -->
|
||||
<g fill="none" stroke="url(#ld)" stroke-linejoin="round" transform="translate(146,116)">
|
||||
<path d="M0 -30 L30 0 L0 30 L-30 0 Z" stroke-width="9"/>
|
||||
<path d="M0 -13 L13 0 L0 13 L-13 0 Z" stroke-width="7"/>
|
||||
</g>
|
||||
</svg>
|
||||
<div class="lbl">D · DIAMOND-O</div><div class="sub">the 'o' becomes the vault</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body></html>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 249 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 351 KiB |
@@ -0,0 +1,61 @@
|
||||
<svg width="1200" height="360" viewBox="0 0 1200 360" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Incredigo — guards your credentials in locked memory, leaves no trace on disk">
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="1200" y2="360" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#161210"/>
|
||||
<stop offset="1" stop-color="#0C0907"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="fur" x1="40" y1="40" x2="300" y2="320" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F08A2C"/>
|
||||
<stop offset="0.55" stop-color="#D9661A"/>
|
||||
<stop offset="1" stop-color="#A8410F"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="word" x1="360" y1="120" x2="360" y2="200" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F7B968"/>
|
||||
<stop offset="1" stop-color="#E8772E"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="core" cx="256" cy="256" r="40" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FBD9A6"/>
|
||||
<stop offset="1" stop-color="#F08A2C"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<rect width="1200" height="360" rx="20" fill="url(#bg)"/>
|
||||
|
||||
<!-- ===== Mark: STRATA (nested diamonds, 512 art scaled *0.5) ===== -->
|
||||
<g transform="translate(54,42) scale(0.5)">
|
||||
<g fill="none" stroke="url(#fur)" stroke-linejoin="round">
|
||||
<path d="M256 32 L480 256 L256 480 L32 256 Z" stroke-width="18" opacity="0.42"/>
|
||||
<path d="M256 104 L408 256 L256 408 L104 256 Z" stroke-width="18" opacity="0.68"/>
|
||||
<path d="M256 172 L340 256 L256 340 L172 256 Z" stroke-width="18"/>
|
||||
</g>
|
||||
<path d="M256 222 L290 256 L256 290 L222 256 Z" fill="url(#core)"/>
|
||||
</g>
|
||||
|
||||
<!-- ===== Wordmark ===== -->
|
||||
<text x="360" y="158" font-family="'JetBrains Mono','Fira Code',ui-monospace,monospace"
|
||||
font-size="68" font-weight="700" letter-spacing="3" fill="#8C5E2E"><tspan>in</tspan><tspan fill="url(#word)">CRED</tspan><tspan>i</tspan><tspan fill="url(#word)">GO</tspan></text>
|
||||
<text x="362" y="206" font-family="'Inter','DejaVu Sans',sans-serif"
|
||||
font-size="25" font-weight="500" fill="#C9BBB0">Guards your credentials in locked memory.</text>
|
||||
<text x="362" y="240" font-family="'Inter','DejaVu Sans',sans-serif"
|
||||
font-size="25" font-weight="500" fill="#C9BBB0">Leaves no trace on disk.</text>
|
||||
|
||||
<!-- guarantee chips -->
|
||||
<g font-family="'JetBrains Mono',ui-monospace,monospace" font-size="16" font-weight="600">
|
||||
<g transform="translate(362,278)">
|
||||
<rect width="150" height="34" rx="17" fill="#1F1611" stroke="#D9661A" stroke-width="1.5"/>
|
||||
<text x="75" y="22" text-anchor="middle" fill="#F7B968">local-first</text>
|
||||
</g>
|
||||
<g transform="translate(524,278)">
|
||||
<rect width="138" height="34" rx="17" fill="#1F1611" stroke="#D9661A" stroke-width="1.5"/>
|
||||
<text x="69" y="22" text-anchor="middle" fill="#F7B968">RAM-only</text>
|
||||
</g>
|
||||
<g transform="translate(674,278)">
|
||||
<rect width="118" height="34" rx="17" fill="#1F1611" stroke="#D9661A" stroke-width="1.5"/>
|
||||
<text x="59" y="22" text-anchor="middle" fill="#F7B968">offline</text>
|
||||
</g>
|
||||
<g transform="translate(804,278)">
|
||||
<rect width="196" height="34" rx="17" fill="#1F1611" stroke="#D9661A" stroke-width="1.5"/>
|
||||
<text x="98" y="22" text-anchor="middle" fill="#F7B968">no plaintext at rest</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.3 KiB |
@@ -0,0 +1,6 @@
|
||||
+------------------------------------------------------------+
|
||||
| /\ INCREDIGO |
|
||||
| /<>\ in-CRED-i-GO : credentials, in Go |
|
||||
| \<>/ Guards your credentials in locked memory. |
|
||||
| \/ Leaves no trace on disk. |
|
||||
+------------------------------------------------------------+
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1,20 @@
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Incredigo logo: nested diamonds around a sealed core — layered credential custody">
|
||||
<defs>
|
||||
<linearGradient id="strata" x1="48" y1="48" x2="464" y2="464" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#F7B968"/>
|
||||
<stop offset="0.5" stop-color="#E8772E"/>
|
||||
<stop offset="1" stop-color="#A8410F"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="core" cx="256" cy="256" r="40" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#FBD9A6"/>
|
||||
<stop offset="1" stop-color="#F08A2C"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<g fill="none" stroke="url(#strata)" stroke-linejoin="round">
|
||||
<path d="M256 32 L480 256 L256 480 L32 256 Z" stroke-width="18" opacity="0.42"/>
|
||||
<path d="M256 104 L408 256 L256 408 L104 256 Z" stroke-width="18" opacity="0.68"/>
|
||||
<path d="M256 172 L340 256 L256 340 L172 256 Z" stroke-width="18"/>
|
||||
</g>
|
||||
<path d="M256 222 L290 256 L256 290 L222 256 Z" fill="url(#core)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
Reference in New Issue
Block a user