@aunucore/baileys 1.0.77 → 1.0.79

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.
@@ -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(senderKeyDistributionMessage.getId(), senderKeyDistributionMessage.getIteration(), senderKeyDistributionMessage.getChainKey(), senderKeyDistributionMessage.getSignatureKey());
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
- this.iteration = iteration;
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
- this.id = distributionMessage.id;
12
- this.iteration = distributionMessage.iteration;
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
- this.keyId = senderKeyMessage.id;
17
- this.iteration = senderKeyMessage.iteration;
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
- if (!structure || typeof structure !== 'object' ||
19
- !structure.senderChainKey || !structure.senderSigningKey) {
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, structure));
110
+ this.senderKeyStates.push(new SenderKeyState(null, null, null, null, null, null, clean));
23
111
  }
24
112
  }
25
113
  }
@@ -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
- const senderName = jidToSignalSenderKeyName(group, meId);
59
- const builder = new GroupSessionBuilder(storage);
60
- const senderNameStr = senderName.toString();
61
- const { [senderNameStr]: senderKey } = await auth.keys.get('sender-key', [senderNameStr]);
62
- if (!senderKey) {
63
- await storage.storeSenderKey(senderName, new SenderKeyRecord());
64
- }
65
- const senderKeyDistributionMessage = await builder.create(senderName);
66
- const session = new GroupCipher(storage, senderName);
67
- const ciphertext = await session.encrypt(data);
68
- return {
69
- ciphertext,
70
- senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
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
- if (err instanceof TypeError) {
137
- throw new Error(`AunuBaileys: stored session record for ${id} is corrupt or malformed and cannot be deserialized; delete the session and re-establish it`);
138
- }
139
- throw err;
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: Buffer.from(key.private),
168
- pubKey: Buffer.from(key.public)
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: Buffer.from(key.keyPair.private),
188
- pubKey: Buffer.from(key.keyPair.public)
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: Buffer.from(signedIdentityKey.private),
243
+ privKey: storedKeyToBuffer(signedIdentityKey.private),
216
244
  pubKey: generateSignalPubKey(signedIdentityKey.public)
217
245
  };
218
246
  }
