Skip to content

Commit a886cd3

Browse files
committed
Routing-table indirection
1 parent eb29e32 commit a886cd3

35 files changed

Lines changed: 2354 additions & 716 deletions

.github/copilot-instructions.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,19 @@
1010

1111
## Architecture
1212

13-
- `src/core/crypto.ts` — PBKDF2-SHA-256 + AES-256-GCM. Stored format: `salt(16)‖iv(12)‖ct‖tag(16)`. Also `rotateKeyName` (deterministic AES-GCM with zero IV used as PRF for key-name obfuscation) and `generateHoneyCiphertext`.
13+
- `src/core/crypto.ts` — PBKDF2-SHA-256 + AES-256-GCM. Stored format: `salt(16)‖iv(12)‖ct‖tag(16)`. Also `rotateKeyName` (HMAC-SHA256 PRF for key-name obfuscation) and `generateHoneyCiphertext`.
1414
- `src/core/session.ts``KeySession` class: per-unlock instance, idle-timeout auto-lock, BroadcastChannel cross-tab sync, `reconfirmKey` for half-life access.
15-
- `src/core/lockout.ts` — exponential backoff, wipe/delay/throw actions
16-
- `src/core/config.ts``resolveConfig()`: merges developer config with defaults and enforces security floors (lockdown threshold ≥ 10, visibility floor ≥ 200 ms, etc.)
15+
- `src/core/lockout.ts` — exponential backoff, wipe/delay/throw actions. `signLockoutRecord` / `verifyLockoutRecord` sign **only `{lockedUntil}`** (not `attempts` or `backoffMs`) — signing the full record caused false-positive tamper errors on every correct-passcode unlock after a failed attempt.
16+
- `src/core/config.ts``resolveConfig()`: merges developer config with defaults and enforces security floors (lockout threshold ≥ 3, visibility floor ≥ 200 ms, etc.)
1717
- `src/core/events.ts``EventEmitter`: informational-only, fires after tessera has already acted. Handlers cannot cancel or delay security responses.
1818
- `src/core/suspicion.ts``SuspicionEngine`: in-memory score, rate-limit detection, visibility-change gating (platform-aware), honey-key tripwires, HMAC-failure recording. Has `destroy()` to remove document listener.
1919
- `src/core/splitter.ts` — XOR-based secret sharing: `splitValue`, `reconstructValue`, base64 helpers.
2020
- `src/core/wipe.ts``hardWipe`: overwrites storage slot with 256 bytes of random noise, then removes it. Best-effort forensic mitigation.
2121
- `src/storage/claim.ts``generateClaimToken` (returns clean token without prefix), `extractTokenId` (strips `CLAIM_TOKEN_PREFIX` from stored value), `isClaimToken`.
2222
- `src/storage/honey.ts``HoneyKeyManager`: tracks per-backend decoy keys in memory only. `generateHoneyKeys` mints new decoys after each real write.
23-
- `src/adapters/``local-storage.ts`, `session-storage.ts`, `cookie.ts`, `indexed-db.ts`. All share: key-name rotation via `rotateKeyName`, encrypted metadata block (`writeTime`, `readCount`, TTL, maxReads, half-life), suspicion rate-limiting, honey-key checks on read.
23+
- `src/adapters/``local-storage.ts`, `session-storage.ts`, `cookie.ts`, `indexed-db.ts`. All share: two-level routing-table indirection (stable index slot at `rotateKeyName(key)` → ephemeral data slot at a fresh random `t_<hex>` key, replaced on every write), encrypted metadata block (`writeTime`, `readCount`, TTL, maxReads, half-life), suspicion rate-limiting, honey-key checks on read.
2424
- `src/adapters/session-storage.ts` — additionally supports `mode: 'split'` (XOR secret sharing with IDB) and `mode: 'claim'` (pointer-in-session, value-in-IDB).
25-
- `src/adapters/cookie.ts` — additionally supports `mode: 'claim'`. Cookie names are NOT rotated (cookies travel with HTTP). Hard wipe on `remove()` targets the actual cookie name.
25+
- `src/adapters/cookie.ts` — additionally supports `mode: 'claim'`. Cookie names (both index and data slots) are `t_`-prefixed rotated names — the developer key name never appears on the wire. Cookies travel with every HTTP request so honey decoys add HTTP overhead; use `honeyKeys.count: 0` if that matters. No `split` mode. No `httpOnly`. Hard wipe on `remove()` uses `Set-Cookie: name=; expires=<past>` rather than `removeItem`.
2626
- `src/ui/pin-pad.ts` — Canvas-based PIN pad (digit zones in closure, never in DOM)
2727
- `src/framework/` — React, Vue, Svelte, Angular adapters
2828
- `src/tessera.ts` — Public API: `Tessera.unlock()` object literal returning `IEnhancedVault`
@@ -38,8 +38,8 @@
3838
- Adapter reads use `session.getKeySafe()` (returns null when locked).
3939
- Adapter writes use `session.getKey()` (throws `LOCKED` when locked).
4040
- All adapters use `encryptWithSalt` / `decryptFull` — never bare `encrypt` / `decrypt`.
41-
- Stored value format: `encryptedMeta.encryptedValue` where meta contains `writeTime`, `readCount`, TTL, maxReads, and half-life thresholds.
42-
- All developer-supplied key names are obfuscated via `rotateKeyName` before hitting storage (except cookie names, which travel with HTTP).
41+
- Stored layout: two-level routing-table. **Index slot** (`rotateKeyName(key)`) stores an encrypted pointer `{ slot: "<random>" }` written LAST. **Data slot** (fresh random `t_<hex>` per write) stores `encryptedMeta.encryptedValue`. Previous data slot forensically wiped before new slot written. Backward-compat: if index fails to decrypt as JSON pointer but raw value contains `.`, adapters fall back to old single-blob format.
42+
- All developer-supplied key names are obfuscated via `rotateKeyName` before hitting storage (all four adapters, including cookie — the developer key name never appears as a cookie name on the wire).
4343
- Claim token format: adapters store `ref:<token>` in the fast backend; the actual value lives encrypted in IDB under the token key. `generateClaimToken()` returns a clean token (no prefix) — adapters prepend `CLAIM_TOKEN_PREFIX` themselves.
4444
- Configuration is resolved once at `unlock` time via `resolveConfig()` and locked for the session. No mid-session reconfiguration.
4545
- `Tessera` is an object literal (not a class) — `unicorn/no-static-only-class`.
@@ -66,7 +66,7 @@
6666

