fix: detect identity key changes and reset sessions (align with WA Web) (#2307)
* feat(signal): add RetryReason enum and MAC error-based session recreation * feat(signal): add identity change detection with automatic session clearing * fix(signal): integrate identity change detection with pkmsg decryption This completes the identity change detection implementation by actually calling saveIdentity() during pkmsg decryption, which is CRITICAL for the feature to work. Changes: - Add extractIdentityFromPkmsg() function that parses PreKeyWhisperMessage protobuf to extract sender's identity key (33 bytes) - Call saveIdentity() BEFORE decryption in decryptMessage() for pkmsg type - Log when identity change is detected Flow: 1. Receive pkmsg from sender 2. Extract identity key from PreKeyWhisperMessage protobuf 3. Call storage.saveIdentity() which compares with stored key 4. If key changed → session is cleared atomically 5. Decryption proceeds with re-established session This matches WhatsApp Web's behavior where extractIdentityKey is called before handleNewSession (GysEGRAXCvh.js:40917, 48815). Ref: WhatsApp Web's extractIdentityKey (GysEGRAXCvh.js:48976-48998)
This commit is contained in:
@@ -39,6 +39,29 @@ export interface RetryStatistics {
|
||||
phoneRequests: number
|
||||
}
|
||||
|
||||
// Retry reason codes matching WhatsApp Web's Signal error codes.
|
||||
export enum RetryReason {
|
||||
UnknownError = 0,
|
||||
SignalErrorNoSession = 1,
|
||||
SignalErrorInvalidKey = 2,
|
||||
SignalErrorInvalidKeyId = 3,
|
||||
/** MAC verification failed - most common cause of decryption failures */
|
||||
SignalErrorInvalidMessage = 4,
|
||||
SignalErrorInvalidSignature = 5,
|
||||
SignalErrorFutureMessage = 6,
|
||||
/** Explicit MAC failure - session is definitely out of sync */
|
||||
SignalErrorBadMac = 7,
|
||||
SignalErrorInvalidSession = 8,
|
||||
SignalErrorInvalidMsgKey = 9,
|
||||
BadBroadcastEphemeralSetting = 10,
|
||||
UnknownCompanionNoPrekey = 11,
|
||||
AdvFailure = 12,
|
||||
StatusRevokeDelay = 13
|
||||
}
|
||||
|
||||
/** Error codes that indicate a MAC failure and require immediate session recreation */
|
||||
const MAC_ERROR_CODES = new Set([RetryReason.SignalErrorInvalidMessage, RetryReason.SignalErrorBadMac])
|
||||
|
||||
export class MessageRetryManager {
|
||||
private recentMessagesMap = new LRUCache<string, RecentMessage>({
|
||||
max: RECENT_MESSAGES_SIZE,
|
||||
@@ -107,9 +130,14 @@ export class MessageRetryManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a session should be recreated based on retry count and history
|
||||
* Check if a session should be recreated based on retry count, history, and error code.
|
||||
* MAC errors (codes 4 and 7) trigger immediate session recreation regardless of timeout.
|
||||
*/
|
||||
shouldRecreateSession(jid: string, hasSession: boolean): { reason: string; recreate: boolean } {
|
||||
shouldRecreateSession(
|
||||
jid: string,
|
||||
hasSession: boolean,
|
||||
errorCode?: RetryReason
|
||||
): { reason: string; recreate: boolean } {
|
||||
// If we don't have a session, always recreate
|
||||
if (!hasSession) {
|
||||
this.sessionRecreateHistory.set(jid, Date.now())
|
||||
@@ -120,6 +148,20 @@ export class MessageRetryManager {
|
||||
}
|
||||
}
|
||||
|
||||
// IMMEDIATE recreation for MAC errors - session is definitely out of sync
|
||||
if (errorCode !== undefined && MAC_ERROR_CODES.has(errorCode)) {
|
||||
this.sessionRecreateHistory.set(jid, Date.now())
|
||||
this.statistics.sessionRecreations++
|
||||
this.logger.warn(
|
||||
{ jid, errorCode: RetryReason[errorCode] },
|
||||
'MAC error detected, forcing immediate session recreation'
|
||||
)
|
||||
return {
|
||||
reason: `MAC error (code ${errorCode}: ${RetryReason[errorCode]}), immediate session recreation`,
|
||||
recreate: true
|
||||
}
|
||||
}
|
||||
|
||||
const now = Date.now()
|
||||
const prevTime = this.sessionRecreateHistory.get(jid)
|
||||
|
||||
@@ -136,6 +178,35 @@ export class MessageRetryManager {
|
||||
return { reason: '', recreate: false }
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse error code from retry receipt's retry node.
|
||||
* Returns undefined if no error code is present.
|
||||
*/
|
||||
parseRetryErrorCode(errorAttr: string | undefined): RetryReason | undefined {
|
||||
if (errorAttr === undefined || errorAttr === '') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const code = parseInt(errorAttr, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// Validate it's a known RetryReason
|
||||
if (code >= RetryReason.UnknownError && code <= RetryReason.StatusRevokeDelay) {
|
||||
return code as RetryReason
|
||||
}
|
||||
|
||||
return RetryReason.UnknownError
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an error code indicates a MAC failure
|
||||
*/
|
||||
isMacError(errorCode: RetryReason | undefined): boolean {
|
||||
return errorCode !== undefined && MAC_ERROR_CODES.has(errorCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Increment retry counter for a message
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user