# v0.9.0 — Multi-Key Registry + Rotation + Master-Key Recovery

**Release date:** 2026-05-20
**Phase:** 0.9.0 — named-key registry, atomic rotation, master-key emergency unlock
**Status:** development (pre-1.0; public API subject to change without notice)

---

## What is `key-vault`?

`key-vault` is an enterprise-grade in-memory key management vault for Rust.
See [README](../../README.md) for the 1.0 design and
[`docs/SECURITY.md`](../SECURITY.md) for the per-layer architecture.

After this release, every defense layer in the 1.0 contract is in place:

| Layer | Status after 0.9.0 |
|-------|--------------------|
| Layer 1 — built-in fetchers (4/4) | ✅ |
| Layer 2 — page locking | ✅ |
| Layer 3 — fragmentation (4/4 strategies) | ✅ |
| Layer 4 — decoy bytes (3 strategies) | ✅ |
| Layer 5 — codex (4/4 implementations) | ✅ |
| Layer 6 — constant-time handle equality | ✅ |
| Layer 7 — zero-on-drop (now covering `RawKey` too) | ✅ |
| Layer 8 — monitor impls + threshold lockout | ✅ |
| Operational: multi-key + rotation + master recovery | ✅ |
| Layer 9 — dedicated audit-event surface | partial — `LogMonitor` carries failure/anomaly events; per-access audit hook deferred |

Phase 0.10 next: criterion benchmark suite + performance verification.

---

## What's in 0.9.0

### Named-key registry

The vault now stores keys by name and hands back opaque `KeyHandle`s:

```rust
use key_vault::{KeyVaultBuilder, RawKey};

# fn main() -> Result<(), key_vault::Error> {
let vault = KeyVaultBuilder::new()
    .normalize_with_blake3(false)
    .build();

let handle = vault.register("db-primary", RawKey::new(b"db key bytes".to_vec()))?;
assert!(vault.contains(handle));
assert_eq!(vault.key_count(), 1);

// Recover the bytes inside a scoped callback. The slice is valid only
// for the call; the underlying RawKey zeroes its buffer when it drops.
vault.with_key(handle, |bytes| {
    // ... use `bytes` to encrypt/decrypt/sign/MAC ...
    assert_eq!(bytes, b"db key bytes");
})?;

// Look up by name.
assert_eq!(vault.handle_for_name("db-primary"), Some(handle));

// Remove when no longer needed.
vault.unregister(handle)?;
assert_eq!(vault.key_count(), 0);
# Ok(())
# }
```

**Storage.** Each registered key is fragmented through the configured
Layer 2/3/4/5/7 stack (mlock'd `LockedBytes` chunks, optional decoys,
optional codex, zero-on-drop). The registry holds `Arc<Fragments>` per
entry so rotation and removal don't tear shared readers' view.

**Lock-free reads.** The registry is an `ArcSwap<HashMap<KeyId, KeyEntry>>`.
`with_key` loads a snapshot, clones the `Arc<Fragments>` it needs, and
releases the snapshot before defragmenting — concurrent rotation can
swap the registry without blocking readers.

### Atomic key rotation

```rust
use key_vault::{KeyVaultBuilder, RawKey};

# fn main() -> Result<(), key_vault::Error> {
let vault = KeyVaultBuilder::new().normalize_with_blake3(false).build();
let h = vault.register("token", RawKey::new(b"old".to_vec()))?;

vault.rotate(h, RawKey::new(b"new".to_vec()))?;

let observed = vault.with_key(h, <[u8]>::to_vec)?;
assert_eq!(observed, b"new");
# Ok(())
# }
```

`KeyVault::rotate(handle, new_key)` uses `ArcSwap::rcu` to atomically
swap the entry's `Arc<Fragments>` for a freshly-fragmented one.
Concurrent `with_key` callers observe either the old or the new
fragmentation — never a partial write.

Verified by [tests/registry_concurrent.rs](../../tests/registry_concurrent.rs)
which runs 4 reader threads × 200 reads each interleaved with 20
rotations on the same handle.

### Master-key emergency unlock

A new builder method registers a master credential at construction:

```rust
use std::time::Duration;
use key_vault::{KeyVaultBuilder, RawKey};

# fn main() -> Result<(), key_vault::Error> {
let master = b"correct horse battery staple".to_vec();
let vault = KeyVaultBuilder::new()
    .with_master_key(RawKey::new(master.clone()))
    .with_failure_threshold(3, Duration::from_secs(60))
    .build();

assert!(vault.has_master_key());

// Simulate three failures → lockout.
vault.report_failure("k", None);
vault.report_failure("k", None);
vault.report_failure("k", None);
assert!(vault.is_locked_out());

// fragment / defragment / with_key all refuse.
let raw = RawKey::new(vec![0u8; 16]);
let err = vault.fragment(&raw).unwrap_err();
assert!(matches!(err, key_vault::Error::LockedOut));

// Operator provides the master credential. Constant-time digest
// comparison; on success the lockout flag clears.
vault.unlock_with_master(&master)?;
assert!(!vault.is_locked_out());
# Ok(())
# }
```

**Storage.** The vault stores the BLAKE3 hash of the master, not the
master bytes. The supplied `RawKey` drops + zeroes immediately after
the hash is computed.

**Constant-time compare.** `unlock_with_master` compares hashes via
`subtle::ConstantTimeEq` — timing leaks nothing about how close an
attempt was.