6767
- Explicit return types on public functions.
6868
- No `any` types — use `unknown` and narrow with type guards.
69-
- Passcode: 6–8 characters (PIN pad is digit-only; direct API accepts alphanumeric).
69+
- Passcode: minimum 8 characters (PIN pad is digit-only; direct API accepts alphanumeric; lower bound enforced by `validatePasscode` in `src/core/crypto.ts`).
7070
- PBKDF2: ≥ 310,000 iterations (OWASP 2024 minimum).
7171
- `for...of` over `.forEach()` and C-style for loops.
7272
- `addEventListener('error', ...)` over `.onerror` assignment.

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,24 @@ Format follows [Conventional Commits](https://www.conventionalcommits.org).
55

66
---
77

8+
## [0.1.8] — 2026-05-21
9+
10+
### Security
11+
12+
- **Routing-table indirection eliminates temporal survivor attack** — Previously, every adapter stored encrypted data at a deterministic slot (`rotateKeyName(key)`). A temporal observer who captured two storage snapshots could identify the real entry as the one that persists and updates across writes, even with honey keys present.
13+
14+
Fix: `localStorage`, `sessionStorage`, and `indexedDB` adapters now use a two-level routing-table scheme. Each developer key has a stable _index slot_ (`rotateKeyName(key)`) that stores an encrypted pointer `{slot: <random>}`. The actual encrypted data lives at a randomly-generated slot (`t_<32-hex-chars>`) that is replaced on every write. Honey slots rotate identically. A temporal observer now sees 1 stable index entry plus N+1 equally-random entries — the real data slot is no longer distinguishable.
15+
16+
The cookie adapter is unchanged; cookie names cannot be rotated because they travel with every HTTP request.
17+
18+
- **Default `honeyKeys.count` raised from 3 to 4** — With routing-table indirection, each developer key produces 1 index + 1 data + N honey entries. Raising the default count to 4 restores an 80% honey ratio (4 out of 5 randomly-keyed entries are decoys) from an external storage view.
19+
20+
### Migration
21+
22+
- **Silent backward-compatible migration** — Vault data written by v0.1.7 and earlier is read transparently: if the index slot fails to decrypt as a routing pointer but the raw value contains a dot separator, the adapter falls back to the old single-blob format. Data migrates to the new routing-table format automatically on the next write.
23+
24+
---
25+
826
## [0.1.7] — 2026-05-20
927

1028
### Security

0 commit comments

Comments
 (0)