@oxidezap/baileyrs 0.0.7 → 0.0.8
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.
- package/README.md +119 -5
- package/lib/Bridge/__tests__/adapt.test.d.ts +2 -0
- package/lib/Bridge/__tests__/adapt.test.d.ts.map +1 -0
- package/lib/Bridge/__tests__/adapt.test.js +355 -0
- package/lib/Bridge/__tests__/adapt.test.js.map +1 -0
- package/lib/Bridge/__tests__/dts-drift.test.d.ts +13 -0
- package/lib/Bridge/__tests__/dts-drift.test.d.ts.map +1 -0
- package/lib/Bridge/__tests__/dts-drift.test.js +92 -0
- package/lib/Bridge/__tests__/dts-drift.test.js.map +1 -0
- package/lib/Bridge/__tests__/fixtures.d.ts +192 -0
- package/lib/Bridge/__tests__/fixtures.d.ts.map +1 -0
- package/lib/Bridge/__tests__/fixtures.js +99 -0
- package/lib/Bridge/__tests__/fixtures.js.map +1 -0
- package/lib/Bridge/adapt.d.ts +28 -0
- package/lib/Bridge/adapt.d.ts.map +1 -0
- package/lib/Bridge/adapt.js +504 -0
- package/lib/Bridge/adapt.js.map +1 -0
- package/lib/Bridge/index.d.ts +12 -0
- package/lib/Bridge/index.d.ts.map +1 -0
- package/lib/Bridge/index.js +12 -0
- package/lib/Bridge/index.js.map +1 -0
- package/lib/Bridge/primitives.d.ts +56 -0
- package/lib/Bridge/primitives.d.ts.map +1 -0
- package/lib/Bridge/primitives.js +58 -0
- package/lib/Bridge/primitives.js.map +1 -0
- package/lib/Bridge/types.d.ts +273 -0
- package/lib/Bridge/types.d.ts.map +1 -0
- package/lib/Bridge/types.js +17 -0
- package/lib/Bridge/types.js.map +1 -0
- package/lib/Defaults/index.d.ts.map +1 -1
- package/lib/Defaults/index.js +2 -2
- package/lib/Defaults/index.js.map +1 -1
- package/lib/Socket/blocking.d.ts.map +1 -1
- package/lib/Socket/chat-actions.d.ts.map +1 -1
- package/lib/Socket/contacts.d.ts +9 -4
- package/lib/Socket/contacts.d.ts.map +1 -1
- package/lib/Socket/contacts.js +11 -8
- package/lib/Socket/contacts.js.map +1 -1
- package/lib/Socket/events.d.ts +7 -1
- package/lib/Socket/events.d.ts.map +1 -1
- package/lib/Socket/events.js +212 -243
- package/lib/Socket/events.js.map +1 -1
- package/lib/Socket/group-notifications.d.ts +25 -0
- package/lib/Socket/group-notifications.d.ts.map +1 -0
- package/lib/Socket/group-notifications.js +162 -0
- package/lib/Socket/group-notifications.js.map +1 -0
- package/lib/Socket/groups.d.ts +12 -1
- package/lib/Socket/groups.d.ts.map +1 -1
- package/lib/Socket/groups.js +44 -1
- package/lib/Socket/groups.js.map +1 -1
- package/lib/Socket/index.d.ts +47 -13
- package/lib/Socket/index.d.ts.map +1 -1
- package/lib/Socket/index.js +158 -24
- package/lib/Socket/index.js.map +1 -1
- package/lib/Socket/messages.d.ts +5 -2
- package/lib/Socket/messages.d.ts.map +1 -1
- package/lib/Socket/messages.js +42 -21
- package/lib/Socket/messages.js.map +1 -1
- package/lib/Socket/newsletter.d.ts.map +1 -1
- package/lib/Socket/presence.d.ts.map +1 -1
- package/lib/Socket/profile.d.ts +7 -1
- package/lib/Socket/profile.d.ts.map +1 -1
- package/lib/Socket/profile.js +20 -5
- package/lib/Socket/profile.js.map +1 -1
- package/lib/Socket/transport.d.ts.map +1 -1
- package/lib/Socket/transport.js +14 -13
- package/lib/Socket/transport.js.map +1 -1
- package/lib/Socket/types.d.ts.map +1 -1
- package/lib/Types/Auth.d.ts +1 -1
- package/lib/Types/Auth.d.ts.map +1 -1
- package/lib/Types/Bussines.d.ts +1 -1
- package/lib/Types/Bussines.d.ts.map +1 -1
- package/lib/Types/Chat.d.ts +1 -1
- package/lib/Types/Chat.d.ts.map +1 -1
- package/lib/Types/Events.d.ts +12 -3
- package/lib/Types/Events.d.ts.map +1 -1
- package/lib/Types/Events.js +1 -1
- package/lib/Types/Events.js.map +1 -1
- package/lib/Types/GroupMetadata.d.ts.map +1 -1
- package/lib/Types/Message.d.ts +16 -1
- package/lib/Types/Message.d.ts.map +1 -1
- package/lib/Types/Message.js +1 -1
- package/lib/Types/Message.js.map +1 -1
- package/lib/Types/Socket.d.ts +42 -1
- package/lib/Types/Socket.d.ts.map +1 -1
- package/lib/Types/State.d.ts +1 -1
- package/lib/Types/State.d.ts.map +1 -1
- package/lib/Types/index.d.ts +1 -1
- package/lib/Types/index.d.ts.map +1 -1
- package/lib/Types/index.js +17 -12
- package/lib/Types/index.js.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts +23 -0
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js +157 -0
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts +31 -0
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js +253 -0
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts +32 -0
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.js +488 -0
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.js.map +1 -0
- package/lib/Utils/boom.d.ts +33 -0
- package/lib/Utils/boom.d.ts.map +1 -0
- package/lib/Utils/boom.js +63 -0
- package/lib/Utils/boom.js.map +1 -0
- package/lib/Utils/browser-utils.d.ts.map +1 -1
- package/lib/Utils/event-buffer.d.ts.map +1 -1
- package/lib/Utils/generics.d.ts +37 -1
- package/lib/Utils/generics.d.ts.map +1 -1
- package/lib/Utils/generics.js +60 -2
- package/lib/Utils/generics.js.map +1 -1
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/index.d.ts.map +1 -1
- package/lib/Utils/index.js +10 -9
- package/lib/Utils/index.js.map +1 -1
- package/lib/Utils/lid-mapping-store.d.ts +81 -0
- package/lib/Utils/lid-mapping-store.d.ts.map +1 -0
- package/lib/Utils/lid-mapping-store.js +126 -0
- package/lib/Utils/lid-mapping-store.js.map +1 -0
- package/lib/Utils/logger.d.ts.map +1 -1
- package/lib/Utils/logger.js +25 -1
- package/lib/Utils/logger.js.map +1 -1
- package/lib/Utils/messages-media.d.ts.map +1 -1
- package/lib/Utils/messages-media.js +1 -1
- package/lib/Utils/messages-media.js.map +1 -1
- package/lib/Utils/messages.d.ts +30 -1
- package/lib/Utils/messages.d.ts.map +1 -1
- package/lib/Utils/messages.js +115 -41
- package/lib/Utils/messages.js.map +1 -1
- package/lib/Utils/native-crypto-provider.d.ts +18 -0
- package/lib/Utils/native-crypto-provider.d.ts.map +1 -0
- package/lib/Utils/native-crypto-provider.js +67 -0
- package/lib/Utils/native-crypto-provider.js.map +1 -0
- package/lib/Utils/use-bridge-store.d.ts.map +1 -1
- package/lib/Utils/use-memory-store.d.ts.map +1 -1
- package/lib/Utils/use-memory-store.js +1 -1
- package/lib/Utils/use-memory-store.js.map +1 -1
- package/lib/Utils/use-multi-file-auth-state.d.ts.map +1 -1
- package/lib/Utils/use-multi-file-auth-state.js +1 -1
- package/lib/Utils/use-multi-file-auth-state.js.map +1 -1
- package/lib/Utils/wrap-legacy-store.d.ts.map +1 -1
- package/lib/Utils/wrap-legacy-store.js +621 -13
- package/lib/Utils/wrap-legacy-store.js.map +1 -1
- package/lib/WABinary/index.d.ts.map +1 -1
- package/lib/WABinary/index.js +1 -1
- package/lib/WABinary/index.js.map +1 -1
- package/lib/__tests__/expect.d.ts +30 -0
- package/lib/__tests__/expect.d.ts.map +1 -0
- package/lib/__tests__/expect.js +60 -0
- package/lib/__tests__/expect.js.map +1 -0
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +9 -6
- package/lib/index.js.map +1 -1
- package/package.json +30 -25
- package/WAProto/index.d.ts +0 -13875
- package/WAProto/index.js +0 -175
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
13
|
import { Buffer } from 'node:buffer';
|
|
14
|
+
import { createHmac } from 'node:crypto';
|
|
14
15
|
import { mkdir, readFile, stat, unlink, writeFile } from 'node:fs/promises';
|
|
15
16
|
import { join } from 'node:path';
|
|
16
|
-
import { proto } from '
|
|
17
|
+
import { proto } from 'whatsapp-rust-bridge/proto-types';
|
|
17
18
|
const protoLookup = proto;
|
|
18
19
|
const ADVSignedDeviceIdentity = protoLookup.ADVSignedDeviceIdentity ?? protoLookup.AdvSignedDeviceIdentity;
|
|
19
20
|
function warn(msg, err) {
|
|
@@ -46,11 +47,20 @@ const STORE_MAP = {
|
|
|
46
47
|
mutation_mac: 'bridge-mutation-mac',
|
|
47
48
|
meta: 'bridge-meta'
|
|
48
49
|
};
|
|
49
|
-
// Stores where bridge data is passthrough binary (no conversion)
|
|
50
|
-
|
|
51
|
-
//
|
|
50
|
+
// Stores where bridge data is passthrough binary (no conversion).
|
|
51
|
+
// `sender_key` and `session` are intentionally excluded — both diverge in
|
|
52
|
+
// on-disk format between Rust libsignal (protobuf `SenderKeyRecordStructure`
|
|
53
|
+
// / `RecordStructure`) and JS libsignal (`SenderKeyRecord` UTF-8 JSON /
|
|
54
|
+
// `SessionRecord` plain JS object). They route through converters so the
|
|
55
|
+
// same auth folder remains readable from either implementation, which is
|
|
56
|
+
// the whole point of `wrap-legacy-store`.
|
|
57
|
+
const BINARY_STORES = new Set(['identity']);
|
|
58
|
+
// Bridge-only stores — raw binary persisted through keys interface.
|
|
59
|
+
// `lid_mapping` is intentionally excluded — it has a converter so the
|
|
60
|
+
// `pn:`/`lid:` prefixed bridge keys translate to upstream's bare-userpart
|
|
61
|
+
// `{pnUser}` + `{lidUser}_reverse` shape, letting both libraries share
|
|
62
|
+
// the same on-disk LID↔PN mapping table.
|
|
52
63
|
const BRIDGE_ONLY_STORES = new Set([
|
|
53
|
-
'lid_mapping',
|
|
54
64
|
'signed_prekey',
|
|
55
65
|
'sender_key_devices',
|
|
56
66
|
'base_key',
|
|
@@ -203,8 +213,521 @@ const converters = {
|
|
|
203
213
|
return [];
|
|
204
214
|
}
|
|
205
215
|
}
|
|
216
|
+
},
|
|
217
|
+
// ── lid_mapping ──
|
|
218
|
+
// Cross-impl converter for LID ↔ PN device mapping table.
|
|
219
|
+
// • Bridge: keyed by `lid:{lidUser}` (value=JSON LidPnMappingEntry
|
|
220
|
+
// with phone_number/created_at/updated_at/learning_source) AND
|
|
221
|
+
// `pn:{pnUser}` (value=raw bytes of the LID user, UTF-8).
|
|
222
|
+
// • Upstream (`Signal/lid-mapping.js`): keyed by bare `{pnUser}`
|
|
223
|
+
// (value=string LID user) AND `{lidUser}_reverse` (value=string
|
|
224
|
+
// PN user). No timestamps.
|
|
225
|
+
// Translation goes both ways; metadata fields default to "now" /
|
|
226
|
+
// "wrap-legacy-store" when reading from upstream-only state since the
|
|
227
|
+
// JS side doesn't track them.
|
|
228
|
+
lid_mapping: {
|
|
229
|
+
toBridge(key, value) {
|
|
230
|
+
// `value` here is whatever upstream stored: a string
|
|
231
|
+
// (`pnUser→lidUser` or `lidUser_reverse→pnUser`) — useMultiFile's
|
|
232
|
+
// BufferJSON.reviver leaves strings as strings.
|
|
233
|
+
try {
|
|
234
|
+
if (key.startsWith('pn:')) {
|
|
235
|
+
// Bridge wants the raw LID user as bytes.
|
|
236
|
+
const lid = typeof value === 'string' ? value : Buffer.from(value).toString('utf-8');
|
|
237
|
+
if (!lid)
|
|
238
|
+
return null;
|
|
239
|
+
return new TextEncoder().encode(lid);
|
|
240
|
+
}
|
|
241
|
+
if (key.startsWith('lid:')) {
|
|
242
|
+
// Bridge wants the JSON LidPnMappingEntry. Upstream stored
|
|
243
|
+
// only the phone number string; reconstruct a synthetic
|
|
244
|
+
// entry — bridge tolerates synthetic timestamps + source.
|
|
245
|
+
const lidUser = key.slice(4);
|
|
246
|
+
const phoneNumber = typeof value === 'string' ? value : Buffer.from(value).toString('utf-8');
|
|
247
|
+
if (!phoneNumber)
|
|
248
|
+
return null;
|
|
249
|
+
const now = Math.floor(Date.now() / 1000);
|
|
250
|
+
return toJson({
|
|
251
|
+
lid: lidUser,
|
|
252
|
+
phone_number: phoneNumber,
|
|
253
|
+
created_at: now,
|
|
254
|
+
updated_at: now,
|
|
255
|
+
learning_source: 'wrap-legacy-store'
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
warn(`lid_mapping.toBridge: unknown key prefix in "${key}"`);
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
catch (e) {
|
|
262
|
+
warn('lid_mapping.toBridge failed:', e);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
fromBridge(key, value) {
|
|
267
|
+
// `value` here is bridge's Uint8Array. We return a string for
|
|
268
|
+
// upstream to store — useMultiFile's BufferJSON.replacer keeps
|
|
269
|
+
// strings as strings, which is what `lid-mapping.js` expects to
|
|
270
|
+
// read back via `keys.get('lid-mapping', […])`.
|
|
271
|
+
try {
|
|
272
|
+
if (key.startsWith('pn:')) {
|
|
273
|
+
// Bridge wrote raw LID bytes. Upstream wants the string.
|
|
274
|
+
return Buffer.from(value).toString('utf-8');
|
|
275
|
+
}
|
|
276
|
+
if (key.startsWith('lid:')) {
|
|
277
|
+
// Bridge wrote JSON entry. Upstream wants the phone_number
|
|
278
|
+
// string under `{lidUser}_reverse`.
|
|
279
|
+
const entry = fromJson(value);
|
|
280
|
+
return entry.phone_number ?? '';
|
|
281
|
+
}
|
|
282
|
+
warn(`lid_mapping.fromBridge: unknown key prefix in "${key}"`);
|
|
283
|
+
return Buffer.from(value);
|
|
284
|
+
}
|
|
285
|
+
catch (e) {
|
|
286
|
+
warn('lid_mapping.fromBridge failed:', e);
|
|
287
|
+
return Buffer.from(value);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
},
|
|
291
|
+
// ── session ──
|
|
292
|
+
// Cross-impl converter for pairwise Signal sessions.
|
|
293
|
+
// • Rust libsignal (bridge): raw protobuf `RecordStructure` bytes
|
|
294
|
+
// (current_session + previous_sessions, each `SessionStructure`).
|
|
295
|
+
// • JS libsignal (upstream): a plain JS object
|
|
296
|
+
// `{ _sessions: { [base64BaseKey]: SessionEntry }, version: 'v1' }`
|
|
297
|
+
// persisted by upstream's `useMultiFileAuthState` via
|
|
298
|
+
// JSON.stringify-with-BufferJSON. The byte fields inside SessionEntry
|
|
299
|
+
// are stored as base64 STRINGS (not BufferJSON Buffer wrappers).
|
|
300
|
+
// We translate at the boundary so the same auth folder works for both.
|
|
301
|
+
// Note: per-message-counter caches (`messageKeys`) are dropped during
|
|
302
|
+
// conversion — the two implementations derive them differently (Rust
|
|
303
|
+
// stores cipher/mac/iv post-split; JS stores the pre-split chain output)
|
|
304
|
+
// and they're a CACHE, not state — protocol re-derives or asks for retry.
|
|
305
|
+
session: {
|
|
306
|
+
toBridge(_key, value) {
|
|
307
|
+
// Upstream stored: a plain JS object (NOT bytes). Convert → proto bytes.
|
|
308
|
+
try {
|
|
309
|
+
if (value == null)
|
|
310
|
+
return null; // eslint-disable-line eqeqeq -- intentional null+undefined check
|
|
311
|
+
return upstreamSessionRecordToProto(value);
|
|
312
|
+
}
|
|
313
|
+
catch (e) {
|
|
314
|
+
warn('session.toBridge encode failed:', e);
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
},
|
|
318
|
+
fromBridge(_key, value) {
|
|
319
|
+
// Bridge wrote: raw proto bytes. Convert → upstream JS object.
|
|
320
|
+
try {
|
|
321
|
+
return bridgeSessionProtoToUpstreamRecord(value);
|
|
322
|
+
}
|
|
323
|
+
catch (e) {
|
|
324
|
+
warn('session.fromBridge decode failed:', e);
|
|
325
|
+
return Buffer.from(value);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
},
|
|
329
|
+
// ── sender_key ──
|
|
330
|
+
// Cross-impl converter for group sender keys. Both libraries produce
|
|
331
|
+
// the same logical record (`SenderKeyRecordStructure`) but persist it
|
|
332
|
+
// under DIFFERENT bytes:
|
|
333
|
+
// • Rust libsignal (bridge): raw protobuf bytes
|
|
334
|
+
// • JS libsignal (upstream): UTF-8 JSON of the record array, with
|
|
335
|
+
// each Buffer wrapped as `{ type:'Buffer', data:'<base64>' }`
|
|
336
|
+
// (BufferJSON.replacer in upstream's `Utils/generics`).
|
|
337
|
+
// We translate at the boundary so the same on-disk file is readable
|
|
338
|
+
// by either implementation — the entire point of `wrap-legacy-store`.
|
|
339
|
+
sender_key: {
|
|
340
|
+
toBridge(_key, value) {
|
|
341
|
+
// Upstream stored: Buffer of UTF-8 JSON. Convert → proto bytes.
|
|
342
|
+
try {
|
|
343
|
+
const buf = toBuf(value);
|
|
344
|
+
if (!buf)
|
|
345
|
+
return null;
|
|
346
|
+
return upstreamSenderKeyJsonToProto(buf);
|
|
347
|
+
}
|
|
348
|
+
catch (e) {
|
|
349
|
+
warn('sender_key.toBridge decode failed:', e);
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
fromBridge(_key, value) {
|
|
354
|
+
// Bridge wrote: raw proto bytes. Convert → upstream JSON Buffer.
|
|
355
|
+
try {
|
|
356
|
+
return bridgeSenderKeyProtoToJson(value);
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
warn('sender_key.fromBridge encode failed:', e);
|
|
360
|
+
// Fall back to passthrough so we don't drop data on the floor.
|
|
361
|
+
return Buffer.from(value);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
};
|
|
366
|
+
const bufferReplacerForSenderKey = (_k, v) => {
|
|
367
|
+
if (Buffer.isBuffer(v) || v instanceof Uint8Array || v?.type === 'Buffer') {
|
|
368
|
+
const data = v?.data ?? v;
|
|
369
|
+
return { type: 'Buffer', data: Buffer.from(data).toString('base64') };
|
|
370
|
+
}
|
|
371
|
+
return v;
|
|
372
|
+
};
|
|
373
|
+
const bufferReviverForSenderKey = (_k, v) => {
|
|
374
|
+
if (typeof v === 'object' && v !== null) {
|
|
375
|
+
const obj = v;
|
|
376
|
+
if (obj.type === 'Buffer' && typeof obj.data === 'string') {
|
|
377
|
+
return Buffer.from(obj.data, 'base64');
|
|
378
|
+
}
|
|
206
379
|
}
|
|
380
|
+
return v;
|
|
207
381
|
};
|
|
382
|
+
function bridgeSenderKeyProtoToJson(protoBytes) {
|
|
383
|
+
const struct = proto.SenderKeyRecordStructure.decode(Buffer.from(protoBytes));
|
|
384
|
+
const states = (struct.senderKeyStates ?? []).map(s => ({
|
|
385
|
+
senderKeyId: s.senderKeyId ?? 0,
|
|
386
|
+
senderChainKey: {
|
|
387
|
+
iteration: s.senderChainKey?.iteration ?? 0,
|
|
388
|
+
seed: Buffer.from(s.senderChainKey?.seed ?? new Uint8Array())
|
|
389
|
+
},
|
|
390
|
+
senderSigningKey: {
|
|
391
|
+
public: Buffer.from(s.senderSigningKey?.public ?? new Uint8Array()),
|
|
392
|
+
private: Buffer.from(s.senderSigningKey?.private ?? new Uint8Array())
|
|
393
|
+
},
|
|
394
|
+
senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
|
|
395
|
+
iteration: mk.iteration ?? 0,
|
|
396
|
+
seed: Buffer.from(mk.seed ?? new Uint8Array())
|
|
397
|
+
}))
|
|
398
|
+
}));
|
|
399
|
+
return Buffer.from(JSON.stringify(states, bufferReplacerForSenderKey), 'utf-8');
|
|
400
|
+
}
|
|
401
|
+
function upstreamSenderKeyJsonToProto(jsonBuf) {
|
|
402
|
+
const text = Buffer.from(jsonBuf).toString('utf-8');
|
|
403
|
+
const states = JSON.parse(text, bufferReviverForSenderKey);
|
|
404
|
+
const senderKeyStates = states.map(s => ({
|
|
405
|
+
senderKeyId: s.senderKeyId ?? 0,
|
|
406
|
+
senderChainKey: {
|
|
407
|
+
iteration: s.senderChainKey?.iteration ?? 0,
|
|
408
|
+
seed: s.senderChainKey?.seed ? new Uint8Array(Buffer.from(s.senderChainKey.seed)) : new Uint8Array()
|
|
409
|
+
},
|
|
410
|
+
senderSigningKey: {
|
|
411
|
+
public: s.senderSigningKey?.public ? new Uint8Array(Buffer.from(s.senderSigningKey.public)) : new Uint8Array(),
|
|
412
|
+
private: s.senderSigningKey?.private ? new Uint8Array(Buffer.from(s.senderSigningKey.private)) : new Uint8Array()
|
|
413
|
+
},
|
|
414
|
+
senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
|
|
415
|
+
iteration: mk.iteration ?? 0,
|
|
416
|
+
seed: mk.seed ? new Uint8Array(Buffer.from(mk.seed)) : new Uint8Array()
|
|
417
|
+
}))
|
|
418
|
+
}));
|
|
419
|
+
const struct = proto.SenderKeyRecordStructure.create({ senderKeyStates });
|
|
420
|
+
return proto.SenderKeyRecordStructure.encode(struct).finish();
|
|
421
|
+
}
|
|
422
|
+
// ── sender_key key translator ──
|
|
423
|
+
// Bridge `cache_key`: `{groupJid}:{user}[:jidDev]@{server}.{signalDev}`
|
|
424
|
+
// e.g. "120363@g.us:100000037037034@lid.0"
|
|
425
|
+
// e.g. "120363@g.us:559980000003:5@s.whatsapp.net.0"
|
|
426
|
+
// Upstream `SenderKeyName.serialize()`: `{groupJid}::{signalUser}::{deviceId}`
|
|
427
|
+
// where signalUser = `${user}` for s.whatsapp.net (domainType 0)
|
|
428
|
+
// = `${user}_${domainType}` for LID (1) / hosted (2) / hosted.lid (3)
|
|
429
|
+
function bridgeSenderKeyToUpstream(bridgeKey) {
|
|
430
|
+
// Group JIDs are `{digits}@g.us` — they never contain `:` — so the
|
|
431
|
+
// FIRST `:` in cache_key cleanly separates group from sender address.
|
|
432
|
+
const sep = bridgeKey.indexOf(':');
|
|
433
|
+
if (sep < 0)
|
|
434
|
+
return null;
|
|
435
|
+
const groupJid = bridgeKey.slice(0, sep);
|
|
436
|
+
const addrStr = bridgeKey.slice(sep + 1);
|
|
437
|
+
const dotIdx = addrStr.lastIndexOf('.');
|
|
438
|
+
if (dotIdx < 0)
|
|
439
|
+
return null;
|
|
440
|
+
const jidPart = addrStr.slice(0, dotIdx); // strip the always-zero signalDev suffix
|
|
441
|
+
const atIdx = jidPart.indexOf('@');
|
|
442
|
+
if (atIdx < 0)
|
|
443
|
+
return null;
|
|
444
|
+
const userPart = jidPart.slice(0, atIdx);
|
|
445
|
+
const server = jidPart.slice(atIdx + 1);
|
|
446
|
+
let user;
|
|
447
|
+
let jidDev;
|
|
448
|
+
if (userPart.includes(':')) {
|
|
449
|
+
const [u, d] = userPart.split(':');
|
|
450
|
+
user = u;
|
|
451
|
+
jidDev = parseInt(d, 10) || 0;
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
454
|
+
user = userPart;
|
|
455
|
+
jidDev = 0;
|
|
456
|
+
}
|
|
457
|
+
const domainType = DOMAIN_TYPE_MAP[server];
|
|
458
|
+
if (domainType === undefined)
|
|
459
|
+
return null;
|
|
460
|
+
const signalUser = domainType !== 0 ? `${user}_${domainType}` : user;
|
|
461
|
+
return `${groupJid}::${signalUser}::${jidDev}`;
|
|
462
|
+
}
|
|
463
|
+
const CHAIN_TYPE_SENDING = 1;
|
|
464
|
+
const CHAIN_TYPE_RECEIVING = 2;
|
|
465
|
+
const BASE_KEY_TYPE_OURS = 1;
|
|
466
|
+
const BASE_KEY_TYPE_THEIRS = 2;
|
|
467
|
+
const b64 = (b) => b ? Buffer.from(b).toString('base64') : Buffer.alloc(0).toString('base64');
|
|
468
|
+
const fromB64 = (s) => (s ? Buffer.from(s, 'base64') : Buffer.alloc(0));
|
|
469
|
+
/**
|
|
470
|
+
* Derive Rust libsignal's per-message split (cipher_key + mac_key + iv) from
|
|
471
|
+
* the JS libsignal `messageKey` seed. The two impls store the cache at
|
|
472
|
+
* different points in the derivation:
|
|
473
|
+
*
|
|
474
|
+
* • JS: stores the 32-byte HMAC(chainKey, [1]) seed; splits on decrypt
|
|
475
|
+
* via `deriveSecrets(seed, ZEROS_32, "WhisperMessageKeys")`.
|
|
476
|
+
* • Rust: stores the post-split `MessageKeys { cipher_key:[u8;32],
|
|
477
|
+
* mac_key:[u8;32], iv:[u8;16] }` directly.
|
|
478
|
+
*
|
|
479
|
+
* Both call HKDF-SHA256 with salt=[0u8;32] and info="WhisperMessageKeys"
|
|
480
|
+
* (Rust passes `None` salt which HKDF defines as a zero-byte string of
|
|
481
|
+
* HashLen=32), so the derivation is byte-identical. We can recover Rust's
|
|
482
|
+
* format from JS's seed losslessly. The reverse (Rust → JS) is impossible
|
|
483
|
+
* because HKDF is one-way — those keys are dropped on Rust→JS conversion.
|
|
484
|
+
*/
|
|
485
|
+
function deriveProtoMessageKey(seed, counter) {
|
|
486
|
+
// HKDF-Extract: PRK = HMAC-SHA256(salt=ZEROS_32, ikm=seed)
|
|
487
|
+
const salt = Buffer.alloc(32);
|
|
488
|
+
const prk = createHmac('sha256', salt).update(seed).digest();
|
|
489
|
+
const info = Buffer.from('WhisperMessageKeys');
|
|
490
|
+
// HKDF-Expand: T(1)=HMAC(PRK, info||0x01); T(N)=HMAC(PRK, T(N-1)||info||N).
|
|
491
|
+
// We need 80 bytes total (cipher 32 + mac 32 + iv 16) → 3 chunks.
|
|
492
|
+
const t1 = createHmac('sha256', prk)
|
|
493
|
+
.update(Buffer.concat([info, Buffer.from([0x01])]))
|
|
494
|
+
.digest();
|
|
495
|
+
const t2 = createHmac('sha256', prk)
|
|
496
|
+
.update(Buffer.concat([t1, info, Buffer.from([0x02])]))
|
|
497
|
+
.digest();
|
|
498
|
+
const t3 = createHmac('sha256', prk)
|
|
499
|
+
.update(Buffer.concat([t2, info, Buffer.from([0x03])]))
|
|
500
|
+
.digest();
|
|
501
|
+
return {
|
|
502
|
+
index: counter,
|
|
503
|
+
cipherKey: new Uint8Array(t1), // 32
|
|
504
|
+
macKey: new Uint8Array(t2), // 32
|
|
505
|
+
iv: new Uint8Array(t3.subarray(0, 16)) // first 16 of T(3)
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
/** Convert all entries in a JS chain's messageKeys cache into Rust's
|
|
509
|
+
* post-split `MessageKey` proto form. Empty/missing → empty array. */
|
|
510
|
+
function jsChainMessageKeysToProto(messageKeys) {
|
|
511
|
+
if (!messageKeys)
|
|
512
|
+
return [];
|
|
513
|
+
const out = [];
|
|
514
|
+
for (const [counterStr, seedB64] of Object.entries(messageKeys)) {
|
|
515
|
+
const counter = parseInt(counterStr, 10);
|
|
516
|
+
if (!Number.isFinite(counter))
|
|
517
|
+
continue;
|
|
518
|
+
const seed = fromB64(seedB64);
|
|
519
|
+
if (seed.length === 0)
|
|
520
|
+
continue;
|
|
521
|
+
out.push(deriveProtoMessageKey(seed, counter));
|
|
522
|
+
}
|
|
523
|
+
return out;
|
|
524
|
+
}
|
|
525
|
+
function sessionStructureToEntry(session, closedTs) {
|
|
526
|
+
if (!session.senderChain || !session.rootKey)
|
|
527
|
+
return null;
|
|
528
|
+
// `aliceBaseKey` is populated by Rust libsignal in BOTH directions of
|
|
529
|
+
// a session: when we're Alice it stores our own ephemeral pubkey, when
|
|
530
|
+
// we're Bob it stores Alice's ephemeral pubkey from the incoming
|
|
531
|
+
// PreKeySignalMessage. So presence alone doesn't tell us which side
|
|
532
|
+
// we are — we use `pendingPreKey` instead (set on Alice until the first
|
|
533
|
+
// reply from Bob clears it; never set on Bob).
|
|
534
|
+
const baseKeyBytes = session.aliceBaseKey ?? session.senderChain.senderRatchetKey;
|
|
535
|
+
if (!baseKeyBytes)
|
|
536
|
+
return null;
|
|
537
|
+
const weAreAlice = !!session.pendingPreKey?.baseKey;
|
|
538
|
+
const baseKeyType = weAreAlice ? BASE_KEY_TYPE_OURS : BASE_KEY_TYPE_THEIRS;
|
|
539
|
+
// lastRemoteEphemeralKey: the most recent peer ratchet step. Order of
|
|
540
|
+
// preference:
|
|
541
|
+
// 1. tail of `receiverChains` — established session, peer has replied
|
|
542
|
+
// 2. `aliceBaseKey` when we're Bob — peer's original ephemeral was
|
|
543
|
+
// our `lastRemoteEphemeralKey` at session creation (matches JS
|
|
544
|
+
// `session_builder.initSession` line 128)
|
|
545
|
+
// 3. empty buffer — degenerate fallback. CANNOT use senderRatchetKey
|
|
546
|
+
// as a placeholder: it collides with the SENDER chain in `_chains`,
|
|
547
|
+
// and upstream's `maybeStepRatchet` would call
|
|
548
|
+
// `delete previousRatchet.chainKey.key` on the sender chain when
|
|
549
|
+
// the peer ratchets, corrupting outbound encryption.
|
|
550
|
+
const receiverChains = session.receiverChains ?? [];
|
|
551
|
+
const lastReceiver = receiverChains[receiverChains.length - 1];
|
|
552
|
+
let lastRemoteEph;
|
|
553
|
+
if (lastReceiver?.senderRatchetKey && lastReceiver.senderRatchetKey.length > 0) {
|
|
554
|
+
lastRemoteEph = lastReceiver.senderRatchetKey;
|
|
555
|
+
}
|
|
556
|
+
else if (!weAreAlice && session.aliceBaseKey && session.aliceBaseKey.length > 0) {
|
|
557
|
+
lastRemoteEph = session.aliceBaseKey;
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
lastRemoteEph = new Uint8Array();
|
|
561
|
+
}
|
|
562
|
+
const senderRatchetPub = session.senderChain.senderRatchetKey ?? new Uint8Array();
|
|
563
|
+
const senderRatchetPriv = session.senderChain.senderRatchetKeyPrivate ?? new Uint8Array();
|
|
564
|
+
const _chains = {};
|
|
565
|
+
// Sender chain: keyed by our own ratchet pubkey base64.
|
|
566
|
+
if (session.senderChain.chainKey?.key && senderRatchetPub.length > 0) {
|
|
567
|
+
_chains[b64(senderRatchetPub)] = {
|
|
568
|
+
chainKey: {
|
|
569
|
+
counter: session.senderChain.chainKey.index ?? 0,
|
|
570
|
+
key: b64(session.senderChain.chainKey.key)
|
|
571
|
+
},
|
|
572
|
+
chainType: CHAIN_TYPE_SENDING,
|
|
573
|
+
messageKeys: {}
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
// Receiver chains: keyed by peer ratchet pubkey base64.
|
|
577
|
+
for (const rc of receiverChains) {
|
|
578
|
+
if (!rc.chainKey?.key || !rc.senderRatchetKey || rc.senderRatchetKey.length === 0)
|
|
579
|
+
continue;
|
|
580
|
+
_chains[b64(rc.senderRatchetKey)] = {
|
|
581
|
+
chainKey: {
|
|
582
|
+
counter: rc.chainKey.index ?? 0,
|
|
583
|
+
key: b64(rc.chainKey.key)
|
|
584
|
+
},
|
|
585
|
+
chainType: CHAIN_TYPE_RECEIVING,
|
|
586
|
+
messageKeys: {}
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const now = Date.now();
|
|
590
|
+
const entry = {
|
|
591
|
+
registrationId: session.remoteRegistrationId ?? 0,
|
|
592
|
+
currentRatchet: {
|
|
593
|
+
ephemeralKeyPair: { pubKey: b64(senderRatchetPub), privKey: b64(senderRatchetPriv) },
|
|
594
|
+
lastRemoteEphemeralKey: b64(lastRemoteEph),
|
|
595
|
+
previousCounter: session.previousCounter ?? 0,
|
|
596
|
+
rootKey: b64(session.rootKey)
|
|
597
|
+
},
|
|
598
|
+
indexInfo: {
|
|
599
|
+
baseKey: b64(baseKeyBytes),
|
|
600
|
+
baseKeyType,
|
|
601
|
+
closed: closedTs,
|
|
602
|
+
used: now,
|
|
603
|
+
created: now,
|
|
604
|
+
remoteIdentityKey: b64(session.remoteIdentityPublic ?? new Uint8Array())
|
|
605
|
+
},
|
|
606
|
+
_chains
|
|
607
|
+
};
|
|
608
|
+
if (session.pendingPreKey?.baseKey) {
|
|
609
|
+
entry.pendingPreKey = {
|
|
610
|
+
preKeyId: session.pendingPreKey.preKeyId ?? undefined,
|
|
611
|
+
signedKeyId: session.pendingPreKey.signedPreKeyId ?? undefined,
|
|
612
|
+
baseKey: b64(session.pendingPreKey.baseKey)
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
return entry;
|
|
616
|
+
}
|
|
617
|
+
function bridgeSessionProtoToUpstreamRecord(protoBytes) {
|
|
618
|
+
const record = proto.RecordStructure.decode(Buffer.from(protoBytes));
|
|
619
|
+
const _sessions = {};
|
|
620
|
+
const current = record.currentSession ? sessionStructureToEntry(record.currentSession, -1) : null;
|
|
621
|
+
if (current)
|
|
622
|
+
_sessions[current.indexInfo.baseKey] = current;
|
|
623
|
+
// Closed sessions: assign descending timestamps so removeOldSessions
|
|
624
|
+
// has something to sort by. Use `1` as the floor (anything > 0 is
|
|
625
|
+
// "closed"; -1 is "open").
|
|
626
|
+
const previous = record.previousSessions ?? [];
|
|
627
|
+
for (let i = 0; i < previous.length; i++) {
|
|
628
|
+
const entry = sessionStructureToEntry(previous[i], Math.max(1, Date.now() - i));
|
|
629
|
+
if (!entry)
|
|
630
|
+
continue;
|
|
631
|
+
// Don't overwrite an existing baseKey (current takes precedence).
|
|
632
|
+
if (!_sessions[entry.indexInfo.baseKey])
|
|
633
|
+
_sessions[entry.indexInfo.baseKey] = entry;
|
|
634
|
+
}
|
|
635
|
+
return { _sessions, version: 'v1' };
|
|
636
|
+
}
|
|
637
|
+
function entryToSessionStructure(entry) {
|
|
638
|
+
const ratchet = entry.currentRatchet;
|
|
639
|
+
const senderRatchetPub = fromB64(ratchet.ephemeralKeyPair.pubKey);
|
|
640
|
+
const senderRatchetPriv = fromB64(ratchet.ephemeralKeyPair.privKey);
|
|
641
|
+
const senderKeyB64 = senderRatchetPub.toString('base64');
|
|
642
|
+
const senderChainEntry = entry._chains[senderKeyB64];
|
|
643
|
+
const senderChain = {
|
|
644
|
+
senderRatchetKey: new Uint8Array(senderRatchetPub),
|
|
645
|
+
senderRatchetKeyPrivate: new Uint8Array(senderRatchetPriv),
|
|
646
|
+
chainKey: senderChainEntry
|
|
647
|
+
? { index: senderChainEntry.chainKey.counter, key: new Uint8Array(fromB64(senderChainEntry.chainKey.key)) }
|
|
648
|
+
: { index: 0, key: new Uint8Array() },
|
|
649
|
+
// Sender chain has no skipped-key cache in either impl — sending
|
|
650
|
+
// is sequential — so this stays empty regardless.
|
|
651
|
+
messageKeys: []
|
|
652
|
+
};
|
|
653
|
+
const receiverChains = [];
|
|
654
|
+
for (const [k, ch] of Object.entries(entry._chains)) {
|
|
655
|
+
if (ch.chainType !== CHAIN_TYPE_RECEIVING)
|
|
656
|
+
continue;
|
|
657
|
+
const ratchetPub = fromB64(k);
|
|
658
|
+
receiverChains.push({
|
|
659
|
+
senderRatchetKey: new Uint8Array(ratchetPub),
|
|
660
|
+
chainKey: { index: ch.chainKey.counter, key: new Uint8Array(fromB64(ch.chainKey.key)) },
|
|
661
|
+
// Recover skipped-key cache: JS holds 32-byte HMAC seeds, Rust
|
|
662
|
+
// holds the post-HKDF cipher/mac/iv split. HKDF is deterministic,
|
|
663
|
+
// so JS→Rust is a lossless one-shot derivation. See
|
|
664
|
+
// `deriveProtoMessageKey` for the algo + cross-impl proof.
|
|
665
|
+
messageKeys: jsChainMessageKeysToProto(ch.messageKeys)
|
|
666
|
+
});
|
|
667
|
+
}
|
|
668
|
+
const session = {
|
|
669
|
+
rootKey: new Uint8Array(fromB64(ratchet.rootKey)),
|
|
670
|
+
previousCounter: ratchet.previousCounter,
|
|
671
|
+
senderChain,
|
|
672
|
+
receiverChains,
|
|
673
|
+
remoteIdentityPublic: new Uint8Array(fromB64(entry.indexInfo.remoteIdentityKey)),
|
|
674
|
+
remoteRegistrationId: entry.registrationId ?? 0,
|
|
675
|
+
// `indexInfo.baseKey` IS the alice base key from the X3DH handshake
|
|
676
|
+
// regardless of which side we are: when we're Alice it's our own
|
|
677
|
+
// ephemeral pubkey, when we're Bob it's the peer's ephemeral pubkey
|
|
678
|
+
// from the incoming PreKeySignalMessage (`session_builder.js:135`).
|
|
679
|
+
// Rust libsignal populates `aliceBaseKey` symmetrically, so we
|
|
680
|
+
// always restore it — losing it would break Rust's
|
|
681
|
+
// `find_matching_previous_session_index` lookup on subsequent
|
|
682
|
+
// PreKeySignal arrivals.
|
|
683
|
+
aliceBaseKey: new Uint8Array(fromB64(entry.indexInfo.baseKey))
|
|
684
|
+
};
|
|
685
|
+
if (entry.pendingPreKey?.baseKey) {
|
|
686
|
+
session.pendingPreKey = {
|
|
687
|
+
preKeyId: entry.pendingPreKey.preKeyId ?? null,
|
|
688
|
+
signedPreKeyId: entry.pendingPreKey.signedKeyId ?? null,
|
|
689
|
+
baseKey: new Uint8Array(fromB64(entry.pendingPreKey.baseKey))
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
return session;
|
|
693
|
+
}
|
|
694
|
+
function upstreamSessionRecordToProto(record) {
|
|
695
|
+
const r = record;
|
|
696
|
+
const sessions = r?._sessions ?? {};
|
|
697
|
+
let currentSession = null;
|
|
698
|
+
const previousSessions = [];
|
|
699
|
+
// Pick the first OPEN session as `current_session`; everything else
|
|
700
|
+
// goes to `previous_sessions`. Both libraries should agree there is
|
|
701
|
+
// at most one open session per peer.
|
|
702
|
+
for (const entry of Object.values(sessions)) {
|
|
703
|
+
const ss = entryToSessionStructure(entry);
|
|
704
|
+
if (!currentSession && entry.indexInfo.closed === -1) {
|
|
705
|
+
currentSession = ss;
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
previousSessions.push(ss);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
// `BridgeSessionProto` is a structural mirror of `ISessionStructure`
|
|
712
|
+
// from `whatsapp-rust-bridge/proto-types`; the differences are nullable
|
|
713
|
+
// vs optional fields. We cast at the boundary to avoid pulling the full
|
|
714
|
+
// proto class types into our internal shape.
|
|
715
|
+
const recordOut = proto.RecordStructure.create({
|
|
716
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
717
|
+
currentSession: (currentSession ?? undefined),
|
|
718
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
719
|
+
previousSessions: previousSessions
|
|
720
|
+
});
|
|
721
|
+
return proto.RecordStructure.encode(recordOut).finish();
|
|
722
|
+
}
|
|
723
|
+
// ── session key translator ──
|
|
724
|
+
// Bridge protocol address: `{user}[:jidDev]@{server}.{signalDev}`
|
|
725
|
+
// Upstream session key: `{signalUser}.{deviceId}` (where deviceId is
|
|
726
|
+
// the JID device for `:jidDev` cases, else the signal device suffix).
|
|
727
|
+
// `bridgeAddrToBaileysAddr` already does this exact transformation —
|
|
728
|
+
// it's just been unused for the session path because sessions were
|
|
729
|
+
// passthrough binary. Now that sessions go through a converter, we
|
|
730
|
+
// route through `translateKey` → `bridgeAddrToBaileysAddr`.
|
|
208
731
|
// ---- Device/Creds ----
|
|
209
732
|
/** Parse a JID string into { user, device, server } */
|
|
210
733
|
function parseJid(jid) {
|
|
@@ -259,6 +782,21 @@ function credsToDeviceJson(creds) {
|
|
|
259
782
|
next_pre_key_id: creds.nextPreKeyId ?? 1
|
|
260
783
|
});
|
|
261
784
|
}
|
|
785
|
+
/**
|
|
786
|
+
* Decode a `[priv(32)…pub(32)]` byte array (the bridge's `kp` shape) back into
|
|
787
|
+
* the upstream Baileys `{public, private}` keypair. Returns `undefined` for
|
|
788
|
+
* absent / all-zero arrays so callers can use `??` to keep prior values.
|
|
789
|
+
*/
|
|
790
|
+
function numArrayToKeyPair(arr) {
|
|
791
|
+
if (!Array.isArray(arr) || arr.length < 64)
|
|
792
|
+
return undefined;
|
|
793
|
+
if (arr.every(b => b === 0))
|
|
794
|
+
return undefined;
|
|
795
|
+
return {
|
|
796
|
+
private: new Uint8Array(arr.slice(0, 32)),
|
|
797
|
+
public: new Uint8Array(arr.slice(32, 64))
|
|
798
|
+
};
|
|
799
|
+
}
|
|
262
800
|
function updateCredsFromDevice(bytes, creds) {
|
|
263
801
|
try {
|
|
264
802
|
const d = fromJson(bytes);
|
|
@@ -293,6 +831,29 @@ function updateCredsFromDevice(bytes, creds) {
|
|
|
293
831
|
creds.advSecretKey = buf.toString('base64');
|
|
294
832
|
}
|
|
295
833
|
}
|
|
834
|
+
// Hydrate the noise / identity / signed pre-key material so the next
|
|
835
|
+
// `credsToDeviceJson(creds)` round-trip emits real bytes instead of
|
|
836
|
+
// zero-filled placeholders. Without this, a paired session looks
|
|
837
|
+
// half-valid on the next process start (`me.id` set, but key slots
|
|
838
|
+
// all zero) and the bridge's noise handshake gets rejected by the
|
|
839
|
+
// server with stream-error precondition-required.
|
|
840
|
+
const noise = numArrayToKeyPair(d.noise_key);
|
|
841
|
+
if (noise)
|
|
842
|
+
creds.noiseKey = noise;
|
|
843
|
+
const identity = numArrayToKeyPair(d.identity_key);
|
|
844
|
+
if (identity)
|
|
845
|
+
creds.signedIdentityKey = identity;
|
|
846
|
+
const signedPreKeyPair = numArrayToKeyPair(d.signed_pre_key);
|
|
847
|
+
if (signedPreKeyPair &&
|
|
848
|
+
Array.isArray(d.signed_pre_key_signature) &&
|
|
849
|
+
d.signed_pre_key_signature.length === 64 &&
|
|
850
|
+
d.signed_pre_key_signature.some(b => b !== 0)) {
|
|
851
|
+
creds.signedPreKey = {
|
|
852
|
+
keyPair: signedPreKeyPair,
|
|
853
|
+
keyId: d.signed_pre_key_id ?? 1,
|
|
854
|
+
signature: new Uint8Array(d.signed_pre_key_signature)
|
|
855
|
+
};
|
|
856
|
+
}
|
|
296
857
|
}
|
|
297
858
|
catch (e) {
|
|
298
859
|
warn('updateCredsFromDevice failed:', e);
|
|
@@ -444,22 +1005,58 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
|
|
|
444
1005
|
}, 100);
|
|
445
1006
|
};
|
|
446
1007
|
/** Route a store to its handler */
|
|
447
|
-
const resolve = (
|
|
448
|
-
if (
|
|
1008
|
+
const resolve = (storeName) => {
|
|
1009
|
+
if (storeName === 'device')
|
|
449
1010
|
return 'device';
|
|
450
|
-
if (BINARY_STORES.has(
|
|
1011
|
+
if (BINARY_STORES.has(storeName))
|
|
451
1012
|
return 'signal';
|
|
452
|
-
if (BRIDGE_ONLY_STORES.has(
|
|
1013
|
+
if (BRIDGE_ONLY_STORES.has(storeName))
|
|
453
1014
|
return 'bridge-only';
|
|
454
|
-
if (STORE_MAP[
|
|
1015
|
+
if (STORE_MAP[storeName])
|
|
455
1016
|
return 'converter';
|
|
456
1017
|
return 'unknown';
|
|
457
1018
|
};
|
|
458
1019
|
/** Translate bridge key format to upstream Baileys key format */
|
|
459
|
-
const translateKey = (
|
|
1020
|
+
const translateKey = (storeName, key) => {
|
|
460
1021
|
// Bridge encodes sync_key IDs as hex; upstream stores use base64
|
|
461
|
-
if (
|
|
1022
|
+
if (storeName === 'sync_key')
|
|
462
1023
|
return hexToBase64(key);
|
|
1024
|
+
// Bridge sender_key cache_key uses single-colon separator + signal-
|
|
1025
|
+
// device suffix; upstream's `SenderKeyName.serialize()` uses double
|
|
1026
|
+
// colons and drops the signal device — see bridgeSenderKeyToUpstream.
|
|
1027
|
+
if (storeName === 'sender_key') {
|
|
1028
|
+
const translated = bridgeSenderKeyToUpstream(key);
|
|
1029
|
+
if (translated == null) {
|
|
1030
|
+
// eslint-disable-line eqeqeq -- intentional null+undefined check
|
|
1031
|
+
warn(`sender_key key translation failed for "${key}", falling back to passthrough`);
|
|
1032
|
+
return key;
|
|
1033
|
+
}
|
|
1034
|
+
return translated;
|
|
1035
|
+
}
|
|
1036
|
+
// Bridge session key is the protocol address (`user[:dev]@server.sig`);
|
|
1037
|
+
// upstream's libsignal looks under `signalUser.deviceId`. Reuses
|
|
1038
|
+
// `bridgeAddrToBaileysAddr` which already does the LID/PN→signalUser
|
|
1039
|
+
// rewrite + JID-device extraction.
|
|
1040
|
+
if (storeName === 'session') {
|
|
1041
|
+
const translated = bridgeAddrToBaileysAddr(key);
|
|
1042
|
+
if (translated == null) {
|
|
1043
|
+
// eslint-disable-line eqeqeq -- intentional null+undefined check
|
|
1044
|
+
warn(`session key translation failed for "${key}", falling back to passthrough`);
|
|
1045
|
+
return key;
|
|
1046
|
+
}
|
|
1047
|
+
return translated;
|
|
1048
|
+
}
|
|
1049
|
+
// Bridge lid_mapping uses prefixed keys (`lid:{X}`, `pn:{X}`) that
|
|
1050
|
+
// upstream's `Signal/lid-mapping.js` doesn't recognize. Map them to
|
|
1051
|
+
// upstream's bare-userpart shape: `pn:{X}` → `{X}`, `lid:{X}` →
|
|
1052
|
+
// `{X}_reverse`.
|
|
1053
|
+
if (storeName === 'lid_mapping') {
|
|
1054
|
+
if (key.startsWith('pn:'))
|
|
1055
|
+
return key.slice(3);
|
|
1056
|
+
if (key.startsWith('lid:'))
|
|
1057
|
+
return `${key.slice(4)}_reverse`;
|
|
1058
|
+
return key;
|
|
1059
|
+
}
|
|
463
1060
|
return key;
|
|
464
1061
|
};
|
|
465
1062
|
/** Read a binary value from the keys store */
|
|
@@ -496,8 +1093,19 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
|
|
|
496
1093
|
const route = resolve(bridgeStore);
|
|
497
1094
|
const type = STORE_MAP[bridgeStore];
|
|
498
1095
|
if (route === 'device') {
|
|
499
|
-
if (key === 'device')
|
|
1096
|
+
if (key === 'device') {
|
|
1097
|
+
// Fresh/stub creds (baileyrs' `initAuthCreds()` shim, or an
|
|
1098
|
+
// upstream flow that hadn't finished its first pair) carry
|
|
1099
|
+
// neither a noise key nor a paired `me.id`. Returning a
|
|
1100
|
+
// zeroed `credsToDeviceJson` in that state poisons the
|
|
1101
|
+
// bridge's noise handshake (server responds 428 Precondition
|
|
1102
|
+
// Required). Signaling `null` instead lets the bridge
|
|
1103
|
+
// bootstrap its own device identity and write it back
|
|
1104
|
+
// through `set('device', 'device', …)` on pair-success.
|
|
1105
|
+
if (!creds.noiseKey && !creds.me?.id)
|
|
1106
|
+
return null;
|
|
500
1107
|
return credsToDeviceJson(creds);
|
|
1108
|
+
}
|
|
501
1109
|
if (key === 'account') {
|
|
502
1110
|
if (!creds.account)
|
|
503
1111
|
return null;
|