# v0.3.0 — StandardFragmenter, mlock + zeroize, constant-time handles

**Release date:** 2026-05-20
**Phase:** 0.3.0 — `StandardFragmenter` + mlock + zeroize (Layers 2, 3, 6, 7)
**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.
It implements **9 layers of defense-in-depth** plus a bonus 10th to keep
cryptographic key material hard to extract from a running process. See
[README](../../README.md) for the 1.0 design and [`docs/SECURITY.md`](../SECURITY.md)
for the per-layer architecture.

This release turns on **four of the nine layers** for the first time:
- **Layer 2** — page locking (mlock / VirtualLock)
- **Layer 3** — fragmentation (`StandardFragmenter`)
- **Layer 6** — constant-time handle equality
- **Layer 7** — zero-on-drop on every fragment and layout buffer

---

## What's in 0.3.0

### Layer 2 — Memory page locking

New crate-internal `LockedBytes` wrapper:

- Constructs from a byte slice with an exact-capacity `Vec<u8>` (no resize
  means the OS lock never refers to a stale address).
- Calls `libc::mlock` on Unix and `windows-sys`'s `VirtualLock` on Windows.
- **Soft-fails:** if the kernel declines (process is over `RLIMIT_MEMLOCK`,
  for example), the buffer is still usable; `LockedBytes::is_locked`
  reports `false` and the caller can surface the condition to operators.
- `Drop` order is **zero → unlock → free** so the kernel still treats the
  pages as locked while we overwrite them. Overwrites use
  `core::ptr::write_volatile` plus a `SeqCst` compiler fence so the
  writes are not eliminated.

### Layer 3 — `StandardFragmenter`

The first real `FragmentStrategy` implementation:

- **Variable chunk sizes** sampled uniformly from a configurable range
  (defaults: min=1, max=8). The last chunk absorbs any short remainder.
- **Fisher-Yates permutation** over the (offset, slice) pairs so chunks
  land in shuffled order.
- **Independent heap allocations** — every chunk lives in its own
  `LockedBytes`, so chunks are at non-contiguous heap addresses by
  construction.
- **Separate locked layout buffer** holding the reconstruction order as
  little-endian `u32` original offsets. Defragmentation sorts by offset
  and concatenates.
- **CSPRNG-sourced randomness** via `getrandom::getrandom`. No userspace
  RNG state lives across calls — each fragmentation is independently
  seeded.

Verified by 1000-iteration round-trip stress test and round-trip across
input sizes 1 B, 7, 16, 32, 64, 128, 255, 256, 500, 1024, 4096 B.

### Layer 6 — Constant-time `KeyHandle` equality

- `KeyHandle: subtle::ConstantTimeEq` now in place.
- `PartialEq` routes through `ConstantTimeEq::ct_eq` so plain `==` on
  handles is also constant-time.
- `Hash` is implemented manually to stay consistent with the manual
  `PartialEq` (equal handles still hash equal).

### Layer 7 — Zero-on-drop

- `LockedBytes::drop` overwrites every byte with `write_volatile` and a
  `SeqCst` compiler fence before unlocking and freeing.
- The intermediate plaintext layout buffer used during fragmentation is
  also volatile-zeroed.

### BLAKE3 normalization wired through

The `KeyVaultBuilder::normalize_with_blake3(bool)` toggle now actually
applies. When `true` (default), input keys are hashed to 32 bytes before
fragmentation, neutralizing format cues (DER envelopes, PEM markers,
ASCII-armored data).

### `KeyVault::fragment` / `KeyVault::defragment`

Convenience methods routing through the configured normalizer and
`StandardFragmenter`. Downstream crates can now exercise the full
Layer 2 + 3 + 7 stack from outside the crate. The full named-key registry
(`register`, `with_key`, rotation, recovery) still arrives in Phase 0.9.

### New / changed public surface

| Item                                          | Status        |
|-----------------------------------------------|---------------|
| `StandardFragmenter` (re-export from crate root) | **new**       |
| `KeyVault::fragment` / `KeyVault::defragment`    | **new**       |
| `KeyVaultBuilder::with_chunk_range`              | **new**       |
| `KeyHandle: ConstantTimeEq`                      | **new**       |
| `Fragments` (real storage backed by `LockedBytes`) | implemented (no public API change) |
| `KeyHandle` `PartialEq` / `Hash`                 | now manual, semantics unchanged |

### Build / CI

- **CI:** `actions/cache@v4` → `Swatinem/rust-cache@v2`. Removes the
  Node.js 20 deprecation warning surfaced on every run since 2026-05.
- Dropped the unused `actions/setup-node@v5` step.
- New platform-conditional dependencies:
  - `libc = "0.2"` under `[target.'cfg(unix)'.dependencies]`
  - `windows-sys = { version = "0.59", features = ["Win32_System_Memory"] }` under `[target.'cfg(windows)'.dependencies]`

### Test surface (relative to 0.2.0)

|                   | 0.2.0 | 0.3.0 |
|-------------------|-------|-------|
| Unit tests        |   27  |   49  |
| Integration tests |    4  |    8  |
| Doc tests         |    5  |    5  |
| **Total**         | **36** | **62** |

---

## Breaking changes

This is a pre-1.0 release; the public API is not yet stable. The 0.3.0 cut
is purely additive on top of 0.2.0:

- All 0.2.0 types and methods are preserved.
- `Fragments` gained internal storage; the type is opaque so no external
  caller could observe the change.
- `KeyHandle`'s `PartialEq` / `Hash` switched from `#[derive]` to manual
  but the observable behavior is identical.

---

## Verification

Verified locally before tagging on both stable and pinned MSRV 1.85:

```text
cargo fmt --all -- --check                                clean
cargo clippy --all-targets --all-features -- -D warnings  clean
cargo test --all-features                                 62 tests; all pass
RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features  clean
```

Three internal `unsafe` blocks (CPUID wrapper from 0.2 + mlock/munlock +
VirtualLock/VirtualUnlock) all carry `// SAFETY:` comments and are
confined to private functions in the `tee` and `memory` modules; no
`unsafe` in the public API.

---

## Installation

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

Default features unchanged from 0.2.0. Minimum supported Rust version:
**1.85** (edition 2024).

---

## Documentation

- [`README.md`](../../README.md) — overview + Current status table
- [`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.2.0...v0.3.0`](https://github.com/jamesgober/key-vault/compare/v0.2.0...v0.3.0)

---

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