Compare commits
3 Commits
676c205666
...
480fc9a1d0
| Author | SHA1 | Date | |
|---|---|---|---|
| 480fc9a1d0 | |||
| 0558a57942 | |||
| 566e1f6c2e |
@@ -23,8 +23,8 @@ val gitShortSha = safeGitOutput("rev-parse", "--short", "HEAD") ?: "unknown"
|
|||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// Rosetta versioning — bump here on each release
|
// Rosetta versioning — bump here on each release
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
val rosettaVersionName = "1.3.9"
|
val rosettaVersionName = "1.4.0"
|
||||||
val rosettaVersionCode = 41 // Increment on each release
|
val rosettaVersionCode = 42 // Increment on each release
|
||||||
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
val customWebRtcAar = file("libs/libwebrtc-custom.aar")
|
||||||
|
|
||||||
android {
|
android {
|
||||||
|
|||||||
@@ -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)
|
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]
|
||||||
|
|||||||
@@ -8,14 +8,24 @@ enum class PushNotificationAction(val value: Int) {
|
|||||||
UNSUBSCRIBE(1)
|
UNSUBSCRIBE(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token type for push notifications.
|
||||||
|
*/
|
||||||
|
enum class PushTokenType(val value: Int) {
|
||||||
|
FCM(0),
|
||||||
|
VOIP_APNS(1)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push Notification packet (ID: 0x10)
|
* Push Notification packet (ID: 0x10)
|
||||||
* Отправка FCM/APNS токена на сервер для push-уведомлений (новый формат)
|
* Отправка FCM/APNS токена на сервер для push-уведомлений
|
||||||
* Совместим с React Native версией
|
* Передаёт tokenType (fcm/voip) и deviceId
|
||||||
*/
|
*/
|
||||||
class PacketPushNotification : Packet() {
|
class PacketPushNotification : Packet() {
|
||||||
var notificationsToken: String = ""
|
var notificationsToken: String = ""
|
||||||
var action: PushNotificationAction = PushNotificationAction.SUBSCRIBE
|
var action: PushNotificationAction = PushNotificationAction.SUBSCRIBE
|
||||||
|
var tokenType: PushTokenType = PushTokenType.FCM
|
||||||
|
var deviceId: String = ""
|
||||||
|
|
||||||
override fun getPacketId(): Int = 0x10
|
override fun getPacketId(): Int = 0x10
|
||||||
|
|
||||||
@@ -25,6 +35,11 @@ class PacketPushNotification : Packet() {
|
|||||||
1 -> PushNotificationAction.UNSUBSCRIBE
|
1 -> PushNotificationAction.UNSUBSCRIBE
|
||||||
else -> PushNotificationAction.SUBSCRIBE
|
else -> PushNotificationAction.SUBSCRIBE
|
||||||
}
|
}
|
||||||
|
tokenType = when (stream.readInt8()) {
|
||||||
|
1 -> PushTokenType.VOIP_APNS
|
||||||
|
else -> PushTokenType.FCM
|
||||||
|
}
|
||||||
|
deviceId = stream.readString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun send(): Stream {
|
override fun send(): Stream {
|
||||||
@@ -32,6 +47,8 @@ class PacketPushNotification : Packet() {
|
|||||||
stream.writeInt16(getPacketId())
|
stream.writeInt16(getPacketId())
|
||||||
stream.writeString(notificationsToken)
|
stream.writeString(notificationsToken)
|
||||||
stream.writeInt8(action.value)
|
stream.writeInt8(action.value)
|
||||||
|
stream.writeInt8(tokenType.value)
|
||||||
|
stream.writeString(deviceId)
|
||||||
return stream
|
return stream
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -738,9 +738,12 @@ object ProtocolManager {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val deviceId = appContext?.let { getOrCreateDeviceId(it) } ?: ""
|
||||||
val subPacket = PacketPushNotification().apply {
|
val subPacket = PacketPushNotification().apply {
|
||||||
notificationsToken = token
|
notificationsToken = token
|
||||||
action = PushNotificationAction.SUBSCRIBE
|
action = PushNotificationAction.SUBSCRIBE
|
||||||
|
tokenType = PushTokenType.FCM
|
||||||
|
this.deviceId = deviceId
|
||||||
}
|
}
|
||||||
send(subPacket)
|
send(subPacket)
|
||||||
lastSubscribedToken = token
|
lastSubscribedToken = token
|
||||||
|
|||||||
@@ -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