// Vault sync — type definitions. // // Pairing happens once per device pair over a relay rendezvous; all ongoing // sync afterwards runs over that same relay with Noise KK. There is one // transport — LAN peer-to-peer sync is retired // (docs/sync-lan-retirement.md). // --------------------------------------------------------------------------- // Device types // --------------------------------------------------------------------------- export type DeviceType = "extension" | "android" | "ios"; // --------------------------------------------------------------------------- // Long-term paired device registry // --------------------------------------------------------------------------- /** * Trust source of a paired-device entry — per v2 brief §4.2. * * "direct" — established via a pairing-code session with this device. * "vouched" — introduced by a directly-paired peer via a signed device * manifest during a normal sync. A valid v3 route is admitted * automatically as same-vault membership. It is equivalent to * direct for sync, but cannot issue manifests itself; * surfaced with an "Added automatically" badge in Settings. */ export type TrustSource = "direct" | "vouched"; /** * v3 paired-device record. Stored in `chrome.storage.local` — cryptographic * keys must not leave the device via a third-party cloud service. * * `longTermPublicKeyHex` is the counterpart's random per-installation X25519 * sync identity static key. It is generated once locally and is never derived * from the vault root. Every WebSocket reconnect runs Noise KK with both local * static keys, followed by a PAIR_IDENTITY exchange bound to the authenticated * keys. */ export interface PairedDevice { deviceId: string; deviceName?: string; /** User-chosen label stored only on this device; never sent to peers. */ localAlias?: string; deviceType?: DeviceType; longTermPublicKeyHex?: string; /** * Long-term Ed25519 verification key for the peer's device-manifest * signatures (v2 brief §4.3). Derived deterministically from the same * master key as `longTermPublicKeyHex`. */ signingPublicKeyHex?: string; /** Immutable Ed25519 signing keys previously trusted for this device. */ signingKeyHistory?: Array<{ version: number; publicKeyHex: string }>; trustSource?: TrustSource; /** Number of vouching edges from a directly paired device. */ vouchDepth?: number; /** * The peer's WebSocket sync port. Current Android hosts use fixed port 49142 * and announce it inside the encrypted pairing channel. */ wsPort?: number; /** Pair-only 32-byte reconnect capability, derived from the first handshake. */ rendezvousSecretHex?: string; /** Validated wss relay used by this pair. */ relayUrl?: string; pairedAt: number; lastSyncedAt?: number; } // --------------------------------------------------------------------------- // v3 wire protocol — phase-2 WebSocket manifest sync // --------------------------------------------------------------------------- /** * v3 wire protocol — after reconnect Noise KK is open and PAIR_IDENTITY has * been bound to both authenticated static keys. * * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise KK. The * WebSocket transport sees only ciphertext. * * The manifest carries per-entry timestamps; the receiver requests entries * where the remote timestamp beats its own. Entries traverse as opaque * encrypted blobs — the sync layer never decrypts vault data. */ export type SyncMessage = | { type: "SYNC_HELLO"; vaultId: string; deviceId: string; version: number; deviceTime: number; /** * Wire protocol version, incremented when the sync sequence itself * changes. Distinct from `version` (vault data version) and * `deviceManifestVersion`. Old clients omit this field; treat missing * as 1. */ protocolVersion?: number; /** * Advertises device-graph exchange support. The step runs only when both * peers advertise the same supported version. */ deviceManifestVersion?: number; } | { type: "SYNC_MANIFEST"; entries: Record; tombstones: Record } | { type: "SYNC_REQUEST"; entryIds: string[] } | { type: "SYNC_ENTRIES"; entries: Record } | { type: "SYNC_TOMBSTONES"; tombstones: Record } | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest | null } | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number } | { type: "SYNC_ERROR"; error: string }; // --------------------------------------------------------------------------- // v3 device manifest — propagates trust between mobile devices (v2 brief §4.3) // --------------------------------------------------------------------------- /** 0x03 adds each peer edge's relay URL so a newly introduced pair can agree * on a route without depending on the introducing device. Coordinated wire * change with peach-android. */ export const SYNC_PROTOCOL_VERSION = 3 as const; export const DEVICE_MANIFEST_VERSION = 0x05 as const; export const MAX_VOUCH_DEPTH = 3; /** Manifests older than this are silently rejected (v2 brief §4.3). */ export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000; /** * One device entry inside a signed manifest. `longTermPublicKeyHex` is the * X25519 sync identity key the receiver will use for Noise KK reconnect. * `signingPublicKeyHex` is the * peer's Ed25519 manifest-verification key — needed so a vouched mobile * can in turn verify manifests issued by the same vault from a different * device. */ export interface ManifestDeviceEntry { deviceId: string; deviceName: string; deviceType: DeviceType; longTermPublicKeyHex: string; signingPublicKeyHex: string; /** * Optional sync WebSocket port for native/mobile hosts. Preserving this in * vouched entries lets mobile peers connect without another audio pairing. */ wsPort?: number; /** Relay the issuer currently uses for this peer edge. */ relayUrl?: string; pairedAt: number; /** Number of vouching edges from a directly paired device. */ vouchDepth?: number; } /** * A device that must no longer be synced with. Revocation is TERMINAL: once a * deviceId OR its long-term key appears here, no manifest may re-vouch it and * no pending entry carrying it may be confirmed. That is deliberate — * tombstone-style "newest timestamp wins" ordering would let a peer resurrect a * revoked device just by issuing a manifest with a fresh `pairedAt`, and every * timestamp in a manifest is peer-supplied. Re-admitting a device requires a * direct, SAS-confirmed re-pair with a physically present user. * * HONEST LIMIT: device signing keys are random per installation and are not * derived from the vault root, so a manifest signature proves possession of * that device's own private key rather than vault membership. Removal is still * enforcement by honest peers, not cryptographic ejection: a revoked device * keeps its own keypair, and a peer that has not yet applied the revocation can * still be convinced. Ejecting a genuinely compromised device requires rotating * the master password and recovery words. */ export interface DeviceRevocation { deviceId: string; longTermPublicKeyHex: string; revokedAt: number; revokedByDeviceId: string; /** Issuer signing-key identity, retained immutably with the record. */ issuerSigningKeyVersion?: number; issuerSigningPublicKeyHex?: string; /** Ed25519 signature over canonical record fields (excluding signature). */ signatureHex?: string; } /** Body that gets canonicalised + signed. Wire format JSON-stable. */ export interface DeviceManifestBody { manifestVersion: number; issuerDeviceId: string; issuedAt: number; devices: ManifestDeviceEntry[]; revoked: DeviceRevocation[]; } export interface SignedDeviceManifest { body: DeviceManifestBody; /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */ signatureHex: string; }