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 vault-derived X25519 sync35 * identity static key — see `deriveSyncIdentityKeypair`. Every WebSocket36 * reconnect runs Noise KK with both local static keys, followed by a37 * PAIR_IDENTITY exchange bound to the authenticated keys.38 */39export interface PairedDevice {40 deviceId: string;41 deviceName?: string;42 /** User-chosen label stored only on this device; never sent to peers. */43 localAlias?: string;44 deviceType?: DeviceType;45 longTermPublicKeyHex?: string;46 /**47 * Long-term Ed25519 verification key for the peer's device-manifest48 * signatures (v2 brief §4.3). Derived deterministically from the same49 * master key as `longTermPublicKeyHex`, via `deriveSyncSigningKeypair`.50 */51 signingPublicKeyHex?: string;52 /** v2 brief §4.2 — distinguishes direct-paired vs vouched-via-manifest. */53 trustSource?: TrustSource;54 /** Number of vouching edges from a directly paired device. */55 vouchDepth?: number;56 /**57 * The peer's WebSocket sync port. Current Android hosts use fixed port 4914258 * and announce it inside the encrypted pairing channel.59 */60 wsPort?: number;61 /** Pair-only 32-byte reconnect capability, derived from the first handshake. */62 rendezvousSecretHex?: string;63 /** Validated wss relay used by this pair. */64 relayUrl?: string;65 pairedAt: number;66 lastSyncedAt?: number;67}6869// ---------------------------------------------------------------------------70// v3 wire protocol — phase-2 WebSocket manifest sync71// ---------------------------------------------------------------------------7273/**74 * v3 wire protocol — after reconnect Noise KK is open and PAIR_IDENTITY has75 * been bound to both authenticated static keys.76 *77 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise KK. The78 * WebSocket transport sees only ciphertext.79 *80 * The manifest carries per-entry timestamps; the receiver requests entries81 * where the remote timestamp beats its own. Entries traverse as opaque82 * encrypted blobs — the sync layer never decrypts vault data.83 */84export type SyncMessage =85 | {86 type: "SYNC_HELLO";87 vaultId: string;88 deviceId: string;89 version: number;90 deviceTime: number;91 /**92 * Wire protocol version, incremented when the sync sequence itself93 * changes. Distinct from `version` (vault data version) and94 * `deviceManifestVersion`. Old clients omit this field; treat missing95 * as 1.96 */97 protocolVersion?: number;98 /**99 * Advertises equal-version device-graph exchange support. Older peers100 * omit this field and retain the legacy SYNC_DONE fast path.101 */102 deviceManifestVersion?: number;103 }104 | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }105 | { type: "SYNC_REQUEST"; entryIds: string[] }106 | { type: "SYNC_ENTRIES"; entries: Record<string, string> }107 | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }108 | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest | null }109 | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number }110 | { type: "SYNC_ERROR"; error: string };111112// ---------------------------------------------------------------------------113// v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)114// ---------------------------------------------------------------------------115116/** 0x03 adds each peer edge's relay URL so a newly introduced pair can agree117 * on a route without depending on the introducing device. Coordinated wire118 * change with peach-android. */119export const SYNC_PROTOCOL_VERSION = 2 as const;120121export const DEVICE_MANIFEST_VERSION = 0x04 as const;122export const MAX_VOUCH_DEPTH = 3;123/** Manifests older than this are silently rejected (v2 brief §4.3). */124export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000;125126/**127 * One device entry inside a signed manifest. `longTermPublicKeyHex` is the128 * X25519 sync identity key the receiver will use for Noise KK reconnect.129 * `signingPublicKeyHex` is the130 * peer's Ed25519 manifest-verification key — needed so a vouched mobile131 * can in turn verify manifests issued by the same vault from a different132 * device.133 */134export interface ManifestDeviceEntry {135 deviceId: string;136 deviceName: string;137 deviceType: DeviceType;138 longTermPublicKeyHex: string;139 signingPublicKeyHex: string;140 /**141 * Optional sync WebSocket port for native/mobile hosts. Preserving this in142 * vouched entries lets mobile peers connect without another audio pairing.143 */144 wsPort?: number;145 /** Relay the issuer currently uses for this peer edge. */146 relayUrl?: string;147 pairedAt: number;148 /** Number of vouching edges from a directly paired device. */149 vouchDepth?: number;150}151152/**153 * A device that must no longer be synced with. Revocation is TERMINAL: once a154 * deviceId OR its long-term key appears here, no manifest may re-vouch it and155 * no pending entry carrying it may be confirmed. That is deliberate —156 * tombstone-style "newest timestamp wins" ordering would let a peer resurrect a157 * revoked device just by issuing a manifest with a fresh `pairedAt`, and every158 * timestamp in a manifest is peer-supplied. Re-admitting a device requires a159 * direct, SAS-confirmed re-pair with a physically present user.160 *161 * HONEST LIMIT: every mesh member holds the vault root key, and signing keys162 * are derived from it (`deriveSyncSigningKeypair(rootKey, deviceId)`), so these163 * signatures prove *vault membership*, not device identity. A revoked device164 * still holds the root key and, running modified software, can ignore its own165 * revocation. This mechanism stops honest peers from talking to a revoked166 * device; it is not a cryptographic ejection. See HOLE 4 — a genuinely167 * compromised device needs the master password and recovery words rotated.168 */169export interface DeviceRevocation {170 deviceId: string;171 /** X25519 sync identity key at the moment of revocation. Load-bearing, not172 * descriptive: `deviceId` is whatever an issuing manifest says it is, so the173 * key is what re-vouching is actually matched against. `0`×64 means no key174 * was ever stored for the device and matches nothing. */175 longTermPublicKeyHex: string;176 revokedAt: number;177 /** deviceId that issued the revocation, for display only. */178 revokedByDeviceId: string;179}180181/** Body that gets canonicalised + signed. Wire format JSON-stable. */182export interface DeviceManifestBody {183 manifestVersion: number;184 issuerDeviceId: string;185 issuedAt: number;186 devices: ManifestDeviceEntry[];187 revoked: DeviceRevocation[];188}189190export interface SignedDeviceManifest {191 body: DeviceManifestBody;192 /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */193 signatureHex: string;194}