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>
92 lines
3.4 KiB
TypeScript
92 lines
3.4 KiB
TypeScript
/**
|
|
* Guardian-side persistence for received shares.
|
|
*
|
|
* Each guardian holds, per protected user, exactly one record per
|
|
* `setupId`:
|
|
*
|
|
* {
|
|
* originalAddress, // the protected user's Shade address
|
|
* setupId, // disambiguates if a user re-runs setup
|
|
* shareIndex, // their assigned x-coordinate
|
|
* shareBytes, // base64-encoded Shamir share
|
|
* shareSecretCiphertext, // base64 — encrypted backup payload
|
|
* shareSecretNonce, // base64 — AES-GCM nonce
|
|
* setupFingerprint, // safety number at deposit time
|
|
* guardianCount, threshold, // informational
|
|
* receivedAt
|
|
* }
|
|
*
|
|
* The interface is deliberately minimal so consumers can back it with
|
|
* whatever they already use for app state — IndexedDB, AsyncStorage,
|
|
* SQLite, etc. The package ships an in-memory implementation suitable
|
|
* for tests and small one-device demos. Guardian apps that survive
|
|
* restart MUST supply a persistent implementation.
|
|
*/
|
|
|
|
export interface GuardianShareEntry {
|
|
originalAddress: string;
|
|
setupId: string;
|
|
shareIndex: number;
|
|
shareBytes: string;
|
|
shareSecretCiphertext: string;
|
|
shareSecretNonce: string;
|
|
setupFingerprint: string;
|
|
guardianCount: number;
|
|
threshold: number;
|
|
receivedAt: number;
|
|
}
|
|
|
|
/**
|
|
* The minimal contract a guardian-side share store must implement.
|
|
*
|
|
* Shapes:
|
|
* - `save` is upsert by (originalAddress, setupId). If a user re-runs
|
|
* setup with a different setupId, both entries persist; the guardian
|
|
* can choose which to release based on the recovery-request's
|
|
* setupId. If the same (originalAddress, setupId) is saved twice,
|
|
* the second write wins (idempotent re-deposit).
|
|
* - `get` returns the deposit for the (originalAddress, setupId) pair,
|
|
* or `null` when none exists.
|
|
* - `list` enumerates everything (used by guardian-UX widgets).
|
|
* - `delete` removes a single deposit; the guardian-UX exposes this so
|
|
* a user can prune deposits from people they no longer wish to
|
|
* protect.
|
|
*/
|
|
export interface RecoveryStore {
|
|
save(entry: GuardianShareEntry): Promise<void>;
|
|
get(originalAddress: string, setupId: string): Promise<GuardianShareEntry | null>;
|
|
list(): Promise<GuardianShareEntry[]>;
|
|
delete(originalAddress: string, setupId: string): Promise<void>;
|
|
}
|
|
|
|
/**
|
|
* Process-local in-memory store. Suitable for tests and one-shot demos;
|
|
* LOSES STATE ON RESTART. Production guardian apps must supply a
|
|
* persistent `RecoveryStore` (see `docs/recovery.md` for backing-store
|
|
* recommendations).
|
|
*/
|
|
export class MemoryRecoveryStore implements RecoveryStore {
|
|
private readonly entries = new Map<string, GuardianShareEntry>();
|
|
|
|
async save(entry: GuardianShareEntry): Promise<void> {
|
|
this.entries.set(keyOf(entry.originalAddress, entry.setupId), { ...entry });
|
|
}
|
|
|
|
async get(originalAddress: string, setupId: string): Promise<GuardianShareEntry | null> {
|
|
const found = this.entries.get(keyOf(originalAddress, setupId));
|
|
return found === undefined ? null : { ...found };
|
|
}
|
|
|
|
async list(): Promise<GuardianShareEntry[]> {
|
|
return Array.from(this.entries.values()).map((e) => ({ ...e }));
|
|
}
|
|
|
|
async delete(originalAddress: string, setupId: string): Promise<void> {
|
|
this.entries.delete(keyOf(originalAddress, setupId));
|
|
}
|
|
}
|
|
|
|
function keyOf(originalAddress: string, setupId: string): string {
|
|
return `${originalAddress} ${setupId}`;
|
|
}
|