release(v4.0.0): Shade GA — V3.x consolidation + audit prep
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Cross-platform vectors / TypeScript vectors (bun) (push) Has been cancelled
Cross-platform vectors / Kotlin vectors (gradle) (push) Has been cancelled
Docker build and publish / docker (push) Has been cancelled
Publish / publish (push) Has been cancelled
V3.1 → V3.12 consolidated and tagged for the first GA release. Wire format unchanged from 0.4.x — 4.0 peers interoperate with 0.4.x peers byte-for-byte. The version bump is semantic: audit-cycle complete, opt-in surface fully exposed, threat model refreshed for every new surface. Highlights: - All 24 @shade/* packages bumped to 4.0.0 in lockstep. - CHANGELOG 4.0.0 section is the canonical manifest of what landed. - THREAT-MODEL extended (§10 fingerprint gates, §11 WebRTC P2P, §12 Web-Worker boundary) + residual-risks table refreshed. - OpenAPI now covers all 27 routes: prekey, transfer, KT, inbox, bridge, observer, /metrics, /healthz, /ready. - MIGRATION 0.3.x → 4.0 documented + smoke-tested against shade migrate-storage on a real SQLite DB. - docs/audit/REVIEW-BUNDLE.md + SCOPE.md ready for external reviewer. - scripts/soak.ts harness for the GA-stable 2-week soak window. - All V*.md plans archived under docs/archive/ with Status: Done. - Voice/Video carved out into V5.0; 4.0 audit focuses on the frozen non-realtime stack. Tests: TS 1000/1000 + Kotlin 11/11 cross-platform vectors green. Docker: gt.zyon.no/stian/shade-prekey:4.0.0 builds and reports version 4.0.0 on /health. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
79
packages/shade-storage-encrypted/src/crypto/kdf.ts
Normal file
79
packages/shade-storage-encrypted/src/crypto/kdf.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Key derivation primitives for at-rest storage encryption.
|
||||
*
|
||||
* Hierarchy:
|
||||
* masterKey (from passphrase / keychain / app-injected)
|
||||
* │
|
||||
* ├─ HKDF("shade-storage-v1") → storageKey (32 bytes)
|
||||
* │ └─ HKDF(storageKey, table || ":" || column) → fieldKey (32 bytes)
|
||||
* │
|
||||
* └─ HKDF("shade-storage-version-v1") → versionKey (used during rotation)
|
||||
*/
|
||||
|
||||
import { scryptAsync } from '@noble/hashes/scrypt.js';
|
||||
import { hkdf } from '@noble/hashes/hkdf.js';
|
||||
import { sha256 } from '@noble/hashes/sha2.js';
|
||||
|
||||
const TEXT = new TextEncoder();
|
||||
|
||||
/** scrypt parameters — interactive; sized for sub-second derivation on commodity HW */
|
||||
export interface ScryptParams {
|
||||
N: number;
|
||||
r: number;
|
||||
p: number;
|
||||
dkLen: number;
|
||||
}
|
||||
|
||||
/** Default: N=2^17, r=8, p=1, 32-byte output. ~250ms on a modern laptop. */
|
||||
export const DEFAULT_SCRYPT: ScryptParams = { N: 1 << 17, r: 8, p: 1, dkLen: 32 };
|
||||
|
||||
/**
|
||||
* Derive a 32-byte master key from a passphrase + salt using scrypt.
|
||||
* The salt MUST be persisted alongside the encrypted database (16-byte random).
|
||||
*/
|
||||
export async function deriveMasterKey(
|
||||
passphrase: string,
|
||||
salt: Uint8Array,
|
||||
params: ScryptParams = DEFAULT_SCRYPT,
|
||||
): Promise<Uint8Array> {
|
||||
if (passphrase.length === 0) {
|
||||
throw new Error('passphrase must be non-empty');
|
||||
}
|
||||
if (salt.length < 16) {
|
||||
throw new Error('salt must be at least 16 bytes');
|
||||
}
|
||||
return scryptAsync(TEXT.encode(passphrase.normalize('NFKC')), salt, params);
|
||||
}
|
||||
|
||||
/** HKDF-SHA-256 with explicit info string. */
|
||||
export function hkdfDerive(ikm: Uint8Array, info: string, length = 32, salt?: Uint8Array): Uint8Array {
|
||||
return hkdf(sha256, ikm, salt, TEXT.encode(info), length);
|
||||
}
|
||||
|
||||
/** Derive the storageKey from masterKey. Stable, deterministic. */
|
||||
export function deriveStorageKey(masterKey: Uint8Array): Uint8Array {
|
||||
return hkdfDerive(masterKey, 'shade-storage-v1', 32);
|
||||
}
|
||||
|
||||
/** Derive the per-(table, column) field key. Stable, deterministic. */
|
||||
export function deriveFieldKey(storageKey: Uint8Array, table: string, column: string): Uint8Array {
|
||||
return hkdfDerive(storageKey, `shade-field-v1:${table}:${column}`, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a deterministic 12-byte AEAD nonce from a row key (typically the
|
||||
* field key) plus (table, pk) binding. With per-field keys, deterministic
|
||||
* nonces are safe because each (key, plaintext) pair appears at most once
|
||||
* — re-saving the same row reuses the (nonce, key) pair only because the
|
||||
* plaintext also changes (chain ratchet, prekey state, etc.). The AAD
|
||||
* also binds (table, column, pk) so swapping is rejected on decrypt.
|
||||
*/
|
||||
export function deriveNonce(rowKey: Uint8Array, table: string, pk: string): Uint8Array {
|
||||
const out = hkdfDerive(rowKey, `shade-row-nonce-v1:${table}:${pk}`, 12);
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Build the AAD that binds (table, column, pk) to a ciphertext. */
|
||||
export function buildAad(table: string, column: string, pk: string): Uint8Array {
|
||||
return TEXT.encode(`shade-aad-v1|${table}|${column}|${pk}`);
|
||||
}
|
||||
Reference in New Issue
Block a user