@meshwhisper/sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/README.md +138 -0
  2. package/dist/browser/index.d.ts +4 -0
  3. package/dist/browser/index.d.ts.map +1 -0
  4. package/dist/browser/index.js +19 -0
  5. package/dist/browser/index.js.map +1 -0
  6. package/dist/chaff/index.d.ts +91 -0
  7. package/dist/chaff/index.d.ts.map +1 -0
  8. package/dist/chaff/index.js +268 -0
  9. package/dist/chaff/index.js.map +1 -0
  10. package/dist/cluster/index.d.ts +159 -0
  11. package/dist/cluster/index.d.ts.map +1 -0
  12. package/dist/cluster/index.js +393 -0
  13. package/dist/cluster/index.js.map +1 -0
  14. package/dist/compliance/index.d.ts +129 -0
  15. package/dist/compliance/index.d.ts.map +1 -0
  16. package/dist/compliance/index.js +315 -0
  17. package/dist/compliance/index.js.map +1 -0
  18. package/dist/crypto/index.d.ts +65 -0
  19. package/dist/crypto/index.d.ts.map +1 -0
  20. package/dist/crypto/index.js +146 -0
  21. package/dist/crypto/index.js.map +1 -0
  22. package/dist/group/index.d.ts +155 -0
  23. package/dist/group/index.d.ts.map +1 -0
  24. package/dist/group/index.js +560 -0
  25. package/dist/group/index.js.map +1 -0
  26. package/dist/index.d.ts +7 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +11 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/namespace/index.d.ts +155 -0
  31. package/dist/namespace/index.d.ts.map +1 -0
  32. package/dist/namespace/index.js +278 -0
  33. package/dist/namespace/index.js.map +1 -0
  34. package/dist/node/index.d.ts +4 -0
  35. package/dist/node/index.d.ts.map +1 -0
  36. package/dist/node/index.js +19 -0
  37. package/dist/node/index.js.map +1 -0
  38. package/dist/packet/index.d.ts +63 -0
  39. package/dist/packet/index.d.ts.map +1 -0
  40. package/dist/packet/index.js +244 -0
  41. package/dist/packet/index.js.map +1 -0
  42. package/dist/permissions/index.d.ts +107 -0
  43. package/dist/permissions/index.d.ts.map +1 -0
  44. package/dist/permissions/index.js +282 -0
  45. package/dist/permissions/index.js.map +1 -0
  46. package/dist/persistence/idb-storage.d.ts +27 -0
  47. package/dist/persistence/idb-storage.d.ts.map +1 -0
  48. package/dist/persistence/idb-storage.js +75 -0
  49. package/dist/persistence/idb-storage.js.map +1 -0
  50. package/dist/persistence/index.d.ts +4 -0
  51. package/dist/persistence/index.d.ts.map +1 -0
  52. package/dist/persistence/index.js +3 -0
  53. package/dist/persistence/index.js.map +1 -0
  54. package/dist/persistence/node-storage.d.ts +33 -0
  55. package/dist/persistence/node-storage.d.ts.map +1 -0
  56. package/dist/persistence/node-storage.js +90 -0
  57. package/dist/persistence/node-storage.js.map +1 -0
  58. package/dist/persistence/serialization.d.ts +4 -0
  59. package/dist/persistence/serialization.d.ts.map +1 -0
  60. package/dist/persistence/serialization.js +49 -0
  61. package/dist/persistence/serialization.js.map +1 -0
  62. package/dist/persistence/types.d.ts +29 -0
  63. package/dist/persistence/types.d.ts.map +1 -0
  64. package/dist/persistence/types.js +5 -0
  65. package/dist/persistence/types.js.map +1 -0
  66. package/dist/ratchet/index.d.ts +80 -0
  67. package/dist/ratchet/index.d.ts.map +1 -0
  68. package/dist/ratchet/index.js +259 -0
  69. package/dist/ratchet/index.js.map +1 -0
  70. package/dist/reciprocity/index.d.ts +109 -0
  71. package/dist/reciprocity/index.d.ts.map +1 -0
  72. package/dist/reciprocity/index.js +311 -0
  73. package/dist/reciprocity/index.js.map +1 -0
  74. package/dist/relay/index.d.ts +87 -0
  75. package/dist/relay/index.d.ts.map +1 -0
  76. package/dist/relay/index.js +286 -0
  77. package/dist/relay/index.js.map +1 -0
  78. package/dist/routing/index.d.ts +136 -0
  79. package/dist/routing/index.d.ts.map +1 -0
  80. package/dist/routing/index.js +478 -0
  81. package/dist/routing/index.js.map +1 -0
  82. package/dist/sdk/index.d.ts +322 -0
  83. package/dist/sdk/index.d.ts.map +1 -0
  84. package/dist/sdk/index.js +1530 -0
  85. package/dist/sdk/index.js.map +1 -0
  86. package/dist/sybil/index.d.ts +123 -0
  87. package/dist/sybil/index.d.ts.map +1 -0
  88. package/dist/sybil/index.js +491 -0
  89. package/dist/sybil/index.js.map +1 -0
  90. package/dist/transport/browser/index.d.ts +34 -0
  91. package/dist/transport/browser/index.d.ts.map +1 -0
  92. package/dist/transport/browser/index.js +176 -0
  93. package/dist/transport/browser/index.js.map +1 -0
  94. package/dist/transport/local/index.d.ts +57 -0
  95. package/dist/transport/local/index.d.ts.map +1 -0
  96. package/dist/transport/local/index.js +442 -0
  97. package/dist/transport/local/index.js.map +1 -0
  98. package/dist/transport/negotiator/index.d.ts +79 -0
  99. package/dist/transport/negotiator/index.d.ts.map +1 -0
  100. package/dist/transport/negotiator/index.js +289 -0
  101. package/dist/transport/negotiator/index.js.map +1 -0
  102. package/dist/transport/node/index.d.ts +56 -0
  103. package/dist/transport/node/index.d.ts.map +1 -0
  104. package/dist/transport/node/index.js +209 -0
  105. package/dist/transport/node/index.js.map +1 -0
  106. package/dist/transport/noop/index.d.ts +11 -0
  107. package/dist/transport/noop/index.d.ts.map +1 -0
  108. package/dist/transport/noop/index.js +20 -0
  109. package/dist/transport/noop/index.js.map +1 -0
  110. package/dist/transport/p2p/index.d.ts +109 -0
  111. package/dist/transport/p2p/index.d.ts.map +1 -0
  112. package/dist/transport/p2p/index.js +237 -0
  113. package/dist/transport/p2p/index.js.map +1 -0
  114. package/dist/transport/websocket/index.d.ts +89 -0
  115. package/dist/transport/websocket/index.d.ts.map +1 -0
  116. package/dist/transport/websocket/index.js +498 -0
  117. package/dist/transport/websocket/index.js.map +1 -0
  118. package/dist/transport/websocket/serialize.d.ts +5 -0
  119. package/dist/transport/websocket/serialize.d.ts.map +1 -0
  120. package/dist/transport/websocket/serialize.js +55 -0
  121. package/dist/transport/websocket/serialize.js.map +1 -0
  122. package/dist/types.d.ts +215 -0
  123. package/dist/types.d.ts.map +1 -0
  124. package/dist/types.js +15 -0
  125. package/dist/types.js.map +1 -0
  126. package/dist/x3dh/index.d.ts +120 -0
  127. package/dist/x3dh/index.d.ts.map +1 -0
  128. package/dist/x3dh/index.js +290 -0
  129. package/dist/x3dh/index.js.map +1 -0
  130. package/package.json +59 -0
  131. package/src/browser/index.ts +19 -0
  132. package/src/chaff/index.ts +340 -0
  133. package/src/cluster/index.ts +482 -0
  134. package/src/compliance/index.ts +407 -0
  135. package/src/crypto/index.ts +193 -0
  136. package/src/group/index.ts +719 -0
  137. package/src/index.ts +87 -0
  138. package/src/lz4js.d.ts +58 -0
  139. package/src/namespace/index.ts +336 -0
  140. package/src/node/index.ts +19 -0
  141. package/src/packet/index.ts +326 -0
  142. package/src/permissions/index.ts +405 -0
  143. package/src/persistence/idb-storage.ts +83 -0
  144. package/src/persistence/index.ts +3 -0
  145. package/src/persistence/node-storage.ts +96 -0
  146. package/src/persistence/serialization.ts +75 -0
  147. package/src/persistence/types.ts +33 -0
  148. package/src/ratchet/index.ts +363 -0
  149. package/src/reciprocity/index.ts +371 -0
  150. package/src/relay/index.ts +382 -0
  151. package/src/routing/index.ts +577 -0
  152. package/src/sdk/index.ts +1994 -0
  153. package/src/sybil/index.ts +661 -0
  154. package/src/transport/browser/index.ts +201 -0
  155. package/src/transport/local/index.ts +540 -0
  156. package/src/transport/negotiator/index.ts +397 -0
  157. package/src/transport/node/index.ts +234 -0
  158. package/src/transport/noop/index.ts +22 -0
  159. package/src/transport/p2p/index.ts +345 -0
  160. package/src/transport/websocket/index.ts +660 -0
  161. package/src/transport/websocket/serialize.ts +68 -0
  162. package/src/types.ts +275 -0
  163. package/src/x3dh/index.ts +388 -0
