Работающие звонки

This commit is contained in:
2026-03-27 03:12:04 +05:00
parent 9cca071bd8
commit b663450db5
10 changed files with 343 additions and 53 deletions

View File

@@ -16,6 +16,7 @@
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <time.h>
#include <stdarg.h>
#include <stdio.h>
#include <android/log.h>
@@ -125,6 +126,12 @@ struct AdditionalTsState {
uint64_t base_timestamp = 0;
};
struct SenderTsOffsetState {
bool initialized = false;
bool enabled = false;
uint64_t offset = 0;
};
static inline uint16_t load16_be(const uint8_t* p) {
return (uint16_t)(((uint16_t)p[0] << 8) | (uint16_t)p[1]);
}
@@ -334,6 +341,25 @@ static inline void fill_nonce_from_ts32(uint32_t ts, uint8_t nonce[24]) {
nonce[7] = (uint8_t)(ts);
}
static inline void fill_nonce_from_ts64(uint64_t ts, uint8_t nonce[24]) {
nonce[0] = (uint8_t)(ts >> 56);
nonce[1] = (uint8_t)(ts >> 48);
nonce[2] = (uint8_t)(ts >> 40);
nonce[3] = (uint8_t)(ts >> 32);
nonce[4] = (uint8_t)(ts >> 24);
nonce[5] = (uint8_t)(ts >> 16);
nonce[6] = (uint8_t)(ts >> 8);
nonce[7] = (uint8_t)(ts);
}
static inline uint64_t monotonic_48k_ticks() {
struct timespec ts {};
if (clock_gettime(CLOCK_MONOTONIC, &ts) != 0) return 0;
const uint64_t sec = (uint64_t)ts.tv_sec;
const uint64_t nsec = (uint64_t)ts.tv_nsec;
return sec * 48000ULL + (nsec * 48000ULL) / 1000000000ULL;
}
static inline uint32_t opus_base_frame_samples(uint8_t config) {
// RFC 6716 TOC config mapping at 48 kHz.
if (config <= 11) {
@@ -550,6 +576,7 @@ public:
bool nonce_from_generated_ts = false;
bool nonce_from_additional_data = false;
bool additional_was_rtp_header = false;
bool additional_used_mono_offset = false;
uint32_t generated_ts_used = 0;
// Build nonce from RTP timestamp in additional_data (preferred).
@@ -579,23 +606,36 @@ public:
}
}
if (nonce_from_rtp_header && header_size <= frame.size()) {
// Keep RTP header clear, encrypt payload only.
if (header_size > 0) {
memcpy(encrypted_frame.data(), frame.data(), header_size);
// Some Android sender pipelines expose stream-relative ad8 timestamps
// (0, 960, 1920, ...), while desktop receiver expects an absolute base.
// For interop, add a monotonic 48k offset once when first ad8 is tiny.
if (nonce_from_additional_data &&
additional_data.size() == 8 &&
!additional_was_rtp_header &&
additional_data.data() != nullptr) {
const uint64_t ad_ts64 = load64_be(additional_data.data());
if (!sender_ts_offset_.initialized) {
sender_ts_offset_.initialized = true;
// Keep pure raw-abs mode by default; desktop is the source of truth.
sender_ts_offset_.enabled = false;
sender_ts_offset_.offset = 0ULL;
diag_event("ENC ad8-base init ssrc=%u ad_ts=%llu use_mono=%d mono_off=%llu\n",
ssrc,
(unsigned long long)ad_ts64,
sender_ts_offset_.enabled ? 1 : 0,
(unsigned long long)sender_ts_offset_.offset);
}
if (sender_ts_offset_.enabled) {
const uint64_t ts_adj = ad_ts64 + sender_ts_offset_.offset;
fill_nonce_from_ts64(ts_adj, nonce);
additional_used_mono_offset = true;
}
const size_t payload_size = frame.size() - header_size;
rosetta_xchacha20_xor(
encrypted_frame.data() + header_size,
frame.data() + header_size,
payload_size,
nonce,
key_);
} else {
// Legacy path: frame is payload-only.
rosetta_xchacha20_xor(encrypted_frame.data(),
frame.data(), frame.size(), nonce, key_);
}
// Desktop createEncodedStreams encrypts full encoded chunk.
// To stay wire-compatible, do not preserve any leading RTP-like bytes.
rosetta_xchacha20_xor(encrypted_frame.data(),
frame.data(), frame.size(), nonce, key_);
*bytes_written = frame.size();
if (nonce_from_generated_ts) {
@@ -628,7 +668,9 @@ public:
: (nonce_from_additional_data
? (additional_was_rtp_header
? "ad-rtp"
: (additional_used_relative_ts ? "raw-rel" : "raw-abs"))
: (additional_used_mono_offset
? "raw-abs+mono"
: (additional_used_relative_ts ? "raw-rel" : "raw-abs")))
: "raw-abs"));
LOGI("ENC frame#%d mt=%s ssrc=%u sz=%zu ad=%zu hdr=%zu mode=%s nonce_ts=%u gen_ts=%u next_step=%u rtp_ok=%d rtp_seq=%u rtp_ts=%u rtp_ssrc=%u opus_ok=%d key_fp=%08x in_h=%08x out_h=%08x ad8=%02x%02x%02x%02x%02x%02x%02x%02x",
n, media_type_name(media_type), ssrc, frame.size(), additional_data.size(), header_size, mode,
@@ -663,6 +705,7 @@ private:
mutable std::atomic<int> diag_count_{0};
mutable RtpProbeState rtp_probe_;
mutable GeneratedTsState generated_ts_;
mutable SenderTsOffsetState sender_ts_offset_;
uint32_t key_fingerprint_ = 0;
uint8_t key_[32];
};
@@ -746,20 +789,8 @@ public:
bool used_generated_resync = false;
if (nonce_from_rtp_header && header_size <= encrypted_frame.size()) {
if (header_size > 0) {
memcpy(frame.data(), encrypted_frame.data(), header_size);
}
const size_t payload_size = encrypted_frame.size() - header_size;
rosetta_xchacha20_xor(
frame.data() + header_size,
encrypted_frame.data() + header_size,
payload_size,
nonce,
key_);
} else {
rosetta_xchacha20_xor(frame.data(), encrypted_frame.data(), encrypted_frame.size(), nonce, key_);
}
// Desktop createEncodedStreams decrypts full encoded chunk.
rosetta_xchacha20_xor(frame.data(), encrypted_frame.data(), encrypted_frame.size(), nonce, key_);
if (nonce_from_additional_data) {
bool plausible = is_plausible_decrypted_audio_frame(frame.data(), encrypted_frame.size());

View File

@@ -6,7 +6,6 @@ import android.util.Log
import com.rosetta.messenger.data.MessageRepository
import java.security.MessageDigest
import java.security.SecureRandom
import java.util.IdentityHashMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@@ -137,8 +136,8 @@ object CallManager {
// E2EE (XChaCha20 — compatible with Desktop)
private var sharedKeyBytes: ByteArray? = null
private val senderEncryptors = IdentityHashMap<RtpSender, XChaCha20E2EE.Encryptor>()
private val receiverDecryptors = IdentityHashMap<RtpReceiver, XChaCha20E2EE.Decryptor>()
private val senderEncryptors = LinkedHashMap<String, XChaCha20E2EE.Encryptor>()
private val receiverDecryptors = LinkedHashMap<String, XChaCha20E2EE.Decryptor>()
private var pendingAudioSenderForE2ee: RtpSender? = null
private var lastRemoteOfferFingerprint: String = ""
private var lastLocalOfferFingerprint: String = ""
@@ -1063,11 +1062,22 @@ object CallManager {
breadcrumb("STATE[$marker] ${buildStateSnapshot()}")
}
private fun senderMapKey(sender: RtpSender): String {
val id = runCatching { sender.id() }.getOrNull().orEmpty()
return if (id.isNotBlank()) "sid:$id" else "sender@${System.identityHashCode(sender)}"
}
private fun receiverMapKey(receiver: RtpReceiver): String {
val id = runCatching { receiver.id() }.getOrNull().orEmpty()
return if (id.isNotBlank()) "rid:$id" else "recv@${System.identityHashCode(receiver)}"
}
private fun attachSenderE2EE(sender: RtpSender?) {
if (!e2eeAvailable) return
val key = sharedKeyBytes ?: return
if (sender == null) return
val existing = senderEncryptors[sender]
val mapKey = senderMapKey(sender)
val existing = senderEncryptors[mapKey]
if (existing != null) {
runCatching { sender.setFrameEncryptor(existing) }
return
@@ -1086,7 +1096,7 @@ object CallManager {
breadcrumb("4. calling sender.setFrameEncryptor…")
sender.setFrameEncryptor(enc)
breadcrumb("5. setFrameEncryptor OK!")
senderEncryptors[sender] = enc
senderEncryptors[mapKey] = enc
pendingAudioSenderForE2ee = null
} catch (e: Throwable) {
saveCrashReport("attachSenderE2EE failed", e)
@@ -1104,7 +1114,8 @@ object CallManager {
if (!e2eeAvailable) return
val key = sharedKeyBytes ?: return
if (receiver == null) return
val existing = receiverDecryptors[receiver]
val mapKey = receiverMapKey(receiver)
val existing = receiverDecryptors[mapKey]
if (existing != null) {
runCatching { receiver.setFrameDecryptor(existing) }
return
@@ -1123,7 +1134,7 @@ object CallManager {
breadcrumb("9. calling receiver.setFrameDecryptor…")
receiver.setFrameDecryptor(dec)
breadcrumb("10. setFrameDecryptor OK!")
receiverDecryptors[receiver] = dec
receiverDecryptors[mapKey] = dec
} catch (e: Throwable) {
saveCrashReport("attachReceiverE2EE failed", e)
Log.e(TAG, "E2EE: receiver decryptor failed", e)

View File

@@ -1,5 +1,6 @@
package com.rosetta.messenger.ui.crashlogs
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
@@ -8,13 +9,16 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Share
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
@@ -263,6 +267,8 @@ private fun CrashDetailScreen(
onDelete: () -> Unit
) {
var showDeleteDialog by remember { mutableStateOf(false) }
val clipboardManager = LocalClipboardManager.current
val context = LocalContext.current
Scaffold(
topBar = {
@@ -274,6 +280,14 @@ private fun CrashDetailScreen(
}
},
actions = {
IconButton(
onClick = {
clipboardManager.setText(AnnotatedString(crashReport.content))
Toast.makeText(context, "Full log copied", Toast.LENGTH_SHORT).show()
}
) {
Icon(Icons.Default.ContentCopy, contentDescription = "Copy Full Log")
}
IconButton(onClick = { /* TODO: Share */ }) {
Icon(Icons.Default.Share, contentDescription = "Share")
}