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
Lands the broadcast-channel primitive Prism asked for in Docs/shade-feature-request-sender-keys.md. The crypto in @shade/core/sender-keys.ts was already in place; this release wires it up as a first-class app-facing API, adds the persistence schema across all six storage backends (memory, sqlite, indexeddb + encrypted variants), introduces wire type 0x21 in @shade/proto, and ships Prism's three acceptance tests verbatim. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
267 lines
10 KiB
TypeScript
267 lines
10 KiB
TypeScript
import type { IdentityKeyPair, SignedPreKey, OneTimePreKey, SessionState } from './types.js';
|
|
|
|
/** A retired identity kept in history during the rotation grace period */
|
|
export interface RetiredIdentity {
|
|
keyPair: IdentityKeyPair;
|
|
retiredAt: number;
|
|
}
|
|
|
|
/**
|
|
* Persisted stream-transfer resume record. Holds enough state for either
|
|
* side of a transfer to resume after restart. The `streamSecret` MUST be
|
|
* encrypted before storage (see `@shade/transfer` for the deviceKey-based
|
|
* AES-GCM at-rest scheme).
|
|
*/
|
|
export interface PersistedStreamState {
|
|
streamId: string;
|
|
direction: 'send' | 'receive';
|
|
peerAddress: string;
|
|
status: 'active' | 'paused' | 'finished' | 'aborted';
|
|
/** JSON-serialized `StreamMetadata`. */
|
|
metadataJson: string;
|
|
/** JSON-serialized `LaneInitSpec[]`. */
|
|
partitionJson: string;
|
|
/** JSON-serialized per-lane progress array (laneId/nextSeq/bytesProcessed). */
|
|
laneStateJson: string;
|
|
/** JSON-serialized I/O descriptor (file path / file handle reference / buffer). */
|
|
ioDescriptorJson: string;
|
|
/** AES-GCM-encrypted streamSecret (under deviceKey). */
|
|
secretEnc: Uint8Array;
|
|
/** AES-GCM nonce used for `secretEnc`. */
|
|
secretNonce: Uint8Array;
|
|
/**
|
|
* Reserved for future hasher serialization. Empty in v0.2.0; resume
|
|
* re-hashes received bytes from disk.
|
|
*/
|
|
overallHashState?: string;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
/**
|
|
* Why a peer's fingerprint was deemed verified.
|
|
* - `user`: human did the side-channel comparison and confirmed.
|
|
* - `transitive`: trust derived from another verified party (reserved for V3.10).
|
|
* - `tofu-after-warning`: caller bypassed gate after seeing a warning.
|
|
*/
|
|
export type PeerVerificationSource = 'user' | 'transitive' | 'tofu-after-warning';
|
|
|
|
/**
|
|
* Persisted broadcast-channel state (V4.6). Holds the sender-key chain that
|
|
* lets the channel owner encrypt a single ciphertext and fan it out to all
|
|
* paired members.
|
|
*
|
|
* - `ownerRole === 'sender'`: this device created the channel; the record
|
|
* carries the full chain (chainKey + iteration + signing keypair).
|
|
* - `ownerRole === 'receiver'`: this device joined a channel that
|
|
* `ownerAddress` owns; we hold a tracking copy of the chain (chainKey +
|
|
* iteration + signing public key only — no private signing key).
|
|
*
|
|
* `generation` is bumped each time `removeMember` rotates the chain. Stale
|
|
* broadcasts at lower generations are silently dropped on receive.
|
|
*/
|
|
export interface BroadcastChannelRecord {
|
|
/** Opaque, stable across restarts. UUID-style. */
|
|
channelId: string;
|
|
ownerRole: 'sender' | 'receiver';
|
|
/** The address that owns the channel (i.e. the only sender). */
|
|
ownerAddress: string;
|
|
label?: string;
|
|
/** Sender-key generation — bumped on rotation. Starts at 0. */
|
|
generation: number;
|
|
/** Current chain key (32 bytes). */
|
|
chainKey: Uint8Array;
|
|
/** Counter advanced by `senderKeyEncrypt` / `senderKeyDecrypt`. */
|
|
iteration: number;
|
|
/** Owner's Ed25519 signing public key (32 bytes). */
|
|
signingPublicKey: Uint8Array;
|
|
/** Owner's Ed25519 signing private key. Present iff `ownerRole === 'sender'`. */
|
|
signingPrivateKey?: Uint8Array;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
/** Membership row for a broadcast channel (sender-side only). */
|
|
export interface BroadcastMemberRecord {
|
|
channelId: string;
|
|
peerAddress: string;
|
|
joinedAt: number;
|
|
/** When this peer was revoked. `null` while still active. */
|
|
removedAt: number | null;
|
|
}
|
|
|
|
/**
|
|
* Persistent record that a peer's safety number was verified at a point
|
|
* in time. `identityVersion` is the local counter for that peer's identity:
|
|
* incrementing it (e.g. via `bumpPeerIdentityVersion` on `acceptIdentityChange`)
|
|
* invalidates the saved verification because `isPeerVerified` requires the
|
|
* stored version to equal the current version.
|
|
*/
|
|
export interface PeerVerification {
|
|
peerAddress: string;
|
|
fingerprint: string;
|
|
verifiedAt: number;
|
|
verifiedBy: PeerVerificationSource;
|
|
identityVersion: number;
|
|
}
|
|
|
|
/**
|
|
* StorageProvider — abstract interface for persisting cryptographic state.
|
|
*
|
|
* Implementations per platform:
|
|
* - In-memory (testing)
|
|
* - IndexedDB (browser)
|
|
* - SQLite/PostgreSQL (server)
|
|
* - EncryptedSharedPreferences (Android)
|
|
*/
|
|
export interface StorageProvider {
|
|
// ─── Identity ──────────────────────────────────────────────
|
|
|
|
/** Get our local identity keypair, or null if not yet generated */
|
|
getIdentityKeyPair(): Promise<IdentityKeyPair | null>;
|
|
|
|
/** Persist our local identity keypair */
|
|
saveIdentityKeyPair(keyPair: IdentityKeyPair): Promise<void>;
|
|
|
|
/** Get our local registration ID (unique per installation) */
|
|
getLocalRegistrationId(): Promise<number>;
|
|
|
|
/** Save our local registration ID */
|
|
saveLocalRegistrationId(id: number): Promise<void>;
|
|
|
|
// ─── Signed Pre-Keys ──────────────────────────────────────
|
|
|
|
/** Get a signed prekey by ID */
|
|
getSignedPreKey(keyId: number): Promise<SignedPreKey | null>;
|
|
|
|
/** Persist a signed prekey */
|
|
saveSignedPreKey(key: SignedPreKey): Promise<void>;
|
|
|
|
/** Remove a signed prekey (after rotation grace period) */
|
|
removeSignedPreKey(keyId: number): Promise<void>;
|
|
|
|
// ─── One-Time Pre-Keys ────────────────────────────────────
|
|
|
|
/** Get a one-time prekey by ID */
|
|
getOneTimePreKey(keyId: number): Promise<OneTimePreKey | null>;
|
|
|
|
/** Persist a one-time prekey */
|
|
saveOneTimePreKey(key: OneTimePreKey): Promise<void>;
|
|
|
|
/** Remove a consumed one-time prekey */
|
|
removeOneTimePreKey(keyId: number): Promise<void>;
|
|
|
|
/** Count remaining one-time prekeys */
|
|
getOneTimePreKeyCount(): Promise<number>;
|
|
|
|
// ─── Sessions ─────────────────────────────────────────────
|
|
|
|
/** Get session state for a peer address (e.g. "device:abc123") */
|
|
getSession(address: string): Promise<SessionState | null>;
|
|
|
|
/** Persist session state for a peer */
|
|
saveSession(address: string, state: SessionState): Promise<void>;
|
|
|
|
/** Remove session for a peer */
|
|
removeSession(address: string): Promise<void>;
|
|
|
|
/** Check if we trust a remote identity key (for TOFU or pinned keys) */
|
|
isTrustedIdentity(address: string, identityKey: Uint8Array): Promise<boolean>;
|
|
|
|
/** Save a trusted remote identity key */
|
|
saveTrustedIdentity(address: string, identityKey: Uint8Array): Promise<void>;
|
|
|
|
// ─── Identity History (rotation with grace period) ──────
|
|
|
|
/** Add an identity to the retired history */
|
|
addRetiredIdentity(identity: RetiredIdentity): Promise<void>;
|
|
|
|
/** Get all retired identities (for grace-period decryption) */
|
|
getRetiredIdentities(): Promise<RetiredIdentity[]>;
|
|
|
|
/** Remove retired identities older than the given timestamp */
|
|
pruneRetiredIdentities(olderThan: number): Promise<void>;
|
|
|
|
// ─── Peer verifications (V3.3) ────────────────────────────
|
|
|
|
/**
|
|
* Persist or replace the verification record for a peer. Idempotent
|
|
* upsert on `peerAddress`.
|
|
*/
|
|
savePeerVerification(verification: PeerVerification): Promise<void>;
|
|
|
|
/** Look up the saved verification for a peer (null if never verified). */
|
|
getPeerVerification(address: string): Promise<PeerVerification | null>;
|
|
|
|
/** Remove a peer's verification record (e.g. user revoked trust). */
|
|
removePeerVerification(address: string): Promise<void>;
|
|
|
|
/**
|
|
* Returns the current local identity-version counter for a peer.
|
|
* Defaults to 1 when the peer has never been seen. Bumped by
|
|
* `bumpPeerIdentityVersion` whenever the peer rotates identity.
|
|
*/
|
|
getPeerIdentityVersion(address: string): Promise<number>;
|
|
|
|
/**
|
|
* Increment the peer's identity-version counter and return the new value.
|
|
* Called from `acceptIdentityChange` so previous verification rows (which
|
|
* carry the old version number) become stale.
|
|
*/
|
|
bumpPeerIdentityVersion(address: string): Promise<number>;
|
|
|
|
// ─── Stream-transfer resume state (optional, added in v0.2.0) ──
|
|
|
|
/**
|
|
* Persist or update the stream-state row for a given streamId. Idempotent:
|
|
* upserts on `streamId`. Optional — providers that don't support resume
|
|
* can omit this and consumers will fall back to in-memory state.
|
|
*/
|
|
saveStreamState?(state: PersistedStreamState): Promise<void>;
|
|
|
|
/** Look up the stream-state row by streamId. Returns null if absent. */
|
|
getStreamState?(streamId: string): Promise<PersistedStreamState | null>;
|
|
|
|
/** Remove a stream-state row (e.g. on completion or abort). */
|
|
removeStreamState?(streamId: string): Promise<void>;
|
|
|
|
/** List active or paused stream-state rows (for resume on startup). */
|
|
listActiveStreamStates?(
|
|
direction?: 'send' | 'receive',
|
|
): Promise<PersistedStreamState[]>;
|
|
|
|
/** Prune stream-state rows in `'finished' | 'aborted'` status older than `olderThan`. */
|
|
pruneStreamStates?(olderThan: number): Promise<void>;
|
|
|
|
// ─── Broadcast channels (V4.6) — optional ─────────────────
|
|
|
|
/**
|
|
* Persist or replace the broadcast-channel record. Idempotent upsert on
|
|
* `channelId`. Backends that don't implement broadcast-channel support
|
|
* may omit this; the SDK throws a clear error when the app tries to
|
|
* call `createBroadcastChannel` on such a backend.
|
|
*/
|
|
saveBroadcastChannel?(channel: BroadcastChannelRecord): Promise<void>;
|
|
|
|
/** Look up a broadcast channel by id. */
|
|
getBroadcastChannel?(channelId: string): Promise<BroadcastChannelRecord | null>;
|
|
|
|
/** Enumerate all broadcast channels persisted on this device. */
|
|
listBroadcastChannels?(): Promise<BroadcastChannelRecord[]>;
|
|
|
|
/** Drop a broadcast channel and all its membership rows. */
|
|
removeBroadcastChannel?(channelId: string): Promise<void>;
|
|
|
|
/** Persist or replace a broadcast-membership row (sender-side only). */
|
|
saveBroadcastMember?(member: BroadcastMemberRecord): Promise<void>;
|
|
|
|
/**
|
|
* List membership rows for a channel. Includes revoked members (with
|
|
* `removedAt !== null`); callers filter as needed.
|
|
*/
|
|
getBroadcastMembers?(channelId: string): Promise<BroadcastMemberRecord[]>;
|
|
|
|
/** Hard-delete a single membership row. */
|
|
removeBroadcastMember?(channelId: string, peerAddress: string): Promise<void>;
|
|
}
|