@aunucore/baileys 1.0.77 → 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.
@@ -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
  }
@@ -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
- Buffer.from(authState.creds.signedIdentityKey.public),
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 (sessionLayerFailure) {
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');
@@ -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 = () => {
@@ -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
@@ -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
- const accountMsg = Buffer.concat([Buffer.from([6, 0]), deviceDetails, signedIdentityKey.public]);
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.77",
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",