From 480fc9a1d0c79865c7622f26329046c2e59b25ba Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 1 Apr 2026 16:28:23 +0500 Subject: [PATCH] Add E2EE diagnostic logging for debugging call encryption - Enable diag file for all builds (was DEBUG-only) - Add native frame count + bad streak query methods (JNI) - Add periodic E2EE-HEALTH log with enc/dec frame counts - Reduce scan receivers spam (only log on state change) - Log E2EE state on call connected - Log when attachSender/attachReceiver skips due to missing key --- app/src/main/cpp/rosetta_e2ee.cpp | 32 ++++++++ .../rosetta/messenger/network/CallManager.kt | 77 +++++++++++++++---- .../messenger/network/XChaCha20E2EE.kt | 8 ++ 3 files changed, 104 insertions(+), 13 deletions(-) diff --git a/app/src/main/cpp/rosetta_e2ee.cpp b/app/src/main/cpp/rosetta_e2ee.cpp index 8dc7ccb..5abd46c 100644 --- a/app/src/main/cpp/rosetta_e2ee.cpp +++ b/app/src/main/cpp/rosetta_e2ee.cpp @@ -531,6 +531,7 @@ public: } uint32_t KeyFingerprint() const { return key_fingerprint_; } + int FrameCount() const { return diag_count_.load(std::memory_order_relaxed); } /* ── RefCountInterface ─────────────────────────────────────── */ void AddRef() const override { @@ -724,6 +725,8 @@ public: } uint32_t KeyFingerprint() const { return key_fingerprint_; } + int FrameCount() const { return diag_count_.load(std::memory_order_relaxed); } + uint32_t BadStreak() const { return bad_audio_streak_.load(std::memory_order_relaxed); } /* ── RefCountInterface ─────────────────────────────────────── */ void AddRef() const override { @@ -1121,4 +1124,33 @@ Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeCloseDiagFile( } } +/* ── Query frame counts for health checks ────────────────────── */ + +JNIEXPORT jint JNICALL +Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeGetEncryptorFrameCount( + JNIEnv *, jclass, jlong ptr) +{ + if (ptr == 0) return -1; + auto *enc = reinterpret_cast(ptr); + return enc->FrameCount(); +} + +JNIEXPORT jint JNICALL +Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeGetDecryptorFrameCount( + JNIEnv *, jclass, jlong ptr) +{ + if (ptr == 0) return -1; + auto *dec = reinterpret_cast(ptr); + return dec->FrameCount(); +} + +JNIEXPORT jint JNICALL +Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeGetDecryptorBadStreak( + JNIEnv *, jclass, jlong ptr) +{ + if (ptr == 0) return -1; + auto *dec = reinterpret_cast(ptr); + return dec->BadStreak(); +} + } /* extern "C" */ diff --git a/app/src/main/java/com/rosetta/messenger/network/CallManager.kt b/app/src/main/java/com/rosetta/messenger/network/CallManager.kt index 8d3e1d3..41dded5 100644 --- a/app/src/main/java/com/rosetta/messenger/network/CallManager.kt +++ b/app/src/main/java/com/rosetta/messenger/network/CallManager.kt @@ -736,6 +736,9 @@ object CallManager { private fun onCallConnected() { appContext?.let { CallSoundManager.play(it, CallSoundManager.CallSound.CONNECTED) } + val keyFp = sharedKeyBytes?.fingerprintHex(8) ?: "null" + breadcrumb("CONNECTED: e2eeAvail=$e2eeAvailable keyFp=$keyFp sEnc=${senderEncryptors.size} rDec=${receiverDecryptors.size} nativeLoaded=${XChaCha20E2EE.nativeLoaded}") + breadcrumbState("onCallConnected") updateState { it.copy(phase = CallPhase.ACTIVE, statusText = "Call active") } durationJob?.cancel() durationJob = @@ -832,6 +835,9 @@ object CallManager { emitCallAttachmentIfNeeded(snapshot) resetRtcObjects() e2eeAvailable = true + lastScanLog = "" + lastHealthLog = "" + healthLogCount = 0 role = null roomId = "" offerSent = false @@ -893,14 +899,15 @@ object CallManager { } sharedKeyBytes = keyBytes.copyOf(32) breadcrumb("KE: shared key fp=${sharedKeyBytes?.fingerprintHex(8)}") - // Frame-level diagnostics are enabled only for debug builds. - if (BuildConfig.DEBUG) { - try { - val dir = java.io.File(appContext!!.filesDir, "crash_reports") - if (!dir.exists()) dir.mkdirs() - val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath - XChaCha20E2EE.nativeOpenDiagFile(diagPath) - } catch (_: Throwable) {} + // Enable frame-level diagnostics for all builds (needed for debugging E2EE issues). + try { + val dir = java.io.File(appContext!!.filesDir, "crash_reports") + if (!dir.exists()) dir.mkdirs() + val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath + XChaCha20E2EE.nativeOpenDiagFile(diagPath) + breadcrumb("E2EE: diag file opened at $diagPath") + } catch (t: Throwable) { + breadcrumb("E2EE: diag file open FAILED: ${t.message}") } // If sender track already exists, bind encryptor now. val existingSender = @@ -916,8 +923,13 @@ object CallManager { Log.i(TAG, "E2EE key ready (XChaCha20)") } + private var lastHealthLog = "" + private var healthLogCount = 0 + private fun startE2EERebindLoopIfNeeded() { if (e2eeRebindJob?.isActive == true) return + healthLogCount = 0 + lastHealthLog = "" e2eeRebindJob = scope.launch { while (true) { @@ -934,10 +946,30 @@ object CallManager { attachSenderE2EE(sender) } attachReceiverE2EEFromPeerConnection() + // Periodic health check: log frame counts from native + healthLogCount++ + if (healthLogCount % 4 == 0) { // every ~6s + logE2EEHealth() + } } } } + private fun logE2EEHealth() { + val encFrames = senderEncryptors.values.firstOrNull()?.frameCount() ?: -1 + val decFrames = receiverDecryptors.values.firstOrNull()?.frameCount() ?: -1 + val badStreak = receiverDecryptors.values.firstOrNull()?.badStreak() ?: -1 + val keyFp = sharedKeyBytes?.fingerprintHex(8) ?: "null" + val health = "enc=$encFrames dec=$decFrames bad=$badStreak keyFp=$keyFp sEnc=${senderEncryptors.size} rDec=${receiverDecryptors.size}" + // Only log if state changed or every 5th iteration to avoid spam + if (health != lastHealthLog || healthLogCount % 20 == 0) { + breadcrumb("E2EE-HEALTH: $health") + lastHealthLog = health + } + } + + private var lastScanLog = "" + private fun attachReceiverE2EEFromPeerConnection() { val pc = peerConnection ?: return runCatching { @@ -956,7 +988,12 @@ object CallManager { fromTransceivers++ } } - breadcrumb("E2EE: scan receivers attached recv=$fromReceivers tx=$fromTransceivers totalMap=${receiverDecryptors.size}") + val hasKey = sharedKeyBytes != null + val scanLog = "recv=$fromReceivers tx=$fromTransceivers totalMap=${receiverDecryptors.size} hasKey=$hasKey e2eeAvail=$e2eeAvailable" + if (scanLog != lastScanLog) { + breadcrumb("E2EE: scan receivers attached $scanLog") + lastScanLog = scanLog + } }.onFailure { breadcrumb("E2EE: attachReceiverE2EEFromPeerConnection failed: ${it.message}") } @@ -1096,8 +1133,15 @@ object CallManager { } private fun attachSenderE2EE(sender: RtpSender?) { - if (!e2eeAvailable) return - val key = sharedKeyBytes ?: return + if (!e2eeAvailable) { + breadcrumb("E2EE: attachSender SKIP — e2eeAvailable=false") + return + } + val key = sharedKeyBytes + if (key == null) { + breadcrumb("E2EE: attachSender SKIP — sharedKeyBytes=null") + return + } if (sender == null) return val mapKey = senderMapKey(sender) val existing = senderEncryptors[mapKey] @@ -1134,8 +1178,15 @@ object CallManager { } private fun attachReceiverE2EE(receiver: RtpReceiver?) { - if (!e2eeAvailable) return - val key = sharedKeyBytes ?: return + if (!e2eeAvailable) { + breadcrumb("E2EE: attachReceiver SKIP — e2eeAvailable=false") + return + } + val key = sharedKeyBytes + if (key == null) { + breadcrumb("E2EE: attachReceiver SKIP — sharedKeyBytes=null") + return + } if (receiver == null) return val mapKey = receiverMapKey(receiver) val existing = receiverDecryptors[mapKey] diff --git a/app/src/main/java/com/rosetta/messenger/network/XChaCha20E2EE.kt b/app/src/main/java/com/rosetta/messenger/network/XChaCha20E2EE.kt index 1e0e1d9..1b128f8 100644 --- a/app/src/main/java/com/rosetta/messenger/network/XChaCha20E2EE.kt +++ b/app/src/main/java/com/rosetta/messenger/network/XChaCha20E2EE.kt @@ -72,6 +72,8 @@ object XChaCha20E2EE { override fun getNativeFrameEncryptor(): Long = nativePtr + fun frameCount(): Int = if (nativePtr != 0L) nativeGetEncryptorFrameCount(nativePtr) else -1 + fun dispose() { if (nativePtr != 0L) nativeReleaseEncryptor(nativePtr) } @@ -89,6 +91,9 @@ object XChaCha20E2EE { override fun getNativeFrameDecryptor(): Long = nativePtr + fun frameCount(): Int = if (nativePtr != 0L) nativeGetDecryptorFrameCount(nativePtr) else -1 + fun badStreak(): Int = if (nativePtr != 0L) nativeGetDecryptorBadStreak(nativePtr) else -1 + fun dispose() { if (nativePtr != 0L) nativeReleaseDecryptor(nativePtr) } @@ -99,8 +104,11 @@ object XChaCha20E2EE { @JvmStatic private external fun nativeHSalsa20(rawDh: ByteArray): ByteArray @JvmStatic private external fun nativeCreateEncryptor(key: ByteArray): Long @JvmStatic private external fun nativeReleaseEncryptor(ptr: Long) + @JvmStatic private external fun nativeGetEncryptorFrameCount(ptr: Long): Int @JvmStatic private external fun nativeCreateDecryptor(key: ByteArray): Long @JvmStatic private external fun nativeReleaseDecryptor(ptr: Long) + @JvmStatic private external fun nativeGetDecryptorFrameCount(ptr: Long): Int + @JvmStatic private external fun nativeGetDecryptorBadStreak(ptr: Long): Int @JvmStatic private external fun nativeInstallCrashHandler(path: String) @JvmStatic external fun nativeOpenDiagFile(path: String) @JvmStatic external fun nativeCloseDiagFile()