@aunucore/baileys 1.0.76 → 1.0.78
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/Signal/Group/group-session-builder.js +19 -1
- package/lib/Signal/Group/sender-chain-key.js +10 -1
- package/lib/Signal/Group/sender-key-distribution-message.js +4 -2
- package/lib/Signal/Group/sender-key-message.js +9 -2
- package/lib/Signal/Group/sender-key-record.js +91 -3
- package/lib/Signal/libsignal.js +50 -22
- package/lib/Socket/messages-recv.js +21 -3
- package/lib/Socket/messages-send.js +28 -7
- package/lib/Socket/socket.js +146 -19
- package/lib/Utils/crypto.d.ts +1 -0
- package/lib/Utils/crypto.js +21 -3
- package/lib/Utils/decode-wa-message.d.ts +1 -0
- package/lib/Utils/decode-wa-message.js +15 -0
- package/lib/Utils/generics.js +11 -1
- package/lib/Utils/noise-handler.js +43 -11
- package/lib/Utils/signal.js +8 -5
- package/lib/Utils/validate-connection.js +5 -3
- package/package.json +1 -1
|
@@ -7,8 +7,26 @@ export class GroupSessionBuilder {
|
|
|
7
7
|
this.senderKeyStore = senderKeyStore;
|
|
8
8
|
}
|
|
9
9
|
async process(senderKeyName, senderKeyDistributionMessage) {
|
|
10
|
+
// AunuBaileys (1.0.78, HB-168): a malformed SKDM used to be
|
|
11
|
+
// stored without any validation — a missing/empty distribution
|
|
12
|
+
// message produced a junk state (keyId 0, empty chain seed,
|
|
13
|
+
// empty signing key) that passed the load-time guard, and five
|
|
14
|
+
// of them (MAX_STATES) evicted the author's good state, after
|
|
15
|
+
// which every legitimate message from that author failed with
|
|
16
|
+
// 'No session found'. Refuse anything that cannot possibly
|
|
17
|
+
// work: a numeric id and iteration, a 32-byte chain key and a
|
|
18
|
+
// 32/33-byte signing key (our own builder emits 32 + 33).
|
|
19
|
+
const id = senderKeyDistributionMessage.getId();
|
|
20
|
+
const iteration = senderKeyDistributionMessage.getIteration();
|
|
21
|
+
const chainKey = senderKeyDistributionMessage.getChainKey();
|
|
22
|
+
const signatureKey = senderKeyDistributionMessage.getSignatureKey();
|
|
23
|
+
if (!Number.isFinite(id) || !Number.isFinite(iteration) ||
|
|
24
|
+
!Buffer.isBuffer(chainKey) || chainKey.length !== 32 ||
|
|
25
|
+
!Buffer.isBuffer(signatureKey) || (signatureKey.length !== 32 && signatureKey.length !== 33)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
10
28
|
const senderKeyRecord = await this.senderKeyStore.loadSenderKey(senderKeyName);
|
|
11
|
-
senderKeyRecord.addSenderKeyState(
|
|
29
|
+
senderKeyRecord.addSenderKeyState(id, iteration, chainKey, signatureKey);
|
|
12
30
|
await this.senderKeyStore.storeSenderKey(senderKeyName, senderKeyRecord);
|
|
13
31
|
}
|
|
14
32
|
async create(senderKeyName) {
|
|
@@ -5,10 +5,19 @@ export class SenderChainKey {
|
|
|
5
5
|
constructor(iteration, chainKey) {
|
|
6
6
|
this.MESSAGE_KEY_SEED = Buffer.from([0x01]);
|
|
7
7
|
this.CHAIN_KEY_SEED = Buffer.from([0x02]);
|
|
8
|
-
|
|
8
|
+
// AunuBaileys (1.0.78, HB-162/HB-180): a stored string seed is
|
|
9
|
+
// base64 (the house format), not UTF-8 text, and a numeric
|
|
10
|
+
// string iteration must become a number — '5' + 1 === '51'
|
|
11
|
+
// concatenation poisoned the chain. Stored records are
|
|
12
|
+
// sanitised at load (sender-key-record.js); this is the same
|
|
13
|
+
// normalisation for direct constructions.
|
|
14
|
+
this.iteration = typeof iteration === 'string' && /^-?\d+$/.test(iteration.trim()) ? Number(iteration) : iteration;
|
|
9
15
|
if (chainKey instanceof Buffer) {
|
|
10
16
|
this.chainKey = chainKey;
|
|
11
17
|
}
|
|
18
|
+
else if (typeof chainKey === 'string') {
|
|
19
|
+
this.chainKey = Buffer.from(chainKey, 'base64');
|
|
20
|
+
}
|
|
12
21
|
else {
|
|
13
22
|
this.chainKey = Buffer.from(chainKey || []);
|
|
14
23
|
}
|
|
@@ -8,8 +8,10 @@ export class SenderKeyDistributionMessage extends CiphertextMessage {
|
|
|
8
8
|
const message = serialized.slice(1);
|
|
9
9
|
const distributionMessage = proto.SenderKeyDistributionMessage.decode(message).toJSON();
|
|
10
10
|
this.serialized = serialized;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
// AunuBaileys (1.0.78, HB-186): same proto3 default
|
|
12
|
+
// omission as SenderKeyMessage — normalise to 0.
|
|
13
|
+
this.id = distributionMessage.id ?? 0;
|
|
14
|
+
this.iteration = distributionMessage.iteration ?? 0;
|
|
13
15
|
this.chainKey =
|
|
14
16
|
typeof distributionMessage.chainKey === 'string'
|
|
15
17
|
? Buffer.from(distributionMessage.chainKey, 'base64')
|
|
@@ -13,8 +13,15 @@ export class SenderKeyMessage extends CiphertextMessage {
|
|
|
13
13
|
const senderKeyMessage = proto.SenderKeyMessage.decode(message).toJSON();
|
|
14
14
|
this.serialized = serialized;
|
|
15
15
|
this.messageVersion = (version & 0xff) >> 4;
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
// AunuBaileys (1.0.78, HB-186/HB-217): proto3 encoders omit
|
|
17
|
+
// default-valued fields, so a canonical peer's id 0 /
|
|
18
|
+
// iteration 0 arrive as undefined. Normalise to 0 — an
|
|
19
|
+
// undefined iteration made getSenderKey return the CURRENT
|
|
20
|
+
// chain key (every comparison against undefined is false)
|
|
21
|
+
// and an undefined keyId selected the LAST state in the
|
|
22
|
+
// record instead of state 0.
|
|
23
|
+
this.keyId = senderKeyMessage.id ?? 0;
|
|
24
|
+
this.iteration = senderKeyMessage.iteration ?? 0;
|
|
18
25
|
this.ciphertext =
|
|
19
26
|
typeof senderKeyMessage.ciphertext === 'string'
|
|
20
27
|
? Buffer.from(senderKeyMessage.ciphertext, 'base64')
|
|
@@ -1,5 +1,86 @@
|
|
|
1
1
|
import { BufferJSON } from '../../Utils/generics.js';
|
|
2
2
|
import { SenderKeyState } from './sender-key-state.js';
|
|
3
|
+
// AunuBaileys (1.0.78, HB-124/HB-162/HB-180/HB-197): stored sender-key
|
|
4
|
+
// states used to be checked for EXISTENCE only — a hand-edited or
|
|
5
|
+
// corrupt sender-key-*.json could carry a number where a seed belongs
|
|
6
|
+
// (raw TypeError on every group decrypt/encrypt), a base64 string the
|
|
7
|
+
// chain key decoded as UTF-8 (silently wrong keys), a string iteration
|
|
8
|
+
// ('5' + 1 === '51' — concatenation poisoned the chain and bricked
|
|
9
|
+
// group sends with 'Over 2000 messages into the future!'), a missing
|
|
10
|
+
// iteration (NaN — every SKM went out with wire iteration 0 forever),
|
|
11
|
+
// or a string/missing senderKeyId (strict === lookup always missed).
|
|
12
|
+
// Normalise what has a documented house format (base64 strings,
|
|
13
|
+
// numeric strings) and DROP what cannot be made valid — a dropped
|
|
14
|
+
// state self-heals via a fresh SKDM / a fresh own state, a poisoned
|
|
15
|
+
// one never does. Key material produced by our own serializer always
|
|
16
|
+
// satisfies these shapes (32-byte seeds, 32/33-byte signing public).
|
|
17
|
+
const storedSeedToBuffer = (value) => Buffer.isBuffer(value)
|
|
18
|
+
? value
|
|
19
|
+
: (typeof value === 'string' ? Buffer.from(value, 'base64') : null);
|
|
20
|
+
const storedCountToNumber = (value) => {
|
|
21
|
+
if (typeof value === 'number') {
|
|
22
|
+
return Number.isInteger(value) ? value : null;
|
|
23
|
+
}
|
|
24
|
+
if (typeof value === 'string' && /^-?\d+$/.test(value.trim())) {
|
|
25
|
+
const n = Number(value);
|
|
26
|
+
return Number.isFinite(n) ? n : null;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
};
|
|
30
|
+
const sanitizeSenderKeyStructure = (structure) => {
|
|
31
|
+
const chainKey = structure.senderChainKey;
|
|
32
|
+
const signingKey = structure.senderSigningKey;
|
|
33
|
+
if (!chainKey || typeof chainKey !== 'object' || !signingKey || typeof signingKey !== 'object') {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const seed = storedSeedToBuffer(chainKey.seed);
|
|
37
|
+
if (!seed || seed.length !== 32) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
const iteration = storedCountToNumber(chainKey.iteration);
|
|
41
|
+
if (iteration === null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const signingPublic = storedSeedToBuffer(signingKey.public);
|
|
45
|
+
if (!signingPublic || (signingPublic.length !== 32 && signingPublic.length !== 33)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const cleanSigning = { public: signingPublic };
|
|
49
|
+
if (typeof signingKey.private !== 'undefined') {
|
|
50
|
+
const signingPrivate = storedSeedToBuffer(signingKey.private);
|
|
51
|
+
if (!signingPrivate || signingPrivate.length !== 32) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
cleanSigning.private = signingPrivate;
|
|
55
|
+
}
|
|
56
|
+
const keyId = storedCountToNumber(structure.senderKeyId);
|
|
57
|
+
if (keyId === null) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
// senderMessageKeys: drop individual entries that cannot be used
|
|
61
|
+
// (HB-124 addenda) instead of dropping the whole state — the chain
|
|
62
|
+
// itself is still good.
|
|
63
|
+
const messageKeys = [];
|
|
64
|
+
if (Array.isArray(structure.senderMessageKeys)) {
|
|
65
|
+
for (const entry of structure.senderMessageKeys) {
|
|
66
|
+
if (!entry || typeof entry !== 'object') {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const mkIteration = storedCountToNumber(entry.iteration);
|
|
70
|
+
const mkSeed = storedSeedToBuffer(entry.seed);
|
|
71
|
+
if (mkIteration === null || !mkSeed || mkSeed.length !== 32) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
messageKeys.push({ iteration: mkIteration, seed: mkSeed });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
senderKeyId: keyId,
|
|
79
|
+
senderChainKey: { iteration, seed },
|
|
80
|
+
senderSigningKey: cleanSigning,
|
|
81
|
+
senderMessageKeys: messageKeys
|
|
82
|
+
};
|
|
83
|
+
};
|
|
3
84
|
export class SenderKeyRecord {
|
|
4
85
|
constructor(serialized) {
|
|
5
86
|
this.MAX_STATES = 5;
|
|
@@ -15,11 +96,18 @@ export class SenderKeyRecord {
|
|
|
15
96
|
// store) used to load "fine" and then throw a bare
|
|
16
97
|
// TypeError on every group send, forever (HB-69).
|
|
17
98
|
// Skip entries that cannot possibly work.
|
|
18
|
-
|
|
19
|
-
|
|
99
|
+
// (1.0.78, HB-124/HB-162/HB-180/HB-197): existence is
|
|
100
|
+
// not enough — sanitise the field TYPES (see
|
|
101
|
+
// sanitizeSenderKeyStructure above) and skip states
|
|
102
|
+
// that stay invalid after normalisation.
|
|
103
|
+
if (!structure || typeof structure !== 'object') {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
const clean = sanitizeSenderKeyStructure(structure);
|
|
107
|
+
if (!clean) {
|
|
20
108
|
continue;
|
|
21
109
|
}
|
|
22
|
-
this.senderKeyStates.push(new SenderKeyState(null, null, null, null, null, null,
|
|
110
|
+
this.senderKeyStates.push(new SenderKeyState(null, null, null, null, null, null, clean));
|
|
23
111
|
}
|
|
24
112
|
}
|
|
25
113
|
}
|
package/lib/Signal/libsignal.js
CHANGED
|
@@ -9,8 +9,15 @@ import { GroupCipher, GroupSessionBuilder, SenderKeyDistributionMessage } from '
|
|
|
9
9
|
// (or a string in a hand-edited creds file). Anything else is a
|
|
10
10
|
// wrong-typed corrupt value, not key material.
|
|
11
11
|
const isKeyMaterial = (value) => Buffer.isBuffer(value) || typeof value === 'string';
|
|
12
|
+
// AunuBaileys (1.0.78, HB-157): string key material is base64 (the
|
|
13
|
+
// house format) — decode it as such instead of Buffer.from()'s UTF-8,
|
|
14
|
+
// which produced 44-byte garbage keys and failed every new session.
|
|
15
|
+
const storedKeyToBuffer = (value) => typeof value === 'string' ? Buffer.from(value, 'base64') : value;
|
|
12
16
|
export function makeLibSignalRepository(auth) {
|
|
13
17
|
const storage = signalStorage(auth);
|
|
18
|
+
// AunuBaileys (1.0.78, HB-240): in-flight serialisation for
|
|
19
|
+
// encryptGroupMessage, per group — see that method.
|
|
20
|
+
const groupEncryptChains = new Map();
|
|
14
21
|
return {
|
|
15
22
|
decryptGroupMessage({ group, authorJid, msg }) {
|
|
16
23
|
const senderName = jidToSignalSenderKeyName(group, authorJid);
|
|
@@ -55,20 +62,38 @@ export function makeLibSignalRepository(auth) {
|
|
|
55
62
|
return { type, ciphertext: Buffer.from(body, 'binary') };
|
|
56
63
|
},
|
|
57
64
|
async encryptGroupMessage({ group, meId, data }) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
// AunuBaileys (1.0.78, HB-240): serialise per group.
|
|
66
|
+
// GroupSessionBuilder.create() runs OUTSIDE GroupCipher's
|
|
67
|
+
// per-group queueJob, so two concurrent first-sends to the
|
|
68
|
+
// same group (empty sender-key record) both created a
|
|
69
|
+
// fresh key and overwrote each other — the loser returned
|
|
70
|
+
// an SKDM that did not match the ciphertext it encrypted
|
|
71
|
+
// (which used the winner's stored state), and every peer
|
|
72
|
+
// failed that first message. Chain the whole
|
|
73
|
+
// create+encrypt per group; different groups still run in
|
|
74
|
+
// parallel.
|
|
75
|
+
const run = async () => {
|
|
76
|
+
const senderName = jidToSignalSenderKeyName(group, meId);
|
|
77
|
+
const builder = new GroupSessionBuilder(storage);
|
|
78
|
+
const senderNameStr = senderName.toString();
|
|
79
|
+
const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
|
|
80
|
+
if (!senderKey) {
|
|
81
|
+
await storage.storeSenderKey(senderName, new SenderKeyRecord());
|
|
82
|
+
}
|
|
83
|
+
const senderKeyDistributionMessage = await builder.create(senderName);
|
|
84
|
+
const session = new GroupCipher(storage, senderName);
|
|
85
|
+
const ciphertext = await session.encrypt(data);
|
|
86
|
+
return {
|
|
87
|
+
ciphertext,
|
|
88
|
+
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
|
|
89
|
+
};
|
|
71
90
|
};
|
|
91
|
+
const prev = groupEncryptChains.get(group) ?? Promise.resolve();
|
|
92
|
+
const chained = prev.then(run);
|
|
93
|
+
// the stored chain must never stay rejected, or one failed
|
|
94
|
+
// send would poison every later send to the group
|
|
95
|
+
groupEncryptChains.set(group, chained.catch(() => { }));
|
|
96
|
+
return chained;
|
|
72
97
|
},
|
|
73
98
|
async deleteSenderKey(group, authorJid) {
|
|
74
99
|
const senderNameStr = jidToSignalSenderKeyName(group, authorJid).toString();
|
|
@@ -133,10 +158,13 @@ function signalStorage({ creds, keys }) {
|
|
|
133
158
|
// classifier and misleads diagnostics. Say what is
|
|
134
159
|
// actually wrong; the 3-strike self-heal still clears
|
|
135
160
|
// the poisoned record via deleteSession.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
161
|
+
// (1.0.78, HB-110): the conversion used to cover only
|
|
162
|
+
// TypeError — a record with a non-v1 version throws a
|
|
163
|
+
// plain Error('Error migrating SessionRecord') from
|
|
164
|
+
// libsignal and escaped raw. Convert EVERY
|
|
165
|
+
// deserialize failure; the behaviour (3-strike
|
|
166
|
+
// self-heal) is identical, only the message improves.
|
|
167
|
+
throw new Error(`AunuBaileys: stored session record for ${id} is corrupt or malformed and cannot be deserialized; delete the session and re-establish it`);
|
|
140
168
|
}
|
|
141
169
|
}
|
|
142
170
|
},
|
|
@@ -164,8 +192,8 @@ function signalStorage({ creds, keys }) {
|
|
|
164
192
|
return undefined;
|
|
165
193
|
}
|
|
166
194
|
return {
|
|
167
|
-
privKey:
|
|
168
|
-
pubKey:
|
|
195
|
+
privKey: storedKeyToBuffer(key.private),
|
|
196
|
+
pubKey: storedKeyToBuffer(key.public)
|
|
169
197
|
};
|
|
170
198
|
}
|
|
171
199
|
},
|
|
@@ -184,8 +212,8 @@ function signalStorage({ creds, keys }) {
|
|
|
184
212
|
throw new Error('AunuBaileys: creds.signedPreKey is missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
|
|
185
213
|
}
|
|
186
214
|
return {
|
|
187
|
-
privKey:
|
|
188
|
-
pubKey:
|
|
215
|
+
privKey: storedKeyToBuffer(key.keyPair.private),
|
|
216
|
+
pubKey: storedKeyToBuffer(key.keyPair.public)
|
|
189
217
|
};
|
|
190
218
|
},
|
|
191
219
|
loadSenderKey: async (senderKeyName) => {
|
|
@@ -212,7 +240,7 @@ function signalStorage({ creds, keys }) {
|
|
|
212
240
|
throw new Error('Our signed identity key is missing or incomplete — the auth state is corrupt; re-pair the device');
|
|
213
241
|
}
|
|
214
242
|
return {
|
|
215
|
-
privKey:
|
|
243
|
+
privKey: storedKeyToBuffer(signedIdentityKey.private),
|
|
216
244
|
pubKey: generateSignalPubKey(signedIdentityKey.public)
|
|
217
245
|
};
|
|
218
246
|
}
|
|
@@ -5,7 +5,7 @@ import Long from 'long';
|
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
6
|
import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
|
|
7
7
|
import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
|
|
8
|
-
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, isBadMacError, isUnrecoverableSessionError, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, buildCallControlStanza, parseRetryReceiptSession, setBotMessageSecret, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
8
|
+
import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, isBadMacError, isSenderKeyUnavailableError, isUnrecoverableSessionError, keyMaterialToBuffer, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, buildCallControlStanza, parseRetryReceiptSession, setBotMessageSecret, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
|
|
9
9
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
10
10
|
import { sentMessageCacheFor } from '../Utils/sent-message-cache.js';
|
|
11
11
|
import { createOfflineNodeQueue } from '../Utils/offline-queue.js';
|
|
@@ -545,7 +545,10 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
545
545
|
info: 'link_code_pairing_key_bundle_encryption_key'
|
|
546
546
|
});
|
|
547
547
|
const encryptPayload = Buffer.concat([
|
|
548
|
-
|
|
548
|
+
// AunuBaileys (1.0.78, HB-157): a string identity key
|
|
549
|
+
// is base64 — Buffer.from() decoded it as UTF-8 and
|
|
550
|
+
// put a 44-byte garbage identity in the payload.
|
|
551
|
+
keyMaterialToBuffer(authState.creds.signedIdentityKey.public),
|
|
549
552
|
primaryIdentityPublicKey,
|
|
550
553
|
random
|
|
551
554
|
]);
|
|
@@ -923,6 +926,16 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
923
926
|
// sender key saja tidak menyentuh sesi E2E yang
|
|
924
927
|
// rusak. Kelas ini direset SEKETIKA.
|
|
925
928
|
const sessionLayerFailure = isUnrecoverableSessionError(msg?.messageStubParameters?.[0]);
|
|
929
|
+
// AunuBaileys (1.0.78, HB-223): a group
|
|
930
|
+
// failure where the sender key is simply
|
|
931
|
+
// absent (or the skmsg payload itself is
|
|
932
|
+
// malformed) says nothing about the
|
|
933
|
+
// author's E2E session — do not count it
|
|
934
|
+
// toward the session reset and do not
|
|
935
|
+
// report it as a healable failure; the
|
|
936
|
+
// retry receipt below is the whole fix,
|
|
937
|
+
// it makes the peer redistribute its key.
|
|
938
|
+
const senderKeyUnavailable = isGroupMsg && isSenderKeyUnavailableError(msg?.messageStubParameters?.[0]);
|
|
926
939
|
let healed = false;
|
|
927
940
|
const resetE2ESession = async () => {
|
|
928
941
|
await signalRepository.deleteSession(author);
|
|
@@ -933,7 +946,10 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
933
946
|
await signalRepository.deleteSession(alt);
|
|
934
947
|
}
|
|
935
948
|
};
|
|
936
|
-
if (
|
|
949
|
+
if (senderKeyUnavailable) {
|
|
950
|
+
logger.debug({ author, addr, group: msg.key.remoteJid }, 'aunu: group decrypt failed because the sender key is unavailable or the skmsg is malformed; retry receipt only, no session reset');
|
|
951
|
+
}
|
|
952
|
+
else if (sessionLayerFailure) {
|
|
937
953
|
await resetE2ESession();
|
|
938
954
|
if (isGroupMsg) {
|
|
939
955
|
await signalRepository.deleteSenderKey(msg.key.remoteJid, author);
|
|
@@ -977,6 +993,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
977
993
|
// AunuBaileys: structured decrypt-failure event so
|
|
978
994
|
// consumers can alert and aggregate on Bad MAC
|
|
979
995
|
// floods (B-01) instead of parsing log lines.
|
|
996
|
+
if (!senderKeyUnavailable) {
|
|
980
997
|
ev.emit('messages.decrypt-failure', {
|
|
981
998
|
remoteJid: msg.key.remoteJid,
|
|
982
999
|
author,
|
|
@@ -988,6 +1005,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
988
1005
|
messageId: node.attrs.id,
|
|
989
1006
|
timestamp: Date.now()
|
|
990
1007
|
});
|
|
1008
|
+
}
|
|
991
1009
|
}
|
|
992
1010
|
catch (healErr) {
|
|
993
1011
|
logger.debug({ err: healErr }, 'aunu: session self-heal step failed, continuing with normal retry');
|
|
@@ -334,12 +334,23 @@ export const makeMessagesSocket = (config) => {
|
|
|
334
334
|
// the transport to reopen, bounded by connectTimeoutMs; a connection
|
|
335
335
|
// that closes for good still fails fast via waitForSocketOpen itself.
|
|
336
336
|
const waitMs = config.connectTimeoutMs ?? 60000;
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
337
|
+
const openWait = waitForSocketOpen();
|
|
338
|
+
try {
|
|
339
|
+
await Promise.race([
|
|
340
|
+
openWait,
|
|
341
|
+
delay(waitMs).then(() => {
|
|
342
|
+
throw new Boom(`Timed out waiting ${waitMs}ms for the connection to open before sending`, { statusCode: 408 }); // 408 = DisconnectReason.timedOut
|
|
343
|
+
})
|
|
344
|
+
]);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
// AunuBaileys (1.0.77, HB-167): when the timeout wins,
|
|
348
|
+
// the socket-open waiter is still pending — detach its
|
|
349
|
+
// ws listeners now instead of leaving three behind per
|
|
350
|
+
// failed send until the socket settles on its own.
|
|
351
|
+
openWait.cancel?.();
|
|
352
|
+
throw err;
|
|
353
|
+
}
|
|
343
354
|
}
|
|
344
355
|
// AunuBaileys (1.0.68): fail with a clear Boom instead of a raw
|
|
345
356
|
// TypeError when sending before pairing completes or with a
|
|
@@ -528,7 +539,17 @@ export const makeMessagesSocket = (config) => {
|
|
|
528
539
|
attrs: { v: '2', type: 'skmsg' },
|
|
529
540
|
content: ciphertext
|
|
530
541
|
});
|
|
531
|
-
|
|
542
|
+
// AunuBaileys (1.0.78, HB-132/HB-155): persist the map
|
|
543
|
+
// only when it was actually loaded from the store
|
|
544
|
+
// (same condition as the load above). A participant-
|
|
545
|
+
// targeted send starts from {} without loading, so
|
|
546
|
+
// writing its partial map back overwrote the full
|
|
547
|
+
// memo — and silently undid sendMessagesAgain's
|
|
548
|
+
// deliberate null, which exists to force a full
|
|
549
|
+
// redistribution on the next normal send.
|
|
550
|
+
if (!participant && !isStatus) {
|
|
551
|
+
await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } });
|
|
552
|
+
}
|
|
532
553
|
}
|
|
533
554
|
else {
|
|
534
555
|
const meUser = jidDecode(meId)?.user;
|
package/lib/Socket/socket.js
CHANGED
|
@@ -9,7 +9,7 @@ import { executeWMexQuery } from './mex.js';
|
|
|
9
9
|
import { makeLIDStore } from '../Utils/lid-store.js';
|
|
10
10
|
import { createIgnoreFilter } from '../Utils/ignore-filter.js';
|
|
11
11
|
import { QueryIds, XWAPaths } from '../Types/index.js';
|
|
12
|
-
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, getPlatformId, fetchLatestWaWebVersion, makeEventBuffer, makeNoiseHandler, promiseTimeout } from '../Utils/index.js';
|
|
12
|
+
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, getPlatformId, fetchLatestWaWebVersion, keyMaterialToBuffer, makeEventBuffer, makeNoiseHandler, promiseTimeout } from '../Utils/index.js';
|
|
13
13
|
import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
14
14
|
import { WebSocketClient } from './Client/index.js';
|
|
15
15
|
/**
|
|
@@ -92,6 +92,15 @@ export const makeSocket = (config) => {
|
|
|
92
92
|
let epoch = 1;
|
|
93
93
|
let keepAliveReq;
|
|
94
94
|
let qrTimer;
|
|
95
|
+
// AunuBaileys (1.0.77, HB-243): true while a pair-device QR chain
|
|
96
|
+
// is cycling — a duplicate pair-device IQ must not start a second
|
|
97
|
+
// chain on the shared qrTimer slot.
|
|
98
|
+
let qrChainActive = false;
|
|
99
|
+
// AunuBaileys (1.0.77, HB-213): resolved when the Noise handshake
|
|
100
|
+
// completes (finishInit in validateConnection), so the pairing
|
|
101
|
+
// request can wait for an encrypted channel.
|
|
102
|
+
let resolveHandshakeComplete;
|
|
103
|
+
const handshakeComplete = new Promise((resolve) => { resolveHandshakeComplete = resolve; });
|
|
95
104
|
let closed = false;
|
|
96
105
|
let lastDisconnectError;
|
|
97
106
|
const uqTagId = generateMdTagPrefix();
|
|
@@ -226,6 +235,7 @@ export const makeSocket = (config) => {
|
|
|
226
235
|
}
|
|
227
236
|
}).finish());
|
|
228
237
|
noise.finishInit();
|
|
238
|
+
resolveHandshakeComplete?.();
|
|
229
239
|
startKeepAliveRequest();
|
|
230
240
|
};
|
|
231
241
|
const getAvailablePreKeysOnServer = async () => {
|
|
@@ -278,12 +288,17 @@ export const makeSocket = (config) => {
|
|
|
278
288
|
await uploadPreKeys();
|
|
279
289
|
}
|
|
280
290
|
};
|
|
291
|
+
// AunuBaileys (1.0.77, HB-156): decodeFrame invocations must never
|
|
292
|
+
// overlap — the noise handler slices ONE shared inBytes buffer, so
|
|
293
|
+
// two concurrent calls interleave and deliver stanzas out of wire
|
|
294
|
+
// order. Every WS message is chained through a single queue.
|
|
295
|
+
let frameDecodeChain = Promise.resolve();
|
|
281
296
|
const onMessageReceived = (data) => {
|
|
282
297
|
// AunuBaileys (1.0.67): decodeFrame is async — a malformed frame
|
|
283
298
|
// (bad GCM tag, truncated binary node) or a throwing frame
|
|
284
299
|
// listener must not become an unhandled rejection that kills
|
|
285
300
|
// the process; route failures to onUnexpectedError instead.
|
|
286
|
-
|
|
301
|
+
frameDecodeChain = frameDecodeChain.then(() => noise.decodeFrame(data, frame => {
|
|
287
302
|
// reset ping timeout
|
|
288
303
|
lastDateRecv = new Date();
|
|
289
304
|
let anyTriggered = false;
|
|
@@ -311,7 +326,20 @@ export const makeSocket = (config) => {
|
|
|
311
326
|
logger.debug({ unhandled: true, msgId, fromMe: false, frame }, 'communication recv');
|
|
312
327
|
}
|
|
313
328
|
}
|
|
314
|
-
})).catch(err =>
|
|
329
|
+
})).catch(err => {
|
|
330
|
+
// AunuBaileys (1.0.77, HB-185/HB-207): a frame-layer failure
|
|
331
|
+
// (oversize header, GCM decrypt failure) leaves the noise
|
|
332
|
+
// stream unrecoverable — the header is never consumed, or
|
|
333
|
+
// the read counter desyncs so every later frame fails too.
|
|
334
|
+
// End the connection so the host reconnects at once instead
|
|
335
|
+
// of wedging until the keep-alive watchdog notices (~65s).
|
|
336
|
+
if (err?.isFrameFatal) {
|
|
337
|
+
end(err);
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
onUnexpectedError(err, 'onMessageReceived');
|
|
341
|
+
}
|
|
342
|
+
});
|
|
315
343
|
};
|
|
316
344
|
// AunuBaileys: socket end handlers (pola v7) + fetch Mex (dari @itsliaaa/baileys)
|
|
317
345
|
const socketEndHandlers = [];
|
|
@@ -329,6 +357,7 @@ export const makeSocket = (config) => {
|
|
|
329
357
|
logger.info({ trace: error?.stack }, error ? 'connection errored' : 'connection closed');
|
|
330
358
|
clearInterval(keepAliveReq);
|
|
331
359
|
clearTimeout(qrTimer);
|
|
360
|
+
qrChainActive = false;
|
|
332
361
|
ws.removeAllListeners('close');
|
|
333
362
|
ws.removeAllListeners('open');
|
|
334
363
|
ws.removeAllListeners('message');
|
|
@@ -354,16 +383,20 @@ export const makeSocket = (config) => {
|
|
|
354
383
|
ev.removeAllListeners('connection.update');
|
|
355
384
|
ev.destroy?.();
|
|
356
385
|
};
|
|
357
|
-
const waitForSocketOpen =
|
|
386
|
+
const waitForSocketOpen = () => {
|
|
358
387
|
if (ws.isOpen) {
|
|
359
|
-
|
|
388
|
+
const resolved = Promise.resolve();
|
|
389
|
+
resolved.cancel = () => { };
|
|
390
|
+
return resolved;
|
|
360
391
|
}
|
|
361
392
|
if (ws.isClosed || ws.isClosing) {
|
|
362
|
-
|
|
393
|
+
const rejected = Promise.reject(new Boom('Connection Closed', { statusCode: DisconnectReason.connectionClosed }));
|
|
394
|
+
rejected.cancel = () => { };
|
|
395
|
+
return rejected;
|
|
363
396
|
}
|
|
364
397
|
let onOpen;
|
|
365
398
|
let onClose;
|
|
366
|
-
|
|
399
|
+
const promise = new Promise((resolve, reject) => {
|
|
367
400
|
onOpen = () => resolve(undefined);
|
|
368
401
|
onClose = mapWebSocketError(reject);
|
|
369
402
|
ws.on('open', onOpen);
|
|
@@ -374,6 +407,16 @@ export const makeSocket = (config) => {
|
|
|
374
407
|
ws.off('close', onClose);
|
|
375
408
|
ws.off('error', onClose);
|
|
376
409
|
});
|
|
410
|
+
// AunuBaileys (1.0.77, HB-167/HB-179): a caller that stops
|
|
411
|
+
// waiting (its own timeout won the race) can detach these
|
|
412
|
+
// listeners immediately instead of leaving them on the ws
|
|
413
|
+
// emitter until the socket happens to open/close/error.
|
|
414
|
+
promise.cancel = () => {
|
|
415
|
+
ws.off('open', onOpen);
|
|
416
|
+
ws.off('close', onClose);
|
|
417
|
+
ws.off('error', onClose);
|
|
418
|
+
};
|
|
419
|
+
return promise;
|
|
377
420
|
};
|
|
378
421
|
const keepAliveTick = () => {
|
|
379
422
|
if (!lastDateRecv) {
|
|
@@ -501,15 +544,55 @@ export const makeSocket = (config) => {
|
|
|
501
544
|
reject(new Boom(`requestPairingCode: socket did not open within ${PAIRING_READY_TIMEOUT_MS / 1000}s (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.timedOut }));
|
|
502
545
|
}, PAIRING_READY_TIMEOUT_MS);
|
|
503
546
|
});
|
|
547
|
+
const rawOpenWait = waitForSocketOpen();
|
|
504
548
|
try {
|
|
505
549
|
await Promise.race([
|
|
506
|
-
|
|
550
|
+
rawOpenWait.catch(() => {
|
|
507
551
|
throw new Boom(`requestPairingCode: connection closed while waiting for the socket to open (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.connectionClosed });
|
|
508
552
|
}),
|
|
509
553
|
closedWhileWaiting,
|
|
510
554
|
timedOut
|
|
511
555
|
]);
|
|
512
556
|
}
|
|
557
|
+
finally {
|
|
558
|
+
clearTimeout(timer);
|
|
559
|
+
// AunuBaileys (1.0.77, HB-179): if the timeout or the close
|
|
560
|
+
// guard won the race, the socket-open waiter is still
|
|
561
|
+
// pending — detach its ws listeners now instead of leaving
|
|
562
|
+
// them behind until the socket settles on its own.
|
|
563
|
+
rawOpenWait.cancel?.();
|
|
564
|
+
if (onUpdate) {
|
|
565
|
+
try {
|
|
566
|
+
ev.off('connection.update', onUpdate);
|
|
567
|
+
}
|
|
568
|
+
catch { }
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
// AunuBaileys (1.0.77, HB-213): the pairing IQ must not leave
|
|
573
|
+
// before the Noise handshake completes — encodeFrame encrypts
|
|
574
|
+
// only once the handshake is finished, so an early IQ travels as
|
|
575
|
+
// a plaintext binary node; a strict server kills the connection
|
|
576
|
+
// over it while requestPairingCode still resolves with the code.
|
|
577
|
+
const waitForHandshakeComplete = async () => {
|
|
578
|
+
let onUpdate;
|
|
579
|
+
const closedWhileWaiting = new Promise((_, reject) => {
|
|
580
|
+
onUpdate = (update) => {
|
|
581
|
+
if (update.connection === 'close') {
|
|
582
|
+
reject(new Boom(`requestPairingCode: connection closed while waiting for the handshake to complete (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.connectionClosed }));
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
ev.on('connection.update', onUpdate);
|
|
586
|
+
});
|
|
587
|
+
let timer;
|
|
588
|
+
const timedOut = new Promise((_, reject) => {
|
|
589
|
+
timer = setTimeout(() => {
|
|
590
|
+
reject(new Boom(`requestPairingCode: handshake did not complete within ${PAIRING_READY_TIMEOUT_MS / 1000}s (last disconnect: ${describeLastDisconnect()})`, { statusCode: DisconnectReason.timedOut }));
|
|
591
|
+
}, PAIRING_READY_TIMEOUT_MS);
|
|
592
|
+
});
|
|
593
|
+
try {
|
|
594
|
+
await Promise.race([handshakeComplete, closedWhileWaiting, timedOut]);
|
|
595
|
+
}
|
|
513
596
|
finally {
|
|
514
597
|
clearTimeout(timer);
|
|
515
598
|
if (onUpdate) {
|
|
@@ -534,13 +617,20 @@ export const makeSocket = (config) => {
|
|
|
534
617
|
pairingCodeRequestInFlight = true;
|
|
535
618
|
try {
|
|
536
619
|
await waitForPairingReady();
|
|
620
|
+
await waitForHandshakeComplete();
|
|
537
621
|
authState.creds.pairingCode = pairingCode;
|
|
538
622
|
authState.creds.me = {
|
|
539
623
|
id: jidEncode(phoneNumber, 's.whatsapp.net'),
|
|
540
624
|
name: '~'
|
|
541
625
|
};
|
|
542
626
|
ev.emit('creds.update', authState.creds);
|
|
543
|
-
|
|
627
|
+
// AunuBaileys (1.0.77, HB-191): query(), not sendNode() — the
|
|
628
|
+
// companion_hello result must be awaited and error-checked.
|
|
629
|
+
// sendNode resolved when the bytes hit the wire, so a server
|
|
630
|
+
// rejection left the optimistic creds.me/pairingCode persisted
|
|
631
|
+
// and sent the next boot into a 401 loop; a rejection now
|
|
632
|
+
// lands in the catch below, which rolls the creds back.
|
|
633
|
+
await query({
|
|
544
634
|
tag: 'iq',
|
|
545
635
|
attrs: {
|
|
546
636
|
to: S_WHATSAPP_NET,
|
|
@@ -644,6 +734,16 @@ export const makeSocket = (config) => {
|
|
|
644
734
|
}
|
|
645
735
|
});
|
|
646
736
|
ws.on('error', mapWebSocketError(end));
|
|
737
|
+
// AunuBaileys (1.0.77, HB-189): the ws client forwards
|
|
738
|
+
// 'unexpected-response', which suppresses ws's own handshake
|
|
739
|
+
// abort — without a listener here a non-101 response (403/502,
|
|
740
|
+
// ISP intercept page) wedges the socket in 'connecting' forever:
|
|
741
|
+
// no open, no error, no close, and the host never reconnects.
|
|
742
|
+
ws.on('unexpected-response', (_req, res) => {
|
|
743
|
+
const code = res?.statusCode;
|
|
744
|
+
const statusCode = Number.isFinite(code) && code >= 400 && code <= 599 ? code : 500;
|
|
745
|
+
end(new Boom(`Unexpected server response: ${code}`, { statusCode }));
|
|
746
|
+
});
|
|
647
747
|
ws.on('close', () => end(new Boom('Connection Terminated', { statusCode: DisconnectReason.connectionClosed })));
|
|
648
748
|
// the server terminated the connection
|
|
649
749
|
ws.on('CB:xmlstreamend', () => end(new Boom('Connection Terminated by Server', { statusCode: DisconnectReason.connectionClosed })));
|
|
@@ -662,14 +762,28 @@ export const makeSocket = (config) => {
|
|
|
662
762
|
}
|
|
663
763
|
};
|
|
664
764
|
await sendNode(iq);
|
|
765
|
+
// AunuBaileys (1.0.77, HB-243): a duplicate pair-device IQ
|
|
766
|
+
// (server retry) must not start a parallel QR chain — both
|
|
767
|
+
// chains share the single qrTimer slot, so the orphaned first
|
|
768
|
+
// chain keeps cycling after login and its ref exhaustion ends
|
|
769
|
+
// the healthy, freshly logged-in connection.
|
|
770
|
+
if (qrChainActive) {
|
|
771
|
+
logger.warn('ignoring duplicate pair-device IQ: a QR chain is already running');
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
qrChainActive = true;
|
|
665
775
|
const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
|
|
666
776
|
const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
|
|
667
|
-
|
|
668
|
-
|
|
777
|
+
// AunuBaileys (1.0.78, HB-157): a string key is base64 already —
|
|
778
|
+
// Buffer.from() re-encoded its UTF-8 bytes, putting a wrong
|
|
779
|
+
// noise/identity key into every QR code.
|
|
780
|
+
const noiseKeyB64 = keyMaterialToBuffer(creds.noiseKey.public).toString('base64');
|
|
781
|
+
const identityKeyB64 = keyMaterialToBuffer(creds.signedIdentityKey.public).toString('base64');
|
|
669
782
|
const advB64 = creds.advSecretKey;
|
|
670
783
|
let qrMs = qrTimeout || 60000; // time to let a QR live
|
|
671
784
|
const genPairQR = () => {
|
|
672
785
|
if (!ws.isOpen) {
|
|
786
|
+
qrChainActive = false;
|
|
673
787
|
return;
|
|
674
788
|
}
|
|
675
789
|
const refNode = refNodes.shift();
|
|
@@ -726,6 +840,7 @@ export const makeSocket = (config) => {
|
|
|
726
840
|
await sendPassiveIq('active');
|
|
727
841
|
logger.info('opened connection to WA');
|
|
728
842
|
clearTimeout(qrTimer); // will never happen in all likelyhood -- but just in case WA sends success on first try
|
|
843
|
+
qrChainActive = false;
|
|
729
844
|
ev.emit('creds.update', { me: { ...authState.creds.me, lid: node.attrs.lid } });
|
|
730
845
|
ev.emit('connection.update', { connection: 'open' });
|
|
731
846
|
}
|
|
@@ -737,9 +852,9 @@ export const makeSocket = (config) => {
|
|
|
737
852
|
// AunuBaileys (1.0.71): 405 client_too_old — fetch the live WA Web
|
|
738
853
|
// build so the caller's reconnect uses it (donor: violetix
|
|
739
854
|
// lib/Socket/socket.js CB:failure handler). Only for static versions;
|
|
740
|
-
// a resolver function re-resolves on the next connect anyway.
|
|
741
|
-
//
|
|
742
|
-
//
|
|
855
|
+
// a resolver function re-resolves on the next connect anyway. Both
|
|
856
|
+
// 405 paths (CB:failure and CB:stream:error) await this before
|
|
857
|
+
// end(), so the clientTooOld signal reaches the host first.
|
|
743
858
|
const handleClientTooOld = async () => {
|
|
744
859
|
if (typeof config.version === 'function') {
|
|
745
860
|
return;
|
|
@@ -759,11 +874,16 @@ export const makeSocket = (config) => {
|
|
|
759
874
|
logger.warn({ err }, 'failed to fetch the latest WA Web version after a 405');
|
|
760
875
|
}
|
|
761
876
|
};
|
|
762
|
-
ws.on('CB:stream:error', (node) => {
|
|
877
|
+
ws.on('CB:stream:error', async (node) => {
|
|
763
878
|
logger.error({ node }, 'stream errored out');
|
|
764
879
|
const { reason, statusCode } = getErrorCodeFromStreamError(node);
|
|
765
880
|
if (statusCode === 405) {
|
|
766
|
-
|
|
881
|
+
// AunuBaileys (1.0.77, HB-148): awaited like the CB:failure
|
|
882
|
+
// path (1.0.72) — firing it void let end() tear the
|
|
883
|
+
// connection.update listeners down before the version
|
|
884
|
+
// fetch landed, so the clientTooOld signal never reached
|
|
885
|
+
// the host and a 405 looked like any other disconnect.
|
|
886
|
+
await handleClientTooOld();
|
|
767
887
|
}
|
|
768
888
|
end(new Boom(`Stream Errored (${reason})`, { statusCode, data: node }));
|
|
769
889
|
});
|
|
@@ -802,7 +922,7 @@ export const makeSocket = (config) => {
|
|
|
802
922
|
if (offlineDrainBatches >= OFFLINE_DRAIN_MAX_BATCHES) {
|
|
803
923
|
stopOfflineDrain();
|
|
804
924
|
logger.warn({ batches: offlineDrainBatches }, 'offline drain hit the batch cap; remaining backlog waits for the next reconnect');
|
|
805
|
-
return;
|
|
925
|
+
return false;
|
|
806
926
|
}
|
|
807
927
|
offlineDrainBatches += 1;
|
|
808
928
|
// AunuBaileys (1.0.70): a floating sendNode here could reject
|
|
@@ -812,12 +932,19 @@ export const makeSocket = (config) => {
|
|
|
812
932
|
attrs: {},
|
|
813
933
|
content: [{ tag: 'offline_batch', attrs: { count: '100' } }]
|
|
814
934
|
}).catch(err => logger.warn({ err }, 'failed to request offline batch'));
|
|
935
|
+
return true;
|
|
815
936
|
};
|
|
816
937
|
const scheduleOfflineDrain = () => {
|
|
817
938
|
stopOfflineDrain();
|
|
818
939
|
offlineDrainTimer = setTimeout(() => {
|
|
819
|
-
|
|
820
|
-
|
|
940
|
+
// AunuBaileys (1.0.77, HB-109): stop re-arming at the batch
|
|
941
|
+
// cap — the unconditional re-schedule used to undo
|
|
942
|
+
// requestOfflineBatch's stopOfflineDrain(), so when the
|
|
943
|
+
// terminal offline bulletin never arrived the cap warning
|
|
944
|
+
// repeated every 500ms for the life of the socket.
|
|
945
|
+
if (requestOfflineBatch()) {
|
|
946
|
+
scheduleOfflineDrain();
|
|
947
|
+
}
|
|
821
948
|
}, OFFLINE_DRAIN_INTERVAL_MS);
|
|
822
949
|
};
|
|
823
950
|
ws.on('CB:ib,,offline_preview', (node) => {
|
package/lib/Utils/crypto.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { KeyPair } from '../Types/index.js';
|
|
2
|
+
export declare const keyMaterialToBuffer: (key: unknown) => unknown;
|
|
2
3
|
/** prefix version byte to the pub keys, required for some curve crypto functions */
|
|
3
4
|
export declare const generateSignalPubKey: (pubKey: Uint8Array | Buffer) => Uint8Array<ArrayBufferLike> | Buffer<ArrayBufferLike>;
|
|
4
5
|
export declare const Curve: {
|
package/lib/Utils/crypto.js
CHANGED
|
@@ -4,6 +4,18 @@ import * as libsignal from 'libsignal';
|
|
|
4
4
|
import { KEY_BUNDLE_TYPE } from '../Defaults/index.js';
|
|
5
5
|
// insure browser & node compatibility
|
|
6
6
|
const { subtle } = globalThis.crypto;
|
|
7
|
+
/**
|
|
8
|
+
* AunuBaileys (1.0.78, HB-157): the house format for string key
|
|
9
|
+
* material is base64 (that is how advSecretKey and every displayed
|
|
10
|
+
* key are stored), but several consumers decoded strings with plain
|
|
11
|
+
* Buffer.from() — UTF-8 — producing wrong-length garbage keys.
|
|
12
|
+
* Decode strings as base64 everywhere; Buffers pass through and
|
|
13
|
+
* anything else is returned untouched so the callers' own type
|
|
14
|
+
* guards (HB-98) still fire.
|
|
15
|
+
*/
|
|
16
|
+
export const keyMaterialToBuffer = (key) => Buffer.isBuffer(key)
|
|
17
|
+
? key
|
|
18
|
+
: (typeof key === 'string' ? Buffer.from(key, 'base64') : key);
|
|
7
19
|
/** prefix version byte to the pub keys, required for some curve crypto functions */
|
|
8
20
|
export const generateSignalPubKey = (pubKey) => {
|
|
9
21
|
// AunuBaileys (1.0.74): HB-98 — a wrong-typed public key (e.g. a
|
|
@@ -14,7 +26,7 @@ export const generateSignalPubKey = (pubKey) => {
|
|
|
14
26
|
if (!Buffer.isBuffer(pubKey) && typeof pubKey !== 'string') {
|
|
15
27
|
throw new Error('AunuBaileys: invalid public key material — the auth state is incomplete or corrupt; restore a full backup or pair again');
|
|
16
28
|
}
|
|
17
|
-
const buf =
|
|
29
|
+
const buf = keyMaterialToBuffer(pubKey);
|
|
18
30
|
return buf.length === 33 ? buf : Buffer.concat([KEY_BUNDLE_TYPE, buf]);
|
|
19
31
|
};
|
|
20
32
|
export const Curve = {
|
|
@@ -27,10 +39,16 @@ export const Curve = {
|
|
|
27
39
|
};
|
|
28
40
|
},
|
|
29
41
|
sharedKey: (privateKey, publicKey) => {
|
|
30
|
-
|
|
42
|
+
// AunuBaileys (1.0.78, HB-214): the private key used to be
|
|
43
|
+
// passed to libsignal RAW — a base64-string private key in a
|
|
44
|
+
// hand-edited/migrated creds file died as
|
|
45
|
+
// 'Invalid private key type: String' at every handshake,
|
|
46
|
+
// pairing and pair-success call site. Decode it like every
|
|
47
|
+
// other key material (HB-157).
|
|
48
|
+
const shared = libsignal.curve.calculateAgreement(generateSignalPubKey(publicKey), keyMaterialToBuffer(privateKey));
|
|
31
49
|
return Buffer.from(shared);
|
|
32
50
|
},
|
|
33
|
-
sign: (privateKey, buf) => libsignal.curve.calculateSignature(privateKey, buf),
|
|
51
|
+
sign: (privateKey, buf) => libsignal.curve.calculateSignature(keyMaterialToBuffer(privateKey), buf),
|
|
34
52
|
verify: (pubKey, message, signature) => {
|
|
35
53
|
try {
|
|
36
54
|
libsignal.curve.verifySignature(generateSignalPubKey(pubKey), message, signature);
|
|
@@ -7,6 +7,7 @@ export declare const getBotRecentSecretsChatCount: () => number;
|
|
|
7
7
|
export declare const NO_MESSAGE_FOUND_ERROR_TEXT = "Message absent from node";
|
|
8
8
|
export declare const isUnrecoverableSessionError: (text: unknown) => boolean;
|
|
9
9
|
export declare const isBadMacError: (text: unknown) => boolean;
|
|
10
|
+
export declare const isSenderKeyUnavailableError: (text: unknown) => boolean;
|
|
10
11
|
export declare const MISSING_KEYS_ERROR_TEXT = "Key used already or never filled";
|
|
11
12
|
export declare const NACK_REASONS: {
|
|
12
13
|
ParsingError: number;
|
|
@@ -429,4 +429,19 @@ export const isUnrecoverableSessionError = (text) => typeof text === 'string' &&
|
|
|
429
429
|
* classic symptom of a desynchronised ratchet on either side.
|
|
430
430
|
*/
|
|
431
431
|
export const isBadMacError = (text) => typeof text === 'string' && /bad mac/i.test(text);
|
|
432
|
+
/**
|
|
433
|
+
* AunuBaileys (1.0.78, HB-223): true when a GROUP decrypt failure says
|
|
434
|
+
* nothing about the author's 1-1 E2E session — the sender key simply
|
|
435
|
+
* is not here yet (no record, or no state for that author: they just
|
|
436
|
+
* joined, or our own self-heal deleted it), or the skmsg payload
|
|
437
|
+
* itself is malformed (proto decode error, signature shape). Counting
|
|
438
|
+
* these toward the 3-strike session reset deleted healthy E2E
|
|
439
|
+
* sessions and reported a misleading `healed: true`; the only correct
|
|
440
|
+
* response is the retry receipt, which makes the peer redistribute
|
|
441
|
+
* its sender key. The CIPHERTEXT stub upsert still happens.
|
|
442
|
+
*/
|
|
443
|
+
export const isSenderKeyUnavailableError = (text) => typeof text === 'string' &&
|
|
444
|
+
(/no session found to decrypt message|no senderkeyrecord found for decryption/i.test(text) ||
|
|
445
|
+
/^invalid signature!?$/i.test(text.trim()) ||
|
|
446
|
+
/invalid wire type|index out of range/i.test(text));
|
|
432
447
|
//# sourceMappingURL=decode-wa-message.js.map
|
package/lib/Utils/generics.js
CHANGED
|
@@ -374,6 +374,16 @@ export const getCallStatusFromNode = ({ tag, attrs }) => {
|
|
|
374
374
|
};
|
|
375
375
|
const UNEXPECTED_SERVER_CODE_TEXT = 'Unexpected server response: ';
|
|
376
376
|
export const getCodeFromWSError = (error) => {
|
|
377
|
+
// AunuBaileys (1.0.77, HB-133): an error that already carries a
|
|
378
|
+
// valid Boom status code keeps it. Re-deriving the code from the
|
|
379
|
+
// message text lost real codes — promiseTimeout's Boom
|
|
380
|
+
// 'Timed Out' (408) was re-wrapped as a 500 by mapWebSocketError
|
|
381
|
+
// because the textual timeout check below is case-sensitive
|
|
382
|
+
// ('Timed Out' vs 'timed out').
|
|
383
|
+
const existingCode = error?.output?.statusCode;
|
|
384
|
+
if (Number.isFinite(existingCode) && existingCode >= 400 && existingCode <= 599) {
|
|
385
|
+
return existingCode;
|
|
386
|
+
}
|
|
377
387
|
let statusCode = 500;
|
|
378
388
|
if (error?.message?.includes(UNEXPECTED_SERVER_CODE_TEXT)) {
|
|
379
389
|
const code = +error?.message.slice(UNEXPECTED_SERVER_CODE_TEXT.length);
|
|
@@ -385,7 +395,7 @@ export const getCodeFromWSError = (error) => {
|
|
|
385
395
|
// AunuBaileys (1.0.67): error.code can be numeric (e.g. WS close
|
|
386
396
|
// 1006) — only call startsWith on strings.
|
|
387
397
|
(typeof error?.code === 'string' && error.code.startsWith('E')) ||
|
|
388
|
-
error?.message?.includes('timed out')) {
|
|
398
|
+
error?.message?.toLowerCase().includes('timed out')) {
|
|
389
399
|
// handle ETIMEOUT, ENOTFOUND etc
|
|
390
400
|
statusCode = 408;
|
|
391
401
|
}
|
|
@@ -15,8 +15,8 @@ const generateIV = (counter) => {
|
|
|
15
15
|
// corrupt header either wedges the stream forever (size 0: the header
|
|
16
16
|
// is never consumed, so every later call re-reads it) or buffers
|
|
17
17
|
// without bound waiting for megabytes that never arrive. Fail loudly
|
|
18
|
-
// instead: the
|
|
19
|
-
//
|
|
18
|
+
// instead: the throw is marked frame-fatal (1.0.77, HB-185) and the
|
|
19
|
+
// caller ends the socket, so the host reconnects at once.
|
|
20
20
|
const MAX_NOISE_FRAME_SIZE = 10 * 1024 * 1024;
|
|
21
21
|
export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publicKey }, NOISE_HEADER, logger, routingInfo }) => {
|
|
22
22
|
logger = logger.child({ class: 'ns' });
|
|
@@ -161,23 +161,55 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
|
|
|
161
161
|
}
|
|
162
162
|
if (size > MAX_NOISE_FRAME_SIZE) {
|
|
163
163
|
// AunuBaileys (1.0.73, HB-91): never wait for a
|
|
164
|
-
// frame that cannot exist
|
|
165
|
-
//
|
|
166
|
-
// the
|
|
167
|
-
//
|
|
168
|
-
|
|
164
|
+
// frame that cannot exist. (1.0.77, HB-185): the
|
|
165
|
+
// throw alone did NOT end the socket — the caller
|
|
166
|
+
// only logged it, the header was never consumed,
|
|
167
|
+
// and the stream wedged until the keep-alive
|
|
168
|
+
// watchdog fired ~65s later. Marked frame-fatal:
|
|
169
|
+
// the caller now ends the connection on it.
|
|
170
|
+
const err = new Boom(`noise frame too large: ${size} bytes`, { statusCode: 400 });
|
|
171
|
+
err.isFrameFatal = true;
|
|
172
|
+
throw err;
|
|
169
173
|
}
|
|
170
174
|
if (inBytes.length < size + 3) {
|
|
171
175
|
break;
|
|
172
176
|
}
|
|
173
177
|
let frame = inBytes.slice(3, size + 3);
|
|
174
178
|
inBytes = inBytes.slice(size + 3);
|
|
179
|
+
let decoded = true;
|
|
175
180
|
if (isFinished) {
|
|
176
|
-
|
|
177
|
-
|
|
181
|
+
let result;
|
|
182
|
+
try {
|
|
183
|
+
result = decrypt(frame);
|
|
184
|
+
}
|
|
185
|
+
catch (err) {
|
|
186
|
+
// AunuBaileys (1.0.77, HB-207): a GCM failure
|
|
187
|
+
// leaves readCounter behind the server's
|
|
188
|
+
// counter, so every later frame decrypts with
|
|
189
|
+
// the wrong IV and fails too — the stream can
|
|
190
|
+
// never resync. Frame-fatal: the caller ends
|
|
191
|
+
// the connection instead of losing every
|
|
192
|
+
// inbound message until the watchdog fires.
|
|
193
|
+
err.isFrameFatal = true;
|
|
194
|
+
throw err;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
frame = await decodeBinaryNode(result);
|
|
198
|
+
}
|
|
199
|
+
catch (err) {
|
|
200
|
+
// AunuBaileys (1.0.77): stanza-level corruption
|
|
201
|
+
// is NOT fatal — the frame was consumed and the
|
|
202
|
+
// counter advanced, so the stream is still in
|
|
203
|
+
// sync. Log and skip the frame instead of
|
|
204
|
+
// aborting the rest of the buffered frames.
|
|
205
|
+
logger.error({ err }, 'failed to decode a stanza; skipping the frame');
|
|
206
|
+
decoded = false;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (decoded) {
|
|
210
|
+
logger.trace({ msg: frame?.attrs?.id }, 'recv frame');
|
|
211
|
+
onFrame(frame);
|
|
178
212
|
}
|
|
179
|
-
logger.trace({ msg: frame?.attrs?.id }, 'recv frame');
|
|
180
|
-
onFrame(frame);
|
|
181
213
|
size = getBytesSize();
|
|
182
214
|
}
|
|
183
215
|
}
|
package/lib/Utils/signal.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { KEY_BUNDLE_TYPE } from '../Defaults/index.js';
|
|
2
2
|
import { assertNodeErrorFree, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildUInt, jidDecode, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
3
|
-
import { Curve, generateSignalPubKey } from './crypto.js';
|
|
3
|
+
import { Curve, generateSignalPubKey, keyMaterialToBuffer } from './crypto.js';
|
|
4
4
|
import { encodeBigEndian } from './generics.js';
|
|
5
5
|
// AunuBaileys (1.0.74): HB-98 — key material is only ever a Buffer
|
|
6
6
|
// (or a string in a hand-edited creds file). Anything else is a
|
|
@@ -52,10 +52,13 @@ export const generateOrGetPreKeys = (creds, range) => {
|
|
|
52
52
|
export const xmppSignedPreKey = (key) => ({
|
|
53
53
|
tag: 'skey',
|
|
54
54
|
attrs: {},
|
|
55
|
+
// AunuBaileys (1.0.78, HB-157): key material may be a base64 string
|
|
56
|
+
// in a hand-edited creds file — the node writer would otherwise
|
|
57
|
+
// publish the base64 TEXT as the key bytes.
|
|
55
58
|
content: [
|
|
56
59
|
{ tag: 'id', attrs: {}, content: encodeBigEndian(key.keyId, 3) },
|
|
57
|
-
{ tag: 'value', attrs: {}, content: key.keyPair.public },
|
|
58
|
-
{ tag: 'signature', attrs: {}, content: key.signature }
|
|
60
|
+
{ tag: 'value', attrs: {}, content: keyMaterialToBuffer(key.keyPair.public) },
|
|
61
|
+
{ tag: 'signature', attrs: {}, content: keyMaterialToBuffer(key.signature) }
|
|
59
62
|
]
|
|
60
63
|
});
|
|
61
64
|
export const xmppPreKey = (pair, id) => ({
|
|
@@ -63,7 +66,7 @@ export const xmppPreKey = (pair, id) => ({
|
|
|
63
66
|
attrs: {},
|
|
64
67
|
content: [
|
|
65
68
|
{ tag: 'id', attrs: {}, content: encodeBigEndian(id, 3) },
|
|
66
|
-
{ tag: 'value', attrs: {}, content: pair.public }
|
|
69
|
+
{ tag: 'value', attrs: {}, content: keyMaterialToBuffer(pair.public) }
|
|
67
70
|
]
|
|
68
71
|
});
|
|
69
72
|
/** AunuBaileys: parse the E2E key bundle embedded in a retry receipt.
|
|
@@ -239,7 +242,7 @@ export const getNextPreKeysNode = async (state, count) => {
|
|
|
239
242
|
content: [
|
|
240
243
|
{ tag: 'registration', attrs: {}, content: encodeBigEndian(creds.registrationId) },
|
|
241
244
|
{ tag: 'type', attrs: {}, content: KEY_BUNDLE_TYPE },
|
|
242
|
-
{ tag: 'identity', attrs: {}, content: creds.signedIdentityKey.public },
|
|
245
|
+
{ tag: 'identity', attrs: {}, content: keyMaterialToBuffer(creds.signedIdentityKey.public) },
|
|
243
246
|
{ tag: 'list', attrs: {}, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) },
|
|
244
247
|
xmppSignedPreKey(creds.signedPreKey)
|
|
245
248
|
]
|
|
@@ -3,7 +3,7 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
import { proto } from '../../WAProto/index.js';
|
|
4
4
|
import { KEY_BUNDLE_TYPE } from '../Defaults/index.js';
|
|
5
5
|
import { getBinaryNodeChild, jidDecode, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
6
|
-
import { Curve, hmacSign } from './crypto.js';
|
|
6
|
+
import { Curve, hmacSign, keyMaterialToBuffer } from './crypto.js';
|
|
7
7
|
import { encodeBigEndian } from './generics.js';
|
|
8
8
|
import { createSignalIdentity } from './signal.js';
|
|
9
9
|
// AunuBaileys (1.0.74): HB-98 — key material is only ever a Buffer
|
|
@@ -147,12 +147,14 @@ export const configureSuccessfulPairing = (stanza, { advSecretKey, signedIdentit
|
|
|
147
147
|
}
|
|
148
148
|
const account = proto.ADVSignedDeviceIdentity.decode(details);
|
|
149
149
|
const { accountSignatureKey, accountSignature, details: deviceDetails } = account;
|
|
150
|
-
|
|
150
|
+
// AunuBaileys (1.0.78, HB-157): a string identity key is base64 —
|
|
151
|
+
// the raw concat used to die with a TypeError on string creds.
|
|
152
|
+
const accountMsg = Buffer.concat([Buffer.from([6, 0]), deviceDetails, keyMaterialToBuffer(signedIdentityKey.public)]);
|
|
151
153
|
if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) {
|
|
152
154
|
throw new Boom('Failed to verify account signature');
|
|
153
155
|
}
|
|
154
156
|
const devicePrefix = isHostedAccount ? Buffer.from([6, 6]) : Buffer.from([6, 1]);
|
|
155
|
-
const deviceMsg = Buffer.concat([devicePrefix, deviceDetails, signedIdentityKey.public, accountSignatureKey]);
|
|
157
|
+
const deviceMsg = Buffer.concat([devicePrefix, deviceDetails, keyMaterialToBuffer(signedIdentityKey.public), accountSignatureKey]);
|
|
156
158
|
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg);
|
|
157
159
|
const identity = createSignalIdentity(jid, accountSignatureKey);
|
|
158
160
|
const accountEnc = encodeSignedDeviceIdentity(account, false);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aunucore/baileys",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.78",
|
|
5
5
|
"description": "AunuBaileys — the self-healing WhatsApp Web API for Node.js. Bad MAC and \"2000 messages into the future\" errors repair themselves, LID and phone-number identities stay in sync, anti-ban is built in, pairing codes are custom — plus 29 experimental features no other Baileys fork ships: invisible watermarks, per-person tag-all cards, side-profile text, ghost mentions and more. ESM, CJS and full TypeScript types.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|