release(v4.5.0): browser-side encrypted storage + multi-factor unlock
Adds the foundations Prism's web client (and any future browser-based Shade app) needs: at-rest-encrypted IndexedDB storage that mirrors the SQLite backend byte-for-byte at the AAD/nonce level, browser-safe subpath imports so Vite/webpack/esbuild stop hitting bun:sqlite, and KeyManager support for argon2id and N-factor composite unlock. @shade/storage-encrypted - EncryptedIndexedDBStorage (subpath: /idb) — full StorageProvider using one object store per _enc table; reuses aeadSeal/aeadOpen + row-codec sealers so a row sealed under the SQLite or Postgres backend decrypts under IDB given the same KeyManager. bumpPeerIdentityVersion is atomic under one IDB transaction. - KeyManager argon2id source — memory-hard KDF for low-entropy secrets (PINs). Backed by @noble/hashes/argon2 (already a transitive dep). DEFAULT_ARGON2ID exported (m=64 MiB, t=3, p=1). - KeyManager composite source — HKDF-combine N sub-sources into one master. Every source mandatory; order significant by design; composite-of-composite rejected; optional info string for app-level domain separation. - Subpath exports (/crypto, /sqlite, /postgres, /idb) plus a `browser` condition on the default import that resolves to a barrel excluding the Bun- and Postgres-specific entries. Browser bundles no longer pull bun:sqlite transitively. Tests - 73 tests in shade-storage-encrypted (was 31). New coverage: argon2id determinism + reject paths, composite same-factors → same master, wrong-PIN/passphrase/order-swap → different master, info domain separation, all 28 StorageProvider methods on EncryptedIndexedDBStorage, fingerprint-mismatch rejection, and cross-impl roundtrip with EncryptedSQLiteStorage proving the AAD/ nonce derivation is implementation-agnostic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
60
CHANGELOG.md
60
CHANGELOG.md
@@ -5,6 +5,66 @@ All notable changes to Shade are documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [4.5.0] — 2026-05-07 — Browser-side encrypted storage + multi-factor unlock
|
||||
|
||||
Browser-based Shade clients (Prism's web client being the first) needed
|
||||
the same at-rest encryption story as the desktop SQLite path: identity,
|
||||
prekeys, sessions and stream-resume state persisted across reloads,
|
||||
unwrapped from a user-supplied passphrase — and on browsers, optionally
|
||||
gated behind a second factor (PIN) since there is no OS-session boundary
|
||||
to lean on. The existing barrel of `@shade/storage-encrypted` also
|
||||
transitively imported `bun:sqlite` and `postgres`, which prevented Vite/
|
||||
webpack/esbuild from producing a clean browser bundle.
|
||||
|
||||
This release adds an encrypted IndexedDB backend that mirrors
|
||||
`EncryptedSQLiteStorage` byte-for-byte at the AAD/nonce level, exposes
|
||||
browser-safe subpath imports, and lets `KeyManager` derive its master
|
||||
key from low-entropy secrets (argon2id) and from N composed factors
|
||||
(every factor mandatory).
|
||||
|
||||
### Added
|
||||
|
||||
#### `@shade/storage-encrypted`
|
||||
- `EncryptedIndexedDBStorage` — IndexedDB-backed `StorageProvider`
|
||||
exposed via `@shade/storage-encrypted/idb`. One object store per
|
||||
`_enc` table from the SQLite schema, sealed payloads as `Uint8Array`,
|
||||
routing/timestamp fields kept plaintext for query efficiency. Reuses
|
||||
`aeadSeal`/`aeadOpen` and the `row-codec` sealers verbatim — a row
|
||||
sealed under the SQLite or Postgres backend decrypts under IDB given
|
||||
the same `KeyManager`. `bumpPeerIdentityVersion` is atomic under one
|
||||
IDB transaction (closes the read-then-upsert race the SQLite version
|
||||
has).
|
||||
- `KeyManager.open({ kind: 'argon2id', ... })` — memory-hard KDF for
|
||||
low-entropy secrets (PINs, short passwords). Backed by
|
||||
`@noble/hashes/argon2` (already a transitive dep — pure JS, browser
|
||||
safe). `DEFAULT_ARGON2ID` exported (m=64 MiB, t=3, p=1, 32-byte
|
||||
output; ~250–400 ms in modern browsers).
|
||||
- `KeyManager.open({ kind: 'composite', sources, info? })` —
|
||||
HKDF-combine N sub-sources into one master key. Every source is
|
||||
required: omitting or substituting any source yields a different
|
||||
master key and `open()` fails on the storage-key-fingerprint check.
|
||||
Order is significant by design (`[pwd, pin]` ≠ `[pin, pwd]`).
|
||||
Composite-of-composite is rejected.
|
||||
- Subpath exports: `@shade/storage-encrypted/crypto` (KeyManager + KDF
|
||||
+ AEAD + row-codec, no SQLite/Postgres bindings), `/sqlite` (Bun),
|
||||
`/postgres` (Node), `/idb` (browser). The `browser` condition on the
|
||||
default import resolves to a barrel that excludes Bun/Postgres
|
||||
imports — `import { KeyManager } from '@shade/storage-encrypted'` now
|
||||
bundles cleanly under Vite without hitting `bun:sqlite` resolution
|
||||
errors.
|
||||
- Dependency: `idb` ^8.0.3.
|
||||
|
||||
### Tests
|
||||
- `packages/shade-storage-encrypted/tests/key-manager-multi-factor.test.ts`
|
||||
— argon2id determinism + reject paths, composite same-factors → same
|
||||
master, wrong-PIN/wrong-passphrase/order-swap → different master,
|
||||
explicit `info` domain separation, nested-composite rejection.
|
||||
- `packages/shade-storage-encrypted/tests/encrypted-indexeddb.test.ts`
|
||||
— full round-trip coverage of all 28 `StorageProvider` methods,
|
||||
fingerprint-mismatch rejection on wrong key, atomic peer-identity
|
||||
bump, plus cross-impl roundtrip with `EncryptedSQLiteStorage`
|
||||
proving the AAD/nonce derivation is implementation-agnostic.
|
||||
|
||||
## [4.4.0] — 2026-05-05 — Public accessor for the device's identity public key
|
||||
|
||||
Browser-based Shade consumers building enrollment flows had no way to
|
||||
|
||||
Reference in New Issue
Block a user