# v0.4.0 — Decoy Strategies (Layer 4)

**Release date:** 2026-05-20
**Phase:** 0.4.0 — Decoy strategies (Layer 4)
**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. See
[README](../../README.md) for the 1.0 design and
[`docs/SECURITY.md`](../SECURITY.md) for the per-layer architecture.

After this release, **five of the nine layers** are live:

- **Layer 2** — page locking (mlock / VirtualLock) ✅
- **Layer 3** — fragmentation (`StandardFragmenter`) ✅
- **Layer 4** — decoy bytes (3 strategies) ✅ *new in 0.4.0*
- **Layer 6** — constant-time handle equality ✅
- **Layer 7** — zero-on-drop ✅

Layers 1 (fetchers), 5 (codex full impls), 8 (monitors), and 9 (audit log)
remain on the roadmap.

---

## What's in 0.4.0

### Three decoy strategies

The `DecoyStrategy` trait (shipped as a surface in 0.2.0) now has three
production implementations:

| Strategy             | Output profile               | Strength            | Default? |
|----------------------|------------------------------|---------------------|----------|
| `RandomDecoy`        | Uniformly random             | Weakest, fastest    |          |
| `KeyDerivedDecoy`    | BLAKE3-XOF (CSPRNG-like)     | Medium              |          |
| `SelfReferenceDecoy` | Drawn from the key itself    | **Strongest**       | ✅       |

#### `RandomDecoy`

Pure CSPRNG bytes from `getrandom`. Use when the keys you store are
already uniformly random (e.g. 256-bit symmetric keys); the strategy
adds zero structural cues but is distinguishable from key material that
has format markers (DER envelopes, PEM, ASCII-armored data).

#### `SelfReferenceDecoy` (recommended)

For each output byte, sample an independent random index into the key
and emit `key[idx]`. The decoy's byte-value distribution is
*identical* to the key's, so an attacker scraping memory cannot use
statistical tools (entropy estimate, chi-squared, byte histogram) to
separate decoy chunks from real chunks. Independent sampling (rather
than shuffling) prevents any contiguous run of bytes from
accidentally matching the key.

#### `KeyDerivedDecoy`

Seed BLAKE3 with `key bytes ‖ 32-byte CSPRNG nonce`, then read
`output_len` bytes from the XOF. Per-call nonce ensures two consecutive
calls produce different output (an attacker who later recovers the key
cannot recompute the decoy stream to confirm a fragmentation).

### Pipeline integration

`StandardFragmenter::with_decoy` and `KeyVaultBuilder::with_decoy` attach
a decoy strategy to the fragmenter. When configured:

- Each real chunk gets a paired decoy chunk, so the total chunk count is
  `2 × n_real`.
- Decoy chunks are sized independently using the same `[min_chunk, max_chunk]`
  range so an attacker can't distinguish them by size.
- The combined real + decoy list is shuffled together via the same
  Fisher-Yates permutation.
- The layout buffer (locked, zeroed on drop) encodes real chunks with their
  original offset and decoy chunks with the sentinel `u32::MAX`.
- `defragment` skips chunks whose layout entry is `u32::MAX` and sorts the
  rest by offset to recover the original byte order.

### New / changed public surface

| Item                                 | Status       |
|--------------------------------------|--------------|
| `RandomDecoy` (crate-root re-export) | **new**      |
| `SelfReferenceDecoy`                 | **new**      |
| `KeyDerivedDecoy`                    | **new**      |
| `StandardFragmenter::with_decoy`     | **new**      |
| `KeyVaultBuilder::with_decoy`        | **new**      |
| `StandardFragmenter` no longer `Copy`| change       |
| `Fragments::chunk_count`             | semantics: now returns *real + decoy* count when decoy is configured |

### Test surface (relative to 0.3.0)

|                   | 0.3.0 | 0.4.0 |
|-------------------|-------|-------|
| Unit tests        |   49  |   66  |
| Integration tests |    9  |    9  |
| Doc tests         |    6  |    9  |
| **Total**         | **64** | **84** |

---

## Breaking changes

The 0.4.0 cut is purely additive on top of 0.3.0:

- All 0.3.0 types and methods are preserved.
- `StandardFragmenter` losing `Copy` is technically a breaking change in
  the strict sense, but no public-API consumer in 0.3.0 could have
  relied on it.

---

## Verification

Verified locally 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                                 84 tests; all pass
RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features  clean
```

No new `unsafe` blocks in this release — decoy bytes flow through safe
`Vec<u8>` and `LockedBytes` on the same path the fragments themselves use.

---

## Installation

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

Default features unchanged from 0.3.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.3.0...v0.4.0`](https://github.com/jamesgober/key-vault/compare/v0.3.0...v0.4.0)

---

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