package/src/types.ts ADDED
@@ -0,0 +1,275 @@
1
+ // ============================================================
2
+ // MeshWhisper SDK — Shared Types & Interfaces
3
+ // All modules code against these types.
4
+ // ============================================================
5
+
6
+ // --- Crypto Types ---
7
+
8
+ export interface KeyPair {
9
+ publicKey: Uint8Array;
10
+ privateKey: Uint8Array;
11
+ }
12
+
13
+ export interface PreKeyBundle {
14
+ identityKey: Uint8Array;
15
+ signedPreKey: Uint8Array;
16
+ signedPreKeySignature: Uint8Array;
17
+ oneTimePreKey?: Uint8Array;
18
+ }
19
+
20
+ export interface EncryptedPayload {
21
+ ciphertext: Uint8Array;
22
+ nonce: Uint8Array;
23
+ tag: Uint8Array;
24
+ }
25
+
26
+ // --- Packet Types ---
27
+
28
+ export enum PacketFlags {
29
+ DATA = 0x01,
30
+ ACK = 0x02,
31
+ CHAFF = 0x03,
32
+ HANDSHAKE = 0x04,
33
+ ROUTE_REQUEST = 0x05,
34
+ ROUTE_OFFER = 0x06,
35
+ }
36
+
37
+ export interface Packet {
38
+ version: number;
39
+ flags: PacketFlags;
40
+ destHash: Uint8Array; // 8 bytes — truncated BLAKE3
41
+ senderEphemeralId: Uint8Array; // 16 bytes — rotating
42
+ ttl: number; // max 7
43
+ payloadLength: number;
44
+ encryptedPayload: Uint8Array;
45
+ }
46
+
47
+ // --- Transport Types ---
48
+
49
+ export type BearerType = 'platform_p2p' | 'local_net' | 'internet';
50
+ export type BatteryState = 'charging' | 'high' | 'medium' | 'low' | 'critical';
51
+ export type RelayWillingness = 'eager' | 'willing' | 'reluctant' | 'unavailable';
52
+
53
+ export interface DeviceCapability {
54
+ bearerPlatformP2P: boolean;
55
+ bearerLocalNet: boolean;
56
+ bearerInternet: boolean;
57
+ inboundConnectable: boolean;
58
+ batteryState: BatteryState;
59
+ relayWillingness: RelayWillingness;
60
+ }
61
+
62
+ export interface Transport {
63
+ readonly type: BearerType;
64
+ send(packet: Packet, destination: string): Promise<void>;
65
+ onReceive(callback: (packet: Packet, source: string) => void): void;
66
+ start(): Promise<void>;
67
+ stop(): Promise<void>;
68
+ isAvailable(): Promise<boolean>;
69
+ }
70
+
71
+ // --- Routing Types ---
72
+
73
+ export interface PeerProximityEntry {
74
+ peerId: string;
75
+ destHash: Uint8Array;
76
+ lastSeen: number;
77
+ hopCount: number;
78
+ latency: number;
79
+ relayPath: string[];
80
+ }
81
+
82
+ export interface RouteRequest {
83
+ destHash: Uint8Array;
84
+ requestId: Uint8Array;
85
+ ttl: number;
86
+ timestamp: number;
87
+ }
88
+
89
+ export interface RouteOffer {
90
+ requestId: Uint8Array;
91
+ hopCount: number;
92
+ estimatedLatency: number;
93
+ offeredBy: string;
94
+ }
95
+
96
+ // --- Session Types ---
97
+
98
+ export type PermissionModel = 'open' | 'mutual' | 'introduction' | 'transactional' | 'custom';
99
+
100
+ export interface Session {
101
+ peerId: string;
102
+ namespaceId: Uint8Array;
103
+ sharedSecret: Uint8Array;
104
+ established: number;
105
+ lastActivity: number;
106
+ }
107
+
108
+ // --- Message Types ---
109
+
110
+ export type MessageUrgency = 'background' | 'normal' | 'urgent' | 'critical';
111
+ export type PresenceStatus = 'online' | 'recently_seen' | 'offline' | 'unknown';
112
+
113
+ export interface Message {
114
+ id: string;
115
+ senderId: string;
116
+ recipientId: string;
117
+ payload: Uint8Array;
118
+ timestamp: number;
119
+ urgency: MessageUrgency;
120
+ expiry?: number;
121
+ }
122
+
123
+ // --- Group Types ---
124
+
125
+ export interface GroupMember {
126
+ id: string;
127
+ senderKey: Uint8Array;
128
+ role: 'admin' | 'member';
129
+ joinedAt: number;
130
+ }
131
+
132
+ export interface Group {
133
+ id: string;
134
+ name: string;
135
+ members: Map<string, GroupMember>;
136
+ treeRoot: string;
137
+ permissionModel: PermissionModel;
138
+ createdAt: number;
139
+ }
140
+
141
+ // --- Reciprocity Types ---
142
+
143
+ export interface RelayLedgerEntry {
144
+ peerId: string;
145
+ bytesRelayedForThem: number;
146
+ bytesTheyRelayedForUs: number;
147
+ lastUpdated: number;
148
+ }
149
+
150
+ export type ReciprocityTier = 'contributor' | 'balanced' | 'consumer' | 'freerider';
151
+
152
+ // --- Cluster Types ---
153
+
154
+ export interface ClusterDevice {
155
+ deviceId: string;
156
+ clusterKey: Uint8Array;
157
+ capabilities: DeviceCapability;
158
+ isPrimary: boolean;
159
+ lastSync: number;
160
+ }
161
+
162
+ // --- Store-and-Forward Types ---
163
+
164
+ export interface StoredBlob {
165
+ id: string;
166
+ destHash: Uint8Array;
167
+ encryptedPayload: Uint8Array;
168
+ receivedAt: number;
169
+ ttlHours: number;
170
+ hopsRemaining: number;
171
+ }
172
+
173
+ // --- Sybil Resistance Types ---
174
+
175
+ export type EntropySensorType = 'accelerometer' | 'gyroscope' | 'magnetometer';
176
+
177
+ export interface EntropyChallenge {
178
+ challengeId: Uint8Array;
179
+ sensorType: EntropySensorType;
180
+ durationMs: number;
181
+ timestamp: number;
182
+ }
183
+
184
+ export interface EntropyResponse {
185
+ challengeId: Uint8Array;
186
+ sensorData: Float64Array;
187
+ deviceSignature: Uint8Array;
188
+ }
189
+
190
+ // --- Chaff Types ---
191
+
192
+ export type ChaffRate = 'low' | 'normal' | 'high';
193
+
194
+ // --- Compliance Types ---
195
+
196
+ export type AuditExportMode = 'plaintext' | 'encrypted';
197
+
198
+ export interface ComplianceConfig {
199
+ logging?: boolean;
200
+ auditExport?: AuditExportMode;
201
+ retentionDays?: number;
202
+ contentScanning?: (msg: Message) => { approved: boolean; reason?: string };
203
+ }
204
+
205
+ export type MessageHook = (message: Message) => boolean | Promise<boolean>;
206
+
207
+ // --- SDK Config Types ---
208
+
209
+ // Re-export persistence types so callers only need one import
210
+ export type { StorageBackend, StoredMessage } from './persistence/types.js';
211
+
212
+ /** Web Push subscription object (serialisable form of PushSubscription). */
213
+ export interface WebPushSubscription {
214
+ endpoint: string;
215
+ expirationTime?: number | null;
216
+ keys: {
217
+ p256dh: string;
218
+ auth: string;
219
+ };
220
+ }
221
+
222
+ export type PushConfig =
223
+ | { platform: 'apns'; token: string; topic?: string }
224
+ | { platform: 'fcm'; token: string }
225
+ | { platform: 'webpush'; subscription: WebPushSubscription };
226
+
227
+ export interface MeshWhisperConfig {
228
+ namespace: string;
229
+ /** MeshWhisper Node endpoint(s). Use "mesh" for Foundation-hosted nodes,
230
+ * a wss:// URL for self-hosted, or an array for hybrid mode. Defaults to "mesh". */
231
+ node?: string | string[];
232
+ /** Optional developer key (base64 public key). If omitted a random key is used,
233
+ * which is fine for development and single-tenant deployments. */
234
+ developerKey?: string;
235
+ /** Default: "open". */
236
+ permissionModel?: PermissionModel;
237
+ /** Push notification token. When set the Node stores the token alongside
238
+ * the device's destination hashes and sends a wake signal via the configured
239
+ * push webhook when a message arrives while the device is offline. */
240
+ push?: PushConfig;
241
+ /**
242
+ * Persistent storage backend. Provide this to survive process restarts:
243
+ * sessions, message history, identity, and contacts are all persisted.
244
+ *
245
+ * For Node.js: `import { NodeStorage } from '@meshwhisper/sdk/persistence/node'`
246
+ * For React Native: wrap AsyncStorage or SQLCipher.
247
+ */
248
+ storage?: import('./persistence/types.js').StorageBackend;
249
+ onMessage?: (message: Message) => void;
250
+ onPresence?: (peerId: string, status: PresenceStatus) => void;
251
+ /** Called when the delivery status of an outbound message changes. */
252
+ onMessageStatus?: (messageId: string, status: import('./persistence/types.js').StoredMessage['status']) => void;
253
+ config?: {
254
+ relayWillingness?: 'auto' | RelayWillingness;
255
+ chaffRate?: ChaffRate;
256
+ storeTTL?: number; // hours, default 72
257
+ clusterEnabled?: boolean;
258
+ };
259
+ }
260
+
261
+ // --- Namespace Types ---
262
+
263
+ export interface NamespaceConfig {
264
+ appBundleId: string;
265
+ developerPublicKey: Uint8Array;
266
+ salt: Uint8Array;
267
+ }
268
+
269
+ // --- Event Emitter ---
270
+
271
+ export interface EventEmitter<T extends Record<string, unknown>> {
272
+ on<K extends keyof T>(event: K, handler: (data: T[K]) => void): void;
273
+ off<K extends keyof T>(event: K, handler: (data: T[K]) => void): void;
274
+ emit<K extends keyof T>(event: K, data: T[K]): void;
275
+ }
@@ -0,0 +1,388 @@
1
+ // ============================================================
2
+ // MeshWhisper SDK — X3DH Key Exchange Module
3
+ // Extended Triple Diffie-Hellman adapted for serverless P2P
4
+ // ============================================================
5
+
6
+ import { x25519, ed25519 } from '@noble/curves/ed25519';
7
+ import { blake3 } from '@noble/hashes/blake3';
8
+ import type { KeyPair, PreKeyBundle } from '../types.js';
9
+
10
+ // --- Constants ---
11
+
12
+ /** Protocol version for serialized pre-key bundles. */
13
+ const BUNDLE_VERSION = 0x01;
14
+
15
+ /** Length of an X25519/Ed25519 public key in bytes. */
16
+ const KEY_LENGTH = 32;
17
+
18
+ /** Length of an Ed25519 signature in bytes. */
19
+ const SIGNATURE_LENGTH = 64;
20
+
21
+ // --- Key Generation ---
22
+
23
+ /**
24
+ * The result of generating a pre-key bundle. The bundle is the public
25
+ * portion distributed to peers; the key pairs must be stored locally
26
+ * so that X3DH can be completed on the responder side.
27
+ */
28
+ export interface GeneratedPreKeyBundle {
29
+ /** The public bundle to distribute (gossip or directory). */
30
+ bundle: PreKeyBundle;
31
+ /** The X25519 signed pre-key pair. Store the private key. */
32
+ signedPreKeyPair: KeyPair;
33
+ /** The X25519 one-time pre-key pair. Store the private key. */
34
+ oneTimePreKeyPair: KeyPair;
35
+ }
36
+
37
+ /**
38
+ * Generates a full PreKeyBundle for distribution to peers.
39
+ *
40
+ * The bundle contains:
41
+ * - Identity key (Ed25519 public key from the provided key pair)
42
+ * - Signed pre-key (X25519, signed by the identity key)
43
+ * - One-time pre-key (X25519, single-use)
44
+ *
45
+ * The returned `signedPreKeyPair` and `oneTimePreKeyPair` private keys
46
+ * must be stored by the caller — they are required to complete X3DH on
47
+ * the responder side when a handshake arrives.
48
+ *
49
+ * @param identityKeyPair - Ed25519 identity key pair
50
+ * @returns Bundle (public) plus the key pairs (private keys must be stored)
51
+ */
52
+ export function generatePreKeyBundle(identityKeyPair: KeyPair): GeneratedPreKeyBundle {
53
+ // Generate signed pre-key (X25519)
54
+ const signedPreKeyPrivate = x25519.utils.randomSecretKey();
55
+ const signedPreKeyPublic = x25519.getPublicKey(signedPreKeyPrivate);
56
+
57
+ // Sign the signed pre-key's public key with the Ed25519 identity key
58
+ const signature = ed25519.sign(signedPreKeyPublic, identityKeyPair.privateKey);
59
+
60
+ // Generate one-time pre-key (X25519)
61
+ const oneTimePreKeyPrivate = x25519.utils.randomSecretKey();
62
+ const oneTimePreKeyPublic = x25519.getPublicKey(oneTimePreKeyPrivate);
63
+
64
+ return {
65
+ bundle: {
66
+ identityKey: identityKeyPair.publicKey,
67
+ signedPreKey: signedPreKeyPublic,
68
+ signedPreKeySignature: signature,
69
+ oneTimePreKey: oneTimePreKeyPublic,
70
+ },
71
+ signedPreKeyPair: {
72
+ publicKey: signedPreKeyPublic,
73
+ privateKey: signedPreKeyPrivate,
74
+ },
75
+ oneTimePreKeyPair: {
76
+ publicKey: oneTimePreKeyPublic,
77
+ privateKey: oneTimePreKeyPrivate,
78
+ },
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Generates a batch of one-time pre-keys for replenishment.
84
+ *
85
+ * Returns full KeyPairs — the caller must store the private keys.
86
+ * Only public keys are distributed in the prekey bundle; private keys
87
+ * are required locally to complete X3DH when a matching handshake arrives.
88
+ *
89
+ * @param _identityKeyPair - Unused; kept for API consistency
90
+ * @param count - Number of one-time pre-keys to generate
91
+ * @returns Array of X25519 key pairs
92
+ */
93
+ export function generateOneTimePreKeys(
94
+ _identityKeyPair: KeyPair,
95
+ count: number,
96
+ ): KeyPair[] {
97
+ if (count < 0 || !Number.isInteger(count)) {
98
+ throw new Error('count must be a non-negative integer');
99
+ }
100
+
101
+ const keys: KeyPair[] = [];
102
+ for (let i = 0; i < count; i++) {
103
+ const privateKey = x25519.utils.randomSecretKey();
104
+ keys.push({ privateKey, publicKey: x25519.getPublicKey(privateKey) });
105
+ }
106
+ return keys;
107
+ }
108
+
109
+ // --- Key Exchange: Initiator (Alice) ---
110
+
111
+ export interface KeyExchangeResult {
112
+ /** Derived shared secret (BLAKE3 hash of concatenated DH outputs). */
113
+ sharedSecret: Uint8Array;
114
+ /** Ephemeral X25519 public key Alice used — must be sent to Bob. */
115
+ ephemeralPublicKey: Uint8Array;
116
+ /** Whether a one-time pre-key was consumed in the exchange. */
117
+ usedOneTimePreKey: boolean;
118
+ }
119
+
120
+ /**
121
+ * Performs the initiator side (Alice) of the X3DH key exchange.
122
+ *
123
+ * Executes 3 or 4 DH operations:
124
+ * DH1 = DH(IK_A, SPK_B) — Alice's identity key with Bob's signed pre-key
125
+ * DH2 = DH(EK_A, IK_B) — Alice's ephemeral key with Bob's identity key
126
+ * DH3 = DH(EK_A, SPK_B) — Alice's ephemeral key with Bob's signed pre-key
127
+ * DH4 = DH(EK_A, OPK_B) — Alice's ephemeral key with Bob's one-time pre-key (optional)
128
+ *
129
+ * The shared secret is derived as BLAKE3(DH1 || DH2 || DH3 [|| DH4]).
130
+ *
131
+ * @param aliceIdentity - Alice's Ed25519 identity key pair
132
+ * @param bobPreKeyBundle - Bob's published PreKeyBundle
133
+ * @returns The derived shared secret, ephemeral public key, and OPK usage flag
134
+ * @throws If the pre-key bundle signature is invalid
135
+ */
136
+ export function initiateKeyExchange(
137
+ aliceIdentity: KeyPair,
138
+ bobPreKeyBundle: PreKeyBundle,
139
+ ): KeyExchangeResult {
140
+ // Verify Bob's signed pre-key before proceeding
141
+ if (!verifyPreKeyBundle(bobPreKeyBundle)) {
142
+ throw new Error('Invalid pre-key bundle: signed pre-key signature verification failed');
143
+ }
144
+
145
+ // Convert Alice's Ed25519 identity key to X25519 for DH operations
146
+ const aliceIdentityX25519Private = ed25519.utils.toMontgomerySecret(aliceIdentity.privateKey);
147
+
148
+ // Convert Bob's Ed25519 identity public key to X25519
149
+ const bobIdentityX25519Public = ed25519.utils.toMontgomery(bobPreKeyBundle.identityKey);
150
+
151
+ // Generate Alice's ephemeral X25519 key pair
152
+ const ephemeralPrivate = x25519.utils.randomSecretKey();
153
+ const ephemeralPublic = x25519.getPublicKey(ephemeralPrivate);
154
+
155
+ // DH1: DH(IK_A, SPK_B)
156
+ const dh1 = x25519.getSharedSecret(aliceIdentityX25519Private, bobPreKeyBundle.signedPreKey);
157
+
158
+ // DH2: DH(EK_A, IK_B)
159
+ const dh2 = x25519.getSharedSecret(ephemeralPrivate, bobIdentityX25519Public);
160
+
161
+ // DH3: DH(EK_A, SPK_B)
162
+ const dh3 = x25519.getSharedSecret(ephemeralPrivate, bobPreKeyBundle.signedPreKey);
163
+
164
+ // DH4: DH(EK_A, OPK_B) — optional
165
+ const usedOneTimePreKey = bobPreKeyBundle.oneTimePreKey != null;
166
+ let dhConcat: Uint8Array;
167
+
168
+ if (usedOneTimePreKey) {
169
+ const dh4 = x25519.getSharedSecret(ephemeralPrivate, bobPreKeyBundle.oneTimePreKey!);
170
+ dhConcat = concatBytes(dh1, dh2, dh3, dh4);
171
+ } else {
172
+ dhConcat = concatBytes(dh1, dh2, dh3);
173
+ }
174
+
175
+ // Derive shared secret via BLAKE3
176
+ const sharedSecret = blake3(dhConcat);
177
+
178
+ return {
179
+ sharedSecret,
180
+ ephemeralPublicKey: ephemeralPublic,
181
+ usedOneTimePreKey,
182
+ };
183
+ }
184
+
185
+ // --- Key Exchange: Responder (Bob) ---
186
+
187
+ /**
188
+ * Performs the responder side (Bob) of the X3DH key exchange.
189
+ *
190
+ * Mirrors the initiator's DH operations to derive the same shared secret:
191
+ * DH1 = DH(SPK_B, IK_A) — Bob's signed pre-key with Alice's identity key
192
+ * DH2 = DH(IK_B, EK_A) — Bob's identity key with Alice's ephemeral key
193
+ * DH3 = DH(SPK_B, EK_A) — Bob's signed pre-key with Alice's ephemeral key
194
+ * DH4 = DH(OPK_B, EK_A) — Bob's one-time pre-key with Alice's ephemeral key (optional)
195
+ *
196
+ * @param bobIdentity - Bob's Ed25519 identity key pair
197
+ * @param bobSignedPreKey - Bob's X25519 signed pre-key pair
198
+ * @param bobOneTimePreKey - Bob's X25519 one-time pre-key pair (null if not used)
199
+ * @param aliceIdentityKey - Alice's Ed25519 identity public key
200
+ * @param aliceEphemeralKey - Alice's X25519 ephemeral public key
201
+ * @returns The derived shared secret (same as Alice's if inputs are correct)
202
+ */
203
+ export function completeKeyExchange(
204
+ bobIdentity: KeyPair,
205
+ bobSignedPreKey: KeyPair,
206
+ bobOneTimePreKey: KeyPair | null,
207
+ aliceIdentityKey: Uint8Array,
208
+ aliceEphemeralKey: Uint8Array,
209
+ ): Uint8Array {
210
+ // Convert Alice's Ed25519 identity public key to X25519
211
+ const aliceIdentityX25519Public = ed25519.utils.toMontgomery(aliceIdentityKey);
212
+
213
+ // Convert Bob's Ed25519 identity key to X25519
214
+ const bobIdentityX25519Private = ed25519.utils.toMontgomerySecret(bobIdentity.privateKey);
215
+
216
+ // DH1: DH(SPK_B, IK_A) — mirrors Alice's DH(IK_A, SPK_B)
217
+ const dh1 = x25519.getSharedSecret(bobSignedPreKey.privateKey, aliceIdentityX25519Public);
218
+
219
+ // DH2: DH(IK_B, EK_A) — mirrors Alice's DH(EK_A, IK_B)
220
+ const dh2 = x25519.getSharedSecret(bobIdentityX25519Private, aliceEphemeralKey);
221
+
222
+ // DH3: DH(SPK_B, EK_A) — mirrors Alice's DH(EK_A, SPK_B)
223
+ const dh3 = x25519.getSharedSecret(bobSignedPreKey.privateKey, aliceEphemeralKey);
224
+
225
+ // DH4: DH(OPK_B, EK_A) — optional
226
+ let dhConcat: Uint8Array;
227
+
228
+ if (bobOneTimePreKey != null) {
229
+ const dh4 = x25519.getSharedSecret(bobOneTimePreKey.privateKey, aliceEphemeralKey);
230
+ dhConcat = concatBytes(dh1, dh2, dh3, dh4);
231
+ } else {
232
+ dhConcat = concatBytes(dh1, dh2, dh3);
233
+ }
234
+
235
+ // Derive shared secret via BLAKE3
236
+ return blake3(dhConcat);
237
+ }
238
+
239
+ // --- PreKey Bundle Verification ---
240
+
241
+ /**
242
+ * Verifies the signed pre-key signature in a PreKeyBundle.
243
+ *
244
+ * Checks that the signedPreKey was signed by the holder of the identityKey
245
+ * using Ed25519.
246
+ *
247
+ * @param bundle - The PreKeyBundle to verify
248
+ * @returns true if the signature is valid, false otherwise
249
+ */
250
+ export function verifyPreKeyBundle(bundle: PreKeyBundle): boolean {
251
+ try {
252
+ return ed25519.verify(
253
+ bundle.signedPreKeySignature,
254
+ bundle.signedPreKey,
255
+ bundle.identityKey,
256
+ );
257
+ } catch {
258
+ return false;
259
+ }
260
+ }
261
+
262
+ // --- Serialization ---
263
+
264
+ /**
265
+ * Serializes a PreKeyBundle to a compact binary format for gossip distribution.
266
+ *
267
+ * Wire format:
268
+ * [version: 1 byte]
269
+ * [flags: 1 byte] — bit 0: hasOneTimePreKey
270
+ * [identityKey: 32 bytes]
271
+ * [signedPreKey: 32 bytes]
272
+ * [signedPreKeySignature: 64 bytes]
273
+ * [oneTimePreKey: 32 bytes] — optional, present if flags bit 0 is set
274
+ *
275
+ * Total: 130 bytes without OPK, 162 bytes with OPK.
276
+ *
277
+ * @param bundle - The PreKeyBundle to serialize
278
+ * @returns Serialized binary representation
279
+ */
280
+ export function serializePreKeyBundle(bundle: PreKeyBundle): Uint8Array {
281
+ const hasOPK = bundle.oneTimePreKey != null;
282
+ const flags = hasOPK ? 0x01 : 0x00;
283
+ const totalLength = 1 + 1 + KEY_LENGTH + KEY_LENGTH + SIGNATURE_LENGTH + (hasOPK ? KEY_LENGTH : 0);
284
+
285
+ const data = new Uint8Array(totalLength);
286
+ let offset = 0;
287
+
288
+ // Version
289
+ data[offset++] = BUNDLE_VERSION;
290
+
291
+ // Flags
292
+ data[offset++] = flags;
293
+
294
+ // Identity key
295
+ data.set(bundle.identityKey, offset);
296
+ offset += KEY_LENGTH;
297
+
298
+ // Signed pre-key
299
+ data.set(bundle.signedPreKey, offset);
300
+ offset += KEY_LENGTH;
301
+
302
+ // Signed pre-key signature
303
+ data.set(bundle.signedPreKeySignature, offset);
304
+ offset += SIGNATURE_LENGTH;
305
+
306
+ // One-time pre-key (optional)
307
+ if (hasOPK) {
308
+ data.set(bundle.oneTimePreKey!, offset);
309
+ }
310
+
311
+ return data;
312
+ }
313
+
314
+ /**
315
+ * Deserializes a PreKeyBundle from its binary representation.
316
+ *
317
+ * @param data - Serialized PreKeyBundle bytes
318
+ * @returns The deserialized PreKeyBundle
319
+ * @throws If the data is malformed or has an unsupported version
320
+ */
321
+ export function deserializePreKeyBundle(data: Uint8Array): PreKeyBundle {
322
+ if (data.length < 2) {
323
+ throw new Error('PreKeyBundle data too short: missing header');
324
+ }
325
+
326
+ let offset = 0;
327
+
328
+ // Version
329
+ const version = data[offset++];
330
+ if (version !== BUNDLE_VERSION) {
331
+ throw new Error(`Unsupported PreKeyBundle version: ${version}`);
332
+ }
333
+
334
+ // Flags
335
+ const flags = data[offset++];
336
+ const hasOPK = (flags & 0x01) !== 0;
337
+
338
+ const expectedLength = 1 + 1 + KEY_LENGTH + KEY_LENGTH + SIGNATURE_LENGTH + (hasOPK ? KEY_LENGTH : 0);
339
+ if (data.length < expectedLength) {
340
+ throw new Error(
341
+ `PreKeyBundle data too short: expected ${expectedLength} bytes, got ${data.length}`,
342
+ );
343
+ }
344
+
345
+ // Identity key
346
+ const identityKey = data.slice(offset, offset + KEY_LENGTH);
347
+ offset += KEY_LENGTH;
348
+
349
+ // Signed pre-key
350
+ const signedPreKey = data.slice(offset, offset + KEY_LENGTH);
351
+ offset += KEY_LENGTH;
352
+
353
+ // Signed pre-key signature
354
+ const signedPreKeySignature = data.slice(offset, offset + SIGNATURE_LENGTH);
355
+ offset += SIGNATURE_LENGTH;
356
+
357
+ // One-time pre-key (optional)
358
+ const oneTimePreKey = hasOPK
359
+ ? data.slice(offset, offset + KEY_LENGTH)
360
+ : undefined;
361
+
362
+ return {
363
+ identityKey,
364
+ signedPreKey,
365
+ signedPreKeySignature,
366
+ oneTimePreKey,
367
+ };
368
+ }
369
+
370
+ // --- Utility ---
371
+
372
+ /**
373
+ * Concatenates multiple Uint8Arrays into a single Uint8Array.
374
+ */
375
+ function concatBytes(...arrays: Uint8Array[]): Uint8Array {
376
+ let totalLength = 0;
377
+ for (const arr of arrays) {
378
+ totalLength += arr.length;
379
+ }
380
+
381
+ const result = new Uint8Array(totalLength);
382
+ let offset = 0;
383
+ for (const arr of arrays) {
384
+ result.set(arr, offset);
385
+ offset += arr.length;
386
+ }
387
+ return result;
388
+ }