fix: align Bad MAC retry receipt with WA Desktop behavior

fix: align Bad MAC retry receipt with WA Desktop behavior
This commit is contained in:
Renato Alcara
2026-03-19 23:48:44 -03:00
committed by GitHub
+41 -39
View File
@@ -49,9 +49,12 @@ import {
getStatusFromReceiptType, getStatusFromReceiptType,
handleIdentityChange, handleIdentityChange,
hkdf, hkdf,
BAD_MAC_ERROR_TEXT,
DECRYPTION_RETRY_CONFIG,
MISSING_KEYS_ERROR_TEXT, MISSING_KEYS_ERROR_TEXT,
NACK_REASONS, NACK_REASONS,
NO_MESSAGE_FOUND_ERROR_TEXT, NO_MESSAGE_FOUND_ERROR_TEXT,
RetryReason,
normalizeKeyLidToPn, normalizeKeyLidToPn,
normalizeMessageJids, normalizeMessageJids,
resolveLidToPn, resolveLidToPn,
@@ -1213,7 +1216,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
return await query(stanza) return await query(stanza)
} }
const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false) => { const sendRetryRequest = async (node: BinaryNode, forceIncludeKeys = false, decryptionError?: string) => {
const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '') const { fullMessage } = decodeMessageNode(node, authState.creds.me!.id, authState.creds.me!.lid || '')
const { key: msgKey } = fullMessage const { key: msgKey } = fullMessage
const msgId = msgKey.id! const msgId = msgKey.id!
@@ -1311,39 +1314,39 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds const { account, signedPreKey, signedIdentityKey: identityKey } = authState.creds
const fromJid = node.attrs.from! const fromJid = node.attrs.from!
// Check if we should recreate the session // Derive the Signal error code from the actual decryption failure message.
let shouldRecreateSession = false // Sent in the retry receipt so the peer (even another InfiniteAPI instance)
let recreateReason = '' // knows the exact failure type and can recreate the session immediately
// instead of falling back to the 1-hour timeout.
if (enableAutoSessionRecreation && messageRetryManager && retryCount >= 1) { //
try { // Codes mirror RetryReason enum in message-retry-manager.ts:
// Check if we have a session with this JID // 0 = UnknownError | 1 = NoSession | 2 = InvalidKey
const sessionId = signalRepository.jidToSignalProtocolAddress(fromJid) // 3 = InvalidKeyId | 4 = InvalidMessage | 7 = BadMac
const hasSession = await signalRepository.validateSession(fromJid) //
// Uses DECRYPTION_RETRY_CONFIG error lists (single source of truth in
// Extract error code from retry node if present (for MAC error detection) // decode-wa-message.ts) so additions to those lists are picked up here
const retryNode = getBinaryNodeChild(node, 'retry') // automatically.
const errorAttr = retryNode?.attrs?.error //
const errorCode = messageRetryManager.parseRetryErrorCode(errorAttr) // NOTE: We do NOT delete the session here (receiver side). The Signal Protocol
// recovers automatically when the sender's pkmsg arrives — it overwrites the
const result = messageRetryManager.shouldRecreateSession(fromJid, hasSession.exists, errorCode) // corrupted session. Deleting prematurely creates a race window where no session
shouldRecreateSession = result.recreate // exists, which can cause "No Session" errors on concurrent messages.
recreateReason = result.reason const retryErrorCode = (() => {
if (!decryptionError) return RetryReason.UnknownError
if (shouldRecreateSession) { // Bad MAC must be checked first — it is also in corruptedSessionErrors
logger.debug({ fromJid, retryCount, reason: recreateReason, errorCode }, 'recreating session for retry') // but warrants the more specific code 7 over the generic code 4.
// Delete existing session to force recreation if (decryptionError.includes(BAD_MAC_ERROR_TEXT)) return RetryReason.SignalErrorBadMac
// CRITICAL: Use same transaction key as encrypt/decrypt operations to prevent race // MessageCounterError and other corrupted-session variants
// Using meId ensures this delete serializes with sendMessage() and other session operations if (DECRYPTION_RETRY_CONFIG.corruptedSessionErrors.some(e => decryptionError.includes(e))) return RetryReason.SignalErrorInvalidMessage
await authState.keys.transaction(async () => { // Missing / invalid session record
await authState.keys.set({ session: { [sessionId]: null } }) if (DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(e => decryptionError.includes(e)) ||
}, authState.creds.me?.id || 'session-operation') /no\s+(open\s+)?sessions?/i.test(decryptionError)) return RetryReason.SignalErrorNoSession
forceIncludeKeys = true // PreKey / key-id errors
} if (/pre\s*key/i.test(decryptionError)) return RetryReason.SignalErrorInvalidKeyId
} catch (error) { // Identity / key errors
logger.warn({ error, fromJid }, 'failed to check session recreation') if (/invalid\s*key|untrusted\s*identity/i.test(decryptionError)) return RetryReason.SignalErrorInvalidKey
} return RetryReason.UnknownError
} })()
if (retryCount <= 2) { if (retryCount <= 2) {
// Use new retry manager for phone requests if available // Use new retry manager for phone requests if available
@@ -1384,8 +1387,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
id: node.attrs.id!, id: node.attrs.id!,
t: node.attrs.t!, t: node.attrs.t!,
v: '1', v: '1',
// ADD ERROR FIELD error: retryErrorCode.toString()
error: '0'
} }
}, },
{ {
@@ -1404,7 +1406,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
receipt.attrs.participant = node.attrs.participant receipt.attrs.participant = node.attrs.participant
} }
if (retryCount > 1 || forceIncludeKeys || shouldRecreateSession) { if (retryCount > 1 || forceIncludeKeys) {
const { update, preKeys } = await getNextPreKeys(authState, 1) const { update, preKeys } = await getNextPreKeys(authState, 1)
const [keyId] = Object.keys(preKeys) const [keyId] = Object.keys(preKeys)
@@ -2507,7 +2509,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
} }
const encNode = getBinaryNodeChild(node, 'enc') const encNode = getBinaryNodeChild(node, 'enc')
await sendRetryRequest(node, !encNode) await sendRetryRequest(node, !encNode, errorMessage)
if (retryRequestDelayMs) { if (retryRequestDelayMs) {
await delay(retryRequestDelayMs) await delay(retryRequestDelayMs)
} }
@@ -2516,7 +2518,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
// Still attempt retry even if pre-key upload failed // Still attempt retry even if pre-key upload failed
try { try {
const encNode = getBinaryNodeChild(node, 'enc') const encNode = getBinaryNodeChild(node, 'enc')
await sendRetryRequest(node, !encNode) await sendRetryRequest(node, !encNode, errorMessage)
} catch (retryErr) { } catch (retryErr) {
logger.error({ retryErr }, 'Failed to send retry after error handling') logger.error({ retryErr }, 'Failed to send retry after error handling')
} }