Add E2EE diagnostic logging for debugging call encryption
All checks were successful
Android Kernel Build / build (push) Successful in 19m17s

- 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
This commit is contained in:
2026-04-01 16:28:23 +05:00
parent 0558a57942
commit 480fc9a1d0
3 changed files with 104 additions and 13 deletions

View File

@@ -531,6 +531,7 @@ public:
} }
uint32_t KeyFingerprint() const { return key_fingerprint_; } uint32_t KeyFingerprint() const { return key_fingerprint_; }
int FrameCount() const { return diag_count_.load(std::memory_order_relaxed); }
/* ── RefCountInterface ─────────────────────────────────────── */ /* ── RefCountInterface ─────────────────────────────────────── */
void AddRef() const override { void AddRef() const override {
@@ -724,6 +725,8 @@ public:
} }
uint32_t KeyFingerprint() const { return key_fingerprint_; } 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 ─────────────────────────────────────── */ /* ── RefCountInterface ─────────────────────────────────────── */
void AddRef() const override { 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<XChaCha20Encryptor *>(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<XChaCha20Decryptor *>(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<XChaCha20Decryptor *>(ptr);
return dec->BadStreak();
}
} /* extern "C" */ } /* extern "C" */

View File

@@ -736,6 +736,9 @@ object CallManager {
private fun onCallConnected() { private fun onCallConnected() {
appContext?.let { CallSoundManager.play(it, CallSoundManager.CallSound.CONNECTED) } 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") } updateState { it.copy(phase = CallPhase.ACTIVE, statusText = "Call active") }
durationJob?.cancel() durationJob?.cancel()
durationJob = durationJob =
@@ -832,6 +835,9 @@ object CallManager {
emitCallAttachmentIfNeeded(snapshot) emitCallAttachmentIfNeeded(snapshot)
resetRtcObjects() resetRtcObjects()
e2eeAvailable = true e2eeAvailable = true
lastScanLog = ""
lastHealthLog = ""
healthLogCount = 0
role = null role = null
roomId = "" roomId = ""
offerSent = false offerSent = false
@@ -893,14 +899,15 @@ object CallManager {
} }
sharedKeyBytes = keyBytes.copyOf(32) sharedKeyBytes = keyBytes.copyOf(32)
breadcrumb("KE: shared key fp=${sharedKeyBytes?.fingerprintHex(8)}") breadcrumb("KE: shared key fp=${sharedKeyBytes?.fingerprintHex(8)}")
// Frame-level diagnostics are enabled only for debug builds. // Enable frame-level diagnostics for all builds (needed for debugging E2EE issues).
if (BuildConfig.DEBUG) { try {
try { val dir = java.io.File(appContext!!.filesDir, "crash_reports")
val dir = java.io.File(appContext!!.filesDir, "crash_reports") if (!dir.exists()) dir.mkdirs()
if (!dir.exists()) dir.mkdirs() val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath
val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath XChaCha20E2EE.nativeOpenDiagFile(diagPath)
XChaCha20E2EE.nativeOpenDiagFile(diagPath) breadcrumb("E2EE: diag file opened at $diagPath")
} catch (_: Throwable) {} } catch (t: Throwable) {
breadcrumb("E2EE: diag file open FAILED: ${t.message}")
} }
// If sender track already exists, bind encryptor now. // If sender track already exists, bind encryptor now.
val existingSender = val existingSender =
@@ -916,8 +923,13 @@ object CallManager {
Log.i(TAG, "E2EE key ready (XChaCha20)") Log.i(TAG, "E2EE key ready (XChaCha20)")
} }
private var lastHealthLog = ""
private var healthLogCount = 0
private fun startE2EERebindLoopIfNeeded() { private fun startE2EERebindLoopIfNeeded() {
if (e2eeRebindJob?.isActive == true) return if (e2eeRebindJob?.isActive == true) return
healthLogCount = 0
lastHealthLog = ""
e2eeRebindJob = e2eeRebindJob =
scope.launch { scope.launch {
while (true) { while (true) {
@@ -934,10 +946,30 @@ object CallManager {
attachSenderE2EE(sender) attachSenderE2EE(sender)
} }
attachReceiverE2EEFromPeerConnection() 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() { private fun attachReceiverE2EEFromPeerConnection() {
val pc = peerConnection ?: return val pc = peerConnection ?: return
runCatching { runCatching {
@@ -956,7 +988,12 @@ object CallManager {
fromTransceivers++ 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 { }.onFailure {
breadcrumb("E2EE: attachReceiverE2EEFromPeerConnection failed: ${it.message}") breadcrumb("E2EE: attachReceiverE2EEFromPeerConnection failed: ${it.message}")
} }
@@ -1096,8 +1133,15 @@ object CallManager {
} }
private fun attachSenderE2EE(sender: RtpSender?) { private fun attachSenderE2EE(sender: RtpSender?) {
if (!e2eeAvailable) return if (!e2eeAvailable) {
val key = sharedKeyBytes ?: return breadcrumb("E2EE: attachSender SKIP — e2eeAvailable=false")
return
}
val key = sharedKeyBytes
if (key == null) {
breadcrumb("E2EE: attachSender SKIP — sharedKeyBytes=null")
return
}
if (sender == null) return if (sender == null) return
val mapKey = senderMapKey(sender) val mapKey = senderMapKey(sender)
val existing = senderEncryptors[mapKey] val existing = senderEncryptors[mapKey]
@@ -1134,8 +1178,15 @@ object CallManager {
} }
private fun attachReceiverE2EE(receiver: RtpReceiver?) { private fun attachReceiverE2EE(receiver: RtpReceiver?) {
if (!e2eeAvailable) return if (!e2eeAvailable) {
val key = sharedKeyBytes ?: return breadcrumb("E2EE: attachReceiver SKIP — e2eeAvailable=false")
return
}
val key = sharedKeyBytes
if (key == null) {
breadcrumb("E2EE: attachReceiver SKIP — sharedKeyBytes=null")
return
}
if (receiver == null) return if (receiver == null) return
val mapKey = receiverMapKey(receiver) val mapKey = receiverMapKey(receiver)
val existing = receiverDecryptors[mapKey] val existing = receiverDecryptors[mapKey]

View File

@@ -72,6 +72,8 @@ object XChaCha20E2EE {
override fun getNativeFrameEncryptor(): Long = nativePtr override fun getNativeFrameEncryptor(): Long = nativePtr
fun frameCount(): Int = if (nativePtr != 0L) nativeGetEncryptorFrameCount(nativePtr) else -1
fun dispose() { fun dispose() {
if (nativePtr != 0L) nativeReleaseEncryptor(nativePtr) if (nativePtr != 0L) nativeReleaseEncryptor(nativePtr)
} }
@@ -89,6 +91,9 @@ object XChaCha20E2EE {
override fun getNativeFrameDecryptor(): Long = nativePtr 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() { fun dispose() {
if (nativePtr != 0L) nativeReleaseDecryptor(nativePtr) if (nativePtr != 0L) nativeReleaseDecryptor(nativePtr)
} }
@@ -99,8 +104,11 @@ object XChaCha20E2EE {
@JvmStatic private external fun nativeHSalsa20(rawDh: ByteArray): ByteArray @JvmStatic private external fun nativeHSalsa20(rawDh: ByteArray): ByteArray
@JvmStatic private external fun nativeCreateEncryptor(key: ByteArray): Long @JvmStatic private external fun nativeCreateEncryptor(key: ByteArray): Long
@JvmStatic private external fun nativeReleaseEncryptor(ptr: 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 nativeCreateDecryptor(key: ByteArray): Long
@JvmStatic private external fun nativeReleaseDecryptor(ptr: 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 private external fun nativeInstallCrashHandler(path: String)
@JvmStatic external fun nativeOpenDiagFile(path: String) @JvmStatic external fun nativeOpenDiagFile(path: String)
@JvmStatic external fun nativeCloseDiagFile() @JvmStatic external fun nativeCloseDiagFile()