improve: add detection and enhanced logging for corrupted Signal sessions

Adds comprehensive handling for Signal Protocol decryption errors including
Bad MAC and MessageCounterError which indicate corrupted/desynchronized sessions.

Changes:
- Add BAD_MAC_ERROR_TEXT constant for error detection
- Extend DECRYPTION_RETRY_CONFIG with corruptedSessionErrors array
- Add NACK_REASONS.CorruptedSession (553) for protocol-level reporting
- Add isCorruptedSessionError() utility function
- Enhanced error logging to differentiate corrupted sessions from other errors
- Include decryptionJid in error context for easier debugging

Impact:
- Better visibility into session corruption issues
- Clearer logs with ⚠️ warning emoji for corrupted sessions
- Foundation for future automatic session cleanup on corruption
- Helps diagnose and resolve "Bad MAC" errors in production

Related to PR #135 (session cleanup) - provides detection layer that
complements the preventive session cleanup already implemented.

https://claude.ai/code/session_01SoNUGBEWbJwWWws3F2fuzh
This commit is contained in:
Claude
2026-02-11 00:45:47 +00:00
parent 6f91152b2a
commit 22e9c12f49
+28 -4
View File
@@ -51,12 +51,14 @@ const storeMappingFromEnvelope = async (
export const NO_MESSAGE_FOUND_ERROR_TEXT = 'Message absent from node'
export const MISSING_KEYS_ERROR_TEXT = 'Key used already or never filled'
export const BAD_MAC_ERROR_TEXT = 'Bad MAC'
// Retry configuration for failed decryption
export const DECRYPTION_RETRY_CONFIG = {
maxRetries: 3,
baseDelayMs: 100,
sessionRecordErrors: ['No session record', 'SessionError: No session record']
sessionRecordErrors: ['No session record', 'SessionError: No session record'],
corruptedSessionErrors: ['Bad MAC', 'MessageCounterError', MISSING_KEYS_ERROR_TEXT]
}
export const NACK_REASONS = {
@@ -72,7 +74,8 @@ export const NACK_REASONS = {
UnhandledError: 500,
UnsupportedAdminRevoke: 550,
UnsupportedLIDGroup: 551,
DBOperationFailed: 552
DBOperationFailed: 552,
CorruptedSession: 553
}
type MessageType =
@@ -326,16 +329,28 @@ export const decryptMessageNode = (
fullMessage.message = msg
}
} catch (err: any) {
const isCorrupted = isCorruptedSessionError(err)
const isSessionRecord = isSessionRecordError(err)
const errorContext = {
key: fullMessage.key,
err,
messageType: tag === 'plaintext' ? 'plaintext' : attrs.type,
sender,
author,
isSessionRecordError: isSessionRecordError(err)
decryptionJid,
isSessionRecordError: isSessionRecord,
isCorruptedSession: isCorrupted
}
logger.error(errorContext, 'failed to decrypt message')
if (isCorrupted) {
logger.error(
errorContext,
'⚠️ Corrupted session detected - Bad MAC or MessageCounter error. Session may need to be recreated.'
)
} else {
logger.error(errorContext, 'failed to decrypt message')
}
fullMessage.messageStubType = proto.WebMessageInfo.StubType.CIPHERTEXT
fullMessage.messageStubParameters = [err.message.toString()]
@@ -359,3 +374,12 @@ function isSessionRecordError(error: any): boolean {
const errorMessage = error?.message || error?.toString() || ''
return DECRYPTION_RETRY_CONFIG.sessionRecordErrors.some(errorPattern => errorMessage.includes(errorPattern))
}
/**
* Utility function to check if an error indicates a corrupted session
* (Bad MAC, MessageCounterError, Key already used)
*/
export function isCorruptedSessionError(error: any): boolean {
const errorMessage = error?.message || error?.toString() || ''
return DECRYPTION_RETRY_CONFIG.corruptedSessionErrors.some(errorPattern => errorMessage.includes(errorPattern))
}