Files
alkvault/CHANGELOG.md
T
glm-5.3-flash d10daa656a Polish docs before the 0.2.0 publish
- Promote the derive_secp256k1_path example from an ignored,
  non-compiling snippet to a running doctest (defines the seed it uses)
- Restate the crate-level docs to the local-only dispatch model
  (ADR-025): the embedding binary assembles, no call-protocol wire;
  fix the Unlock/Lock method-name capitalization and disambiguate the
  ADR-038 reference (alknet-main seed-lifecycle decision)
- List the cache module in the crate-level module organization
- Update the CHANGELOG 0.2.0 Verified block (115 tests, README example
  compile/run-verified) and fold the doctest/doc fixes into the docs
  bullet

Verification: 115 tests (all-features), 107 (default), clippy clean,
cargo doc warning-free, fuzz/shared replay 8/8, publish dry-run clean.
2026-11-04 08:53:46 +00:00

6.6 KiB

Changelog

All notable changes to this crate are documented here. The format is based on Keep a Changelog, and this crate adheres to Semantic Versioning.

0.2.0 - 2026-11-04

The decrypt plaintext window fix (review 001, consumer input from alkssh 0.1.0): VaultServiceHandle::decrypt now returns Zeroizing<String> (the unlock_new precedent), so the decrypted plaintext is zeroized on drop from the moment it materializes inside the vault through to the moment the caller drops it. Previously the plaintext materialized as a bare heap String (encryption::decrypt, via String::from_utf8 on the AES-GCM plaintext) and crossed the API boundary with no zeroization guarantee; consumers holding the customer's secret (frequently private-key material, by construction, since this vault holds the keys the family derives) could not close that window from the consumer seat. The wrap is now the vault's job and travels with the return value.

The in-crate instances of the same window are closed in the same change: rotate's transient plaintext now transits through the zeroizing return instead of a bare String, and the module-internal encryption::decrypt zeroizes the decrypted byte buffer on every path, including the UTF-8 validation failure path (which previously returned the unzeroized bytes inside the error value before they were dropped).

The EncryptedData wire format is untouched (ADR-018 holds): this is return-type API surface only. One breaking change to the public API (hence the minor bump), with the first consumer (alkssh) riding the release; alkssh's adapter-side Zeroizing::new wrap around the decrypt result can be dropped against 0.2.0 (alkssh ADR-005 §3 pointer update is their one-line doc touch).

Changed

  • decrypt returns Result<Zeroizing<String>, VaultServiceError> (breaking API change). Zeroizing<String> derefs to String, so read-path call sites carry over unchanged; comparisons against &str need .as_str() (or &*) for the explicit PartialEq. This matches unlock_new's handling of the mnemonic: the same class of secret, same posture.
  • parse_derivation_path rejects hardened indices ≥ 2^31 instead of overflowing. The hardened-index encoding (index + 0x80000000) panicked on index > 2147483647 in debug builds and silently wrapped into a meaningless index in release builds (e.g. m/4294967295'). Such paths now return DerivationError::InvalidPath on every profile.
  • EncryptionKey::from_derived_bytes returns Result<Self, EncryptionError> (breaking API change). It panicked on derived material shorter than 32 bytes (&bytes[..32] with no length check); it now returns EncryptionError::InvalidLength and never panics. The two internal call sites (VaultServiceHandle:: encrypt/decrypt) already derive 32-byte keys and are unaffected beyond the ?.
  • encryption_path_for_version rejects versions whose index overflows the hardened encoding. Versions above 0x80000001 previously produced paths (e.g. m/74'/2'/0'/2147483648') that the path parser rejects; such versions now return DerivationError::InvalidPath directly. In-range versions produce identical paths.
  • encryption::decrypt (module-internal) returns Zeroizing<String> and zeroizes all plaintext intermediates. The decrypted byte buffer is zeroized before an invalid-UTF-8 error surfaces, so a failed decryption no longer leaks the (malformed) plaintext through the error path.
  • rotate internal plaintext is zeroized. It consumes the zeroizing decrypt output; no bare String transit during re-encryption.
  • KeyCache honors max_entries = 0 (cache disabled). The eviction path previously admitted one entry against a zero bound (len >= 0 triggered an eviction from an empty order, a no-op, before the unconditional insert). Inserts are now no-ops under a max_entries = 0 config; the key is dropped and zeroized. Found during the fuzzing-plan scope review (docs/plans/fuzzing.md).
  • Docs aligned: README usage example, service.md §decrypt and encryption.md §module-internal signatures now document the zeroizing return; review 001 closed as resolved. The derive_secp256k1_path example now runs as a doctest (was ignore with an undefined seed); crate-level docs restate the local-only dispatch model and list the cache module.

Verified

  • 115 tests pass (cargo test --all-features; 107 with default features); clippy --all-features --all-targets clean. cargo publish --dry-run --all-features --allow-dirty before publish. The README usage example is compile- and run-verified against the published API surface.
  • Fuzzing introduced (fuzz/, see docs/plans/fuzzing.md): four libFuzzer targets on the untrusted-input surfaces, one binary per target in fuzz_targets/: path_parse (path parser and version/device path helpers), encrypted_blob (EncryptedData deserialization plus the decrypt/rotate lifecycle), derived_key_json (DerivedKey serde with the redacted-payload guard), and mnemonic_unlock (Mnemonic::from_phrase plus the unlock lifecycle). All invariant logic lives in the fuzz/shared library crate, so a stable toolchain replays the committed seed corpora through the same invariants (cargo test --manifest-path fuzz/shared/Cargo.toml); the fuzz binaries are nightly-only, pinned via fuzz/rust-toolchain.toml, and the fuzz workspace is excluded from both the root workspace and the published package. Detached campaigns for all four targets completed their budgets with zero crashes, OOMs, or timeouts. Findings: the parse_derivation_path and encryption_path_for_version overflows above surfaced in the scope review before any campaign ran, as did the max_entries = 0 cache bound bug above; all three are covered by regression tests.

0.1.0 - 2026-10-31

Initial release: BIP39 mnemonic generation (12/15/18/21/24 words) and restore, SLIP-0010 Ed25519 HD derivation at the alket family paths (PATHS::IDENTITY / SSH_HOST / ENCRYPTION plus Ethereum under secp256k1), AES-256-GCM encryption of external credentials with an HD-derived key (key_version ≥ 2, version-indexed derivation paths per ADR-021), key rotation (rotate), and the VaultServiceHandle lock/unlock lifecycle with a TTL/LRU key cache. EncryptedData is the frozen wire format shared with alknet-storage and the TypeScript @alkdev/storage consumer (ADR-018).