@nexustechpro/baileys 2.1.2 → 2.1.6
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 +471 -10
- package/lib/Defaults/index.js +0 -4
- package/lib/Signal/libsignal.js +138 -249
- package/lib/Signal/lid-mapping.js +23 -4
- package/lib/Socket/chats.js +1 -0
- package/lib/Socket/messages-recv.js +30 -18
- package/lib/Socket/messages-send.js +57 -37
- package/lib/Socket/newsletter.js +22 -17
- package/lib/Socket/nexus-handler.js +222 -328
- package/lib/Socket/socket.js +27 -18
- package/lib/Utils/browser-utils.js +12 -3
- package/lib/Utils/chat-utils.js +2 -2
- package/lib/Utils/key-store.js +1 -15
- package/lib/Utils/lt-hash.js +2 -43
- package/lib/Utils/messages-media.js +21 -16
- package/lib/Utils/messages.js +4 -2
- package/lib/Utils/process-message.js +234 -215
- package/lib/Utils/tc-token-utils.js +71 -71
- package/lib/Utils/use-multi-file-auth-state.js +211 -63
- package/lib/Utils/validate-connection.js +15 -3
- package/lib/WABinary/constants.js +14 -0
- package/lib/index.js +1 -1
- package/package.json +27 -12
package/lib/Signal/libsignal.js
CHANGED
|
@@ -8,8 +8,9 @@ import {
|
|
|
8
8
|
SenderKeyName,
|
|
9
9
|
SenderKeyDistributionMessage,
|
|
10
10
|
} from 'whatsapp-rust-bridge'
|
|
11
|
+
import { proto } from '../../WAProto/index.js'
|
|
11
12
|
import { LRUCache } from 'lru-cache'
|
|
12
|
-
import { generateSignalPubKey
|
|
13
|
+
import { generateSignalPubKey } from '../Utils/index.js'
|
|
13
14
|
import { isHostedLidUser, isHostedPnUser, isLidUser, isPnUser, jidDecode, transferDevice, WAJIDDomains } from '../WABinary/index.js'
|
|
14
15
|
import { LIDMappingStore } from './lid-mapping.js'
|
|
15
16
|
|
|
@@ -27,8 +28,6 @@ const jidToAddr = (jid) => {
|
|
|
27
28
|
|
|
28
29
|
const jidToSenderKeyName = (group, user) => new SenderKeyName(group, jidToAddr(user))
|
|
29
30
|
|
|
30
|
-
const v2Key = (addr) => `${addr}:v2`
|
|
31
|
-
|
|
32
31
|
// ─── Buffer Utils ─────────────────────────────────────────────────────────────
|
|
33
32
|
|
|
34
33
|
const toBuffer = (raw) => {
|
|
@@ -62,31 +61,19 @@ const isOldJson = (raw) => {
|
|
|
62
61
|
const bufEqual = (a, b) =>
|
|
63
62
|
a && b && a.length === b.length && a.every((byte, i) => byte === b[i])
|
|
64
63
|
|
|
65
|
-
// ─── Identity Extraction
|
|
66
|
-
// Reads field 4 (identity key, 33 bytes) from PreKeyWhisperMessage protobuf envelope
|
|
64
|
+
// ─── Identity Key Extraction ──────────────────────────────────────────────────
|
|
67
65
|
|
|
68
66
|
const extractIdentityFromPkmsg = (ciphertext) => {
|
|
69
67
|
try {
|
|
70
68
|
if (!ciphertext || ciphertext.length < 2) return undefined
|
|
71
69
|
if ((ciphertext[0] & 0xf) !== 3) return undefined
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
let len = 0, shift = 0
|
|
80
|
-
while (i < buf.length) { const b = buf[i++]; len |= (b & 0x7f) << shift; if (!(b & 0x80)) break; shift += 7 }
|
|
81
|
-
if (fieldNum === 4 && len === 33) return new Uint8Array(buf.slice(i, i + len))
|
|
82
|
-
i += len
|
|
83
|
-
} else if (wireType === 0) { while (i < buf.length && buf[i++] & 0x80) { } }
|
|
84
|
-
else if (wireType === 5) { i += 4 }
|
|
85
|
-
else if (wireType === 1) { i += 8 }
|
|
86
|
-
else break
|
|
87
|
-
}
|
|
88
|
-
} catch { }
|
|
89
|
-
return undefined
|
|
70
|
+
const decoded = proto.PreKeySignalMessage.decode(ciphertext.slice(1))
|
|
71
|
+
const key = decoded.identityKey
|
|
72
|
+
if (key && key.length === 33) return new Uint8Array(key)
|
|
73
|
+
return undefined
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
90
77
|
}
|
|
91
78
|
|
|
92
79
|
// ─── Main Factory ─────────────────────────────────────────────────────────────
|
|
@@ -95,29 +82,29 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
95
82
|
const lidMapping = new LIDMappingStore(auth.keys, logger, pnToLIDFunc)
|
|
96
83
|
const parsedKeys = auth.keys
|
|
97
84
|
|
|
98
|
-
//
|
|
85
|
+
// LRU cache for PN→LID address resolution to avoid repeated DB lookups
|
|
99
86
|
const lidCache = new LRUCache({ max: 500, ttl: 5 * 60 * 1000 })
|
|
100
87
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
const withSessionLock = (fn) => {
|
|
104
|
-
const next = sessionIndexWriteLock.then(fn).catch(fn)
|
|
105
|
-
sessionIndexWriteLock = next.then(() => { }, () => { })
|
|
106
|
-
return next
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// #2 — session read cache: avoids full index read on every decrypt
|
|
88
|
+
// Per-address session read cache: avoids a DB round-trip on every decrypt.
|
|
89
|
+
// Keyed by resolved LID addr string. Value is Uint8Array | null.
|
|
110
90
|
const sessionReadCache = new LRUCache({ max: 1000, ttl: 5 * 60 * 1000, ttlAutopurge: true })
|
|
111
91
|
|
|
112
|
-
const storage = signalStorage(auth, lidMapping, logger, lidCache, sessionReadCache
|
|
92
|
+
const storage = signalStorage(auth, lidMapping, logger, lidCache, sessionReadCache)
|
|
93
|
+
|
|
94
|
+
// Tracks which PN→LID session migrations have already been done this process
|
|
95
|
+
// lifetime so migrateSession is idempotent even if called repeatedly.
|
|
113
96
|
const migratedCache = new LRUCache({ ttl: 7 * 24 * 60 * 60 * 1000, ttlAutopurge: true, updateAgeOnGet: true })
|
|
97
|
+
|
|
114
98
|
const txn = (fn, key) => parsedKeys.transaction(fn, key)
|
|
115
99
|
|
|
116
100
|
return {
|
|
117
|
-
// ── Group
|
|
101
|
+
// ── Group messaging ───────────────────────────────────────────────────────
|
|
118
102
|
|
|
119
103
|
decryptGroupMessage({ group, authorJid, msg }) {
|
|
120
|
-
return txn(
|
|
104
|
+
return txn(
|
|
105
|
+
() => new GroupCipher(storage, group, jidToAddr(authorJid)).decrypt(toU8(msg)),
|
|
106
|
+
group
|
|
107
|
+
)
|
|
121
108
|
},
|
|
122
109
|
|
|
123
110
|
async processSenderKeyDistributionMessage({ item, authorJid }) {
|
|
@@ -126,7 +113,6 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
126
113
|
const senderMsg = SenderKeyDistributionMessage.deserialize(
|
|
127
114
|
toU8(item.axolotlSenderKeyDistributionMessage)
|
|
128
115
|
)
|
|
129
|
-
// do NOT pre-store a blank SenderKeyRecord — serialize() returns 0 bytes and bridge throws
|
|
130
116
|
return txn(() => new GroupSessionBuilder(storage).process(senderName, senderMsg), item.groupId)
|
|
131
117
|
},
|
|
132
118
|
|
|
@@ -139,7 +125,6 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
139
125
|
}, group)
|
|
140
126
|
},
|
|
141
127
|
|
|
142
|
-
// #10 — kept for API completeness but not called by messages-send.js
|
|
143
128
|
getSenderKeyDistributionMessage({ group, meId }) {
|
|
144
129
|
return txn(async () => {
|
|
145
130
|
const senderName = jidToSenderKeyName(group, meId)
|
|
@@ -149,15 +134,21 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
149
134
|
|
|
150
135
|
async hasSenderKey({ group, meId }) {
|
|
151
136
|
const name = jidToSenderKeyName(group, meId).toString()
|
|
152
|
-
const { [name]:
|
|
153
|
-
|
|
137
|
+
const { [name]: raw } = await parsedKeys.get('sender-key', [name])
|
|
138
|
+
const buf = toU8(raw)
|
|
139
|
+
if (!buf) return false
|
|
140
|
+
try {
|
|
141
|
+
return !SenderKeyRecord.deserialize(buf).isEmpty()
|
|
142
|
+
} catch {
|
|
143
|
+
return false
|
|
144
|
+
}
|
|
154
145
|
},
|
|
155
146
|
|
|
156
147
|
deleteSenderKey(group, authorJid) {
|
|
157
148
|
return parsedKeys.set({ 'sender-key': { [jidToSenderKeyName(group, authorJid).toString()]: null } })
|
|
158
149
|
},
|
|
159
150
|
|
|
160
|
-
// ── 1:1
|
|
151
|
+
// ── 1:1 messaging ─────────────────────────────────────────────────────────
|
|
161
152
|
|
|
162
153
|
async decryptMessage({ jid, type, ciphertext }) {
|
|
163
154
|
const addr = jidToAddr(jid)
|
|
@@ -166,26 +157,26 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
166
157
|
|
|
167
158
|
try {
|
|
168
159
|
return await txn(async () => {
|
|
169
|
-
// #8 — identity save inside transaction to prevent concurrent pkmsg race
|
|
170
160
|
if (type === 'pkmsg') {
|
|
171
161
|
const identityKey = extractIdentityFromPkmsg(ciphertext)
|
|
172
162
|
if (identityKey) {
|
|
173
163
|
const changed = await storage.saveIdentity(addrStr, identityKey)
|
|
174
|
-
if (changed) logger?.info?.({ jid }, '[Signal] Identity key changed
|
|
164
|
+
if (changed) logger?.info?.({ jid }, '[Signal] Identity key changed — session cleared for re-handshake')
|
|
175
165
|
} else {
|
|
176
|
-
logger?.
|
|
166
|
+
logger?.warn?.({ jid }, '[Signal] pkmsg: could not extract identity key from envelope')
|
|
177
167
|
}
|
|
178
|
-
return cipher.decryptPreKeyWhisperMessage(ciphertext)
|
|
168
|
+
return cipher.decryptPreKeyWhisperMessage(toU8(ciphertext))
|
|
179
169
|
}
|
|
180
|
-
if (type === 'msg')
|
|
181
|
-
|
|
170
|
+
if (type === 'msg') {
|
|
171
|
+
return cipher.decryptWhisperMessage(toU8(ciphertext))
|
|
172
|
+
}
|
|
173
|
+
throw new Error(`[Signal] Unknown message type: ${type}`)
|
|
182
174
|
}, jid)
|
|
183
175
|
} catch (e) {
|
|
184
176
|
if (e?.message?.includes('DuplicatedMessage')) {
|
|
185
177
|
logger?.debug?.({ jid }, '[Signal] Duplicate message ignored')
|
|
186
178
|
return null
|
|
187
179
|
}
|
|
188
|
-
// #11 — on untrusted identity / session corruption, wipe and let caller retry
|
|
189
180
|
if (e?.message?.includes('UntrustedIdentity') || e?.message?.includes('InvalidMessage')) {
|
|
190
181
|
logger?.warn?.({ jid, err: e.message }, '[Signal] Session error — wiping session for re-handshake')
|
|
191
182
|
await storage.wipeSession(addrStr)
|
|
@@ -197,7 +188,7 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
197
188
|
|
|
198
189
|
encryptMessage({ jid, data }) {
|
|
199
190
|
return txn(async () => {
|
|
200
|
-
const { type: sigType, body } = await new SessionCipher(storage, jidToAddr(jid)).encrypt(data)
|
|
191
|
+
const { type: sigType, body } = await new SessionCipher(storage, jidToAddr(jid)).encrypt(toU8(data))
|
|
201
192
|
return { type: sigType === 3 ? 'pkmsg' : 'msg', ciphertext: Buffer.from(body) }
|
|
202
193
|
}, jid)
|
|
203
194
|
},
|
|
@@ -206,181 +197,106 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
206
197
|
return txn(() => new SessionBuilder(storage, jidToAddr(jid)).processPreKeyBundle(session), jid)
|
|
207
198
|
},
|
|
208
199
|
|
|
209
|
-
// ── Session
|
|
200
|
+
// ── Session utilities ──────────────────────────────────────────────────────
|
|
210
201
|
|
|
211
|
-
jidToSignalProtocolAddress: jid => jidToAddr(jid).toString(),
|
|
202
|
+
jidToSignalProtocolAddress: (jid) => jidToAddr(jid).toString(),
|
|
212
203
|
|
|
213
204
|
lidMapping,
|
|
214
205
|
|
|
215
206
|
async validateSession(jid) {
|
|
216
207
|
try {
|
|
217
208
|
const addr = jidToAddr(jid).toString()
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
209
|
+
const raw = await storage.loadSession(addr)
|
|
210
|
+
if (!raw) return { exists: false, reason: 'no session' }
|
|
211
|
+
try {
|
|
212
|
+
return SessionRecord.deserialize(raw).haveOpenSession()
|
|
213
|
+
? { exists: true }
|
|
214
|
+
: { exists: false, reason: 'no open session' }
|
|
215
|
+
} catch {
|
|
216
|
+
return { exists: false, reason: 'deserialize error' }
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
return { exists: false, reason: 'error' }
|
|
220
|
+
}
|
|
225
221
|
},
|
|
226
222
|
|
|
227
223
|
async deleteSession(jids) {
|
|
228
224
|
if (!jids?.length) return
|
|
229
225
|
return txn(async () => {
|
|
230
|
-
|
|
231
|
-
const batch = await migrateIndexKey(parsedKeys, 'session')
|
|
232
|
-
const updated = { ...batch }
|
|
226
|
+
const updates = {}
|
|
233
227
|
for (const jid of jids) {
|
|
234
228
|
const addr = jidToAddr(jid).toString()
|
|
235
|
-
|
|
236
|
-
delete updated[v2Key(addr)]
|
|
229
|
+
updates[addr] = null
|
|
237
230
|
sessionReadCache.delete(addr)
|
|
238
231
|
}
|
|
239
|
-
await parsedKeys.set({ session:
|
|
232
|
+
await parsedKeys.set({ session: updates })
|
|
240
233
|
}, `del-${jids.length}`)
|
|
241
234
|
},
|
|
242
235
|
|
|
243
|
-
// ── Session migration
|
|
236
|
+
// ── Session migration (PN → LID) ──────────────────────────────────────────
|
|
244
237
|
|
|
245
238
|
async migrateSession(fromJid, toJid) {
|
|
246
239
|
if (!fromJid || (!isLidUser(toJid) && !isHostedLidUser(toJid))) return { migrated: 0, skipped: 0, total: 0 }
|
|
247
240
|
if (!isPnUser(fromJid) && !isHostedPnUser(fromJid)) return { migrated: 0, skipped: 0, total: 1 }
|
|
248
241
|
const { user } = jidDecode(fromJid)
|
|
249
242
|
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
migrateIndexKey(parsedKeys, 'device-list'),
|
|
253
|
-
migrateIndexKey(parsedKeys, 'session'),
|
|
254
|
-
])
|
|
255
|
-
|
|
256
|
-
const userDevices = deviceListBatch[user] ? [...deviceListBatch[user]] : []
|
|
243
|
+
const { [user]: userDevices_ } = await parsedKeys.get('device-list', [user])
|
|
244
|
+
const userDevices = userDevices_ ? [...userDevices_] : []
|
|
257
245
|
const fromDeviceStr = jidDecode(fromJid).device?.toString() || '0'
|
|
258
246
|
if (!userDevices.includes(fromDeviceStr)) userDevices.push(fromDeviceStr)
|
|
259
247
|
|
|
260
|
-
|
|
248
|
+
// Build addr strings for each device, skip already-migrated
|
|
249
|
+
const candidates = userDevices
|
|
261
250
|
.filter(d => !migratedCache.has(`${user}.${d}`))
|
|
262
251
|
.map(d => {
|
|
263
252
|
const num = parseInt(d)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
})
|
|
269
|
-
.filter(({ jid }) => {
|
|
270
|
-
const addr = jidToAddr(jid).toString()
|
|
271
|
-
return sessionBatch[v2Key(addr)] || sessionBatch[addr]
|
|
253
|
+
const jid = num === 99 ? `${user}:99@hosted`
|
|
254
|
+
: num === 0 ? `${user}@s.whatsapp.net`
|
|
255
|
+
: `${user}:${num}@s.whatsapp.net`
|
|
256
|
+
return { cacheKey: `${user}.${d}`, jid, addr: jidToAddr(jid).toString() }
|
|
272
257
|
})
|
|
273
258
|
|
|
274
|
-
if (!
|
|
275
|
-
|
|
276
|
-
return txn(async () => {
|
|
277
|
-
// #5 — re-read inside txn for a fresh snapshot
|
|
278
|
-
const freshBatch = await migrateIndexKey(parsedKeys, 'session')
|
|
279
|
-
const updated = { ...freshBatch }
|
|
280
|
-
let migrated = 0
|
|
281
|
-
|
|
282
|
-
for (const { jid, cacheKey } of deviceJids) {
|
|
283
|
-
const pnAddr = jidToAddr(jid).toString()
|
|
284
|
-
const lidAddr = jidToAddr(transferDevice(jid, toJid)).toString()
|
|
285
|
-
const raw = toBuffer(updated[v2Key(pnAddr)]) || toBuffer(updated[pnAddr])
|
|
286
|
-
if (!raw || isOldJson(raw)) continue
|
|
287
|
-
const sess = SessionRecord.deserialize(raw)
|
|
288
|
-
if (!sess.haveOpenSession()) continue
|
|
289
|
-
updated[v2Key(lidAddr)] = sess.serialize()
|
|
290
|
-
updated[lidAddr] = { version: 'v1', _sessions: {} }
|
|
291
|
-
delete updated[v2Key(pnAddr)]
|
|
292
|
-
delete updated[pnAddr]
|
|
293
|
-
// #3 — invalidate lidCache for migrated PN addr so next resolve hits store
|
|
294
|
-
lidCache.delete(pnAddr)
|
|
295
|
-
sessionReadCache.delete(pnAddr)
|
|
296
|
-
migrated++
|
|
297
|
-
migratedCache.set(cacheKey, true)
|
|
298
|
-
}
|
|
259
|
+
if (!candidates.length) return { migrated: 0, skipped: 0, total: 0 }
|
|
299
260
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
},
|
|
261
|
+
// Bulk-fetch only the session keys we actually need
|
|
262
|
+
const addrs = candidates.map(c => c.addr)
|
|
263
|
+
const existing = await parsedKeys.get('session', addrs)
|
|
304
264
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
// lid-mapping read outside txn to avoid nested key-namespace lock
|
|
310
|
-
// #5 — session batch read outside is intentional; re-read inside txn before write
|
|
311
|
-
const [sessionBatch, stored] = await (async () => {
|
|
312
|
-
const sb = await migrateIndexKey(parsedKeys, 'session')
|
|
313
|
-
const sessionKeys = Object.keys(sb)
|
|
314
|
-
if (!sessionKeys.length) return [sb, {}]
|
|
315
|
-
const pnAddrs = sessionKeys.filter(addr => {
|
|
316
|
-
if (addr.endsWith(':v2') || !addr.includes('.')) return false
|
|
317
|
-
const [, dt] = addr.split('.')[0].split('_')
|
|
318
|
-
const domainType = parseInt(dt || '0')
|
|
319
|
-
return domainType === WAJIDDomains.WHATSAPP || domainType === WAJIDDomains.HOSTED
|
|
320
|
-
})
|
|
321
|
-
if (!pnAddrs.length) return [sb, {}]
|
|
322
|
-
const pnUserSet = new Set(pnAddrs.map(addr => addr.split('.')[0].split('_')[0]))
|
|
323
|
-
const s = await parsedKeys.get('lid-mapping', [...pnUserSet])
|
|
324
|
-
return [sb, s]
|
|
325
|
-
})()
|
|
326
|
-
|
|
327
|
-
const sessionKeys = Object.keys(sessionBatch)
|
|
328
|
-
if (!sessionKeys.length) return 0
|
|
329
|
-
|
|
330
|
-
const pnAddrs = sessionKeys.filter(addr => {
|
|
331
|
-
if (addr.endsWith(':v2') || !addr.includes('.')) return false
|
|
332
|
-
const [, dt] = addr.split('.')[0].split('_')
|
|
333
|
-
const domainType = parseInt(dt || '0')
|
|
334
|
-
return domainType === WAJIDDomains.WHATSAPP || domainType === WAJIDDomains.HOSTED
|
|
265
|
+
const toMigrate = candidates.filter(c => {
|
|
266
|
+
const raw = toU8(existing[c.addr])
|
|
267
|
+
return raw && !isOldJson(raw)
|
|
335
268
|
})
|
|
336
|
-
if (!pnAddrs.length) return 0
|
|
337
269
|
|
|
338
|
-
|
|
339
|
-
for (const pnUser of new Set(pnAddrs.map(addr => addr.split('.')[0].split('_')[0]))) {
|
|
340
|
-
const lidUser = stored[pnUser]
|
|
341
|
-
if (lidUser && typeof lidUser === 'string') pnToLidUserMap.set(pnUser, lidUser)
|
|
342
|
-
}
|
|
343
|
-
if (!pnToLidUserMap.size) return 0
|
|
270
|
+
if (!toMigrate.length) return { migrated: 0, skipped: candidates.length, total: candidates.length }
|
|
344
271
|
|
|
345
272
|
return txn(async () => {
|
|
346
|
-
//
|
|
347
|
-
const
|
|
348
|
-
const
|
|
273
|
+
// Re-fetch inside txn for freshness
|
|
274
|
+
const fresh = await parsedKeys.get('session', toMigrate.map(c => c.addr))
|
|
275
|
+
const updates = {}
|
|
349
276
|
let migrated = 0
|
|
350
277
|
|
|
351
|
-
for (const addr of
|
|
352
|
-
const
|
|
353
|
-
const [user, dt] = deviceId.split('_')
|
|
354
|
-
const domainType = parseInt(dt || '0')
|
|
355
|
-
const lidUser = pnToLidUserMap.get(user)
|
|
356
|
-
if (!lidUser) continue
|
|
357
|
-
const lidDomainType = domainType === WAJIDDomains.HOSTED ? WAJIDDomains.HOSTED_LID : WAJIDDomains.LID
|
|
358
|
-
const lidAddr = `${lidUser}_${lidDomainType}.${device}`
|
|
359
|
-
if (updated[v2Key(lidAddr)]) continue
|
|
360
|
-
const raw = toBuffer(updated[v2Key(addr)]) || toBuffer(updated[addr])
|
|
278
|
+
for (const { jid, addr, cacheKey } of toMigrate) {
|
|
279
|
+
const raw = toU8(fresh[addr])
|
|
361
280
|
if (!raw || isOldJson(raw)) continue
|
|
362
|
-
|
|
281
|
+
let sess
|
|
282
|
+
try { sess = SessionRecord.deserialize(raw) } catch { continue }
|
|
363
283
|
if (!sess.haveOpenSession()) continue
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
delete updated[addr]
|
|
368
|
-
// #3 — invalidate caches for migrated addresses
|
|
284
|
+
const lidAddr = jidToAddr(transferDevice(jid, toJid)).toString()
|
|
285
|
+
updates[lidAddr] = sess.serialize()
|
|
286
|
+
updates[addr] = null
|
|
369
287
|
lidCache.delete(addr)
|
|
370
288
|
sessionReadCache.delete(addr)
|
|
371
289
|
migrated++
|
|
372
|
-
migratedCache.set(
|
|
290
|
+
migratedCache.set(cacheKey, true)
|
|
373
291
|
}
|
|
374
292
|
|
|
375
293
|
if (migrated > 0) {
|
|
376
|
-
await parsedKeys.set({ session:
|
|
377
|
-
logger?.info?.({ migrated, totalPN: pnAddrs.length, mappingsFound: pnToLidUserMap.size }, '[Signal] Batch-migrated PN sessions to LID on connect')
|
|
294
|
+
await parsedKeys.set({ session: updates })
|
|
378
295
|
}
|
|
379
|
-
return migrated
|
|
380
|
-
},
|
|
296
|
+
return { migrated, skipped: toMigrate.length - migrated, total: candidates.length }
|
|
297
|
+
}, `migrate-${jidDecode(toJid)?.user}`)
|
|
381
298
|
},
|
|
382
299
|
|
|
383
|
-
// #17 — warm the LID cache on connect from stored mappings
|
|
384
300
|
async warmLIDCache(mappings) {
|
|
385
301
|
for (const { pn, lid } of mappings) {
|
|
386
302
|
try {
|
|
@@ -401,16 +317,16 @@ export function makeLibSignalRepository(auth, logger, pnToLIDFunc) {
|
|
|
401
317
|
}
|
|
402
318
|
|
|
403
319
|
// ─── Storage Adapter ──────────────────────────────────────────────────────────
|
|
404
|
-
// Implements SignalStorage interface
|
|
320
|
+
// Implements the SignalStorage interface consumed by whatsapp-rust-bridge WASM.
|
|
405
321
|
//
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
//
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
|
|
322
|
+
// One key per record — exactly as original Baileys does:
|
|
323
|
+
// session: addr string → Uint8Array (SessionRecord.serialize())
|
|
324
|
+
// identity-key: addr string → Uint8Array (33-byte identity key)
|
|
325
|
+
// pre-key: id string → { public, private }
|
|
326
|
+
// sender-key: keyId string → Buffer
|
|
327
|
+
|
|
328
|
+
function signalStorage({ creds, keys }, lidMapping, logger, lidCache, sessionReadCache) {
|
|
412
329
|
|
|
413
|
-
function signalStorage({ creds, keys }, lidMapping, logger, lidCache, sessionReadCache, withSessionLock) {
|
|
414
330
|
const resolveLID = async (id) => {
|
|
415
331
|
if (!id.includes('.')) return id
|
|
416
332
|
const cached = lidCache.get(id)
|
|
@@ -426,77 +342,48 @@ function signalStorage({ creds, keys }, lidMapping, logger, lidCache, sessionRea
|
|
|
426
342
|
return result
|
|
427
343
|
}
|
|
428
344
|
|
|
429
|
-
// #1/#2 — all index reads go through the read cache; all writes go through the write lock
|
|
430
|
-
const getIndex = async () => {
|
|
431
|
-
const cached = sessionReadCache.get('__index__')
|
|
432
|
-
if (cached) return cached
|
|
433
|
-
const batch = await migrateIndexKey(keys, 'session')
|
|
434
|
-
sessionReadCache.set('__index__', batch)
|
|
435
|
-
return batch
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const setIndex = (batch) => withSessionLock(async () => {
|
|
439
|
-
sessionReadCache.set('__index__', batch)
|
|
440
|
-
try {
|
|
441
|
-
await keys.set({ session: { index: batch } })
|
|
442
|
-
} catch (e) {
|
|
443
|
-
// #18 — on write failure invalidate cache so next read hits store
|
|
444
|
-
sessionReadCache.delete('__index__')
|
|
445
|
-
logger?.error?.(`[Signal] storeSession write failed: ${e.message}`)
|
|
446
|
-
throw e
|
|
447
|
-
}
|
|
448
|
-
})
|
|
449
|
-
|
|
450
345
|
return {
|
|
346
|
+
// ── Sessions ─────────────────────────────────────────────────────────────
|
|
347
|
+
|
|
451
348
|
loadSession: async (id) => {
|
|
452
349
|
try {
|
|
453
350
|
const addr = await resolveLID(id)
|
|
454
|
-
// #2 — check per-addr cache before reading full index
|
|
455
351
|
const cached = sessionReadCache.get(addr)
|
|
456
352
|
if (cached !== undefined) return cached === null ? null : toU8(cached)
|
|
457
|
-
const
|
|
458
|
-
|
|
459
|
-
if (v2) {
|
|
460
|
-
if (isOldJson(v2)) { logger?.debug?.(`[Signal] Corrupt v2 for ${addr}, fresh handshake`); sessionReadCache.set(addr, null); return null }
|
|
461
|
-
const buf = toU8(v2)
|
|
462
|
-
if (buf) { sessionReadCache.set(addr, buf); return buf }
|
|
463
|
-
}
|
|
464
|
-
const plain = batch[addr]
|
|
465
|
-
if (!plain || isOldJson(plain)) {
|
|
466
|
-
if (plain) logger?.debug?.(`[Signal] Old JSON session for ${addr}, fresh handshake`)
|
|
353
|
+
const { [addr]: raw } = await keys.get('session', [addr])
|
|
354
|
+
if (!raw || isOldJson(raw)) {
|
|
467
355
|
sessionReadCache.set(addr, null)
|
|
468
356
|
return null
|
|
469
357
|
}
|
|
470
|
-
const buf = toU8(
|
|
358
|
+
const buf = toU8(raw)
|
|
471
359
|
sessionReadCache.set(addr, buf)
|
|
472
360
|
return buf
|
|
473
|
-
} catch (e) {
|
|
361
|
+
} catch (e) {
|
|
362
|
+
logger?.error?.(`[Signal] loadSession error for ${id}: ${e.message}`)
|
|
363
|
+
return null
|
|
364
|
+
}
|
|
474
365
|
},
|
|
475
366
|
|
|
476
367
|
storeSession: async (id, record) => {
|
|
477
368
|
const addr = await resolveLID(id)
|
|
478
369
|
const serialized = record.serialize()
|
|
479
|
-
// #19 — only update tombstone on first write; v2 always updated
|
|
480
|
-
const batch = await getIndex()
|
|
481
|
-
const needsTombstone = !batch[addr] || !isOldJson(batch[addr])
|
|
482
|
-
const updated = {
|
|
483
|
-
...batch,
|
|
484
|
-
[v2Key(addr)]: serialized,
|
|
485
|
-
...(needsTombstone ? { [addr]: { version: 'v1', _sessions: {} } } : {}),
|
|
486
|
-
}
|
|
487
370
|
sessionReadCache.set(addr, toU8(serialized))
|
|
488
|
-
await
|
|
371
|
+
await keys.set({ session: { [addr]: serialized } })
|
|
489
372
|
},
|
|
490
373
|
|
|
491
|
-
//
|
|
374
|
+
// ── Identity keys ─────────────────────────────────────────────────────────
|
|
375
|
+
|
|
492
376
|
isTrustedIdentity: async (id, identityKey) => {
|
|
493
377
|
try {
|
|
494
378
|
const addr = await resolveLID(id)
|
|
495
|
-
const { [addr]:
|
|
496
|
-
const existing = toU8(raw)
|
|
379
|
+
const { [addr]: existing } = await keys.get('identity-key', [addr])
|
|
497
380
|
if (!existing) return true
|
|
498
|
-
|
|
499
|
-
|
|
381
|
+
const a = toU8(existing)
|
|
382
|
+
const b = identityKey instanceof Uint8Array ? identityKey : toU8(identityKey)
|
|
383
|
+
return !!bufEqual(a, b)
|
|
384
|
+
} catch {
|
|
385
|
+
return true
|
|
386
|
+
}
|
|
500
387
|
},
|
|
501
388
|
|
|
502
389
|
loadIdentityKey: async (id) => {
|
|
@@ -507,37 +394,33 @@ function signalStorage({ creds, keys }, lidMapping, logger, lidCache, sessionRea
|
|
|
507
394
|
|
|
508
395
|
saveIdentity: async (id, identityKey) => {
|
|
509
396
|
const addr = await resolveLID(id)
|
|
510
|
-
const { [addr]:
|
|
511
|
-
const
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
delete updated[addr]
|
|
516
|
-
delete updated[v2Key(addr)]
|
|
397
|
+
const { [addr]: existing } = await keys.get('identity-key', [addr])
|
|
398
|
+
const a = toU8(existing)
|
|
399
|
+
const b = identityKey instanceof Uint8Array ? identityKey : toU8(identityKey)
|
|
400
|
+
if (a && !bufEqual(a, b)) {
|
|
401
|
+
// Identity changed — wipe session and update key atomically
|
|
517
402
|
sessionReadCache.delete(addr)
|
|
518
|
-
sessionReadCache.delete('__index__')
|
|
519
|
-
await setIndex(updated)
|
|
520
|
-
await keys.set({ 'identity-key': { [addr]: identityKey } })
|
|
521
403
|
lidCache.delete(id)
|
|
404
|
+
await keys.set({
|
|
405
|
+
session: { [addr]: null },
|
|
406
|
+
'identity-key': { [addr]: b }
|
|
407
|
+
})
|
|
522
408
|
return true
|
|
523
409
|
}
|
|
524
|
-
if (!
|
|
525
|
-
await keys.set({ 'identity-key': { [addr]:
|
|
410
|
+
if (!a) {
|
|
411
|
+
await keys.set({ 'identity-key': { [addr]: b } })
|
|
526
412
|
return true
|
|
527
413
|
}
|
|
528
414
|
return false
|
|
529
415
|
},
|
|
530
416
|
|
|
531
|
-
// #11 — exposed for decryptMessage error recovery
|
|
532
417
|
wipeSession: async (addr) => {
|
|
533
|
-
const batch = await getIndex()
|
|
534
|
-
const updated = { ...batch }
|
|
535
|
-
delete updated[addr]
|
|
536
|
-
delete updated[v2Key(addr)]
|
|
537
418
|
sessionReadCache.delete(addr)
|
|
538
|
-
await
|
|
419
|
+
await keys.set({ session: { [addr]: null } })
|
|
539
420
|
},
|
|
540
421
|
|
|
422
|
+
// ── PreKeys ───────────────────────────────────────────────────────────────
|
|
423
|
+
|
|
541
424
|
loadPreKey: async (id) => {
|
|
542
425
|
const { [id.toString()]: key } = await keys.get('pre-key', [id.toString()])
|
|
543
426
|
if (!key) return null
|
|
@@ -549,7 +432,6 @@ function signalStorage({ creds, keys }, lidMapping, logger, lidCache, sessionRea
|
|
|
549
432
|
|
|
550
433
|
removePreKey: (id) => keys.set({ 'pre-key': { [id]: null } }),
|
|
551
434
|
|
|
552
|
-
// #13 — return null on id mismatch instead of silently returning wrong key
|
|
553
435
|
loadSignedPreKey: (id) => {
|
|
554
436
|
const key = creds.signedPreKey
|
|
555
437
|
if (key.keyId !== id) {
|
|
@@ -566,18 +448,25 @@ function signalStorage({ creds, keys }, lidMapping, logger, lidCache, sessionRea
|
|
|
566
448
|
}
|
|
567
449
|
},
|
|
568
450
|
|
|
451
|
+
// ── Sender keys ───────────────────────────────────────────────────────────
|
|
452
|
+
|
|
569
453
|
loadSenderKey: async (keyId) => {
|
|
570
454
|
try {
|
|
571
455
|
const id = keyId.toString()
|
|
572
456
|
const { [id]: key } = await keys.get('sender-key', [id])
|
|
573
|
-
return
|
|
574
|
-
} catch (e) {
|
|
457
|
+
return toU8(key) ?? null
|
|
458
|
+
} catch (e) {
|
|
459
|
+
logger?.error?.(`[Signal] loadSenderKey error: ${e.message}`)
|
|
460
|
+
return null
|
|
461
|
+
}
|
|
575
462
|
},
|
|
576
463
|
|
|
577
464
|
storeSenderKey: async (keyId, record) => {
|
|
578
465
|
await keys.set({ 'sender-key': { [keyId.toString()]: Buffer.from(record) } })
|
|
579
466
|
},
|
|
580
467
|
|
|
468
|
+
// ── Own identity ──────────────────────────────────────────────────────────
|
|
469
|
+
|
|
581
470
|
getOurRegistrationId: () => creds.registrationId,
|
|
582
471
|
|
|
583
472
|
getOurIdentity: () => ({
|