sync-types.ts

7.9 KB · raw

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`.50   */51  signingPublicKeyHex?: string;52  /** Immutable Ed25519 signing keys previously trusted for this device. */53  signingKeyHistory?: Array<{ version: number; publicKeyHex: string }>;54  trustSource?: TrustSource;55  /** Number of vouching edges from a directly paired device. */56  vouchDepth?: number;57  /**58   * The peer's WebSocket sync port. Current Android hosts use fixed port 4914259   * and announce it inside the encrypted pairing channel.60   */61  wsPort?: number;62  /** Pair-only 32-byte reconnect capability, derived from the first handshake. */63  rendezvousSecretHex?: string;64  /** Validated wss relay used by this pair. */65  relayUrl?: string;66  pairedAt: number;67  lastSyncedAt?: number;68}6970// ---------------------------------------------------------------------------71// v3 wire protocol — phase-2 WebSocket manifest sync72// ---------------------------------------------------------------------------7374/**75 * v3 wire protocol — after reconnect Noise KK is open and PAIR_IDENTITY has76 * been bound to both authenticated static keys.77 *78 * Every frame is JSON, then ChaCha20-Poly1305 encrypted by Noise KK. The79 * WebSocket transport sees only ciphertext.80 *81 * The manifest carries per-entry timestamps; the receiver requests entries82 * where the remote timestamp beats its own. Entries traverse as opaque83 * encrypted blobs — the sync layer never decrypts vault data.84 */85export type SyncMessage =86  | {87      type: "SYNC_HELLO";88      vaultId: string;89      deviceId: string;90      version: number;91      deviceTime: number;92      /**93       * Wire protocol version, incremented when the sync sequence itself94       * changes. Distinct from `version` (vault data version) and95       * `deviceManifestVersion`. Old clients omit this field; treat missing96       * as 1.97       */98      protocolVersion?: number;99      /**100       * Advertises device-graph exchange support. The step runs only when both101       * peers advertise the same supported version.102       */103      deviceManifestVersion?: number;104    }105  | { type: "SYNC_MANIFEST"; entries: Record<string, number>; tombstones: Record<string, number> }106  | { type: "SYNC_REQUEST"; entryIds: string[] }107  | { type: "SYNC_ENTRIES"; entries: Record<string, string> }108  | { type: "SYNC_TOMBSTONES"; tombstones: Record<string, number> }109  | { type: "SYNC_DEVICE_MANIFEST"; manifest: SignedDeviceManifest | null }110  | { type: "SYNC_DONE"; entriesReceived?: number; entriesSent?: number }111  | { type: "SYNC_ERROR"; error: string };112113// ---------------------------------------------------------------------------114// v3 device manifest — propagates trust between mobile devices (v2 brief §4.3)115// ---------------------------------------------------------------------------116117/** 0x03 adds each peer edge's relay URL so a newly introduced pair can agree118 *  on a route without depending on the introducing device. Coordinated wire119 *  change with peach-android. */120export const SYNC_PROTOCOL_VERSION = 3 as const;121122export const DEVICE_MANIFEST_VERSION = 0x05 as const;123export const MAX_VOUCH_DEPTH = 3;124/** Manifests older than this are silently rejected (v2 brief §4.3). */125export const DEVICE_MANIFEST_FRESHNESS_MS = 5 * 60_000;126127/**128 * One device entry inside a signed manifest. `longTermPublicKeyHex` is the129 * X25519 sync identity key the receiver will use for Noise KK reconnect.130 * `signingPublicKeyHex` is the131 * peer's Ed25519 manifest-verification key — needed so a vouched mobile132 * can in turn verify manifests issued by the same vault from a different133 * device.134 */135export interface ManifestDeviceEntry {136  deviceId: string;137  deviceName: string;138  deviceType: DeviceType;139  longTermPublicKeyHex: string;140  signingPublicKeyHex: string;141  /**142   * Optional sync WebSocket port for native/mobile hosts. Preserving this in143   * vouched entries lets mobile peers connect without another audio pairing.144   */145  wsPort?: number;146  /** Relay the issuer currently uses for this peer edge. */147  relayUrl?: string;148  pairedAt: number;149  /** Number of vouching edges from a directly paired device. */150  vouchDepth?: number;151}152153/**154 * A device that must no longer be synced with. Revocation is TERMINAL: once a155 * deviceId OR its long-term key appears here, no manifest may re-vouch it and156 * no pending entry carrying it may be confirmed. That is deliberate —157 * tombstone-style "newest timestamp wins" ordering would let a peer resurrect a158 * revoked device just by issuing a manifest with a fresh `pairedAt`, and every159 * timestamp in a manifest is peer-supplied. Re-admitting a device requires a160 * direct, SAS-confirmed re-pair with a physically present user.161 *162 * HONEST LIMIT: every mesh member holds the vault root key, and signing keys163 * are derived from it (`deriveSyncSigningKeypair(rootKey, deviceId)`), so these164 * signatures prove *vault membership*, not device identity. A revoked device165 * still holds the root key and, running modified software, can ignore its own166 * revocation. This mechanism stops honest peers from talking to a revoked167 * device; it is not a cryptographic ejection. See HOLE 4 — a genuinely168 * compromised device needs the master password and recovery words rotated.169 */170export interface DeviceRevocation {171  deviceId: string;172  longTermPublicKeyHex: string;173  revokedAt: number;174  revokedByDeviceId: string;175  /** Issuer signing-key identity, retained immutably with the record. */176  issuerSigningKeyVersion?: number;177  issuerSigningPublicKeyHex?: string;178  /** Ed25519 signature over canonical record fields (excluding signature). */179  signatureHex?: string;180}181182/** Body that gets canonicalised + signed. Wire format JSON-stable. */183export interface DeviceManifestBody {184  manifestVersion: number;185  issuerDeviceId: string;186  issuedAt: number;187  devices: ManifestDeviceEntry[];188  revoked: DeviceRevocation[];189}190191export interface SignedDeviceManifest {192  body: DeviceManifestBody;193  /** Hex-encoded 64-byte Ed25519 signature over canonicalised body. */194  signatureHex: string;195}