sync-types.ts
1// Vault sync — type definitions.2//3// Pairing happens once per device pair over a relay rendezvous; all ongoing4// sync afterwards runs over that same relay with Noise KK. There is one5// transport — LAN peer-to-peer sync is retired6// (docs/sync-lan-retirement.md).78// ---------------------------------------------------------------------------9// Device types10// ---------------------------------------------------------------------------1112export type DeviceType = "extension" | "android" | "ios";1314// ---------------------------------------------------------------------------15// Long-term paired device registry16// ---------------------------------------------------------------------------1718/**19 * Trust source of a paired-device entry — per v2 brief §4.2.20 *21 * "direct" — established via a pairing-code session with this device.22 * "vouched" — introduced by a directly-paired peer via a signed device23 * manifest during a normal sync. A valid v3 route is admitted24 * automatically as same-vault membership. It is equivalent to25 * direct for sync, but cannot issue manifests itself;26 * surfaced with an "Added automatically" badge in Settings.27 */28export type TrustSource = "direct" | "vouched";2930/**31 * v3 paired-device record. Stored in `chrome.storage.local` — cryptographic32 * keys must not leave the device via a third-party cloud service.33 *34 * `longTermPublicKeyHex` is the counterpart's random per-installation X2551935 * sync identity static key. It is generated once locally and is never derived36 * from the vault root. Every WebSocket reconnect runs Noise KK with both local37 * static keys, followed by a PAIR_IDENTITY exchange bound to the authenticated38 * keys.39 */40export interface PairedDevice {41 deviceId: string;42 deviceName?: string;43 /** User-chosen label stored only on this device; never sent to peers. */44 localAlias?: string;45 deviceType?: DeviceType;46 longTermPublicKeyHex?: string;47 /**48 * Long-term Ed25519 verification key for the peer's device-manifest49 * signatures (v2 brief §4.3). Derived deterministically from the same50 * master key as `longTermPublicKeyHex`.51 */52 signingPublicKeyHex?: string;53 /** Immutable Ed25519 signing keys previously trusted for this device. */54 signingKeyHistory?: Array<{ version: number; publicKeyHex: string }>;55 trustSource?: TrustSource;56 /** Number of vouching edges from a directly paired device. */57 vouchDepth?: number;58 /**59 * The peer's WebSocket sync port. Current Android hosts use fixed port 4914260 * and announce it inside the encrypted pairing channel.61 */62 wsPort?: number;63 /** Pair-only 32-byte reconnect capability, derived from the first handshake. */64 rendezvousSecretHex?: string;65 /** Validated wss relay used by this pair. */66 relayUrl?: string;67 pairedAt: number;68 lastSyncedAt?: number;69}7071// ---------------------------------------------------------------------------72// v3 wire protocol — phase-2 WebSocket manifest sync73// ---------------------------------------------------------------------------7475/**76 * v3 wire protocol — after reconnect Noise KK is open and PAIR_IDENTITY has77 * been bound to both authenticated static keys.78 *79 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise KK. The80 * WebSocket transport sees only ciphertext.81 *82 * The manifest carries per-entry timestamps; the receiver requests entries83 * where the remote timestamp beats its own. Entries traverse as opaque84 * encrypted blobs — the sync layer never decrypts vault data.85 */86export type SyncMessage =87 | {88 type: "SYNC_HELLO";89 vaultId: string;90 deviceId: string;91 version: number;92 deviceTime: number;93 /**94 * Wire protocol version, incremented when the sync sequence itself95 * changes. Distinct from `version` (vault data version) and96 * `deviceManifestVersion`. Old clients omit this field; treat missing97 * as 1.98 */99 protocolVersion?: number;100 /**101 * Advertises device-graph exchange support. The step runs only when both102 * peers advertise the same supported version.103 */104 deviceManifestVersion?: number;105 }106 | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }107 | { type: "SYNC_REQUEST"; entryIds: string[] }108 | { type: "SYNC_ENTRIES"; entries: Record<string, string> }109 | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }110 | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest | null }111 | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number }112 | { type: "SYNC_ERROR"; error: string };113114// ---------------------------------------------------------------------------115// v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)116// ---------------------------------------------------------------------------117118/** 0x03 adds each peer edge's relay URL so a newly introduced pair can agree119 * on a route without depending on the introducing device. Coordinated wire120 * change with peach-android. */121export const SYNC_PROTOCOL_VERSION = 3 as const;122123export const DEVICE_MANIFEST_VERSION = 0x05 as const;124export const MAX_VOUCH_DEPTH = 3;125/** Manifests older than this are silently rejected (v2 brief §4.3). */126export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000;127128/**129 * One device entry inside a signed manifest. `longTermPublicKeyHex` is the130 * X25519 sync identity key the receiver will use for Noise KK reconnect.131 * `signingPublicKeyHex` is the132 * peer's Ed25519 manifest-verification key — needed so a vouched mobile133 * can in turn verify manifests issued by the same vault from a different134 * device.135 */136export interface ManifestDeviceEntry {137 deviceId: string;138 deviceName: string;139 deviceType: DeviceType;140 longTermPublicKeyHex: string;141 signingPublicKeyHex: string;142 /**143 * Optional sync WebSocket port for native/mobile hosts. Preserving this in144 * vouched entries lets mobile peers connect without another audio pairing.145 */146 wsPort?: number;147 /** Relay the issuer currently uses for this peer edge. */148 relayUrl?: string;149 pairedAt: number;150 /** Number of vouching edges from a directly paired device. */151 vouchDepth?: number;152}153154/**155 * A device that must no longer be synced with. Revocation is TERMINAL: once a156 * deviceId OR its long-term key appears here, no manifest may re-vouch it and157 * no pending entry carrying it may be confirmed. That is deliberate —158 * tombstone-style "newest timestamp wins" ordering would let a peer resurrect a159 * revoked device just by issuing a manifest with a fresh `pairedAt`, and every160 * timestamp in a manifest is peer-supplied. Re-admitting a device requires a161 * direct, SAS-confirmed re-pair with a physically present user.162 *163 * HONEST LIMIT: device signing keys are random per installation and are not164 * derived from the vault root, so a manifest signature proves possession of165 * that device's own private key rather than vault membership. Removal is still166 * enforcement by honest peers, not cryptographic ejection: a revoked device167 * keeps its own keypair, and a peer that has not yet applied the revocation can168 * still be convinced. Ejecting a genuinely compromised device requires rotating169 * the master password and recovery words.170 */171export interface DeviceRevocation {172 deviceId: string;173 longTermPublicKeyHex: string;174 revokedAt: number;175 revokedByDeviceId: string;176 /**177 * Frozen record version, part of the signed canonical bytes so a peer cannot178 * reinterpret the record under a different version.179 */180 recordVersion?: number;181 /** Ed25519 signature over the canonical record fields (excluding this one). */182 signatureHex?: string;183}184185/**186 * Frozen revocation record version (campaign §5.5, Amendment A-2).187 *188 * The record deliberately carries no issuer key: a receiver resolves189 * `revokedByDeviceId` through its own immutable known-key history, so a peer190 * that merely relays a manifest cannot authorise a revocation by supplying a191 * key of its own.192 */193export const DEVICE_REVOCATION_RECORD_VERSION = 2 as const;194195/** Body that gets canonicalised + signed. Wire format JSON-stable. */196export interface DeviceManifestBody {197 manifestVersion: number;198 issuerDeviceId: string;199 issuedAt: number;200 devices: ManifestDeviceEntry[];201 revoked: DeviceRevocation[];202}203204export interface SignedDeviceManifest {205 body: DeviceManifestBody;206 /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */207 signatureHex: string;208}