From 2a15bf2bc40a1b024236a46f4ec6fdc80b39f8f7 Mon Sep 17 00:00:00 2001 From: "glm-5.3-flash" Date: Sat, 31 Oct 2026 12:56:05 +0000 Subject: [PATCH] =?UTF-8?q?docs(review):=20first=20review=20=E2=80=94=20al?= =?UTF-8?q?kssh=20consumer=20input=20on=20the=20decrypt=20plaintext=20wind?= =?UTF-8?q?ow=20(establishes=20docs/reviews/)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...1-decrypt-plaintext-window-alkssh-input.md | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 docs/reviews/001-decrypt-plaintext-window-alkssh-input.md diff --git a/docs/reviews/001-decrypt-plaintext-window-alkssh-input.md b/docs/reviews/001-decrypt-plaintext-window-alkssh-input.md new file mode 100644 index 0000000..8211769 --- /dev/null +++ b/docs/reviews/001-decrypt-plaintext-window-alkssh-input.md @@ -0,0 +1,93 @@ +# Review 001 — The decrypt plaintext window: consumer input from alkssh on zeroizing decryption output + +## Status + +Open — filed 2026-10-31. Consumer input from alkssh 0.1.0 (published +2026-10-31), under the family's upstream posture: file asks upstream, +never absorb silently. The ask was recorded alkssh-side (alkssh ADR-005 +§3, "Zeroization ownership (the known gap)") and is now filed where the +fix belongs. This is the first review in this crate's series; the +numbering follows the alkcall/alktty/alktunnels house pattern +(`docs/reviews/00N-name.md`, numbering independent per crate). + +Not blocking: the consuming contract treats the adapter-side wrap as +the shipped answer. The ask is an API-hygiene improvement that removes +a window a consumer cannot otherwise close. + +## The finding + +`VaultService::decrypt` returns `Result` +(`src/service.rs:335`; the plaintext materializes in +`encryption::decrypt`, `src/encryption.rs:211`). The decrypted +plaintext is a **bare heap `String`** from the moment decryption +succeeds inside the vault until the caller drops it — no zeroization, +nothing the caller can do about it at that layer's seam: the plaintext +has already been materialized when the call returns. + +Contrast this crate's own established posture: + +- The house principle is "zeroize everything sensitive" — the README's + zeroization section lists mnemonic, seed, private keys, encryption + keys, cached keys as `Zeroize` + `ZeroizeOnDrop`, "secret material + does not linger in freed heap memory". +- `unlock_new` already returns `Zeroizing` for exactly this + reason (`docs/architecture/service.md` §unlock_new: the returned + phrase is the root of trust, heap-allocated and zeroized on drop). +- `decrypt`'s output is the same class of material with weaker + handling than the mnemonic path — a decrypted secret blob is the + customer's secret, and by construction (this vault holds the keys + the family uses), frequently a private key's plaintext. + +An in-crate instance of the same window: `rotate` decrypts to +plaintext and re-encrypts internally (`src/service.rs:350`); the +transient plaintext there is at least fully in-crate — `decrypt` +hands it across the API boundary into caller-controlled heap that the +vault's zeroization guarantees do not cover. + +## The consumer that surfaced it (alkssh's key-custody path) + +alkssh's at-rest key custody (alkssh ADR-005, verified in POC-2): +`vault.decrypt(blob)` → the plaintext wrapped in `Zeroizing` +**at the adapter seam** → one-time `decode_secret_key` → the decoded +`PrivateKey` (which ssh-key zeroizes on `Drop`). The wrap is +alkssh's contract-owning step and works — but from the vault's return +to the adapter's wrap, the plaintext sits in unfenced heap. That +window is unfixable from the consumer seat; only the vault's return +type can close it. + +The alkssh-side resolution session (alkssh OQ-07, closed) established +the division: the adapter owns the wrap as *its* contract; a +zeroizing-out decrypt API is an alkvault ask, not an alkssh +obligation. When it lands, alkssh ADR-005 §3's pointer updates as a +one-line doc touch. + +## What a resolution decides (framing, not prescription) + +1. A decrypt-shaped surface whose output is zeroized on drop + (`Zeroizing` — the `unlock_new` precedent — or an + equivalent owned type). The exact shape is this crate's call; the + consumer requirement is only: **the returned type itself fences the + plaintext**, so the wrap no longer happens "on faith" downstream. +2. Whether `rotate`'s internal plaintext handling becomes part of the + same resolution (in-crate hygiene; both paths materialize the same + class of buffer). +3. Scope note: the `EncryptedData` wire format is frozen (ADR-018) and + is **not implicated** — this is return-type API surface only. If + alkssh's first consumer needs it before the next regular release, + the semver class is this crate's decision; alkssh 0.1.0 pins + `alkvault = "0.1"` and rides either answer. + +## References + +- ADR-018 (vault standalone; the `EncryptedData` wire lock — untouched here) +- ADR-021 (key rotation via version-indexed derivation paths — the + `rotate` internals noted above) +- `docs/architecture/service.md` — the zeroization posture; + `unlock_new`'s `Zeroizing` precedent; the rotate contract +- `src/service.rs` (`decrypt` :335, `rotate` :350), `src/encryption.rs` + (`decrypt` :211) +- alkssh ADR-005 (`/workspace/@alkdev/alkssh/docs/architecture/ + decisions/005-at-rest-key-custody.md`) §2/§3 — the consuming + contract; the "Zeroization ownership (the known gap)" paragraph +- alkssh OQ-07 (closed — the adapter-convention-level resolution that + produced this filing) \ No newline at end of file