Add E2EE diagnostic logging for debugging call encryption
All checks were successful
Android Kernel Build / build (push) Successful in 19m17s
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:
@@ -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" */
|
||||||
|
|||||||
@@ -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)
|
||||||
} catch (_: Throwable) {}
|
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.
|
// 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,9 +946,29 @@ 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
|
||||||
@@ -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]
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user