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>
192 lines
7.3 KiB
TypeScript
192 lines
7.3 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';
|
|
|
|
/**
|
|
* 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>;
|
|
}
|