Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,25 @@ All notable changes to the CacheKit Protocol Specification.

## [Unreleased]

### SDK feature matrix — TypeScript `cache.secure.wrap()` now fails closed (LAB-513)

- [`sdk-feature-matrix.md`](sdk-feature-matrix.md): the Encryption row "Does the
`secure` API enforce encryption?" flips ❌ → ✅ for TypeScript. Both
`cache.secure.wrap()` and `cache.withExecutionContext(ctx).secure.wrap()` now
throw `ConfigurationError` at wrap time on any instance without `encryption`
configured ([cachekit-ts#123](https://github.com/cachekit-io/cachekit-ts/pull/123));
before, both were unconditional aliases for `wrap()`, so on an instance
without configured encryption a "secure" registration stored plaintext
(CWE-311) — on an encrypted instance they always encrypted. All three SDKs
now refuse a missing key on the secure entry point — py raises at decoration
time, rs `secure()` returns `Err`, ts throws at wrap time — with no opt-in to
run the secure entry point unencrypted in any of them; ts callers who want
plaintext call `wrap()` explicitly. The "Intent-preset
semantics" warning is rewritten to the enforced contract, the "cells that
reversed" summary and the Rust builder-stub cross-reference are updated to
match, and the stale `cache-core.ts:832` / `:873` / `:486`, `cache.ts:87` and
`intents-core.ts:240` references are replaced with current ones.

### Wire format — compressed-byte reproducibility scoped per-vector (LAB-1751)

- LZ4 compressed bytes are **not canonical** across conforming block encoders.
Expand Down
10 changes: 5 additions & 5 deletions sdk-feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

*Last updated: 2026-08-04 — LAB-1400 consolidation of ten open matrix PRs into one end-state. Every version-keyed claim is verified against the **published artifact** (registry metadata, and the `.crate`/`.tgz` contents where an embedded dependency version decides the answer), not against a repo branch — see [decisions/matrix-version-verification.md](decisions/matrix-version-verification.md) for why and how. Per-PR fold verdicts are in [CHANGELOG.md](CHANGELOG.md); per-row history is `git log sdk-feature-matrix.md`.*

*__Cells that reversed — check these if you built on them:__ Key rotation (py/rs ✅ → ❌ fleet-wide), Rust `::secure` preset and Rust sync support (both ✅ → do not exist), Builder API (py/ts ✅ → ❌), Hardware acceleration (rs ✅ → not re-exported, ts N/A → ❌), TypeScript Arrow (🔜 → ❌), Python's encrypted read path (documented fail-closed → **fail-open by default**), and `cache.secure.wrap()` in TypeScript (implied encryption → **no guarantee**). The TypeScript protocol-1.1 `bin` rollout also reversed twice in two days: it is **not** shipped on either ts path (per-artifact evidence in the [cachekit-core architecture note](#architecture-notes)).*
*__Cells that reversed — check these if you built on them:__ Key rotation (py/rs ✅ → ❌ fleet-wide), Rust `::secure` preset and Rust sync support (both ✅ → do not exist), Builder API (py/ts ✅ → ❌), Hardware acceleration (rs ✅ → not re-exported, ts N/A → ❌), TypeScript Arrow (🔜 → ❌), Python's encrypted read path (documented fail-closed → **fail-open by default**), and `cache.secure.wrap()` in TypeScript (implied encryption → no guarantee → **enforced since LAB-513: throws without encryption**). The TypeScript protocol-1.1 `bin` rollout also reversed twice in two days: it is **not** shipped on either ts path (per-artifact evidence in the [cachekit-core architecture note](#architecture-notes)).*

</div>

Expand Down Expand Up @@ -68,7 +68,7 @@
| AAD v0x03 (cache_key binding) | ✅ | ✅ | ✅ | ❌ |
| Key rotation | ❌ mismatch detection only⁵ | ❌⁵ | ❌ — nonce-exhaustion detection exists but is **unobservable** to the caller⁵ ⁸ | ❌ |
| **Tamper / wrong-key failure mode** | ⚠️ **fail-OPEN by default** — warn + recompute; switchable with `CACHEKIT_ENCRYPTION_FAIL_CLOSED=true`¹⁸ | ✅ **Fails closed** — `decrypt(…)?` propagates (`client.rs:830`, `:847`), and `#[cachekit(secure)]` emits no fail-open arm (`cachekit-macros/src/lib.rs:439-451`) | ⚠️ **fail-OPEN on reads, silently drops writes, not switchable**⁸ | — |
| **Does the `secure` API enforce encryption?** | ✅ Raises without a key | ✅ `secure()` returns `Err` | **`cache.secure.wrap()` is an unconditional alias for `wrap()`** — silently caches plaintext on any instance not built by `createCache.secure()` (LAB-513, CWE-311); see [Intent-preset semantics](#intent-preset-semantics-parity-not-presence) | — |
| **Does the `secure` API enforce encryption?** | ✅ Raises without a key | ✅ `secure()` returns `Err` | **`cache.secure.wrap()` throws `ConfigurationError` at wrap time** on any instance without `encryption` configured — instance and `withExecutionContext(ctx)` view alike (LAB-513; before it, an unconditional alias for `wrap()`, so on an instance without configured encryption a "secure" registration cached plaintext, CWE-311); see [Intent-preset semantics](#intent-preset-semantics-parity-not-presence) | — |
| Hardware acceleration detection | ✅ surfaced (`hardware_acceleration_enabled()`) | ⚠️ core-internal, not re-exported⁶ | ❌ not exposed⁶ | N/A |
| Counter-based nonces | ✅ via Rust | ✅ | ✅ via NAPI (Rust) | ❌ use random |

Expand Down Expand Up @@ -115,7 +115,7 @@
>
> ¹⁹ **`cargo add cachekit-rs --features workers` does not compile.** Cargo features are additive, so that command keeps the default set — and the published 0.6.0 crate declares `default = ["cachekitio", "encryption", "l1", "reliability"]` while `src/lib.rs` carries `compile_error!` for `workers`×`l1` (moka needs std threads) *and* `workers`×`reliability` (retry/breaker timers need tokio `time`), plus `workers`×`redis`, `workers`×`memcached` and `workers`×`file`. The Workers build is therefore `--no-default-features --features workers,encryption,cachekitio`.
>
> **Do not drop `encryption` from that list.** With the feature off, `CacheKitBuilder::encryption()` and `::encryption_from_bytes()` are compiled as **silent no-op stubs that return `Ok(self)`** (`crates/cachekit/src/client.rs:1019-1032`) — so the documented builder call succeeds, no error surfaces anywhere, and the cache stores **plaintext at rest**. Only `secure()` is `#[cfg]`-gated and fails loudly; the builder path is not. That is the same CWE-311 shape as the `cache.secure.wrap()` row above, reached by following a feature list instead of an API. Every ✅ in this document's Encryption table assumes `encryption` is enabled.
> **Do not drop `encryption` from that list.** With the feature off, `CacheKitBuilder::encryption()` and `::encryption_from_bytes()` are compiled as **silent no-op stubs that return `Ok(self)`** (`crates/cachekit/src/client.rs:1019-1032`) — so the documented builder call succeeds, no error surfaces anywhere, and the cache stores **plaintext at rest**. Only `secure()` is `#[cfg]`-gated and fails loudly; the builder path is not. That is the CWE-311 shape TypeScript's `cache.secure.wrap()` had before LAB-513 (row above), reached by following a feature list instead of an API. Every ✅ in this document's Encryption table assumes `encryption` is enabled.
>
> Verified in the published `cachekit-rs-0.6.0.crate`, not the branch. The ✅ is real — the invocation most readers would try is not, which is why it is stated on the cell rather than left to a compiler error.

Expand Down Expand Up @@ -254,9 +254,9 @@ The four shared preset names configure **different things per SDK**. Each cell i
| `CACHEKIT_MASTER_KEY` auto-enables encryption | **all presets** | only `CacheKit::from_env()` | only `createCache.secure()` |

> [!WARNING]
> Preset names promise more parity than they deliver. The sharpest traps: Python preset entries **live forever** where Rust and TypeScript expire in 300–3 600 s; the master key is a **hex string** in py/ts but **raw bytes** in rs, so passing the same value across SDKs is a type error at best and a wrong key at worst; and the same `CACHEKIT_MASTER_KEY` activates encryption everywhere in Python, only in `from_env()` in Rust, and only in `.secure()` in TypeScript. All three do reject a *missing* key on the encrypted preset — Python raises, Rust returns `Err`, and `createCache.secure()` throws `ConfigurationError` (`intents-core.ts:240`).
> Preset names promise more parity than they deliver. The sharpest traps: Python preset entries **live forever** where Rust and TypeScript expire in 300–3 600 s; the master key is a **hex string** in py/ts but **raw bytes** in rs, so passing the same value across SDKs is a type error at best and a wrong key at worst; and the same `CACHEKIT_MASTER_KEY` activates encryption everywhere in Python, only in `from_env()` in Rust, and only in `.secure()` in TypeScript. All three do reject a *missing* key on the encrypted preset — Python raises, Rust returns `Err`, and `createCache.secure()` throws `ConfigurationError` (`intents-core.ts:257`).
>
> **`cache.secure.wrap()` guarantees nothing in TypeScript (LAB-513).** It is an unconditional alias for `cache.wrap()` — `secure = { wrap: (fn, options) => this.wrap(fn, options) }` (`cache-core.ts:832`, mirrored at `:873` for `withExecutionContext`) — and every intent, `minimal` and `production` and `io` included, is typed as `SecureCache` (`cache.ts:87`). Encryption applies only where an encryption manager was configured (`if (this.encryption)`, `cache-core.ts:486`), so moving sensitive values behind `cache.secure.wrap` on a cache **not** built by `createCache.secure()` stores them as plaintext with no error, no warning, and no type error. Only `createCache.secure()` turns encryption on. Python and Rust have no equivalent trap: py raises and rs's `secure()` accessor returns `Err` without configured encryption.
> **`cache.secure.wrap()` fails closed in TypeScript (LAB-513).** Before LAB-513 it was an unconditional alias for `cache.wrap()` at both sites — the instance property and the `withExecutionContext(ctx)` view — so a "secure" registration on a cache without `encryption` configured — typically one **not** built by `createCache.secure()` — stored plaintext with no error, no warning, and no type error. Both sites now route through one private guard, `CacheImpl.secureWrap` (`cache-core.ts:1207`), that throws `ConfigurationError` at wrap time — not on first call — when no encryption manager is configured (`this.encryption === null`). The type surface is unchanged: every intent, `minimal` and `production` and `io` included, is still typed `SecureCache` (`types/cache.ts:344`), so `.secure` is *present* on all of them and *refuses* on the unencrypted ones. There is deliberately no option to run it unencrypted — a caller who wants plaintext calls `wrap()`. This is the py/rs contract: py raises at decoration time (`decorators/intent.py:208`) and rs's `secure()` returns `Err` (`client.rs:664`) without configured encryption.

---

Expand Down
Loading