docs(review): first review — alkssh consumer input on the decrypt plaintext window (establishes docs/reviews/)
This commit is contained in:
1 parent
5462a9e560
commit
2a15bf2bc4
1 file changed
+93
@@ -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<String, VaultServiceError>`
|
||||
(`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<String>` 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<String>`
|
||||
**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<String>` — 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<String>` 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)
|
||||
Reference in new issue
Block a user