Files
alknet/tasks/vault/mnemonic-debug-redaction.md
glm-5.2 d149932e2a tasks: decompose review #004 findings into 4 fix tasks + review gate
W1 (call/protocol/abort-cascade-wiring): wire AbortCascade into CallAdapter
handle_stream for EVENT_ABORTED. W2 (core/endpoint-client-fingerprint):
extract TLS client cert fingerprint in dispatch_quinn/dispatch_iroh.
W3 (vault/mnemonic-debug-redaction): replace Mnemonic derive(Debug) with
redacting impl. W4 (core/auth-apikey-resources, level: research): decide
whether ApiKeyEntry should carry resources, then implement or drop from
spec. review-post-impl-fixes gates on all four. Graph: 33 tasks, 12 gens.
2026-06-24 10:02:03 +00:00

110 lines
3.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: vault/mnemonic-debug-redaction
name: Replace Mnemonic derive(Debug) with redacting impl to prevent seed phrase leak
status: pending
depends_on: []
scope: single
risk: low
impact: isolated
level: implementation
---
## Description
`Mnemonic` (crates/alknet-vault/src/mnemonic.rs:35) currently derives
`Debug`:
```rust
#[derive(Debug)]
pub struct Mnemonic {
inner: Bip39Mnemonic,
phrase: String,
}
```
The derived `Debug` prints both fields, including `phrase: "abandon
abandon abandon ..."`. The crate's own module doc (mnemonic.rs:8) says
"Seed material is protected with `Zeroize`" and the `Mnemonic::phrase()`
accessor (line 82) warns "Handle with care — this is the root of trust
for all derived keys." The `Zeroize + Drop` impls (lines 8899) wipe
the phrase from memory on drop — but `Debug` will happily hand the
phrase to any `tracing::debug!`, `format!("{:?}")`, panic backtrace,
or error-context printer that touches a `Mnemonic` value.
This is inconsistent with the rest of the crate's secret handling:
- `DerivedKey` has a custom redacting `Debug` (protocol.rs:4856)
printing `private_key: "[REDACTED]"`.
- `EncryptionKey` has a custom redacting `Debug` (encryption.rs:132139).
- `Capabilities` has a redacting `Debug` (types.rs:112118).
- `Secret<T>` has a redacting `Debug` (types.rs:5155).
- `Mnemonic` ... derives `Debug` and prints the phrase.
The root of trust should never have a `Debug` impl that can print it.
A future debugging session that adds `tracing::debug!(?mnemonic, ...)`
would land the seed phrase in a log file.
### Resolution
Replace `#[derive(Debug)]` with a manual impl that redacts, matching
the pattern used for `DerivedKey`:
```rust
impl std::fmt::Debug for Mnemonic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Mnemonic")
.field("phrase", &"[REDACTED]")
.finish()
}
}
```
Also check `Seed` (mnemonic.rs:107129) — it derives
`#[derive(Clone, Zeroize)]` with `#[zeroize(drop)]` but does not derive
`Debug`. Verify it has no `Debug` impl that prints `bytes`; if it does
(or if a future `derive(Debug)` would), add a redacting impl there too.
### Test
Add a test asserting `format!("{:?}", mnemonic)` does not contain any
word from the generated phrase. Pattern after
`test_derived_key_debug_redacts_private_key` (protocol.rs:106118):
```rust
#[test]
fn test_mnemonic_debug_redacts_phrase() {
let mnemonic = Mnemonic::generate(24).unwrap();
let debug_output = format!("{:?}", mnemonic);
assert!(
debug_output.contains("[REDACTED]"),
"Debug must show [REDACTED] for phrase, got: {debug_output}"
);
for word in mnemonic.phrase().split_whitespace() {
assert!(
!debug_output.contains(word),
"Debug must not leak phrase word '{word}', got: {debug_output}"
);
}
}
```
## Acceptance Criteria
- [ ] `Mnemonic` no longer derives `Debug`; has a manual redacting `Debug` impl
- [ ] `format!("{:?}", mnemonic)` contains `"[REDACTED]"` and no phrase word
- [ ] `Seed` checked — either has no `Debug` impl, or has a redacting one
- [ ] Unit test: `test_mnemonic_debug_redacts_phrase` passes
- [ ] Existing tests still pass (`cargo test -p alknet-vault`)
- [ ] `cargo clippy -p alknet-vault --all-targets` succeeds with no warnings
## References
- docs/reviews/004-post-implementation-sanity-check.md — W3 (full finding)
- crates/alknet-vault/src/protocol.rs:4856 — `DerivedKey` redacting `Debug` (pattern to follow)
- crates/alknet-vault/src/encryption.rs:132139 — `EncryptionKey` redacting `Debug`
- crates/alknet-core/src/types.rs:5155 — `Secret<T>` redacting `Debug`
## Notes
> Small fix, but eliminates a latent root-of-trust leak. The same
> pattern (custom redacting `Debug`) is already established in three
> other places in this codebase — this task brings `Mnemonic` in line.