@oxidezap/baileyrs 0.0.8 → 0.0.9
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/lib/Utils/__tests__/_legacy-store-fixtures.d.ts +65 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.d.ts.map +1 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.js +107 -0
- package/lib/Utils/__tests__/_legacy-store-fixtures.js.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.d.ts +10 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.d.ts.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.js +298 -0
- package/lib/Utils/__tests__/wrap-legacy-store-coverage.test.js.map +1 -0
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts +4 -19
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.d.ts.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js +31 -97
- package/lib/Utils/__tests__/wrap-legacy-store-lid-mapping.test.js.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts +6 -27
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.d.ts.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js +48 -175
- package/lib/Utils/__tests__/wrap-legacy-store-sender-key.test.js.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts +10 -28
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.d.ts.map +1 -1
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.js +118 -297
- package/lib/Utils/__tests__/wrap-legacy-store-session.test.js.map +1 -1
- package/lib/Utils/wrap-legacy-store.d.ts.map +1 -1
- package/lib/Utils/wrap-legacy-store.js +128 -273
- package/lib/Utils/wrap-legacy-store.js.map +1 -1
- package/package.json +4 -1
|
@@ -48,13 +48,15 @@ const STORE_MAP = {
|
|
|
48
48
|
meta: 'bridge-meta'
|
|
49
49
|
};
|
|
50
50
|
// Stores where bridge data is passthrough binary (no conversion).
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
|
|
51
|
+
// All Signal-protocol records (`identity`, `session`, `sender_key`) now
|
|
52
|
+
// route through converters because every one of them either:
|
|
53
|
+
// • has a different key shape between bridge and upstream, OR
|
|
54
|
+
// • has a different value-byte encoding (proto / JSON / 32-vs-33-byte
|
|
55
|
+
// prefix), OR
|
|
56
|
+
// • both.
|
|
57
|
+
// Keeping the empty set as an extension point — future bridge stores that
|
|
58
|
+
// are byte-compatible with upstream can land here without a converter.
|
|
59
|
+
const BINARY_STORES = new Set();
|
|
58
60
|
// Bridge-only stores — raw binary persisted through keys interface.
|
|
59
61
|
// `lid_mapping` is intentionally excluded — it has a converter so the
|
|
60
62
|
// `pn:`/`lid:` prefixed bridge keys translate to upstream's bare-userpart
|
|
@@ -214,42 +216,36 @@ const converters = {
|
|
|
214
216
|
}
|
|
215
217
|
}
|
|
216
218
|
},
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
219
|
+
// identity: bridge holds 32-byte raw DJB pubkey, upstream holds 33 bytes
|
|
220
|
+
// (0x05 || 32-byte) — the curve25519-XEdDSA wire form.
|
|
221
|
+
identity: {
|
|
222
|
+
toBridge(_key, value) {
|
|
223
|
+
const buf = toBuf(value);
|
|
224
|
+
if (!buf)
|
|
225
|
+
return null;
|
|
226
|
+
return buf.length === 33 && buf[0] === 0x05 ? new Uint8Array(buf.slice(1)) : buf;
|
|
227
|
+
},
|
|
228
|
+
fromBridge(_key, value) {
|
|
229
|
+
const v = Buffer.isBuffer(value) ? value : Buffer.from(value);
|
|
230
|
+
return v.length === 32 ? Buffer.concat([Buffer.from([0x05]), v]) : Buffer.from(v);
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
// lid_mapping: bridge uses prefixed keys (`lid:{X}` JSON entry,
|
|
234
|
+
// `pn:{X}` raw LID bytes); upstream uses bare keys (`{pnUser}` string LID,
|
|
235
|
+
// `{lidUser}_reverse` string PN). No upstream timestamps; we synthesise.
|
|
228
236
|
lid_mapping: {
|
|
229
237
|
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
238
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
return new TextEncoder().encode(lid);
|
|
240
|
-
}
|
|
239
|
+
const asString = typeof value === 'string' ? value : Buffer.from(value).toString('utf-8');
|
|
240
|
+
if (!asString)
|
|
241
|
+
return null;
|
|
242
|
+
if (key.startsWith('pn:'))
|
|
243
|
+
return new TextEncoder().encode(asString);
|
|
241
244
|
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
245
|
const now = Math.floor(Date.now() / 1000);
|
|
250
246
|
return toJson({
|
|
251
|
-
lid:
|
|
252
|
-
phone_number:
|
|
247
|
+
lid: key.slice(4),
|
|
248
|
+
phone_number: asString,
|
|
253
249
|
created_at: now,
|
|
254
250
|
updated_at: now,
|
|
255
251
|
learning_source: 'wrap-legacy-store'
|
|
@@ -264,18 +260,10 @@ const converters = {
|
|
|
264
260
|
}
|
|
265
261
|
},
|
|
266
262
|
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
263
|
try {
|
|
272
|
-
if (key.startsWith('pn:'))
|
|
273
|
-
// Bridge wrote raw LID bytes. Upstream wants the string.
|
|
264
|
+
if (key.startsWith('pn:'))
|
|
274
265
|
return Buffer.from(value).toString('utf-8');
|
|
275
|
-
}
|
|
276
266
|
if (key.startsWith('lid:')) {
|
|
277
|
-
// Bridge wrote JSON entry. Upstream wants the phone_number
|
|
278
|
-
// string under `{lidUser}_reverse`.
|
|
279
267
|
const entry = fromJson(value);
|
|
280
268
|
return entry.phone_number ?? '';
|
|
281
269
|
}
|
|
@@ -288,26 +276,16 @@ const converters = {
|
|
|
288
276
|
}
|
|
289
277
|
}
|
|
290
278
|
},
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
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.
|
|
279
|
+
// session: bridge=protobuf RecordStructure, upstream=JS object
|
|
280
|
+
// `{_sessions: {[b64BaseKey]: SessionEntry}, version: 'v1'}` with byte
|
|
281
|
+
// fields stored as base64 strings inside the entry. messageKeys cache
|
|
282
|
+
// is lossy in the Rust→JS direction (HKDF is one-way) — see
|
|
283
|
+
// `bridgeSessionProtoToUpstreamRecord`.
|
|
305
284
|
session: {
|
|
306
285
|
toBridge(_key, value) {
|
|
307
|
-
// Upstream stored: a plain JS object (NOT bytes). Convert → proto bytes.
|
|
308
286
|
try {
|
|
309
287
|
if (value == null)
|
|
310
|
-
return null; // eslint-disable-line eqeqeq
|
|
288
|
+
return null; // eslint-disable-line eqeqeq
|
|
311
289
|
return upstreamSessionRecordToProto(value);
|
|
312
290
|
}
|
|
313
291
|
catch (e) {
|
|
@@ -316,7 +294,6 @@ const converters = {
|
|
|
316
294
|
}
|
|
317
295
|
},
|
|
318
296
|
fromBridge(_key, value) {
|
|
319
|
-
// Bridge wrote: raw proto bytes. Convert → upstream JS object.
|
|
320
297
|
try {
|
|
321
298
|
return bridgeSessionProtoToUpstreamRecord(value);
|
|
322
299
|
}
|
|
@@ -326,19 +303,11 @@ const converters = {
|
|
|
326
303
|
}
|
|
327
304
|
}
|
|
328
305
|
},
|
|
329
|
-
//
|
|
330
|
-
//
|
|
331
|
-
//
|
|
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`.
|
|
306
|
+
// sender_key: bridge=protobuf SenderKeyRecordStructure, upstream=
|
|
307
|
+
// `Buffer.from(JSON.stringify(states, BufferJSON.replacer), 'utf-8')`
|
|
308
|
+
// — Buffer fields wrapped as `{type:'Buffer', data:'<base64>'}`.
|
|
339
309
|
sender_key: {
|
|
340
310
|
toBridge(_key, value) {
|
|
341
|
-
// Upstream stored: Buffer of UTF-8 JSON. Convert → proto bytes.
|
|
342
311
|
try {
|
|
343
312
|
const buf = toBuf(value);
|
|
344
313
|
if (!buf)
|
|
@@ -351,84 +320,69 @@ const converters = {
|
|
|
351
320
|
}
|
|
352
321
|
},
|
|
353
322
|
fromBridge(_key, value) {
|
|
354
|
-
// Bridge wrote: raw proto bytes. Convert → upstream JSON Buffer.
|
|
355
323
|
try {
|
|
356
324
|
return bridgeSenderKeyProtoToJson(value);
|
|
357
325
|
}
|
|
358
326
|
catch (e) {
|
|
359
327
|
warn('sender_key.fromBridge encode failed:', e);
|
|
360
|
-
// Fall back to passthrough so we don't drop data on the floor.
|
|
361
328
|
return Buffer.from(value);
|
|
362
329
|
}
|
|
363
330
|
}
|
|
364
331
|
}
|
|
365
332
|
};
|
|
366
|
-
|
|
333
|
+
// BufferJSON-compatible replacer/reviver matching upstream `Utils/generics`.
|
|
334
|
+
const bufferJsonReplacer = (_k, v) => {
|
|
367
335
|
if (Buffer.isBuffer(v) || v instanceof Uint8Array || v?.type === 'Buffer') {
|
|
368
336
|
const data = v?.data ?? v;
|
|
369
337
|
return { type: 'Buffer', data: Buffer.from(data).toString('base64') };
|
|
370
338
|
}
|
|
371
339
|
return v;
|
|
372
340
|
};
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return Buffer.from(obj.data, 'base64');
|
|
378
|
-
}
|
|
341
|
+
const bufferJsonReviver = (_k, v) => {
|
|
342
|
+
const obj = v;
|
|
343
|
+
if (obj && typeof obj === 'object' && obj.type === 'Buffer' && typeof obj.data === 'string') {
|
|
344
|
+
return Buffer.from(obj.data, 'base64');
|
|
379
345
|
}
|
|
380
346
|
return v;
|
|
381
347
|
};
|
|
348
|
+
const sk_toBytes = (v) => v ? new Uint8Array(Buffer.from(v)) : new Uint8Array();
|
|
349
|
+
const sk_toBuffer = (v) => Buffer.from(v ?? new Uint8Array());
|
|
382
350
|
function bridgeSenderKeyProtoToJson(protoBytes) {
|
|
383
351
|
const struct = proto.SenderKeyRecordStructure.decode(Buffer.from(protoBytes));
|
|
384
352
|
const states = (struct.senderKeyStates ?? []).map(s => ({
|
|
385
353
|
senderKeyId: s.senderKeyId ?? 0,
|
|
386
|
-
senderChainKey: {
|
|
387
|
-
iteration: s.senderChainKey?.iteration ?? 0,
|
|
388
|
-
seed: Buffer.from(s.senderChainKey?.seed ?? new Uint8Array())
|
|
389
|
-
},
|
|
354
|
+
senderChainKey: { iteration: s.senderChainKey?.iteration ?? 0, seed: sk_toBuffer(s.senderChainKey?.seed) },
|
|
390
355
|
senderSigningKey: {
|
|
391
|
-
public:
|
|
392
|
-
private:
|
|
356
|
+
public: sk_toBuffer(s.senderSigningKey?.public),
|
|
357
|
+
private: sk_toBuffer(s.senderSigningKey?.private)
|
|
393
358
|
},
|
|
394
359
|
senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
|
|
395
360
|
iteration: mk.iteration ?? 0,
|
|
396
|
-
seed:
|
|
361
|
+
seed: sk_toBuffer(mk.seed)
|
|
397
362
|
}))
|
|
398
363
|
}));
|
|
399
|
-
return Buffer.from(JSON.stringify(states,
|
|
364
|
+
return Buffer.from(JSON.stringify(states, bufferJsonReplacer), 'utf-8');
|
|
400
365
|
}
|
|
401
366
|
function upstreamSenderKeyJsonToProto(jsonBuf) {
|
|
402
|
-
const
|
|
403
|
-
const states = JSON.parse(text, bufferReviverForSenderKey);
|
|
367
|
+
const states = JSON.parse(Buffer.from(jsonBuf).toString('utf-8'), bufferJsonReviver);
|
|
404
368
|
const senderKeyStates = states.map(s => ({
|
|
405
369
|
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
|
-
},
|
|
370
|
+
senderChainKey: { iteration: s.senderChainKey?.iteration ?? 0, seed: sk_toBytes(s.senderChainKey?.seed) },
|
|
410
371
|
senderSigningKey: {
|
|
411
|
-
public: s.senderSigningKey?.public
|
|
412
|
-
private: s.senderSigningKey?.private
|
|
372
|
+
public: sk_toBytes(s.senderSigningKey?.public),
|
|
373
|
+
private: sk_toBytes(s.senderSigningKey?.private)
|
|
413
374
|
},
|
|
414
375
|
senderMessageKeys: (s.senderMessageKeys ?? []).map(mk => ({
|
|
415
376
|
iteration: mk.iteration ?? 0,
|
|
416
|
-
seed:
|
|
377
|
+
seed: sk_toBytes(mk.seed)
|
|
417
378
|
}))
|
|
418
379
|
}));
|
|
419
|
-
|
|
420
|
-
return proto.SenderKeyRecordStructure.encode(struct).finish();
|
|
380
|
+
return proto.SenderKeyRecordStructure.encode(proto.SenderKeyRecordStructure.create({ senderKeyStates })).finish();
|
|
421
381
|
}
|
|
422
|
-
//
|
|
423
|
-
//
|
|
424
|
-
//
|
|
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)
|
|
382
|
+
// sender_key key translation: bridge `{group}:{user}[:dev]@{server}.{sig}`
|
|
383
|
+
// → upstream `{group}::{signalUser}::{deviceId}`. Group JIDs never contain
|
|
384
|
+
// `:`, so the first `:` separates group from sender address.
|
|
429
385
|
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
386
|
const sep = bridgeKey.indexOf(':');
|
|
433
387
|
if (sep < 0)
|
|
434
388
|
return null;
|
|
@@ -437,28 +391,19 @@ function bridgeSenderKeyToUpstream(bridgeKey) {
|
|
|
437
391
|
const dotIdx = addrStr.lastIndexOf('.');
|
|
438
392
|
if (dotIdx < 0)
|
|
439
393
|
return null;
|
|
440
|
-
const jidPart = addrStr.slice(0, dotIdx);
|
|
394
|
+
const jidPart = addrStr.slice(0, dotIdx);
|
|
441
395
|
const atIdx = jidPart.indexOf('@');
|
|
442
396
|
if (atIdx < 0)
|
|
443
397
|
return null;
|
|
444
398
|
const userPart = jidPart.slice(0, atIdx);
|
|
445
399
|
const server = jidPart.slice(atIdx + 1);
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
}
|
|
400
|
+
const [user, jidDev] = userPart.includes(':')
|
|
401
|
+
? [userPart.slice(0, userPart.indexOf(':')), parseInt(userPart.slice(userPart.indexOf(':') + 1), 10) || 0]
|
|
402
|
+
: [userPart, 0];
|
|
457
403
|
const domainType = DOMAIN_TYPE_MAP[server];
|
|
458
404
|
if (domainType === undefined)
|
|
459
405
|
return null;
|
|
460
|
-
|
|
461
|
-
return `${groupJid}::${signalUser}::${jidDev}`;
|
|
406
|
+
return `${groupJid}::${domainType !== 0 ? `${user}_${domainType}` : user}::${jidDev}`;
|
|
462
407
|
}
|
|
463
408
|
const CHAIN_TYPE_SENDING = 1;
|
|
464
409
|
const CHAIN_TYPE_RECEIVING = 2;
|
|
@@ -467,46 +412,28 @@ const BASE_KEY_TYPE_THEIRS = 2;
|
|
|
467
412
|
const b64 = (b) => b ? Buffer.from(b).toString('base64') : Buffer.alloc(0).toString('base64');
|
|
468
413
|
const fromB64 = (s) => (s ? Buffer.from(s, 'base64') : Buffer.alloc(0));
|
|
469
414
|
/**
|
|
470
|
-
* Derive Rust
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
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.
|
|
415
|
+
* Derive Rust's per-message split (cipher 32 / mac 32 / iv 16) from JS's
|
|
416
|
+
* 32-byte `messageKey` seed. Both impls compute HKDF-SHA256 with
|
|
417
|
+
* salt=[0u8;32] and info="WhisperMessageKeys" — Rust's `None` salt
|
|
418
|
+
* defaults to a zero-byte HashLen string, so the output is byte-identical.
|
|
419
|
+
* Reverse direction (Rust→JS) is impossible: HKDF is one-way.
|
|
484
420
|
*/
|
|
485
421
|
function deriveProtoMessageKey(seed, counter) {
|
|
486
|
-
|
|
487
|
-
const salt = Buffer.alloc(32);
|
|
488
|
-
const prk = createHmac('sha256', salt).update(seed).digest();
|
|
422
|
+
const prk = createHmac('sha256', Buffer.alloc(32)).update(seed).digest();
|
|
489
423
|
const info = Buffer.from('WhisperMessageKeys');
|
|
490
|
-
|
|
491
|
-
|
|
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])]))
|
|
424
|
+
const expand = (prev, n) => createHmac('sha256', prk)
|
|
425
|
+
.update(Buffer.concat([prev, info, Buffer.from([n])]))
|
|
500
426
|
.digest();
|
|
427
|
+
const t1 = expand(Buffer.alloc(0), 0x01);
|
|
428
|
+
const t2 = expand(t1, 0x02);
|
|
429
|
+
const t3 = expand(t2, 0x03);
|
|
501
430
|
return {
|
|
502
431
|
index: counter,
|
|
503
|
-
cipherKey: new Uint8Array(t1),
|
|
504
|
-
macKey: new Uint8Array(t2),
|
|
505
|
-
iv: new Uint8Array(t3.subarray(0, 16))
|
|
432
|
+
cipherKey: new Uint8Array(t1),
|
|
433
|
+
macKey: new Uint8Array(t2),
|
|
434
|
+
iv: new Uint8Array(t3.subarray(0, 16))
|
|
506
435
|
};
|
|
507
436
|
}
|
|
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
437
|
function jsChainMessageKeysToProto(messageKeys) {
|
|
511
438
|
if (!messageKeys)
|
|
512
439
|
return [];
|
|
@@ -516,72 +443,49 @@ function jsChainMessageKeysToProto(messageKeys) {
|
|
|
516
443
|
if (!Number.isFinite(counter))
|
|
517
444
|
continue;
|
|
518
445
|
const seed = fromB64(seedB64);
|
|
519
|
-
if (seed.length
|
|
520
|
-
|
|
521
|
-
out.push(deriveProtoMessageKey(seed, counter));
|
|
446
|
+
if (seed.length > 0)
|
|
447
|
+
out.push(deriveProtoMessageKey(seed, counter));
|
|
522
448
|
}
|
|
523
449
|
return out;
|
|
524
450
|
}
|
|
525
451
|
function sessionStructureToEntry(session, closedTs) {
|
|
526
452
|
if (!session.senderChain || !session.rootKey)
|
|
527
453
|
return null;
|
|
528
|
-
//
|
|
529
|
-
//
|
|
530
|
-
//
|
|
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).
|
|
454
|
+
// aliceBaseKey is set by Rust on BOTH sides (alice=our ephemeral, bob=
|
|
455
|
+
// peer's ephemeral). pendingPreKey is the side discriminator (alice
|
|
456
|
+
// only, cleared on first reply).
|
|
534
457
|
const baseKeyBytes = session.aliceBaseKey ?? session.senderChain.senderRatchetKey;
|
|
535
458
|
if (!baseKeyBytes)
|
|
536
459
|
return null;
|
|
537
460
|
const weAreAlice = !!session.pendingPreKey?.baseKey;
|
|
538
461
|
const baseKeyType = weAreAlice ? BASE_KEY_TYPE_OURS : BASE_KEY_TYPE_THEIRS;
|
|
539
|
-
// lastRemoteEphemeralKey:
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
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.
|
|
462
|
+
// lastRemoteEphemeralKey precedence: tail receiverChain → aliceBaseKey
|
|
463
|
+
// (when bob, matches JS init) → empty. NEVER senderRatchetKey: that
|
|
464
|
+
// collides with the SENDER chain in `_chains`, and upstream's
|
|
465
|
+
// `maybeStepRatchet` would `delete previousRatchet.chainKey.key` on
|
|
466
|
+
// the sender chain when the peer ratchets, corrupting outbound encryption.
|
|
550
467
|
const receiverChains = session.receiverChains ?? [];
|
|
551
468
|
const lastReceiver = receiverChains[receiverChains.length - 1];
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
lastRemoteEph = session.aliceBaseKey;
|
|
558
|
-
}
|
|
559
|
-
else {
|
|
560
|
-
lastRemoteEph = new Uint8Array();
|
|
561
|
-
}
|
|
469
|
+
const lastRemoteEph = lastReceiver?.senderRatchetKey && lastReceiver.senderRatchetKey.length > 0
|
|
470
|
+
? lastReceiver.senderRatchetKey
|
|
471
|
+
: !weAreAlice && session.aliceBaseKey && session.aliceBaseKey.length > 0
|
|
472
|
+
? session.aliceBaseKey
|
|
473
|
+
: new Uint8Array();
|
|
562
474
|
const senderRatchetPub = session.senderChain.senderRatchetKey ?? new Uint8Array();
|
|
563
475
|
const senderRatchetPriv = session.senderChain.senderRatchetKeyPrivate ?? new Uint8Array();
|
|
564
476
|
const _chains = {};
|
|
565
|
-
// Sender chain: keyed by our own ratchet pubkey base64.
|
|
566
477
|
if (session.senderChain.chainKey?.key && senderRatchetPub.length > 0) {
|
|
567
478
|
_chains[b64(senderRatchetPub)] = {
|
|
568
|
-
chainKey: {
|
|
569
|
-
counter: session.senderChain.chainKey.index ?? 0,
|
|
570
|
-
key: b64(session.senderChain.chainKey.key)
|
|
571
|
-
},
|
|
479
|
+
chainKey: { counter: session.senderChain.chainKey.index ?? 0, key: b64(session.senderChain.chainKey.key) },
|
|
572
480
|
chainType: CHAIN_TYPE_SENDING,
|
|
573
481
|
messageKeys: {}
|
|
574
482
|
};
|
|
575
483
|
}
|
|
576
|
-
// Receiver chains: keyed by peer ratchet pubkey base64.
|
|
577
484
|
for (const rc of receiverChains) {
|
|
578
485
|
if (!rc.chainKey?.key || !rc.senderRatchetKey || rc.senderRatchetKey.length === 0)
|
|
579
486
|
continue;
|
|
580
487
|
_chains[b64(rc.senderRatchetKey)] = {
|
|
581
|
-
chainKey: {
|
|
582
|
-
counter: rc.chainKey.index ?? 0,
|
|
583
|
-
key: b64(rc.chainKey.key)
|
|
584
|
-
},
|
|
488
|
+
chainKey: { counter: rc.chainKey.index ?? 0, key: b64(rc.chainKey.key) },
|
|
585
489
|
chainType: CHAIN_TYPE_RECEIVING,
|
|
586
490
|
messageKeys: {}
|
|
587
491
|
};
|
|
@@ -620,16 +524,12 @@ function bridgeSessionProtoToUpstreamRecord(protoBytes) {
|
|
|
620
524
|
const current = record.currentSession ? sessionStructureToEntry(record.currentSession, -1) : null;
|
|
621
525
|
if (current)
|
|
622
526
|
_sessions[current.indexInfo.baseKey] = current;
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
// "closed"; -1 is "open").
|
|
527
|
+
// Synthesize descending close timestamps so removeOldSessions can sort:
|
|
528
|
+
// front of Rust `previous_sessions` is the most recently archived.
|
|
626
529
|
const previous = record.previousSessions ?? [];
|
|
627
530
|
for (let i = 0; i < previous.length; i++) {
|
|
628
531
|
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])
|
|
532
|
+
if (entry && !_sessions[entry.indexInfo.baseKey])
|
|
633
533
|
_sessions[entry.indexInfo.baseKey] = entry;
|
|
634
534
|
}
|
|
635
535
|
return { _sessions, version: 'v1' };
|
|
@@ -646,25 +546,23 @@ function entryToSessionStructure(entry) {
|
|
|
646
546
|
chainKey: senderChainEntry
|
|
647
547
|
? { index: senderChainEntry.chainKey.counter, key: new Uint8Array(fromB64(senderChainEntry.chainKey.key)) }
|
|
648
548
|
: { index: 0, key: new Uint8Array() },
|
|
649
|
-
//
|
|
650
|
-
// is sequential — so this stays empty regardless.
|
|
651
|
-
messageKeys: []
|
|
549
|
+
messageKeys: [] // sender chain is sequential — no skipped cache either side
|
|
652
550
|
};
|
|
653
551
|
const receiverChains = [];
|
|
654
552
|
for (const [k, ch] of Object.entries(entry._chains)) {
|
|
655
553
|
if (ch.chainType !== CHAIN_TYPE_RECEIVING)
|
|
656
554
|
continue;
|
|
657
|
-
const ratchetPub = fromB64(k);
|
|
658
555
|
receiverChains.push({
|
|
659
|
-
senderRatchetKey: new Uint8Array(
|
|
556
|
+
senderRatchetKey: new Uint8Array(fromB64(k)),
|
|
660
557
|
chainKey: { index: ch.chainKey.counter, key: new Uint8Array(fromB64(ch.chainKey.key)) },
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
// so JS→Rust is a lossless one-shot derivation. See
|
|
664
|
-
// `deriveProtoMessageKey` for the algo + cross-impl proof.
|
|
558
|
+
// JS holds 32-byte HMAC seeds; Rust holds the post-HKDF split.
|
|
559
|
+
// Lossless JS→Rust derivation via deriveProtoMessageKey.
|
|
665
560
|
messageKeys: jsChainMessageKeysToProto(ch.messageKeys)
|
|
666
561
|
});
|
|
667
562
|
}
|
|
563
|
+
// `indexInfo.baseKey` IS the alice_base_key in either direction
|
|
564
|
+
// (alice=our ephemeral, bob=peer's ephemeral). Rust populates it
|
|
565
|
+
// symmetrically; dropping it would break find_matching_previous_session_index.
|
|
668
566
|
const session = {
|
|
669
567
|
rootKey: new Uint8Array(fromB64(ratchet.rootKey)),
|
|
670
568
|
previousCounter: ratchet.previousCounter,
|
|
@@ -672,14 +570,6 @@ function entryToSessionStructure(entry) {
|
|
|
672
570
|
receiverChains,
|
|
673
571
|
remoteIdentityPublic: new Uint8Array(fromB64(entry.indexInfo.remoteIdentityKey)),
|
|
674
572
|
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
573
|
aliceBaseKey: new Uint8Array(fromB64(entry.indexInfo.baseKey))
|
|
684
574
|
};
|
|
685
575
|
if (entry.pendingPreKey?.baseKey) {
|
|
@@ -692,21 +582,15 @@ function entryToSessionStructure(entry) {
|
|
|
692
582
|
return session;
|
|
693
583
|
}
|
|
694
584
|
function upstreamSessionRecordToProto(record) {
|
|
695
|
-
const
|
|
696
|
-
const sessions = r?._sessions ?? {};
|
|
585
|
+
const sessions = record?._sessions ?? {};
|
|
697
586
|
let currentSession = null;
|
|
698
587
|
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
588
|
for (const entry of Object.values(sessions)) {
|
|
703
589
|
const ss = entryToSessionStructure(entry);
|
|
704
|
-
if (!currentSession && entry.indexInfo.closed === -1)
|
|
590
|
+
if (!currentSession && entry.indexInfo.closed === -1)
|
|
705
591
|
currentSession = ss;
|
|
706
|
-
|
|
707
|
-
else {
|
|
592
|
+
else
|
|
708
593
|
previousSessions.push(ss);
|
|
709
|
-
}
|
|
710
594
|
}
|
|
711
595
|
// `BridgeSessionProto` is a structural mirror of `ISessionStructure`
|
|
712
596
|
// from `whatsapp-rust-bridge/proto-types`; the differences are nullable
|
|
@@ -720,14 +604,8 @@ function upstreamSessionRecordToProto(record) {
|
|
|
720
604
|
});
|
|
721
605
|
return proto.RecordStructure.encode(recordOut).finish();
|
|
722
606
|
}
|
|
723
|
-
//
|
|
724
|
-
//
|
|
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`.
|
|
607
|
+
// session/identity key translation: bridge `{user}[:dev]@{server}.{sig}`
|
|
608
|
+
// → upstream `{signalUser}.{deviceId}` via `bridgeAddrToBaileysAddr`.
|
|
731
609
|
// ---- Device/Creds ----
|
|
732
610
|
/** Parse a JID string into { user, device, server } */
|
|
733
611
|
function parseJid(jid) {
|
|
@@ -1033,15 +911,15 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
|
|
|
1033
911
|
}
|
|
1034
912
|
return translated;
|
|
1035
913
|
}
|
|
1036
|
-
// Bridge session
|
|
1037
|
-
// upstream's libsignal looks under
|
|
1038
|
-
// `bridgeAddrToBaileysAddr`
|
|
1039
|
-
// rewrite + JID-device extraction.
|
|
1040
|
-
if (storeName === 'session') {
|
|
914
|
+
// Bridge session/identity keys are the protocol address
|
|
915
|
+
// (`user[:dev]@server.sig`); upstream's libsignal looks under
|
|
916
|
+
// `signalUser.deviceId`. `bridgeAddrToBaileysAddr` does the LID/PN
|
|
917
|
+
// → signalUser rewrite + JID-device extraction.
|
|
918
|
+
if (storeName === 'session' || storeName === 'identity') {
|
|
1041
919
|
const translated = bridgeAddrToBaileysAddr(key);
|
|
1042
920
|
if (translated == null) {
|
|
1043
921
|
// eslint-disable-line eqeqeq -- intentional null+undefined check
|
|
1044
|
-
warn(
|
|
922
|
+
warn(`${storeName} key translation failed for "${key}", falling back to passthrough`);
|
|
1045
923
|
return key;
|
|
1046
924
|
}
|
|
1047
925
|
return translated;
|
|
@@ -1114,13 +992,12 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
|
|
|
1114
992
|
return readBinary(type, key);
|
|
1115
993
|
}
|
|
1116
994
|
if (route === 'signal') {
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
//
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
return null;
|
|
995
|
+
// `BINARY_STORES` is empty as of the move-to-converter
|
|
996
|
+
// migration — keeping the branch for future passthrough
|
|
997
|
+
// stores. No identity fallback needed: identity now goes
|
|
998
|
+
// through the converter, which handles both the address
|
|
999
|
+
// rewrite and the 32↔33-byte prefix.
|
|
1000
|
+
return readBinary(type, key);
|
|
1124
1001
|
}
|
|
1125
1002
|
if (route === 'bridge-only')
|
|
1126
1003
|
return readBinary(type, key);
|
|
@@ -1202,28 +1079,6 @@ export async function wrapLegacyStore(state, saveCreds, logger) {
|
|
|
1202
1079
|
}
|
|
1203
1080
|
}
|
|
1204
1081
|
};
|
|
1205
|
-
// Identity key fallback: upstream Baileys uses different address format
|
|
1206
|
-
// (user_domainType.device) and stores 33 bytes (0x05 + 32-byte DJB key)
|
|
1207
|
-
async function identityFallback(key, type) {
|
|
1208
|
-
try {
|
|
1209
|
-
const baileysAddr = bridgeAddrToBaileysAddr(key);
|
|
1210
|
-
if (!baileysAddr)
|
|
1211
|
-
return null;
|
|
1212
|
-
const raw = await storeGetOne('identity-key', baileysAddr);
|
|
1213
|
-
if (!raw || (!Buffer.isBuffer(raw) && !(raw instanceof Uint8Array)))
|
|
1214
|
-
return null;
|
|
1215
|
-
let buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
|
|
1216
|
-
if (buf.length === 33 && buf[0] === 0x05)
|
|
1217
|
-
buf = buf.slice(1);
|
|
1218
|
-
const arr = new Uint8Array(buf);
|
|
1219
|
-
await writeBinary(type, key, arr); // persist under bridge key for future reads
|
|
1220
|
-
return arr;
|
|
1221
|
-
}
|
|
1222
|
-
catch (e) {
|
|
1223
|
-
warn(`identity fallback failed for ${key}:`, e);
|
|
1224
|
-
return null;
|
|
1225
|
-
}
|
|
1226
|
-
}
|
|
1227
1082
|
}
|
|
1228
1083
|
const bufferReviver = (_, value) => {
|
|
1229
1084
|
if (value !== null &&
|