Wraps long-lived cryptographic key material in zeroize::Zeroizing<T> so it is
overwritten on drop instead of lingering as plain heap bytes for the rest of
the process lifetime. vault.rs's ZeroizingSecret was already a workspace
dependency but was decorative -- nothing wrapped real keys in it. This story
starts actually using zeroizing wrappers for real key material:
DatabaseEngine (crates/nuncio-store/src/db.rs): storage_key: Zeroizing<[u8; 32]>, worm_key: Zeroizing<Vec<u8>>, ledger_key: Zeroizing<Vec<u8>>. resolve_engine_keys
wraps the raw bytes at construction; all existing call sites (PayloadCipher, HmacSha256, WORM chain verification) compile unchanged via deref coercion.
The manual Debug impl still redacts key fields.
WebhookDispatcher (crates/nuncio-filter/src/webhook.rs): secret_key: Zeroizing<String>.
Added zeroize.workspace = true to crates/nuncio-filter/Cargo.toml.
AccountsGrpcService::add_account (crates/nunciod/src/grpc.rs): the incoming
plaintext password is moved into a Zeroizing<String> immediately after request.into_inner(), before it's passed to set_secret. Added zeroize.workspace = true to crates/nunciod/Cargo.toml. Did not touch the
already-merged update_account rotation / keyring-rollback logic in the
same file.
No encryption/HMAC/signing behavior changes -- this is purely additive
zeroize-on-drop.
Tests
db::tests::zeroize_mechanism_actually_wipes_key_bytes -- proves the
mechanism directly: constructs Zeroizing<[u8; 32]> and Zeroizing<Vec<u8>>
with known non-zero bytes, calls .zeroize() explicitly (safe, no unsafe), and asserts every byte is now zero. This is the "real" test --
inspecting freed memory would require unsafe, which is forbidden, so this
proves the same wipe that Zeroizing's Drop performs internally actually
works, rather than a no-op drop test that asserts nothing.
db::tests::debug_impl_never_prints_raw_key_bytes -- asserts DatabaseEngine's Debug output never contains the key field names/values.
All existing nuncio-store (encryption roundtrip, WORM chain, ledger HMAC)
and nuncio-filter webhook tests pass unchanged.
None found in scope. No real bugs discovered outside this story.
Closes #199.
## Summary
Wraps long-lived cryptographic key material in `zeroize::Zeroizing<T>` so it is
overwritten on drop instead of lingering as plain heap bytes for the rest of
the process lifetime. `vault.rs`'s `ZeroizingSecret` was already a workspace
dependency but was decorative -- nothing wrapped real keys in it. This story
starts actually using zeroizing wrappers for real key material:
- `DatabaseEngine` (`crates/nuncio-store/src/db.rs`): `storage_key: Zeroizing<[u8; 32]>`,
`worm_key: Zeroizing<Vec<u8>>`, `ledger_key: Zeroizing<Vec<u8>>`. `resolve_engine_keys`
wraps the raw bytes at construction; all existing call sites (`PayloadCipher`,
`HmacSha256`, WORM chain verification) compile unchanged via deref coercion.
The manual `Debug` impl still redacts key fields.
- `WebhookDispatcher` (`crates/nuncio-filter/src/webhook.rs`): `secret_key: Zeroizing<String>`.
Added `zeroize.workspace = true` to `crates/nuncio-filter/Cargo.toml`.
- `AccountsGrpcService::add_account` (`crates/nunciod/src/grpc.rs`): the incoming
plaintext password is moved into a `Zeroizing<String>` immediately after
`request.into_inner()`, before it's passed to `set_secret`. Added
`zeroize.workspace = true` to `crates/nunciod/Cargo.toml`. Did not touch the
already-merged `update_account` rotation / keyring-rollback logic in the
same file.
No encryption/HMAC/signing behavior changes -- this is purely additive
zeroize-on-drop.
## Tests
- `db::tests::zeroize_mechanism_actually_wipes_key_bytes` -- proves the
mechanism directly: constructs `Zeroizing<[u8; 32]>` and `Zeroizing<Vec<u8>>`
with known non-zero bytes, calls `.zeroize()` explicitly (safe, no
`unsafe`), and asserts every byte is now zero. This is the "real" test --
inspecting freed memory would require `unsafe`, which is forbidden, so this
proves the same wipe that `Zeroizing`'s `Drop` performs internally actually
works, rather than a no-op drop test that asserts nothing.
- `db::tests::debug_impl_never_prints_raw_key_bytes` -- asserts `DatabaseEngine`'s
`Debug` output never contains the key field names/values.
- All existing `nuncio-store` (encryption roundtrip, WORM chain, ledger HMAC)
and `nuncio-filter` webhook tests pass unchanged.
## Gate output
```
cargo fmt --all -- --check -> clean, no diff
cargo check-all -> clippy --all-targets --workspace -D warnings: 0 warnings
cargo test-all -> 421 passed; 0 failed; 0 ignored (across all crates + doctests)
```
## Follow-ups
None found in scope. No real bugs discovered outside this story.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #199.
Summary
Wraps long-lived cryptographic key material in
zeroize::Zeroizing<T>so it isoverwritten on drop instead of lingering as plain heap bytes for the rest of
the process lifetime.
vault.rs'sZeroizingSecretwas already a workspacedependency but was decorative -- nothing wrapped real keys in it. This story
starts actually using zeroizing wrappers for real key material:
DatabaseEngine(crates/nuncio-store/src/db.rs):storage_key: Zeroizing<[u8; 32]>,worm_key: Zeroizing<Vec<u8>>,ledger_key: Zeroizing<Vec<u8>>.resolve_engine_keyswraps the raw bytes at construction; all existing call sites (
PayloadCipher,HmacSha256, WORM chain verification) compile unchanged via deref coercion.The manual
Debugimpl still redacts key fields.WebhookDispatcher(crates/nuncio-filter/src/webhook.rs):secret_key: Zeroizing<String>.Added
zeroize.workspace = truetocrates/nuncio-filter/Cargo.toml.AccountsGrpcService::add_account(crates/nunciod/src/grpc.rs): the incomingplaintext password is moved into a
Zeroizing<String>immediately afterrequest.into_inner(), before it's passed toset_secret. Addedzeroize.workspace = truetocrates/nunciod/Cargo.toml. Did not touch thealready-merged
update_accountrotation / keyring-rollback logic in thesame file.
No encryption/HMAC/signing behavior changes -- this is purely additive
zeroize-on-drop.
Tests
db::tests::zeroize_mechanism_actually_wipes_key_bytes-- proves themechanism directly: constructs
Zeroizing<[u8; 32]>andZeroizing<Vec<u8>>with known non-zero bytes, calls
.zeroize()explicitly (safe, nounsafe), and asserts every byte is now zero. This is the "real" test --inspecting freed memory would require
unsafe, which is forbidden, so thisproves the same wipe that
Zeroizing'sDropperforms internally actuallyworks, rather than a no-op drop test that asserts nothing.
db::tests::debug_impl_never_prints_raw_key_bytes-- assertsDatabaseEngine'sDebugoutput never contains the key field names/values.nuncio-store(encryption roundtrip, WORM chain, ledger HMAC)and
nuncio-filterwebhook tests pass unchanged.Gate output
Follow-ups
None found in scope. No real bugs discovered outside this story.