aunubaileys 1.0.16 → 1.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/lib/Signal/libsignal.js +4 -0
- package/lib/Socket/messages-recv.js +40 -12
- package/lib/Types/Signal.d.ts +2 -0
- package/lib/Utils/signal.d.ts +2 -0
- package/lib/Utils/signal.js +31 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,6 +64,14 @@ Everything else is pure upstream. When upstream moves, we rebase the patch layer
|
|
|
64
64
|
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
### 🆕 What's new in 1.0.18 / Baru di 1.0.18
|
|
68
|
+
|
|
69
|
+
- 🔁 **Retry receipts: the peer's own key bundle is finally used** — when a recipient device asks for a resend, its retry receipt often embeds that device's own current key bundle. Upstream ignored it and always re-fetched a bundle from the server (extra round trip, and JID→bundle mapping is fragile for LID participants). AunuBaileys now parses the bundle (new exported `parseRetryReceiptSession`, unit-verified) and injects it as the fresh session for the resend, skipping the server fetch for that resend. Receipts without a bundle behave exactly as before.
|
|
70
|
+
|
|
71
|
+
### 🆕 What's new in 1.0.17 / Baru di 1.0.17
|
|
72
|
+
|
|
73
|
+
- 👥 **Group sender-key heal** — the 1.0.14 failure counter now covers group messages too: after 3 consecutive group decrypt failures from the same author in the same group, the stale sender-key record is deleted (new `signalRepository.deleteSenderKey`), so the next retry makes the author resend with a fresh distribution message and the group cipher rebuilds from scratch. One rotten sender-key record can no longer mute a group member forever.
|
|
74
|
+
|
|
67
75
|
### 🆕 What's new in 1.0.16 / Baru di 1.0.16
|
|
68
76
|
|
|
69
77
|
- 🪪 **Identity changes are handled, not just logged** — when WhatsApp notifies that a contact's identity changed (new phone / reinstall / recycled number), upstream Baileys logs “identity changed” and does nothing, leaving your stored session bound to the old identity so that contact's messages keep failing. AunuBaileys now deletes the stored session for that JID on the spot, so the next exchange re-initiates cleanly against the new identity.
|
package/lib/Signal/libsignal.js
CHANGED
|
@@ -66,6 +66,10 @@ export function makeLibSignalRepository(auth) {
|
|
|
66
66
|
senderKeyDistributionMessage: senderKeyDistributionMessage.serialize()
|
|
67
67
|
};
|
|
68
68
|
},
|
|
69
|
+
async deleteSenderKey(group, authorJid) {
|
|
70
|
+
const senderNameStr = jidToSignalSenderKeyName(group, authorJid).toString();
|
|
71
|
+
await auth.keys.set({ 'sender-key': { [senderNameStr]: null } });
|
|
72
|
+
},
|
|
69
73
|
async deleteSession(jid) {
|
|
70
74
|
const addr = jidToSignalProtocolAddress(jid).toString();
|
|
71
75
|
await auth.keys.set({ session: { [addr]: null } });
|
|
@@ -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, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, 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, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, parseRetryReceiptSession, 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 { areJidsSameUser, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidStatusBroadcast, isJidUser, isLidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
@@ -481,8 +481,7 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
481
481
|
const newValue = (msgRetryCache.get(key) || 0) + 1;
|
|
482
482
|
msgRetryCache.set(key, newValue);
|
|
483
483
|
};
|
|
484
|
-
const sendMessagesAgain = async (key, ids, retryNode) => {
|
|
485
|
-
// todo: implement a cache to store the last 256 sent messages (copy whatsmeow)
|
|
484
|
+
const sendMessagesAgain = async (key, ids, retryNode, sessionPreInjected = false) => {
|
|
486
485
|
const msgs = await Promise.all(ids.map(async (id) => (await getMessage({ ...key, id })) ?? sentMessageCacheFor(config).get(key.remoteJid, id)));
|
|
487
486
|
const remoteJid = key.remoteJid;
|
|
488
487
|
const participant = key.participant || remoteJid;
|
|
@@ -490,7 +489,9 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
490
489
|
// just re-send the message to everyone
|
|
491
490
|
// prevents the first message decryption failure
|
|
492
491
|
const sendToAll = !jidDecode(participant)?.device;
|
|
493
|
-
|
|
492
|
+
if (!sessionPreInjected) {
|
|
493
|
+
await assertSessions([participant], true);
|
|
494
|
+
}
|
|
494
495
|
if (isJidGroup(remoteJid)) {
|
|
495
496
|
await authState.keys.set({ 'sender-key-memory': { [remoteJid]: null } });
|
|
496
497
|
}
|
|
@@ -572,7 +573,24 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
572
573
|
if (key.fromMe) {
|
|
573
574
|
try {
|
|
574
575
|
logger.debug({ attrs, key }, 'recv retry request');
|
|
575
|
-
|
|
576
|
+
// AunuBaileys: when the retry receipt carries the
|
|
577
|
+
// requester's own key bundle, build the fresh
|
|
578
|
+
// session from THAT (authoritative for the exact
|
|
579
|
+
// device, no server round trip) and skip the
|
|
580
|
+
// force-fetch inside sendMessagesAgain.
|
|
581
|
+
let sessionPreInjected = false;
|
|
582
|
+
const bundleSession = parseRetryReceiptSession(node);
|
|
583
|
+
if (bundleSession && key.participant) {
|
|
584
|
+
try {
|
|
585
|
+
await signalRepository.injectE2ESession({ jid: key.participant, session: bundleSession });
|
|
586
|
+
sessionPreInjected = true;
|
|
587
|
+
logger.debug({ participant: key.participant }, 'aunu: injected E2E session from retry receipt bundle');
|
|
588
|
+
}
|
|
589
|
+
catch (injErr) {
|
|
590
|
+
logger.debug({ err: injErr }, 'aunu: retry receipt bundle injection failed; falling back to server fetch');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
await sendMessagesAgain(key, ids, retryNode, sessionPreInjected);
|
|
576
594
|
}
|
|
577
595
|
catch (error) {
|
|
578
596
|
logger.error({ key, ids, trace: error.stack }, 'error in sending message again');
|
|
@@ -677,18 +695,26 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
677
695
|
}
|
|
678
696
|
const encNode = getBinaryNodeChild(node, 'enc');
|
|
679
697
|
let forceIncludeKeys = !encNode;
|
|
680
|
-
if (
|
|
698
|
+
if (author) {
|
|
681
699
|
try {
|
|
700
|
+
const isGroupMsg = isJidGroup(msg.key.remoteJid);
|
|
682
701
|
const addr = signalRepository.jidToSignalProtocolAddress(author);
|
|
683
|
-
const
|
|
702
|
+
const failKey = isGroupMsg ? `sk:${msg.key.remoteJid}:${addr}` : addr;
|
|
703
|
+
const fails = (decryptFailCounts.get(failKey) || 0) + 1;
|
|
684
704
|
if (fails >= DECRYPT_FAIL_SESSION_RESET_LIMIT) {
|
|
685
|
-
|
|
686
|
-
|
|
705
|
+
if (isGroupMsg) {
|
|
706
|
+
await signalRepository.deleteSenderKey(msg.key.remoteJid, author);
|
|
707
|
+
logger.warn({ author, group: msg.key.remoteJid }, 'aunu: deleted stale sender-key record after repeated group decrypt failures; retry request asks the author to resend with a fresh distribution message');
|
|
708
|
+
}
|
|
709
|
+
else {
|
|
710
|
+
await signalRepository.deleteSession(author);
|
|
711
|
+
logger.warn({ author, addr }, 'aunu: deleted stale E2E session after repeated decrypt failures (Bad MAC self-heal); retry receipt carries fresh keys');
|
|
712
|
+
}
|
|
713
|
+
decryptFailCounts.delete(failKey);
|
|
687
714
|
forceIncludeKeys = true;
|
|
688
|
-
logger.warn({ author, addr }, 'aunu: deleted stale E2E session after repeated decrypt failures (Bad MAC self-heal); retry receipt carries fresh keys');
|
|
689
715
|
}
|
|
690
716
|
else {
|
|
691
|
-
decryptFailCounts.set(
|
|
717
|
+
decryptFailCounts.set(failKey, fails);
|
|
692
718
|
}
|
|
693
719
|
}
|
|
694
720
|
catch (healErr) {
|
|
@@ -708,7 +734,9 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
708
734
|
else {
|
|
709
735
|
if (author) {
|
|
710
736
|
try {
|
|
711
|
-
|
|
737
|
+
const addr = signalRepository.jidToSignalProtocolAddress(author);
|
|
738
|
+
decryptFailCounts.delete(addr);
|
|
739
|
+
decryptFailCounts.delete(`sk:${msg.key.remoteJid}:${addr}`);
|
|
712
740
|
}
|
|
713
741
|
catch { }
|
|
714
742
|
}
|
package/lib/Types/Signal.d.ts
CHANGED
|
@@ -54,6 +54,8 @@ export type SignalRepository = {
|
|
|
54
54
|
injectE2ESession(opts: E2ESessionOpts): Promise<void>;
|
|
55
55
|
/** AunuBaileys: delete the stored E2E session for a JID (used by the Bad MAC self-heal). */
|
|
56
56
|
deleteSession(jid: string): Promise<void>;
|
|
57
|
+
/** AunuBaileys: delete the stored sender-key record for a group author (stale group session heal). */
|
|
58
|
+
deleteSenderKey(group: string, authorJid: string): Promise<void>;
|
|
57
59
|
jidToSignalProtocolAddress(jid: string): string;
|
|
58
60
|
};
|
|
59
61
|
export {};
|
package/lib/Utils/signal.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export declare const generateOrGetPreKeys: (creds: AuthenticationCreds, range: n
|
|
|
16
16
|
export declare const xmppSignedPreKey: (key: SignedKeyPair) => BinaryNode;
|
|
17
17
|
export declare const xmppPreKey: (pair: KeyPair, id: number) => BinaryNode;
|
|
18
18
|
export declare const parseAndInjectE2ESessions: (node: BinaryNode, repository: SignalRepository) => Promise<void>;
|
|
19
|
+
/** AunuBaileys: session parsed from a retry receipt's embedded key bundle, or undefined when absent. */
|
|
20
|
+
export declare const parseRetryReceiptSession: (node: BinaryNode) => { registrationId: number; identityKey: Buffer; signedPreKey?: { keyId: number; publicKey: Buffer; signature?: Buffer }; preKey?: { keyId: number; publicKey: Buffer } } | undefined;
|
|
19
21
|
export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, excludeZeroDevices: boolean) => JidWithDevice[];
|
|
20
22
|
/**
|
|
21
23
|
* get the next N keys for upload or processing
|
package/lib/Utils/signal.js
CHANGED
|
@@ -55,6 +55,37 @@ export const xmppPreKey = (pair, id) => ({
|
|
|
55
55
|
{ tag: 'value', attrs: {}, content: pair.public }
|
|
56
56
|
]
|
|
57
57
|
});
|
|
58
|
+
/** AunuBaileys: parse the E2E key bundle embedded in a retry receipt.
|
|
59
|
+
* When a recipient device asks us to resend, its receipt often carries the
|
|
60
|
+
* device's OWN current key bundle (<keys> + <registration>) — the device
|
|
61
|
+
* itself stating "build your session to me from THIS". Upstream ignores it
|
|
62
|
+
* and re-fetches a bundle from the server instead; consuming the receipt's
|
|
63
|
+
* bundle skips that round trip and, more importantly, is authoritative for
|
|
64
|
+
* the exact requesting device (server lookups by JID are fragile for LID
|
|
65
|
+
* participants). Returns undefined when the receipt carries no bundle. */
|
|
66
|
+
export const parseRetryReceiptSession = (node) => {
|
|
67
|
+
const keysNode = getBinaryNodeChild(node, 'keys');
|
|
68
|
+
if (!keysNode) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
const identity = getBinaryNodeChildBuffer(keysNode, 'identity');
|
|
72
|
+
if (!identity) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
const extractKey = (key) => key
|
|
76
|
+
? {
|
|
77
|
+
keyId: getBinaryNodeChildUInt(key, 'id', 3),
|
|
78
|
+
publicKey: generateSignalPubKey(getBinaryNodeChildBuffer(key, 'value')),
|
|
79
|
+
signature: getBinaryNodeChildBuffer(key, 'signature')
|
|
80
|
+
}
|
|
81
|
+
: undefined;
|
|
82
|
+
return {
|
|
83
|
+
registrationId: getBinaryNodeChildUInt(node, 'registration', 4),
|
|
84
|
+
identityKey: generateSignalPubKey(identity),
|
|
85
|
+
signedPreKey: extractKey(getBinaryNodeChild(keysNode, 'skey')),
|
|
86
|
+
preKey: extractKey(getBinaryNodeChild(keysNode, 'key'))
|
|
87
|
+
};
|
|
88
|
+
};
|
|
58
89
|
export const parseAndInjectE2ESessions = async (node, repository) => {
|
|
59
90
|
const extractKey = (key) => key
|
|
60
91
|
? {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aunubaileys",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.18",
|
|
5
5
|
"description": "AunuBaileys \u2014 a WhatsApp Web API for bots that never sleep. Full upstream power with the beloved in-memory store restored and pairing that just works. Forged by AunuCore.",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"whatsapp",
|