**Failure feedback.** Mismatched attempts are reported under the
reserved name `"<master>"` so threshold rules apply (e.g. three bad
master attempts in a row can also trigger lockout, preventing brute
force).

### `RawKey: Drop` (zero-on-drop)

`RawKey` now overwrites its internal `Vec<u8>` with `write_volatile`
+ a `SeqCst` compiler fence before the underlying allocation is freed.
This closes the gap where the temporary defragmented buffer in
`with_key` and the `RawKey` returned from `KeyVault::defragment` could
linger.

Practical impact: every byte path through the vault is now zeroed
when it leaves scope, end-to-end.

### New `KeyVault` API surface

| Method | Description |
|--------|-------------|
| `register(name, RawKey) -> Result<KeyHandle>` | Add a named key to the registry. |
| `unregister(handle) -> Result<()>` | Remove from the registry; zeroes on drop. |
| `with_key(handle, |bytes| { ... }) -> Result<T>` | Scoped byte access; auto-zeroes. |
| `rotate(handle, new_key) -> Result<()>` | Atomic swap via ArcSwap RCU. |
| `contains(handle) -> bool` | Membership check. |
| `metadata(handle) -> Option<KeyMetadata>` | Non-secret metadata (length, registration time). |
| `handle_for_name(name) -> Option<KeyHandle>` | Lookup by name. |
| `key_count() -> usize` | Number of registered keys. |
| `unlock_with_master(bytes) -> Result<()>` | Constant-time master-key recovery. |
| `has_master_key() -> bool` | Whether a master credential was registered. |

### New `KeyVaultBuilder` method

| Method | Description |
|--------|-------------|
| `with_master_key(RawKey) -> Self` | Register a master credential at build time; stores BLAKE3 digest only. |

### Test surface (relative to 0.8.0)

|                   | 0.8.0 | 0.9.0 |
|-------------------|-------|-------|
| Unit tests        |  119  |  133  |
| Integration tests |   15  |   18  |
| Doc tests         |   20  |   20  |
| **Total**         | **154**| **171** |

New tests cover:

- `register` happy path + duplicate-name rejection.
- `unregister` + unknown-handle error.
- `with_key` round-trip (no-normalization byte equality + normalization length).
- `rotate` correctness + unknown-handle error.
- `handle_for_name` lookup.
- `metadata` records correct registration length.
- Lockout interaction (registered key refuses access when locked).
- Master-key unlock match + mismatch + missing-master error.
- **Concurrent rotation**: 4 readers × 200 reads vs. 20 rotations.
- **Many independent keys**: 16-key registry with independent values.
- **Concurrent unregister vs. with_key**: no panics under race.

---

## Breaking changes

Purely additive on top of 0.8.0:

- All 0.8.0 types and methods are preserved.
- The new registry API lives alongside the existing one-shot
  `fragment` / `defragment` methods (still available for ad-hoc use).
- `RawKey: Drop` is a behavior addition; callers that previously
  inspected `RawKey` after dropping (impossible without `unsafe`)
  would observe zeroed bytes now.

---

## Verification

Verified locally on both stable and pinned MSRV 1.85:

```text
cargo {fmt, clippy, test, doc} --all-features      clean
171 tests pass; -D warnings on clippy + RUSTDOCFLAGS
```

New `unsafe` block:

- `RawKey::drop` uses `core::ptr::write_volatile` + `compiler_fence`
  to defeat dead-store elimination on the zeroize. Same pattern as
  the existing `LockedBytes` and `zero_buffer` helpers. Carries
  `// SAFETY:` annotation.

---

## Documentation sync (this release)

Per the project's doc-sync rule:

- `README.md` — "Current status" marks multi-key vaults / rotation /
  master recovery **shipped**; Quick-start dep `0.8` → `0.9`;
  operational-features section rewritten.
- `docs/API.md` — pending update with the new registry + master-key
  surface in the next docs pass.
- `CHANGELOG.md` — `[0.9.0]` section under Added / Changed / Security.
- `docs/release/v0.9.0.md` — this file.

---

## What's next — Phase 0.10.0

**Performance verification + criterion benchmark suite.**

Exit criteria for 0.10:

- `benches/access_latency.rs` — single-key access, all layer
  combinations.
- `benches/concurrent_access.rs` — 1, 4, 16, 64 thread contention.
- `benches/fragment_strategies.rs` — comparison across the four
  strategies.
- `benches/decoy_strategies.rs` — overhead per strategy.
- `benches/codex_overhead.rs` — codex on vs. off.
- `benches/memory_overhead.rs` — per-key footprint.
- All performance-contract targets in the README verified or tuned.
- `docs/PERFORMANCE.md` — methodology + results + tuning guide.

Estimated effort: 1 week.

---

## Installation

```toml
[dependencies]
key-vault = "0.9"
```

Minimum supported Rust version: **1.85** (edition 2024).

---

## Documentation

- [`README.md`](../../README.md) — overview + Current status table
- [`docs/API.md`](../API.md) — full public-API reference
- [`docs/SECURITY.md`](../SECURITY.md) — 9-layer architecture
- [`docs/TRANSFORMATION.md`](../TRANSFORMATION.md) — visual walkthrough
- [`CHANGELOG.md`](../../CHANGELOG.md) — full change log

---

## Diff

Full diff: [`v0.8.0...v0.9.0`](https://github.com/jamesgober/key-vault/compare/v0.8.0...v0.9.0)

---

<sub>key-vault v0.9.0 — Copyright (c) 2026 James Gober. Apache-2.0 OR MIT.</sub>
