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}`;
|
||
|
|
}
|