@@ -189,7 +189,15 @@ export const makeChatsSocket = (config) => {
189
189
  if (!jid) {
190
190
  throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update');
191
191
  }
192
- if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) {
192
+ // AunuBaileys (1.0.79, HB-125): creds.me is undefined until
193
+ // pairing completes (and again after a pairing rollback) —
194
+ // the bare deref below threw a raw TypeError for a host
195
+ // calling too early. Fail like the guarded send paths.
196
+ const meId = authState.creds.me?.id;
197
+ if (!meId) {
198
+ throw new Boom('Not authenticated');
199
+ }
200
+ if (jidNormalizedUser(jid) !== jidNormalizedUser(meId)) {
193
201
  targetJid = jidNormalizedUser(jid); // in case it is someone other than us
194
202
  }
195
203
  else {
@@ -219,7 +227,13 @@ export const makeChatsSocket = (config) => {
219
227
  if (!jid) {
220
228
  throw new Boom('Illegal no-jid profile update. Please specify either your ID or the ID of the chat you wish to update');
221
229
  }
222
- if (jidNormalizedUser(jid) !== jidNormalizedUser(authState.creds.me.id)) {
230
+ // AunuBaileys (1.0.79, HB-125): same bare creds.me.id deref
231
+ // as updateProfilePicture — same guard.
232
+ const meId = authState.creds.me?.id;
233
+ if (!meId) {
234
+ throw new Boom('Not authenticated');
235
+ }
236
+ if (jidNormalizedUser(jid) !== jidNormalizedUser(meId)) {
223
237
  targetJid = jidNormalizedUser(jid); // in case it is someone other than us
224
238
  }
225
239
  else {
@@ -529,6 +543,13 @@ export const makeChatsSocket = (config) => {
529
543
  };
530
544
  const sendPresenceUpdate = async (type, toJid) => {
531
545
  const me = authState.creds.me;
546
+ // AunuBaileys (1.0.79, HB-125): me is undefined until pairing
547
+ // completes; both branches below deref it bare (me.name in
548
+ // this one, me.lid/me.id in the chatstate branch) and threw
549
+ // a raw TypeError for an early host call.
550
+ if (!me?.id) {
551
+ throw new Boom('Not authenticated');
552
+ }
532
553
  if (type === 'available' || type === 'unavailable') {
533
554
  if (!me.name) {
534
555
  logger.warn('no name present, ignoring presence update request...');
@@ -851,7 +872,12 @@ export const makeChatsSocket = (config) => {
851
872
  }
852
873
  ev.emit('messages.upsert', { messages: [msg], type });
853
874
  if (!!msg.pushName) {
854
- let jid = msg.key.fromMe ? authState.creds.me.id : msg.key.participant || msg.key.remoteJid;
875
+ // AunuBaileys (1.0.79, HB-125): me?.id — a fromMe message
876
+ // with a pushName processed before pairing completes
877
+ // threw a raw TypeError here; jidNormalizedUser maps an
878
+ // undefined jid to '' and the fromMe path does not use
879
+ // the computed jid for anything but normalisation.
880
+ let jid = msg.key.fromMe ? authState.creds.me?.id : msg.key.participant || msg.key.remoteJid;
855
881
  jid = jidNormalizedUser(jid);
856
882
  if (!msg.key.fromMe) {
857
883
  ev.emit('contacts.update', [{ id: jid, notify: msg.pushName, verifiedName: msg.verifiedBizName }]);
@@ -246,6 +246,11 @@ export const makeCommunitiesSocket = (config) => {
246
246
  */
247
247
  communityAcceptInviteV4: ev.createBufferedFunction(async (key, inviteMessage) => {
248
248
  key = typeof key === 'string' ? { remoteJid: key } : key;
249
+ // AunuBaileys (1.0.79, HB-125): same bare creds.me.id
250
+ // deref as groupAcceptInviteV4 — same guard.
251
+ if (!authState.creds.me?.id) {
252
+ throw new Boom('Not authenticated');
253
+ }
249
254
  const results = await communityQuery(inviteMessage.groupJid, 'set', [
250
255
  {
251
256
  tag: 'accept',
@@ -207,6 +207,13 @@ export const makeGroupsSocket = (config) => {
207
207
  */
208
208
  groupAcceptInviteV4: ev.createBufferedFunction(async (key, inviteMessage) => {
209
209
  key = typeof key === 'string' ? { remoteJid: key } : key;
210
+ // AunuBaileys (1.0.79, HB-125): creds.me.id is
211
+ // dereferenced for the stub message below (and inside
212
+ // processMessage); before pairing completes it threw a
213
+ // raw TypeError. Fail before any network work.
214
+ if (!authState.creds.me?.id) {
215
+ throw new Boom('Not authenticated');
216
+ }
210
217
  const results = await groupQuery(inviteMessage.groupJid, 'set', [
211
218
  {
212
219
  tag: 'accept',
@@ -5,13 +5,17 @@ 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';
12
12
  import { areJidsSameUser, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidStatusBroadcast, isJidUser, isLidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
13
13
  import { extractGroupMetadata } from './groups.js';
14
14
  import { makeMessagesSocket } from './messages-send.js';
15
+ /** AunuBaileys (1.0.79, HB-201): the HB-98 key-material test — key
16
+ * material is a Buffer (or a base64 string in a hand-edited creds
17
+ * file); anything else is a wrong-typed corrupt value. */
18
+ const isKeyMaterial = (value) => Buffer.isBuffer(value) || typeof value === 'string';
15
19
  /** AunuBaileys: classify an <unavailable/> placeholder — 'bot' when a
16
20
  * <bot/> child rides along, 'hosted' when the node says so, otherwise
17
21
  * the unavailable type attribute (e.g. 'view_once'). */
@@ -82,6 +86,13 @@ export const makeMessagesRecvSocket = (config) => {
82
86
  // AunuBaileys: terima panggilan masuk (node kontrol <accept>) — pasangan
83
87
  // rejectCall bawaan; media VOIP-nya sendiri di luar cakupan protokol chat.
84
88
  const acceptCall = async (callId, callFrom) => {
89
+ // AunuBaileys (1.0.79, HB-172): creds.me.id was dereferenced
90
+ // bare in the stanza literal — a host auto-accepting a call
91
+ // before pairing completes got a raw TypeError instead of
92
+ // the guarded failure the neighbouring helpers give.
93
+ if (!authState.creds.me?.id) {
94
+ throw new Boom('Not authenticated');
95
+ }
85
96
  await query(buildCallControlStanza({
86
97
  from: authState.creds.me.id,
87
98
  to: callFrom,
@@ -92,6 +103,11 @@ export const makeMessagesRecvSocket = (config) => {
92
103
  };
93
104
  // AunuBaileys: akhiri panggilan yang sedang berjalan (<terminate>).
94
105
  const terminateCall = async (callId, callFrom, reason = 'user-terminated') => {
106
+ // AunuBaileys (1.0.79, HB-172): same bare creds.me.id deref
107
+ // as acceptCall — guard first.
108
+ if (!authState.creds.me?.id) {
109
+ throw new Boom('Not authenticated');
110
+ }
95
111
  await query(buildCallControlStanza({
96
112
  from: authState.creds.me.id,
97
113
  to: callFrom,
@@ -102,6 +118,11 @@ export const makeMessagesRecvSocket = (config) => {
102
118
  }));
103
119
  };
104
120
  const rejectCall = async (callId, callFrom) => {
121
+ // AunuBaileys (1.0.79, HB-172): same bare creds.me.id deref
122
+ // as acceptCall — guard first.
123
+ if (!authState.creds.me?.id) {
124
+ throw new Boom('Not authenticated');
125
+ }
105
126
  const stanza = {
106
127
  tag: 'call',
107
128
  attrs: {
@@ -144,9 +165,28 @@ export const makeMessagesRecvSocket = (config) => {
144
165
  logger.debug({ retryCount, msgId }, 'reached retry limit');
145
166
  return;
146
167
  }
168
+ const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
169
+ // AunuBaileys (1.0.79, HB-201 + addendum HB-80): the receipt
170
+ // built below always embeds creds.registrationId, and from
171
+ // the second retry on it also embeds the identity and
172
+ // signed-pre-key material — all of it dereferenced bare
173
+ // inside the transaction callback. With a partial creds
174
+ // restore those derefs threw TypeError AFTER the retry
175
+ // counter above had already been consumed: the receipt was
176
+ // never sent, the counter was burned, and every later retry
177
+ // failed at the same spot until the cap left the message a
178
+ // stub forever. (A missing registrationId was worse — it
179
+ // silently encoded as 00000000.) Validate BEFORE the
180
+ // increment and fail descriptively: the counter is only
181
+ // spent on a receipt that can actually be built.
182
+ if (!Number.isFinite(authState.creds.registrationId)) {
183
+ throw new Error('AunuBaileys: creds.registrationId is missing or invalid — cannot send a retry receipt; restore a full backup or pair again');
184
+ }
185
+ if ((retryCount >= 1 || forceIncludeKeys) && (!isKeyMaterial(identityKey?.public) || !isKeyMaterial(signedPreKey?.keyPair?.public) || !isKeyMaterial(signedPreKey?.signature) || typeof signedPreKey?.keyId === 'undefined')) {
186
+ throw new Error('AunuBaileys: creds.signedIdentityKey / creds.signedPreKey are missing or incomplete — cannot include keys in the retry receipt; restore a full backup or pair again');
187
+ }
147
188
  retryCount += 1;
148
189
  msgRetryCache.set(key, retryCount);
149
- const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds;
150
190
  if (retryCount === 1) {
151
191
  // AunuBaileys (1.0.70): requestPlaceholderResend waits 5s
152
192
  // for the message to arrive by other means — awaiting it
@@ -206,7 +246,7 @@ export const makeMessagesRecvSocket = (config) => {
206
246
  attrs: {},
207
247
  content: [
208
248
  { tag: 'type', attrs: {}, content: Buffer.from(KEY_BUNDLE_TYPE) },
209
- { tag: 'identity', attrs: {}, content: identityKey.public },
249
+ { tag: 'identity', attrs: {}, content: keyMaterialToBuffer(identityKey.public) },
210
250
  xmppPreKey(key, +keyId),
211
251
  xmppSignedPreKey(signedPreKey),
212
252
  { tag: 'device-identity', attrs: {}, content: deviceIdentity }
@@ -545,7 +585,10 @@ export const makeMessagesRecvSocket = (config) => {
545
585
  info: 'link_code_pairing_key_bundle_encryption_key'
546
586
  });
547
587
  const encryptPayload = Buffer.concat([
548
- Buffer.from(authState.creds.signedIdentityKey.public),
588
+ // AunuBaileys (1.0.78, HB-157): a string identity key
589
+ // is base64 — Buffer.from() decoded it as UTF-8 and
590
+ // put a 44-byte garbage identity in the payload.
591
+ keyMaterialToBuffer(authState.creds.signedIdentityKey.public),
549
592
  primaryIdentityPublicKey,
550
593
  random
551
594
  ]);
@@ -923,6 +966,16 @@ export const makeMessagesRecvSocket = (config) => {
923
966
  // sender key saja tidak menyentuh sesi E2E yang
924
967
  // rusak. Kelas ini direset SEKETIKA.
925
968
  const sessionLayerFailure = isUnrecoverableSessionError(msg?.messageStubParameters?.[0]);
969
+ // AunuBaileys (1.0.78, HB-223): a group
970
+ // failure where the sender key is simply
971
+ // absent (or the skmsg payload itself is
972
+ // malformed) says nothing about the
973
+ // author's E2E session — do not count it
974
+ // toward the session reset and do not
975
+ // report it as a healable failure; the
976
+ // retry receipt below is the whole fix,
977
+ // it makes the peer redistribute its key.
978
+ const senderKeyUnavailable = isGroupMsg && isSenderKeyUnavailableError(msg?.messageStubParameters?.[0]);
926
979
  let healed = false;
927
980
  const resetE2ESession = async () => {
928
981
  await signalRepository.deleteSession(author);
@@ -933,7 +986,10 @@ export const makeMessagesRecvSocket = (config) => {
933
986
  await signalRepository.deleteSession(alt);
934
987
  }
935
988
  };
936
- if (sessionLayerFailure) {
989
+ if (senderKeyUnavailable) {
990
+ 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');
991
+ }
992
+ else if (sessionLayerFailure) {
937
993
  await resetE2ESession();
938
994
  if (isGroupMsg) {
939
995
  await signalRepository.deleteSenderKey(msg.key.remoteJid, author);
@@ -977,6 +1033,7 @@ export const makeMessagesRecvSocket = (config) => {
977
1033
  // AunuBaileys: structured decrypt-failure event so
978
1034
  // consumers can alert and aggregate on Bad MAC
979
1035
  // floods (B-01) instead of parsing log lines.
1036
+ if (!senderKeyUnavailable) {
980
1037
  ev.emit('messages.decrypt-failure', {
981
1038
  remoteJid: msg.key.remoteJid,
982
1039
  author,
@@ -988,6 +1045,7 @@ export const makeMessagesRecvSocket = (config) => {
988
1045
  messageId: node.attrs.id,
989
1046
  timestamp: Date.now()
990
1047
  });
1048
+ }
991
1049
  }
992
1050
  catch (healErr) {
993
1051
  logger.debug({ err: healErr }, 'aunu: session self-heal step failed, continuing with normal retry');
@@ -1024,7 +1082,14 @@ export const makeMessagesRecvSocket = (config) => {
1024
1082
  // vanished silently if the retry also failed
1025
1083
  // (HB-90; upstream v7 falls through to the
1026
1084
  // upsert for exactly this reason).
1027
- cleanMessage(msg, authState.creds.me.id);
1085
+ // AunuBaileys (1.0.79, HB-131): me?.id — in
1086
+ // the pre-success window creds.me is still
1087
+ // undefined and the bare deref threw AFTER the
1088
+ // retry receipt had already been fired: the
1089
+ // stub was never upserted and the stanza was
1090
+ // NACKed 500 (delivered + failed). cleanMessage
1091
+ // tolerates an undefined meId.
1092
+ cleanMessage(msg, authState.creds.me?.id);
1028
1093
  await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1029
1094
  }
1030
1095
  else {
@@ -1062,7 +1127,12 @@ export const makeMessagesRecvSocket = (config) => {
1062
1127
  await sendReceipt(jid, undefined, [msg.key.id], 'hist_sync');
1063
1128
  }
1064
1129
  }
1065
- cleanMessage(msg, authState.creds.me.id);
1130
+ // AunuBaileys (1.0.79, HB-131): me?.id — the
1131
+ // bare deref threw here AFTER sendReceipt had
1132
+ // already gone out, NACKing a delivered message
1133
+ // 500 and bursting redeliveries until CB:success
1134
+ // landed. cleanMessage tolerates undefined.
1135
+ cleanMessage(msg, authState.creds.me?.id);
1066
1136
  await upsertMessage(msg, node.attrs.offline ? 'append' : 'notify');
1067
1137
  })
1068
1138
  ]);
@@ -539,7 +539,17 @@ export const makeMessagesSocket = (config) => {
539
539
  attrs: { v: '2', type: 'skmsg' },
540
540
  content: ciphertext
541
541
  });
542
- await authState.keys.set({ 'sender-key-memory': { [jid]: senderKeyMap } });
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
+ }
543
553
  }
544
554
  else {
545
555
  const meUser = jidDecode(meId)?.user;
@@ -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
  /**
@@ -774,8 +774,11 @@ export const makeSocket = (config) => {
774
774
  qrChainActive = true;
775
775
  const pairDeviceNode = getBinaryNodeChild(stanza, 'pair-device');
776
776
  const refNodes = getBinaryNodeChildren(pairDeviceNode, 'ref');
777
- const noiseKeyB64 = Buffer.from(creds.noiseKey.public).toString('base64');
778
- const identityKeyB64 = Buffer.from(creds.signedIdentityKey.public).toString('base64');
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');
779
782
  const advB64 = creds.advSecretKey;
780
783
  let qrMs = qrTimeout || 60000; // time to let a QR live
781
784
  const genPairQR = () => {
@@ -96,8 +96,16 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
96
96
  // commit attempt. Fall back per-field to the Defaults values, and
97
97
  // clamp retries to at least 1 so an explicit 0 means exactly one
98
98
  // attempt, matching host intent.
99
- const commitRetries = Math.max(1, maxCommitRetries ?? 10);
100
- const commitDelayMs = delayBetweenTriesMs ?? 3000;
99
+ // AunuBaileys (1.0.79, addendum HB-104): the ?? fallback lets NaN
100
+ // through (Math.max(1, NaN) is NaN — the commit loop below then
101
+ // runs ZERO times and every mutating transaction throws without
102
+ // a single attempt) and lets Infinity through (the loop never
103
+ // ends on a failing store and wedges the transaction mutex —
104
+ // the whole key store deadlocks until restart). Sanitise at the
105
+ // boundary: retries must be a finite integer, clamped to 1..10;
106
+ // a non-finite or negative delay falls back to the default.
107
+ const commitRetries = Number.isFinite(maxCommitRetries) ? Math.min(10, Math.max(1, Math.floor(maxCommitRetries))) : 10;
108
+ const commitDelayMs = Number.isFinite(delayBetweenTriesMs) && delayBetweenTriesMs >= 0 ? delayBetweenTriesMs : 3000;
101
109
  // number of queries made to the DB during the transaction
102
110
  // only there for logging purposes
103
111
  let dbQueriesInTransaction = 0;
@@ -114,6 +122,20 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
114
122
  const transactionContext = new AsyncLocalStorage();
115
123
  const runTransaction = async (work) => {
116
124
  let result;
125
+ // AunuBaileys (1.0.79, HB-236): a nested transaction joins the
126
+ // running one and SHARES its cache/mutations — snapshot both
127
+ // when entering a nested frame. If the nested work throws,
128
+ // the snapshot is restored before the error propagates (see
129
+ // the catch below): without it, an outer frame that catches
130
+ // the throw would commit the inner frame's partial writes as
131
+ // if nothing had failed — the only "rollback" was a throw
132
+ // escaping the OUTERMOST frame.
133
+ const snapshot = transactionsInProgress > 0
134
+ ? {
135
+ cache: Object.fromEntries(Object.entries(transactionCache).map(([type, dict]) => [type, { ...dict }])),
136
+ mutations: Object.fromEntries(Object.entries(mutations).map(([type, dict]) => [type, { ...dict }]))
137
+ }
138
+ : undefined;
117
139
  transactionsInProgress += 1;
118
140
  if (transactionsInProgress === 1) {
119
141
  logger.trace('entering transaction');
@@ -159,6 +181,17 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
159
181
  }
160
182
  }
161
183
  }
184
+ catch (error) {
185
+ // AunuBaileys (1.0.79, HB-236): roll a failed NESTED frame
186
+ // back to its entry snapshot (see above); the outermost
187
+ // frame needs no restore — the finally below resets the
188
+ // shared state outright.
189
+ if (snapshot) {
190
+ transactionCache = snapshot.cache;
191
+ mutations = snapshot.mutations;
192
+ }
193
+ throw error;
194
+ }
162
195
  finally {
163
196
  transactionsInProgress -= 1;
164
197
  if (transactionsInProgress === 0) {
@@ -173,7 +206,14 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
173
206
  get: async (type, ids) => {
174
207
  if (isInTransaction()) {
175
208
  const dict = transactionCache[type];
176
- const idsRequiringFetch = dict ? ids.filter(item => typeof dict[item] === 'undefined') : ids;
209
+ // AunuBaileys (1.0.79, HB-233): cache PRESENCE — not
210
+ // the value — decides whether a fetch is needed. A
211
+ // key explicitly set to undefined in this transaction
212
+ // is a deletion marker; the typeof test could not
213
+ // tell it from "never fetched", so get re-read the
214
+ // stale persisted value and resurrected a record the
215
+ // transaction itself had just deleted.
216
+ const idsRequiringFetch = dict ? ids.filter(item => !Object.prototype.hasOwnProperty.call(dict, item)) : ids;
177
217
  // only fetch if there are any items to fetch
178
218
  if (idsRequiringFetch.length) {
179
219
  dbQueriesInTransaction += 1;
@@ -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: {
@@ -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 = typeof pubKey === 'string' ? Buffer.from(pubKey) : pubKey;
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
- const shared = libsignal.curve.calculateAgreement(generateSignalPubKey(publicKey), privateKey);
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
@@ -2,7 +2,7 @@ import { Boom } from '@hapi/boom';
2
2
  import { proto } from '../../WAProto/index.js';
3
3
  import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults/index.js';
4
4
  import { decodeBinaryNode } from '../WABinary/index.js';
5
- import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto.js';
5
+ import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, keyMaterialToBuffer, sha256 } from './crypto.js';
6
6
  const generateIV = (counter) => {
7
7
  const iv = new ArrayBuffer(12);
8
8
  new DataView(iv).setUint32(8, counter);
@@ -94,6 +94,30 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
94
94
  if (!noiseKey?.public || !noiseKey?.private) {
95
95
  throw new Error('AunuBaileys: creds.noiseKey is missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
96
96
  }
97
+ // AunuBaileys (1.0.79, addendum HB-102): the truthiness
98
+ // check above lets a WRONG-TYPED noiseKey through — a
99
+ // number/object died as a bare TypeError inside
100
+ // aesEncryptGCM, and a base64 string was silently
101
+ // encrypted as UTF-8 bytes, so the server received a
102
+ // garbage public key and the handshake failed remotely
103
+ // with no local diagnosis. Normalise with
104
+ // keyMaterialToBuffer (the HB-157 house rule) and
105
+ // require real 32-byte keys.
106
+ const noisePublic = keyMaterialToBuffer(noiseKey.public);
107
+ const noisePrivate = keyMaterialToBuffer(noiseKey.private);
108
+ if (!Buffer.isBuffer(noisePublic) || noisePublic.length !== 32 || !Buffer.isBuffer(noisePrivate) || noisePrivate.length !== 32) {
109
+ throw new Error('AunuBaileys: creds.noiseKey is malformed (public/private must be 32-byte keys) — the auth state is incomplete or corrupt; restore a full backup or pair again');
110
+ }
111
+ // AunuBaileys (1.0.79, HB-238): the first handshake frame
112
+ // is plaintext, so an intercepting middlebox (ISP
113
+ // intercept page, broken proxy) can deliver a decoded
114
+ // handshake with NO serverHello at all — the derefs
115
+ // below then threw a bare TypeError and the host
116
+ // reconnect-looped on it. Fail descriptively, in the
117
+ // HB-80/HB-102 style.
118
+ if (!serverHello || typeof serverHello !== 'object' || !(serverHello.ephemeral instanceof Uint8Array) || !(serverHello.static instanceof Uint8Array) || !(serverHello.payload instanceof Uint8Array)) {
119
+ throw new Error('AunuBaileys: the server handshake is malformed — serverHello or its encrypted parts are missing (the connection may have been intercepted); reconnecting will retry');
120
+ }
97
121
  authenticate(serverHello.ephemeral);
98
122
  await mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral));
99
123
  const decStaticContent = decrypt(serverHello.static);
@@ -104,8 +128,8 @@ export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publi
104
128
  if (issuerSerial !== WA_CERT_DETAILS.SERIAL) {
105
129
  throw new Boom('certification match failed', { statusCode: 400 });
106
130
  }
107
- const keyEnc = encrypt(noiseKey.public);
108
- await mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello.ephemeral));
131
+ const keyEnc = encrypt(noisePublic);
132
+ await mixIntoKey(Curve.sharedKey(noisePrivate, serverHello.ephemeral));
109
133
  return keyEnc;
110
134
  },
111
135
  encodeFrame: (data) => {
@@ -100,7 +100,13 @@ export function decryptPollVote({ encPayload, encIv }, { pollCreatorJid, pollMsg
100
100
  }
101
101
  }
102
102
  const processMessage = async (message, { shouldProcessHistoryMsg, placeholderResendCache, ev, creds, keyStore, logger, options }) => {
103
- const meId = creds.me.id;
103
+ // AunuBaileys (1.0.79, HB-131): creds.me is undefined until
104
+ // pairing completes; the bare deref threw a TypeError for any
105
+ // message/notification processed in that window (reached via
106
+ // upsertMessage, which does not pass cleanMessage first). Every
107
+ // consumer of meId below (isRealMessage, areJidsSameUser,
108
+ // jidNormalizedUser) already tolerates undefined.
109
+ const meId = creds.me?.id;
104
110
  const { accountSettings } = creds;
105
111
  const chat = { id: jidNormalizedUser(getChatId(message.key)) };
106
112
  const isRealMsg = isRealMessage(message, meId);
@@ -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.
@@ -228,6 +231,12 @@ export const getNextPreKeysNode = async (state, count) => {
228
231
  if (!isKeyMaterial(creds.signedIdentityKey?.public) || !isKeyMaterial(creds.signedPreKey?.keyPair?.public) || !isKeyMaterial(creds.signedPreKey?.signature) || typeof creds.signedPreKey?.keyId === 'undefined') {
229
232
  throw new Error('AunuBaileys: creds.signedIdentityKey / creds.signedPreKey are missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
230
233
  }
234
+ // AunuBaileys (1.0.79, addendum HB-80): same gap as
235
+ // generateRegistrationNode — a missing registrationId silently
236
+ // became registration 00000000 in the node below.
237
+ if (!Number.isFinite(creds.registrationId)) {
238
+ throw new Error('AunuBaileys: creds.registrationId is missing or invalid — the auth state is incomplete or corrupt; restore a full backup or pair again');
239
+ }
231
240
  const { update, preKeys } = await getNextPreKeys(state, count);
232
241
  const node = {
233
242
  tag: 'iq',
@@ -239,7 +248,7 @@ export const getNextPreKeysNode = async (state, count) => {
239
248
  content: [
240
249
  { tag: 'registration', attrs: {}, content: encodeBigEndian(creds.registrationId) },
241
250
  { tag: 'type', attrs: {}, content: KEY_BUNDLE_TYPE },
242
- { tag: 'identity', attrs: {}, content: creds.signedIdentityKey.public },
251
+ { tag: 'identity', attrs: {}, content: keyMaterialToBuffer(creds.signedIdentityKey.public) },
243
252
  { tag: 'list', attrs: {}, content: Object.keys(preKeys).map(k => xmppPreKey(preKeys[+k], +k)) },
244
253
  xmppSignedPreKey(creds.signedPreKey)
245
254
  ]
@@ -36,6 +36,21 @@ const getFileLock = (path) => {
36
36
  * Again, I wouldn't endorse this for any production level use other than perhaps a bot.
37
37
  * Would recommend writing an auth state for use with a proper SQL or No-SQL DB
38
38
  * */
39
+ // AunuBaileys (1.0.79, HB-151): the shape a complete creds state
40
+ // must have at load. Key material follows the HB-98 test (a Buffer,
41
+ // or a base64 string in a hand-edited file); the ids must be finite
42
+ // numbers. Anything else is a partial/corrupt restore, not a state
43
+ // the socket can run on.
44
+ const isCredsKeyPair = (kp) => !!kp && typeof kp === 'object' && (Buffer.isBuffer(kp.private) || typeof kp.private === 'string') && (Buffer.isBuffer(kp.public) || typeof kp.public === 'string');
45
+ const isValidCredsShape = (creds) => !!creds &&
46
+ typeof creds === 'object' &&
47
+ !Array.isArray(creds) &&
48
+ isCredsKeyPair(creds.noiseKey) &&
49
+ isCredsKeyPair(creds.signedIdentityKey) &&
50
+ isCredsKeyPair(creds.signedPreKey?.keyPair) &&
51
+ (Buffer.isBuffer(creds.signedPreKey?.signature) || typeof creds.signedPreKey?.signature === 'string') &&
52
+ Number.isFinite(creds.signedPreKey?.keyId) &&
53
+ Number.isFinite(creds.registrationId);
39
54
  export const useMultiFileAuthState = async (folder, logger) => {
40
55
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
41
56
  const writeData = async (data, file) => {
@@ -118,7 +133,21 @@ export const useMultiFileAuthState = async (folder, logger) => {
118
133
  await mkdir(folder, { recursive: true });
119
134
  }
120
135
  const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-');
121
- const creds = (await readData('creds.json')) || initAuthCreds();
136
+ const storedCreds = await readData('creds.json');
137
+ // AunuBaileys (1.0.79, HB-151): a creds.json that parses to {} /
138
+ // a partial object / a non-object used to be accepted as a
139
+ // complete auth state (it is truthy), and every downstream
140
+ // consumer then failed in its own way — reconnect loops with
141
+ // misleading TypeErrors, indistinguishable from a lib bug.
142
+ // Validate at this one load choke point; a present-but-invalid
143
+ // creds is discarded loudly, exactly like a corrupt-JSON file
144
+ // since 1.0.63.
145
+ const creds = isValidCredsShape(storedCreds) ? storedCreds : (() => {
146
+ if (storedCreds) {
147
+ logger?.error({ file: 'creds.json' }, 'aunu: creds.json does not have the shape of a complete auth state — discarding it and starting fresh; restore a full backup or pair again');
148
+ }
149
+ return initAuthCreds();
150
+ })();
122
151
  let credsWriteInFlight = null;
123
152
  let credsWriteQueued = false;
124
153
  return {
@@ -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
@@ -60,7 +60,21 @@ export const generateLoginNode = (userJid, config) => {
60
60
  if (!decoded) {
61
61
  throw new Boom(`Invalid own JID for login node: ${userJid}`, { statusCode: 400 });
62
62
  }
63
- const { user, device } = decoded;
63
+ const { user, device, server } = decoded;
64
+ // AunuBaileys (1.0.79, HB-234): the HB-43 guard above only
65
+ // checks that the JID decodes — a non-numeric user
66
+ // ('abc@s.whatsapp.net') was then silently coerced by +user to
67
+ // username 0 (or clamped to int64-max on a 26-digit overflow)
68
+ // and the login node went out carrying a WRONG identity,
69
+ // looping on server rejects. Validate the parts: the user must
70
+ // be all digits, non-zero and within int64 range; the server
71
+ // must be the user server; and a present device must be a
72
+ // finite number (jidDecode's +device turns 'xx' into NaN,
73
+ // which protobuf then silently encoded as 0).
74
+ const userValue = /^\d+$/.test(user) ? BigInt(user) : 0n;
75
+ if (userValue <= 0n || userValue > 9223372036854775807n || server !== 's.whatsapp.net' || (typeof device !== 'undefined' && !Number.isFinite(device))) {
76
+ throw new Boom(`Invalid own JID for login node: ${userJid}`, { statusCode: 400 });
77
+ }
64
78
  const payload = {
65
79
  ...getClientPayload(config),
66
80
  passive: false,
@@ -83,6 +97,14 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
83
97
  if (!isKeyMaterial(signedIdentityKey?.public) || !isKeyMaterial(signedPreKey?.keyPair?.public) || typeof signedPreKey?.keyId === 'undefined' || !isKeyMaterial(signedPreKey?.signature)) {
84
98
  throw new Error('AunuBaileys: creds.signedIdentityKey / creds.signedPreKey are missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
85
99
  }
100
+ // AunuBaileys (1.0.79, addendum HB-80): registrationId was NOT
101
+ // part of the guard — encodeBigEndian(undefined) does not
102
+ // throw, it silently produces eRegid 00000000, so a partial
103
+ // creds restore registered with a bogus registration id and
104
+ // no local signal at all.
105
+ if (!Number.isFinite(registrationId)) {
106
+ throw new Error('AunuBaileys: creds.registrationId is missing or invalid — the auth state is incomplete or corrupt; restore a full backup or pair again');
107
+ }
86
108
  // the app version needs to be md5 hashed
87
109
  // and passed in
88
110
  const safeVersion = Array.isArray(config.version) && config.version.length ? config.version : [2, 3000, 1043857760];
@@ -128,6 +150,14 @@ export const generateRegistrationNode = ({ registrationId, signedPreKey, signedI
128
150
  };
129
151
  export const configureSuccessfulPairing = (stanza, { advSecretKey, signedIdentityKey, signalIdentities }) => {
130
152
  const msgId = stanza.attrs.id;
153
+ // AunuBaileys (1.0.79, HB-80 sub-instance): the creds fields are
154
+ // dereferenced bare below (Buffer.from(advSecretKey, 'base64'),
155
+ // the identity concat/sign) — a partial creds died as a bare
156
+ // TypeError inside the pair-success handler. Fail descriptively,
157
+ // like the registration guards.
158
+ if (typeof advSecretKey !== 'string' || !advSecretKey || !isKeyMaterial(signedIdentityKey?.public) || !isKeyMaterial(signedIdentityKey?.private)) {
159
+ throw new Error('AunuBaileys: creds.advSecretKey / creds.signedIdentityKey are missing or incomplete — the auth state is incomplete or corrupt; restore a full backup or pair again');
160
+ }
131
161
  const pairSuccessNode = getBinaryNodeChild(stanza, 'pair-success');
132
162
  const deviceIdentityNode = getBinaryNodeChild(pairSuccessNode, 'device-identity');
133
163
  const platformNode = getBinaryNodeChild(pairSuccessNode, 'platform');
@@ -138,6 +168,19 @@ export const configureSuccessfulPairing = (stanza, { advSecretKey, signedIdentit
138
168
  }
139
169
  const bizName = businessNode?.attrs.name;
140
170
  const jid = deviceNode.attrs.jid;
171
+ // AunuBaileys (1.0.79, HB-239): a pair-success stanza without a
172
+ // device jid used to store me.id = undefined SILENTLY — pairing
173
+ // "succeeded", and the NEXT boot took the login path and looped
174
+ // on the HB-43 Boom 400 forever. Reject the stanza here, where
175
+ // the defect is. The addendum: a device-identity node without
176
+ // content died as protobufjs' bare 'Error: illegal buffer' —
177
+ // same missing-guard class, same fix.
178
+ if (typeof jid !== 'string' || !jid) {
179
+ throw new Boom('Missing jid on the device node in pair success stanza', { data: stanza });
180
+ }
181
+ if (!deviceIdentityNode.content?.length) {
182
+ throw new Boom('Missing content on the device-identity node in pair success stanza', { data: stanza });
183
+ }
141
184
  const { details, hmac, accountType } = proto.ADVSignedDeviceIdentityHMAC.decode(deviceIdentityNode.content);
142
185
  const isHostedAccount = accountType !== undefined && accountType === proto.ADVEncryptionType.HOSTED;
143
186
  const hmacPrefix = isHostedAccount ? Buffer.from([6, 5]) : Buffer.alloc(0);
@@ -147,12 +190,14 @@ export const configureSuccessfulPairing = (stanza, { advSecretKey, signedIdentit
147
190
  }
148
191
  const account = proto.ADVSignedDeviceIdentity.decode(details);
149
192
  const { accountSignatureKey, accountSignature, details: deviceDetails } = account;
150
- const accountMsg = Buffer.concat([Buffer.from([6, 0]), deviceDetails, signedIdentityKey.public]);
193
+ // AunuBaileys (1.0.78, HB-157): a string identity key is base64 —
194
+ // the raw concat used to die with a TypeError on string creds.
195
+ const accountMsg = Buffer.concat([Buffer.from([6, 0]), deviceDetails, keyMaterialToBuffer(signedIdentityKey.public)]);
151
196
  if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) {
152
197
  throw new Boom('Failed to verify account signature');
153
198
  }
154
199
  const devicePrefix = isHostedAccount ? Buffer.from([6, 6]) : Buffer.from([6, 1]);
155
- const deviceMsg = Buffer.concat([devicePrefix, deviceDetails, signedIdentityKey.public, accountSignatureKey]);
200
+ const deviceMsg = Buffer.concat([devicePrefix, deviceDetails, keyMaterialToBuffer(signedIdentityKey.public), accountSignatureKey]);
156
201
  account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg);
157
202
  const identity = createSignalIdentity(jid, accountSignatureKey);
158
203
  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.77",
4
+ "version": "1.0.79",
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",