Encryption spike for Cairn — chunked-STREAM seekable age reads + credential-derived keys. Throwaway prototype feeding ENCRYPTION-COMPARISON.md. Not part of Cairn's codebase.
Find a file
2026-07-25 19:04:53 +00:00
.forgejo/workflows ci: non-interactive age -d -i interop + job timeout safety net 2026-07-25 19:01:41 +00:00
cmd cmd/interop: non-interactive X25519 identity (age -d -i, no TTY prompt) 2026-07-25 19:01:16 +00:00
credkey credkey: tests (roundtrip, PIN load-bearing, zeroing) 2026-07-25 18:52:49 +00:00
ranged ranged: tests (interop, PQ, covering-chunk-only assertions) 2026-07-25 18:52:14 +00:00
BRIEF.md Flint spike brief 2026-07-25 18:14:41 +00:00
FINDINGS.md FINDINGS: scored verdicts for both questions (measured on runner #298) 2026-07-25 19:04:53 +00:00
go.mod Add go.mod (age v1.3.1 pin) 2026-07-25 18:51:09 +00:00
README.md docs: README 2026-07-25 18:54:18 +00:00

Flint

Throwaway Go spike to de-risk two encryption decisions for Cairn before we commit to them. Not part of Cairn's codebase and never will be — a lab bench. Findings get folded into Cairn's ENCRYPTION-COMPARISON.md; see FINDINGS.md for the verdict. The brief is in BRIEF.md.

Stack: Go + filippo.io/age v1.3.1 (same pin as Cairn).

What's here

Path Question What it shows
ranged/ Seekable chunked-STREAM reads (the performance half) age v1.3.1 already ships the seekable primitive (age.DecryptReaderAt). Thin instrumented adapter + S3-style ranged-GET backend proving a Range fetches only the covering 64 KiB chunks.
cmd/bench/ " Full-object decrypt vs ranged read: bytes fetched, backend GETs, latency.
credkey/ Credential-derived keys (the risky half) Derive a key from credential + PIN, decrypt-on-read, zero it — and demonstrate exactly how far Go zeroing goes.
cmd/credkey-demo/ " End-to-end lifecycle with before/after key bytes.
cmd/interop/ interop Writes a file with the library so the age CLI can decrypt it.
.forgejo/workflows/spike.yml run it Builds, tests, benches, and runs the demos on the homelab LAN runner.

Headline result

age's built-in DecryptReaderAt maps an HTTP Range to the covering STREAM chunks and issues one backend ReadAt per covering chunk. To serve a 1 MiB Range from the middle of a ~1 GiB object it fetches ~1 MiB, not ~1 GiB — a ~1000× reduction in bytes fetched, with O(1) memory and no temp file. The custom reader the brief imagined does not need to be written; Cairn should adopt the upstream primitive (issue #15). Post-quantum keys don't change this (HPKE only wraps the file key; the STREAM payload is identical).

The credential-derived-key half is the risky one; the honest verdict (local/LDAP-only, and even there it relocates the E2E trade-off) is in FINDINGS.md.

Run

Locally (needs Go 1.24+ and network for modules):

go test ./...
go run ./cmd/bench -size $((1024*1024*1024)) -range $((1024*1024))
go run ./cmd/credkey-demo

Or on the homelab runner: dispatch the spike workflow, then read the runner pod logs (kubectl logs -n forgejo-runner deploy/forgejo-runner) — the job-log HTTP API 404s on this Forgejo version.

Isolation

Per the brief: work lives only in this repo. Nothing here touches Cairn's repos, its dogfood instance, or homelab-config.