Попытка обновления шифрования звонков и работа над UI

This commit is contained in:
2026-03-25 01:47:12 +05:00
parent 419101a4a9
commit 530047c5d0
14 changed files with 1326 additions and 99 deletions

View File

@@ -43,6 +43,19 @@ android {
// Optimize Lottie animations // Optimize Lottie animations
manifestPlaceholders["enableLottieOptimizations"] = "true" manifestPlaceholders["enableLottieOptimizations"] = "true"
ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a", "x86_64") }
externalNativeBuild {
cmake { cppFlags("-std=c++17") }
}
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
} }
signingConfigs { signingConfigs {

View File

@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.22.1)
project(rosetta_e2ee LANGUAGES C CXX)
set(CMAKE_C_STANDARD 11)
set(CMAKE_CXX_STANDARD 17)
add_library(rosetta_e2ee SHARED
crypto.c
rosetta_e2ee.cpp
)
target_include_directories(rosetta_e2ee PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
# Hide all C++ symbols to avoid ODR clashes with WebRTC's .so
set_target_properties(rosetta_e2ee PROPERTIES
CXX_VISIBILITY_PRESET hidden
C_VISIBILITY_PRESET hidden
)
# Match WebRTC SDK build flags:
# -fno-rtti -fno-exceptions — standard WebRTC flags
# -fexperimental-relative-c++-abi-vtables — WebRTC uses relative vtables
# (32-bit offsets instead of 64-bit absolute pointers in vtable).
# Without this, setFrameEncryptor crashes with SIGSEGV because WebRTC
# reads our 64-bit pointers as 32-bit offsets.
target_compile_options(rosetta_e2ee PRIVATE
-fno-rtti
-fno-exceptions
-fexperimental-relative-c++-abi-vtables
)
find_library(log-lib log)
target_link_libraries(rosetta_e2ee ${log-lib})

248
app/src/main/cpp/crypto.c Normal file
View File

@@ -0,0 +1,248 @@
/**
* Minimal crypto primitives for Rosetta E2EE:
* - HSalsa20 (for nacl.box.before() compatible key exchange)
* - HChaCha20 + ChaCha20-IETF → XChaCha20 (for frame encryption)
*
* Based on the public-domain algorithms by D.J. Bernstein.
*/
#include "crypto.h"
#include <string.h>
/* ── helpers ─────────────────────────────────────────────────── */
#define ROTL32(v, n) (((v) << (n)) | ((v) >> (32 - (n))))
static uint32_t load32_le(const uint8_t *p) {
return (uint32_t)p[0]
| (uint32_t)p[1] << 8
| (uint32_t)p[2] << 16
| (uint32_t)p[3] << 24;
}
static void store32_le(uint8_t *p, uint32_t v) {
p[0] = (uint8_t)(v);
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
}
/* "expand 32-byte k" as four little-endian uint32 */
static const uint32_t SIGMA[4] = {
0x61707865u, 0x3320646eu, 0x79622d32u, 0x6b206574u
};
/* ── HSalsa20 (Salsa20 family) ──────────────────────────────── */
void rosetta_hsalsa20(uint8_t out[32],
const uint8_t inp[16],
const uint8_t key[32])
{
uint32_t x[16];
x[ 0] = SIGMA[0];
x[ 1] = load32_le(key + 0);
x[ 2] = load32_le(key + 4);
x[ 3] = load32_le(key + 8);
x[ 4] = load32_le(key + 12);
x[ 5] = SIGMA[1];
x[ 6] = load32_le(inp + 0);
x[ 7] = load32_le(inp + 4);
x[ 8] = load32_le(inp + 8);
x[ 9] = load32_le(inp + 12);
x[10] = SIGMA[2];
x[11] = load32_le(key + 16);
x[12] = load32_le(key + 20);
x[13] = load32_le(key + 24);
x[14] = load32_le(key + 28);
x[15] = SIGMA[3];
for (int i = 0; i < 20; i += 2) {
/* column round */
x[ 4] ^= ROTL32(x[ 0] + x[12], 7);
x[ 8] ^= ROTL32(x[ 4] + x[ 0], 9);
x[12] ^= ROTL32(x[ 8] + x[ 4], 13);
x[ 0] ^= ROTL32(x[12] + x[ 8], 18);
x[ 9] ^= ROTL32(x[ 5] + x[ 1], 7);
x[13] ^= ROTL32(x[ 9] + x[ 5], 9);
x[ 1] ^= ROTL32(x[13] + x[ 9], 13);
x[ 5] ^= ROTL32(x[ 1] + x[13], 18);
x[14] ^= ROTL32(x[10] + x[ 6], 7);
x[ 2] ^= ROTL32(x[14] + x[10], 9);
x[ 6] ^= ROTL32(x[ 2] + x[14], 13);
x[10] ^= ROTL32(x[ 6] + x[ 2], 18);
x[ 3] ^= ROTL32(x[15] + x[11], 7);
x[ 7] ^= ROTL32(x[ 3] + x[15], 9);
x[11] ^= ROTL32(x[ 7] + x[ 3], 13);
x[15] ^= ROTL32(x[11] + x[ 7], 18);
/* row round */
x[ 1] ^= ROTL32(x[ 0] + x[ 3], 7);
x[ 2] ^= ROTL32(x[ 1] + x[ 0], 9);
x[ 3] ^= ROTL32(x[ 2] + x[ 1], 13);
x[ 0] ^= ROTL32(x[ 3] + x[ 2], 18);
x[ 6] ^= ROTL32(x[ 5] + x[ 4], 7);
x[ 7] ^= ROTL32(x[ 6] + x[ 5], 9);
x[ 4] ^= ROTL32(x[ 7] + x[ 6], 13);
x[ 5] ^= ROTL32(x[ 4] + x[ 7], 18);
x[11] ^= ROTL32(x[10] + x[ 9], 7);
x[ 8] ^= ROTL32(x[11] + x[10], 9);
x[ 9] ^= ROTL32(x[ 8] + x[11], 13);
x[10] ^= ROTL32(x[ 9] + x[ 8], 18);
x[12] ^= ROTL32(x[15] + x[14], 7);
x[13] ^= ROTL32(x[12] + x[15], 9);
x[14] ^= ROTL32(x[13] + x[12], 13);
x[15] ^= ROTL32(x[14] + x[13], 18);
}
/* output words: 0, 5, 10, 15, 6, 7, 8, 9 */
store32_le(out + 0, x[ 0]);
store32_le(out + 4, x[ 5]);
store32_le(out + 8, x[10]);
store32_le(out + 12, x[15]);
store32_le(out + 16, x[ 6]);
store32_le(out + 20, x[ 7]);
store32_le(out + 24, x[ 8]);
store32_le(out + 28, x[ 9]);
}
/* ── HChaCha20 (ChaCha20 family) ────────────────────────────── */
static void hchacha20(uint8_t out[32],
const uint8_t inp[16],
const uint8_t key[32])
{
uint32_t x[16];
x[ 0] = SIGMA[0];
x[ 1] = SIGMA[1];
x[ 2] = SIGMA[2];
x[ 3] = SIGMA[3];
x[ 4] = load32_le(key + 0);
x[ 5] = load32_le(key + 4);
x[ 6] = load32_le(key + 8);
x[ 7] = load32_le(key + 12);
x[ 8] = load32_le(key + 16);
x[ 9] = load32_le(key + 20);
x[10] = load32_le(key + 24);
x[11] = load32_le(key + 28);
x[12] = load32_le(inp + 0);
x[13] = load32_le(inp + 4);
x[14] = load32_le(inp + 8);
x[15] = load32_le(inp + 12);
for (int i = 0; i < 20; i += 2) {
/* column round */
#define QR(a, b, c, d) \
a += b; d ^= a; d = ROTL32(d, 16); \
c += d; b ^= c; b = ROTL32(b, 12); \
a += b; d ^= a; d = ROTL32(d, 8); \
c += d; b ^= c; b = ROTL32(b, 7);
QR(x[0], x[4], x[8], x[12]);
QR(x[1], x[5], x[9], x[13]);
QR(x[2], x[6], x[10], x[14]);
QR(x[3], x[7], x[11], x[15]);
/* diagonal round */
QR(x[0], x[5], x[10], x[15]);
QR(x[1], x[6], x[11], x[12]);
QR(x[2], x[7], x[8], x[13]);
QR(x[3], x[4], x[9], x[14]);
#undef QR
}
/* output words: 0, 1, 2, 3, 12, 13, 14, 15 */
store32_le(out + 0, x[ 0]);
store32_le(out + 4, x[ 1]);
store32_le(out + 8, x[ 2]);
store32_le(out + 12, x[ 3]);
store32_le(out + 16, x[12]);
store32_le(out + 20, x[13]);
store32_le(out + 24, x[14]);
store32_le(out + 28, x[15]);
}
/* ── ChaCha20-IETF (RFC 8439) ───────────────────────────────── */
static void chacha20_block(uint8_t out[64],
const uint8_t key[32],
uint32_t counter,
const uint8_t nonce[12])
{
uint32_t s[16], x[16];
s[ 0] = SIGMA[0];
s[ 1] = SIGMA[1];
s[ 2] = SIGMA[2];
s[ 3] = SIGMA[3];
for (int i = 0; i < 8; i++) s[4 + i] = load32_le(key + i * 4);
s[12] = counter;
s[13] = load32_le(nonce + 0);
s[14] = load32_le(nonce + 4);
s[15] = load32_le(nonce + 8);
memcpy(x, s, sizeof(x));
for (int i = 0; i < 20; i += 2) {
#define QR(a, b, c, d) \
a += b; d ^= a; d = ROTL32(d, 16); \
c += d; b ^= c; b = ROTL32(b, 12); \
a += b; d ^= a; d = ROTL32(d, 8); \
c += d; b ^= c; b = ROTL32(b, 7);
QR(x[0], x[4], x[8], x[12]);
QR(x[1], x[5], x[9], x[13]);
QR(x[2], x[6], x[10], x[14]);
QR(x[3], x[7], x[11], x[15]);
QR(x[0], x[5], x[10], x[15]);
QR(x[1], x[6], x[11], x[12]);
QR(x[2], x[7], x[8], x[13]);
QR(x[3], x[4], x[9], x[14]);
#undef QR
}
for (int i = 0; i < 16; i++) store32_le(out + i * 4, x[i] + s[i]);
}
static void chacha20_ietf_xor(uint8_t *out,
const uint8_t *in,
size_t len,
const uint8_t nonce[12],
const uint8_t key[32],
uint32_t initial_counter)
{
uint8_t block[64];
uint32_t ctr = initial_counter;
size_t off = 0;
while (off < len) {
chacha20_block(block, key, ctr++, nonce);
size_t chunk = len - off;
if (chunk > 64) chunk = 64;
for (size_t i = 0; i < chunk; i++) {
out[off + i] = in[off + i] ^ block[i];
}
off += chunk;
}
memset(block, 0, sizeof(block));
}
/* ── XChaCha20 XOR ───────────────────────────────────────────── */
void rosetta_xchacha20_xor(uint8_t *out,
const uint8_t *in,
size_t len,
const uint8_t nonce[24],
const uint8_t key[32])
{
/* Step 1: derive sub-key with HChaCha20(key, nonce[0..15]) */
uint8_t subkey[32];
hchacha20(subkey, nonce, key);
/* Step 2: ChaCha20-IETF with sub-key and nonce' = [0,0,0,0, nonce[16..23]] */
uint8_t sub_nonce[12] = {0};
memcpy(sub_nonce + 4, nonce + 16, 8);
chacha20_ietf_xor(out, in, len, sub_nonce, subkey, 0);
memset(subkey, 0, sizeof(subkey));
}

33
app/src/main/cpp/crypto.h Normal file
View File

@@ -0,0 +1,33 @@
#ifndef ROSETTA_CRYPTO_H
#define ROSETTA_CRYPTO_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/**
* HSalsa20 core — used by nacl.box.before() to derive shared key.
* out: 32 bytes, inp: 16 bytes (nonce, zeros for box.before), key: 32 bytes
*/
void rosetta_hsalsa20(uint8_t out[32],
const uint8_t inp[16],
const uint8_t key[32]);
/**
* XChaCha20 XOR (encrypt = decrypt, symmetric stream cipher).
* out and in may overlap. nonce: 24 bytes, key: 32 bytes.
*/
void rosetta_xchacha20_xor(uint8_t *out,
const uint8_t *in,
size_t len,
const uint8_t nonce[24],
const uint8_t key[32]);
#ifdef __cplusplus
}
#endif
#endif /* ROSETTA_CRYPTO_H */

View File

@@ -0,0 +1,390 @@
/**
* JNI bridge for Rosetta E2EE.
*
* Provides:
* 1. HSalsa20 — for nacl.box.before() compatible key derivation
* 2. XChaCha20 FrameEncryptor / FrameDecryptor — inherits DIRECTLY from
* webrtc::FrameEncryptorInterface / FrameDecryptorInterface so the
* vtable is generated by the compiler, not guessed by us.
*/
#include <jni.h>
#include <cstdint>
#include <cstring>
#include <atomic>
#include <vector>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <android/log.h>
/* WebRTC M125 interface stubs (exact copies of the real headers) */
#include "webrtc/api/crypto/frame_encryptor_interface.h"
#include "webrtc/api/crypto/frame_decryptor_interface.h"
#include "crypto.h"
#define TAG "RosettaE2EE"
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
/* ── Diagnostics file — written from native, visible in crash reports ── */
static char g_diag_path[512] = {0};
static int g_diag_fd = -1;
static void diag_write(const char *fmt, ...) {
if (g_diag_fd < 0) return;
char buf[512];
va_list ap;
va_start(ap, fmt);
int n = vsnprintf(buf, sizeof(buf), fmt, ap);
va_end(ap);
if (n > 0) write(g_diag_fd, buf, n);
}
/* ── Native crash handler — writes to file before dying ──────── */
static char g_crash_path[512] = {0};
static struct sigaction g_old_sigsegv = {};
static struct sigaction g_old_sigabrt = {};
static void native_crash_handler(int sig, siginfo_t *info, void *ctx) {
if (g_crash_path[0] != 0) {
int fd = open(g_crash_path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd >= 0) {
const char *msg;
if (sig == SIGSEGV)
msg = "NATIVE CRASH: SIGSEGV (segmentation fault) in rosetta_e2ee\n"
"Fault address: see logcat for details.\n";
else if (sig == SIGABRT)
msg = "NATIVE CRASH: SIGABRT (abort) in rosetta_e2ee\n";
else
msg = "NATIVE CRASH: unknown signal in rosetta_e2ee\n";
write(fd, msg, strlen(msg));
close(fd);
}
}
LOGE("NATIVE CRASH sig=%d addr=%p", sig, info ? info->si_addr : nullptr);
struct sigaction *old = (sig == SIGSEGV) ? &g_old_sigsegv : &g_old_sigabrt;
sigaction(sig, old, nullptr);
raise(sig);
}
/* ════════════════════════════════════════════════════════════════
* XChaCha20 Encryptor — inherits from the REAL interface
* ════════════════════════════════════════════════════════════════ */
class XChaCha20Encryptor final : public webrtc::FrameEncryptorInterface {
public:
explicit XChaCha20Encryptor(const uint8_t key[32]) {
memcpy(key_, key, 32);
}
/* ── RefCountInterface ─────────────────────────────────────── */
void AddRef() const override {
ref_.fetch_add(1, std::memory_order_relaxed);
}
rtc::RefCountReleaseStatus Release() const override {
if (ref_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete this;
return rtc::RefCountReleaseStatus::kDroppedLastRef;
}
return rtc::RefCountReleaseStatus::kOtherRefsRemained;
}
/**
* Frame format: [4-byte counter BE] + [xchacha20_xor(frame)]
*
* Nonce (24 bytes): [0,0,0,0, counter_BE_4bytes, 0,...,0]
* This matches Desktop's layout where nonce[4..7] = timestamp.
* The counter is embedded so the receiver can reconstruct the nonce
* even if frames are dropped/reordered.
*/
int Encrypt(cricket::MediaType /*media_type*/,
uint32_t /*ssrc*/,
rtc::ArrayView<const uint8_t> /*additional_data*/,
rtc::ArrayView<const uint8_t> frame,
rtc::ArrayView<uint8_t> encrypted_frame,
size_t* bytes_written) override {
const size_t HEADER = 4; // counter prefix
if (frame.size() == 0 || encrypted_frame.size() < frame.size() + HEADER) {
*bytes_written = 0;
return -1;
}
uint32_t ctr = counter_.fetch_add(1, std::memory_order_relaxed);
// Write 4-byte counter as big-endian prefix
encrypted_frame.data()[0] = (uint8_t)(ctr >> 24);
encrypted_frame.data()[1] = (uint8_t)(ctr >> 16);
encrypted_frame.data()[2] = (uint8_t)(ctr >> 8);
encrypted_frame.data()[3] = (uint8_t)(ctr);
// Build nonce from counter (same positions as Desktop's timestamp)
uint8_t nonce[24] = {0};
nonce[4] = encrypted_frame.data()[0];
nonce[5] = encrypted_frame.data()[1];
nonce[6] = encrypted_frame.data()[2];
nonce[7] = encrypted_frame.data()[3];
rosetta_xchacha20_xor(encrypted_frame.data() + HEADER,
frame.data(), frame.size(), nonce, key_);
*bytes_written = frame.size() + HEADER;
// Diag: log first 3 frames
int n = diag_count_.fetch_add(1, std::memory_order_relaxed);
if (n < 3) {
LOGI("ENC frame#%d: sz=%zu ctr=%u out=%zu",
n, frame.size(), ctr, frame.size() + HEADER);
diag_write("ENC frame#%d: sz=%zu ctr=%u nonce[4..7]=%02x%02x%02x%02x\n",
n, frame.size(), ctr, nonce[4], nonce[5], nonce[6], nonce[7]);
}
return 0;
}
size_t GetMaxCiphertextByteSize(cricket::MediaType, size_t frame_size) override {
return frame_size + 4; // +4 for counter prefix
}
protected:
~XChaCha20Encryptor() override { memset(key_, 0, 32); }
private:
mutable std::atomic<int> ref_{0};
mutable std::atomic<uint32_t> counter_{0};
mutable std::atomic<int> diag_count_{0};
uint8_t key_[32];
};
/* ════════════════════════════════════════════════════════════════
* XChaCha20 Decryptor — inherits from the REAL interface
* ════════════════════════════════════════════════════════════════ */
class XChaCha20Decryptor final : public webrtc::FrameDecryptorInterface {
public:
explicit XChaCha20Decryptor(const uint8_t key[32]) {
memcpy(key_, key, 32);
}
/* ── RefCountInterface ─────────────────────────────────────── */
void AddRef() const override {
ref_.fetch_add(1, std::memory_order_relaxed);
}
rtc::RefCountReleaseStatus Release() const override {
if (ref_.fetch_sub(1, std::memory_order_acq_rel) == 1) {
delete this;
return rtc::RefCountReleaseStatus::kDroppedLastRef;
}
return rtc::RefCountReleaseStatus::kOtherRefsRemained;
}
/**
* Decrypt frame: read 4-byte counter prefix → derive nonce → decrypt.
* If frame has no prefix (< 5 bytes or from Desktop), fallback to
* nonce derived from additional_data (RTP header) or zeros.
*/
Result Decrypt(cricket::MediaType /*media_type*/,
const std::vector<uint32_t>& /*csrcs*/,
rtc::ArrayView<const uint8_t> additional_data,
rtc::ArrayView<const uint8_t> encrypted_frame,
rtc::ArrayView<uint8_t> frame) override {
const size_t HEADER = 4;
uint8_t nonce[24] = {0};
const uint8_t *payload;
size_t payload_sz;
if (encrypted_frame.size() > HEADER) {
// Android format: [4-byte counter] + [encrypted data]
nonce[4] = encrypted_frame.data()[0];
nonce[5] = encrypted_frame.data()[1];
nonce[6] = encrypted_frame.data()[2];
nonce[7] = encrypted_frame.data()[3];
payload = encrypted_frame.data() + HEADER;
payload_sz = encrypted_frame.size() - HEADER;
} else {
// Fallback: no counter prefix
payload = encrypted_frame.data();
payload_sz = encrypted_frame.size();
}
if (payload_sz == 0 || frame.size() < payload_sz) {
return {Result::Status::kFailedToDecrypt, 0};
}
rosetta_xchacha20_xor(frame.data(), payload, payload_sz, nonce, key_);
// Diag: log first 3 frames
int n = diag_count_.fetch_add(1, std::memory_order_relaxed);
if (n < 3) {
LOGI("DEC frame#%d: enc_sz=%zu payload=%zu nonce=%02x%02x%02x%02x",
n, encrypted_frame.size(), payload_sz,
nonce[4], nonce[5], nonce[6], nonce[7]);
diag_write("DEC frame#%d: enc_sz=%zu payload=%zu nonce[4..7]=%02x%02x%02x%02x\n",
n, encrypted_frame.size(), payload_sz,
nonce[4], nonce[5], nonce[6], nonce[7]);
}
return {Result::Status::kOk, payload_sz};
}
size_t GetMaxPlaintextByteSize(cricket::MediaType, size_t encrypted_frame_size) override {
return encrypted_frame_size; // >= actual (payload = enc - 4)
}
protected:
~XChaCha20Decryptor() override { memset(key_, 0, 32); }
private:
mutable std::atomic<int> ref_{0};
mutable std::atomic<int> diag_count_{0};
uint8_t key_[32];
};
/* ════════════════════════════════════════════════════════════════
* JNI exports
* ════════════════════════════════════════════════════════════════ */
extern "C" {
/* ── HSalsa20 for nacl.box.before() ──────────────────────────── */
JNIEXPORT jbyteArray JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeHSalsa20(
JNIEnv *env, jclass, jbyteArray jRawDh)
{
jsize len = env->GetArrayLength(jRawDh);
if (len < 32) return nullptr;
auto *raw = (uint8_t *)env->GetByteArrayElements(jRawDh, nullptr);
uint8_t out[32];
uint8_t zeros[16] = {0};
rosetta_hsalsa20(out, zeros, raw);
env->ReleaseByteArrayElements(jRawDh, (jbyte *)raw, JNI_ABORT);
jbyteArray result = env->NewByteArray(32);
env->SetByteArrayRegion(result, 0, 32, (jbyte *)out);
memset(out, 0, 32);
return result;
}
/* ── Create / destroy encryptor ──────────────────────────────── */
JNIEXPORT jlong JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeCreateEncryptor(
JNIEnv *env, jclass, jbyteArray jKey)
{
jsize len = env->GetArrayLength(jKey);
if (len < 32) return 0;
auto *key = (uint8_t *)env->GetByteArrayElements(jKey, nullptr);
auto *enc = new XChaCha20Encryptor(key);
env->ReleaseByteArrayElements(jKey, (jbyte *)key, JNI_ABORT);
// AddRef so the pointer we hand out has ref=1.
// WebRTC's scoped_refptr will AddRef again when it takes ownership.
enc->AddRef();
LOGI("Created XChaCha20 encryptor %p", enc);
return reinterpret_cast<jlong>(enc);
}
JNIEXPORT void JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeReleaseEncryptor(
JNIEnv *, jclass, jlong ptr)
{
if (ptr == 0) return;
auto *enc = reinterpret_cast<XChaCha20Encryptor *>(ptr);
enc->Release();
}
/* ── Create / destroy decryptor ──────────────────────────────── */
JNIEXPORT jlong JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeCreateDecryptor(
JNIEnv *env, jclass, jbyteArray jKey)
{
jsize len = env->GetArrayLength(jKey);
if (len < 32) return 0;
auto *key = (uint8_t *)env->GetByteArrayElements(jKey, nullptr);
auto *dec = new XChaCha20Decryptor(key);
env->ReleaseByteArrayElements(jKey, (jbyte *)key, JNI_ABORT);
dec->AddRef();
LOGI("Created XChaCha20 decryptor %p", dec);
return reinterpret_cast<jlong>(dec);
}
JNIEXPORT void JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeReleaseDecryptor(
JNIEnv *, jclass, jlong ptr)
{
if (ptr == 0) return;
auto *dec = reinterpret_cast<XChaCha20Decryptor *>(ptr);
dec->Release();
}
/* ── Install native crash handler ─────────────────────────────── */
JNIEXPORT void JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeInstallCrashHandler(
JNIEnv *env, jclass, jstring jPath)
{
const char *path = env->GetStringUTFChars(jPath, nullptr);
strncpy(g_crash_path, path, sizeof(g_crash_path) - 1);
env->ReleaseStringUTFChars(jPath, path);
struct sigaction sa = {};
sa.sa_sigaction = native_crash_handler;
sa.sa_flags = SA_SIGINFO;
sigemptyset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, &g_old_sigsegv);
sigaction(SIGABRT, &sa, &g_old_sigabrt);
LOGI("Native crash handler installed, path=%s", g_crash_path);
}
/* ── Open diagnostics file for E2EE frame logging ────────────── */
JNIEXPORT void JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeOpenDiagFile(
JNIEnv *env, jclass, jstring jPath)
{
if (g_diag_fd >= 0) { close(g_diag_fd); g_diag_fd = -1; }
const char *path = env->GetStringUTFChars(jPath, nullptr);
strncpy(g_diag_path, path, sizeof(g_diag_path) - 1);
g_diag_fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
env->ReleaseStringUTFChars(jPath, path);
if (g_diag_fd >= 0) {
diag_write("=== E2EE DIAGNOSTICS ===\n");
LOGI("Diag file opened: %s", g_diag_path);
} else {
LOGE("Failed to open diag file: %s", g_diag_path);
}
}
JNIEXPORT void JNICALL
Java_com_rosetta_messenger_network_XChaCha20E2EE_nativeCloseDiagFile(
JNIEnv *, jclass)
{
if (g_diag_fd >= 0) {
diag_write("=== END ===\n");
close(g_diag_fd);
g_diag_fd = -1;
}
}
} /* extern "C" */

View File

@@ -0,0 +1,24 @@
// Minimal stub matching WebRTC M125 api/array_view.h
#ifndef API_ARRAY_VIEW_H_
#define API_ARRAY_VIEW_H_
#include <cstddef>
namespace rtc {
template <typename T>
class ArrayView final {
public:
constexpr ArrayView() noexcept : ptr_(nullptr), size_(0) {}
constexpr ArrayView(T* ptr, size_t size) noexcept : ptr_(ptr), size_(size) {}
constexpr T* data() const { return ptr_; }
constexpr size_t size() const { return size_; }
private:
T* ptr_;
size_t size_;
};
} // namespace rtc
#endif // API_ARRAY_VIEW_H_

View File

@@ -0,0 +1,42 @@
// Minimal stub matching WebRTC M125 api/crypto/frame_decryptor_interface.h
#ifndef API_CRYPTO_FRAME_DECRYPTOR_INTERFACE_H_
#define API_CRYPTO_FRAME_DECRYPTOR_INTERFACE_H_
#include <cstddef>
#include <cstdint>
#include <vector>
#include "webrtc/rtc_base/ref_count.h"
#include "webrtc/api/array_view.h"
#include "webrtc/api/media_types.h"
namespace webrtc {
class FrameDecryptorInterface : public rtc::RefCountInterface {
public:
struct Result {
enum class Status { kOk = 0, kRecoverable, kFailedToDecrypt };
Result(Status s, size_t bw) : status(s), bytes_written(bw) {}
bool IsOk() const { return status == Status::kOk; }
Status status;
size_t bytes_written;
};
virtual Result Decrypt(cricket::MediaType media_type,
const std::vector<uint32_t>& csrcs,
rtc::ArrayView<const uint8_t> additional_data,
rtc::ArrayView<const uint8_t> encrypted_frame,
rtc::ArrayView<uint8_t> frame) = 0;
virtual size_t GetMaxPlaintextByteSize(cricket::MediaType media_type,
size_t encrypted_frame_size) = 0;
protected:
~FrameDecryptorInterface() override {}
};
} // namespace webrtc
#endif // API_CRYPTO_FRAME_DECRYPTOR_INTERFACE_H_

View File

@@ -0,0 +1,32 @@
// Minimal stub matching WebRTC M125 api/crypto/frame_encryptor_interface.h
#ifndef API_CRYPTO_FRAME_ENCRYPTOR_INTERFACE_H_
#define API_CRYPTO_FRAME_ENCRYPTOR_INTERFACE_H_
#include <cstddef>
#include <cstdint>
#include "webrtc/rtc_base/ref_count.h"
#include "webrtc/api/array_view.h"
#include "webrtc/api/media_types.h"
namespace webrtc {
class FrameEncryptorInterface : public rtc::RefCountInterface {
public:
virtual int Encrypt(cricket::MediaType media_type,
uint32_t ssrc,
rtc::ArrayView<const uint8_t> additional_data,
rtc::ArrayView<const uint8_t> frame,
rtc::ArrayView<uint8_t> encrypted_frame,
size_t* bytes_written) = 0;
virtual size_t GetMaxCiphertextByteSize(cricket::MediaType media_type,
size_t frame_size) = 0;
protected:
~FrameEncryptorInterface() override {}
};
} // namespace webrtc
#endif // API_CRYPTO_FRAME_ENCRYPTOR_INTERFACE_H_

View File

@@ -0,0 +1,14 @@
// Minimal stub matching WebRTC M125 api/media_types.h
#ifndef API_MEDIA_TYPES_H_
#define API_MEDIA_TYPES_H_
namespace cricket {
enum MediaType {
MEDIA_TYPE_AUDIO,
MEDIA_TYPE_VIDEO,
MEDIA_TYPE_DATA,
MEDIA_TYPE_UNSUPPORTED
};
} // namespace cricket
#endif // API_MEDIA_TYPES_H_

View File

@@ -0,0 +1,21 @@
// Minimal stub matching WebRTC M125 rtc_base/ref_count.h
#ifndef RTC_BASE_REF_COUNT_H_
#define RTC_BASE_REF_COUNT_H_
namespace rtc {
enum class RefCountReleaseStatus { kDroppedLastRef, kOtherRefsRemained };
// Must match the EXACT virtual layout of the real rtc::RefCountInterface.
class RefCountInterface {
public:
virtual void AddRef() const = 0;
virtual RefCountReleaseStatus Release() const = 0;
protected:
virtual ~RefCountInterface() {}
};
} // namespace rtc
#endif // RTC_BASE_REF_COUNT_H_

View File

@@ -19,10 +19,6 @@ import org.bouncycastle.math.ec.rfc7748.X25519
import org.json.JSONObject import org.json.JSONObject
import org.webrtc.AudioSource import org.webrtc.AudioSource
import org.webrtc.AudioTrack import org.webrtc.AudioTrack
import org.webrtc.FrameCryptor
import org.webrtc.FrameCryptorAlgorithm
import org.webrtc.FrameCryptorFactory
import org.webrtc.FrameCryptorKeyProvider
import org.webrtc.IceCandidate import org.webrtc.IceCandidate
import org.webrtc.MediaConstraints import org.webrtc.MediaConstraints
import org.webrtc.PeerConnection import org.webrtc.PeerConnection
@@ -120,10 +116,10 @@ object CallManager {
private var localAudioTrack: AudioTrack? = null private var localAudioTrack: AudioTrack? = null
private val bufferedRemoteCandidates = mutableListOf<IceCandidate>() private val bufferedRemoteCandidates = mutableListOf<IceCandidate>()
// E2EE (FrameCryptor AES-GCM) // E2EE (XChaCha20 — compatible with Desktop)
private var keyProvider: FrameCryptorKeyProvider? = null private var sharedKeyBytes: ByteArray? = null
private var senderCryptor: FrameCryptor? = null private var senderEncryptor: XChaCha20E2EE.Encryptor? = null
private var receiverCryptor: FrameCryptor? = null private var receiverDecryptor: XChaCha20E2EE.Decryptor? = null
private var iceServers: List<PeerConnection.IceServer> = emptyList() private var iceServers: List<PeerConnection.IceServer> = emptyList()
@@ -132,6 +128,7 @@ object CallManager {
initialized = true initialized = true
appContext = context.applicationContext appContext = context.applicationContext
CallSoundManager.initialize(context) CallSoundManager.initialize(context)
XChaCha20E2EE.initWithContext(context)
signalWaiter = ProtocolManager.waitCallSignal { packet -> signalWaiter = ProtocolManager.waitCallSignal { packet ->
scope.launch { handleSignalPacket(packet) } scope.launch { handleSignalPacket(packet) }
@@ -255,16 +252,21 @@ object CallManager {
} }
private suspend fun handleSignalPacket(packet: PacketSignalPeer) { private suspend fun handleSignalPacket(packet: PacketSignalPeer) {
breadcrumb("SIG: ${packet.signalType} from=${packet.src.take(8)}… phase=${_state.value.phase}")
when (packet.signalType) { when (packet.signalType) {
SignalType.END_CALL_BECAUSE_BUSY -> { SignalType.END_CALL_BECAUSE_BUSY -> {
breadcrumb("SIG: peer busy → reset")
resetSession(reason = "User is busy", notifyPeer = false) resetSession(reason = "User is busy", notifyPeer = false)
return return
} }
SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED -> { SignalType.END_CALL_BECAUSE_PEER_DISCONNECTED -> {
breadcrumb("SIG: peer disconnected → reset")
resetSession(reason = "Peer disconnected", notifyPeer = false) resetSession(reason = "Peer disconnected", notifyPeer = false)
return return
} }
SignalType.END_CALL -> { SignalType.END_CALL -> {
breadcrumb("SIG: END_CALL → reset")
resetSession(reason = "Call ended", notifyPeer = false) resetSession(reason = "Call ended", notifyPeer = false)
return return
} }
@@ -274,16 +276,18 @@ object CallManager {
val currentPeer = _state.value.peerPublicKey val currentPeer = _state.value.peerPublicKey
val src = packet.src.trim() val src = packet.src.trim()
if (currentPeer.isNotBlank() && src.isNotBlank() && src != currentPeer && src != ownPublicKey) { if (currentPeer.isNotBlank() && src.isNotBlank() && src != currentPeer && src != ownPublicKey) {
breadcrumb("SIG: IGNORED (src mismatch: expected=${currentPeer.take(8)}… got=${src.take(8)}…)")
return return
} }
when (packet.signalType) { when (packet.signalType) {
SignalType.CALL -> { SignalType.CALL -> {
if (_state.value.phase != CallPhase.IDLE) { if (_state.value.phase != CallPhase.IDLE) {
breadcrumb("SIG: CALL but busy → sending END_CALL_BECAUSE_BUSY")
val callerKey = packet.src.trim() val callerKey = packet.src.trim()
if (callerKey.isNotBlank() && ownPublicKey.isNotBlank()) { if (callerKey.isNotBlank() && ownPublicKey.isNotBlank()) {
ProtocolManager.sendCallSignal( ProtocolManager.sendCallSignal(
signalType = SignalType.END_CALL, signalType = SignalType.END_CALL_BECAUSE_BUSY,
src = ownPublicKey, src = ownPublicKey,
dst = callerKey dst = callerKey
) )
@@ -292,6 +296,7 @@ object CallManager {
} }
val incomingPeer = packet.src.trim() val incomingPeer = packet.src.trim()
if (incomingPeer.isBlank()) return if (incomingPeer.isBlank()) return
breadcrumb("SIG: CALL from ${incomingPeer.take(8)}… → INCOMING")
role = CallRole.CALLEE role = CallRole.CALLEE
resetRtcObjects() resetRtcObjects()
setPeer(incomingPeer, "", "") setPeer(incomingPeer, "", "")
@@ -305,11 +310,16 @@ object CallManager {
resolvePeerIdentity(incomingPeer) resolvePeerIdentity(incomingPeer)
} }
SignalType.KEY_EXCHANGE -> { SignalType.KEY_EXCHANGE -> {
breadcrumb("SIG: KEY_EXCHANGE → handleKeyExchange")
handleKeyExchange(packet) handleKeyExchange(packet)
} }
SignalType.CREATE_ROOM -> { SignalType.CREATE_ROOM -> {
val incomingRoomId = packet.roomId.trim() val incomingRoomId = packet.roomId.trim()
if (incomingRoomId.isBlank()) return breadcrumb("SIG: CREATE_ROOM roomId=${incomingRoomId.take(16)}")
if (incomingRoomId.isBlank()) {
breadcrumb("SIG: CREATE_ROOM IGNORED — empty roomId!")
return
}
roomId = incomingRoomId roomId = incomingRoomId
updateState { updateState {
it.copy( it.copy(
@@ -320,23 +330,35 @@ object CallManager {
ensurePeerConnectionAndOffer() ensurePeerConnectionAndOffer()
} }
SignalType.ACTIVE_CALL -> Unit SignalType.ACTIVE_CALL -> Unit
else -> Unit else -> breadcrumb("SIG: unhandled ${packet.signalType}")
} }
} }
private suspend fun handleKeyExchange(packet: PacketSignalPeer) { private suspend fun handleKeyExchange(packet: PacketSignalPeer) {
val peerKey = packet.src.trim().ifBlank { _state.value.peerPublicKey } val peerKey = packet.src.trim().ifBlank { _state.value.peerPublicKey }
if (peerKey.isBlank()) return if (peerKey.isBlank()) {
breadcrumb("KE: ABORT — peerKey blank")
return
}
setPeer(peerKey, _state.value.peerTitle, _state.value.peerUsername) setPeer(peerKey, _state.value.peerTitle, _state.value.peerUsername)
val peerPublicHex = packet.sharedPublic.trim() val peerPublicHex = packet.sharedPublic.trim()
if (peerPublicHex.isBlank()) return if (peerPublicHex.isBlank()) {
breadcrumb("KE: ABORT — sharedPublic blank")
return
}
breadcrumb("KE: role=$role peerPub=${peerPublicHex.take(16)}")
if (role == CallRole.CALLER) { if (role == CallRole.CALLER) {
generateSessionKeys() generateSessionKeys()
val sharedKey = computeSharedSecretHex(peerPublicHex) ?: return val sharedKey = computeSharedSecretHex(peerPublicHex)
if (sharedKey == null) {
breadcrumb("KE: CALLER — computeSharedSecret FAILED")
return
}
setupE2EE(sharedKey) setupE2EE(sharedKey)
updateState { it.copy(keyCast = sharedKey.take(32), statusText = "Creating room...") } breadcrumb("KE: CALLER — E2EE ready, sending KEY_EXCHANGE + CREATE_ROOM")
updateState { it.copy(keyCast = sharedKey, statusText = "Creating room...") }
val localPublic = localPublicKey ?: return val localPublic = localPublicKey ?: return
ProtocolManager.sendCallSignal( ProtocolManager.sendCallSignal(
signalType = SignalType.KEY_EXCHANGE, signalType = SignalType.KEY_EXCHANGE,
@@ -355,39 +377,58 @@ object CallManager {
if (role == CallRole.CALLEE) { if (role == CallRole.CALLEE) {
if (localPrivateKey == null || localPublicKey == null) { if (localPrivateKey == null || localPublicKey == null) {
breadcrumb("KE: CALLEE — regenerating session keys (were null)")
generateSessionKeys() generateSessionKeys()
} }
val sharedKey = computeSharedSecretHex(peerPublicHex) ?: return val sharedKey = computeSharedSecretHex(peerPublicHex)
if (sharedKey == null) {
breadcrumb("KE: CALLEE — computeSharedSecret FAILED")
return
}
setupE2EE(sharedKey) setupE2EE(sharedKey)
updateState { it.copy(keyCast = sharedKey.take(32), phase = CallPhase.CONNECTING) } breadcrumb("KE: CALLEE — E2EE ready, waiting for CREATE_ROOM")
updateState { it.copy(keyCast = sharedKey, phase = CallPhase.CONNECTING) }
} }
} }
private suspend fun handleWebRtcPacket(packet: PacketWebRTC) { private suspend fun handleWebRtcPacket(packet: PacketWebRTC) {
val phase = _state.value.phase val phase = _state.value.phase
if (phase != CallPhase.CONNECTING && phase != CallPhase.ACTIVE) return if (phase != CallPhase.CONNECTING && phase != CallPhase.ACTIVE) {
val pc = peerConnection ?: return breadcrumb("RTC: IGNORED ${packet.signalType} — phase=$phase")
return
}
val pc = peerConnection
if (pc == null) {
breadcrumb("RTC: IGNORED ${packet.signalType} — peerConnection=null!")
return
}
when (packet.signalType) { when (packet.signalType) {
WebRTCSignalType.ANSWER -> { WebRTCSignalType.ANSWER -> {
breadcrumb("RTC: ANSWER received")
val answer = parseSessionDescription(packet.sdpOrCandidate) ?: return val answer = parseSessionDescription(packet.sdpOrCandidate) ?: return
try { try {
pc.setRemoteDescriptionAwait(answer) pc.setRemoteDescriptionAwait(answer)
remoteDescriptionSet = true remoteDescriptionSet = true
flushBufferedRemoteCandidates() flushBufferedRemoteCandidates()
breadcrumb("RTC: ANSWER applied OK, remoteDesc=true")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to set remote answer", e) breadcrumb("RTC: ANSWER FAILED — ${e.message}")
saveCrashReport("setRemoteDescription(answer) failed", e)
} }
} }
WebRTCSignalType.ICE_CANDIDATE -> { WebRTCSignalType.ICE_CANDIDATE -> {
val candidate = parseIceCandidate(packet.sdpOrCandidate) ?: return val candidate = parseIceCandidate(packet.sdpOrCandidate) ?: return
if (!remoteDescriptionSet) { if (!remoteDescriptionSet) {
breadcrumb("RTC: ICE buffered (remoteDesc not set yet)")
bufferedRemoteCandidates.add(candidate) bufferedRemoteCandidates.add(candidate)
return return
} }
breadcrumb("RTC: ICE added: ${candidate.sdp.take(40)}")
runCatching { pc.addIceCandidate(candidate) } runCatching { pc.addIceCandidate(candidate) }
} }
WebRTCSignalType.OFFER -> { WebRTCSignalType.OFFER -> {
breadcrumb("RTC: OFFER received (offerSent=$offerSent)")
val remoteOffer = parseSessionDescription(packet.sdpOrCandidate) ?: return val remoteOffer = parseSessionDescription(packet.sdpOrCandidate) ?: return
try { try {
pc.setRemoteDescriptionAwait(remoteOffer) pc.setRemoteDescriptionAwait(remoteOffer)
@@ -399,8 +440,10 @@ object CallManager {
signalType = WebRTCSignalType.ANSWER, signalType = WebRTCSignalType.ANSWER,
sdpOrCandidate = serializeSessionDescription(answer) sdpOrCandidate = serializeSessionDescription(answer)
) )
breadcrumb("RTC: OFFER handled → ANSWER sent")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to handle remote offer", e) breadcrumb("RTC: OFFER FAILED — ${e.message}")
saveCrashReport("handleOffer failed", e)
} }
} }
} }
@@ -422,12 +465,27 @@ object CallManager {
private suspend fun ensurePeerConnectionAndOffer() { private suspend fun ensurePeerConnectionAndOffer() {
val peerKey = _state.value.peerPublicKey val peerKey = _state.value.peerPublicKey
if (peerKey.isBlank() || roomId.isBlank()) return if (peerKey.isBlank() || roomId.isBlank()) {
if (offerSent) return breadcrumb("PC: ensurePCAndOffer SKIP — peer=${peerKey.take(8)}… room=${roomId.take(8)}")
return
}
if (offerSent) {
breadcrumb("PC: ensurePCAndOffer SKIP — offerSent=true")
return
}
breadcrumb("PC: ensurePCAndOffer START role=$role room=${roomId.take(8)}")
ensurePeerFactory() ensurePeerFactory()
val factory = peerConnectionFactory ?: return val factory = peerConnectionFactory
val pc = peerConnection ?: createPeerConnection(factory) ?: return if (factory == null) {
breadcrumb("PC: ABORT — factory=null")
return
}
val pc = peerConnection ?: createPeerConnection(factory)
if (pc == null) {
breadcrumb("PC: ABORT — createPeerConnection returned null")
return
}
if (audioSource == null) { if (audioSource == null) {
audioSource = factory.createAudioSource(MediaConstraints()) audioSource = factory.createAudioSource(MediaConstraints())
@@ -436,6 +494,7 @@ object CallManager {
localAudioTrack = factory.createAudioTrack(LOCAL_AUDIO_TRACK_ID, audioSource) localAudioTrack = factory.createAudioTrack(LOCAL_AUDIO_TRACK_ID, audioSource)
localAudioTrack?.setEnabled(!_state.value.isMuted) localAudioTrack?.setEnabled(!_state.value.isMuted)
pc.addTrack(localAudioTrack, listOf(LOCAL_MEDIA_STREAM_ID)) pc.addTrack(localAudioTrack, listOf(LOCAL_MEDIA_STREAM_ID))
breadcrumb("PC: audio track added, attaching E2EE…")
attachSenderE2EE(pc) attachSenderE2EE(pc)
} }
@@ -447,16 +506,20 @@ object CallManager {
sdpOrCandidate = serializeSessionDescription(offer) sdpOrCandidate = serializeSessionDescription(offer)
) )
offerSent = true offerSent = true
breadcrumb("PC: OFFER sent OK")
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to create/send offer", e) breadcrumb("PC: OFFER FAILED — ${e.message}")
saveCrashReport("createOffer failed", e)
} }
} }
private fun createPeerConnection(factory: PeerConnectionFactory): PeerConnection? { private fun createPeerConnection(factory: PeerConnectionFactory): PeerConnection? {
breadcrumb("PC: createPeerConnection iceServers=${iceServers.size}")
val rtcIceServers = val rtcIceServers =
if (iceServers.isNotEmpty()) { if (iceServers.isNotEmpty()) {
iceServers iceServers
} else { } else {
breadcrumb("PC: no TURN servers — using Google STUN fallback")
listOf(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer()) listOf(PeerConnection.IceServer.builder("stun:stun.l.google.com:19302").createIceServer())
} }
@@ -466,12 +529,19 @@ object CallManager {
val observer = val observer =
object : PeerConnection.Observer { object : PeerConnection.Observer {
override fun onSignalingChange(newState: PeerConnection.SignalingState?) = Unit override fun onSignalingChange(newState: PeerConnection.SignalingState?) {
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) = Unit breadcrumb("PC: signalingState=$newState")
}
override fun onIceConnectionChange(newState: PeerConnection.IceConnectionState?) {
breadcrumb("PC: iceConnState=$newState")
}
override fun onIceConnectionReceivingChange(receiving: Boolean) = Unit override fun onIceConnectionReceivingChange(receiving: Boolean) = Unit
override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) = Unit override fun onIceGatheringChange(newState: PeerConnection.IceGatheringState?) {
breadcrumb("PC: iceGathering=$newState")
}
override fun onIceCandidate(candidate: IceCandidate?) { override fun onIceCandidate(candidate: IceCandidate?) {
if (candidate == null) return if (candidate == null) return
breadcrumb("PC: local ICE: ${candidate.sdp.take(30)}")
ProtocolManager.sendWebRtcSignal( ProtocolManager.sendWebRtcSignal(
signalType = WebRTCSignalType.ICE_CANDIDATE, signalType = WebRTCSignalType.ICE_CANDIDATE,
sdpOrCandidate = serializeIceCandidate(candidate) sdpOrCandidate = serializeIceCandidate(candidate)
@@ -484,9 +554,11 @@ object CallManager {
override fun onRenegotiationNeeded() = Unit override fun onRenegotiationNeeded() = Unit
override fun onAddTrack(receiver: RtpReceiver?, mediaStreams: Array<out org.webrtc.MediaStream>?) = Unit override fun onAddTrack(receiver: RtpReceiver?, mediaStreams: Array<out org.webrtc.MediaStream>?) = Unit
override fun onTrack(transceiver: RtpTransceiver?) { override fun onTrack(transceiver: RtpTransceiver?) {
breadcrumb("PC: onTrack → attachReceiverE2EE")
attachReceiverE2EE(transceiver) attachReceiverE2EE(transceiver)
} }
override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) { override fun onConnectionChange(newState: PeerConnection.PeerConnectionState?) {
breadcrumb("PC: connState=$newState")
when (newState) { when (newState) {
PeerConnection.PeerConnectionState.CONNECTED -> { PeerConnection.PeerConnectionState.CONNECTED -> {
onCallConnected() onCallConnected()
@@ -494,7 +566,10 @@ object CallManager {
PeerConnection.PeerConnectionState.DISCONNECTED, PeerConnection.PeerConnectionState.DISCONNECTED,
PeerConnection.PeerConnectionState.FAILED, PeerConnection.PeerConnectionState.FAILED,
PeerConnection.PeerConnectionState.CLOSED -> { PeerConnection.PeerConnectionState.CLOSED -> {
resetSession(reason = "Connection lost", notifyPeer = false) // Dispatch to our scope — this callback fires on WebRTC thread
scope.launch {
resetSession(reason = "Connection lost", notifyPeer = false)
}
} }
else -> Unit else -> Unit
} }
@@ -551,6 +626,7 @@ object CallManager {
} }
private fun resetSession(reason: String?, notifyPeer: Boolean) { private fun resetSession(reason: String?, notifyPeer: Boolean) {
breadcrumb("RESET: reason=$reason notifyPeer=$notifyPeer phase=${_state.value.phase}")
val snapshot = _state.value val snapshot = _state.value
val wasActive = snapshot.phase != CallPhase.IDLE val wasActive = snapshot.phase != CallPhase.IDLE
val peerToNotify = snapshot.peerPublicKey val peerToNotify = snapshot.peerPublicKey
@@ -571,6 +647,7 @@ object CallManager {
Log.d(TAG, reason) Log.d(TAG, reason)
} }
resetRtcObjects() resetRtcObjects()
e2eeAvailable = true
role = null role = null
roomId = "" roomId = ""
offerSent = false offerSent = false
@@ -585,6 +662,9 @@ object CallManager {
private fun resetRtcObjects() { private fun resetRtcObjects() {
bufferedRemoteCandidates.clear() bufferedRemoteCandidates.clear()
// Teardown E2EE BEFORE closing PeerConnection — WebRTC may access
// encryptor/decryptor during close(), causing SIGSEGV if done after.
teardownE2EE()
runCatching { localAudioTrack?.setEnabled(false) } runCatching { localAudioTrack?.setEnabled(false) }
runCatching { localAudioTrack?.dispose() } runCatching { localAudioTrack?.dispose() }
runCatching { audioSource?.dispose() } runCatching { audioSource?.dispose() }
@@ -592,7 +672,6 @@ object CallManager {
localAudioTrack = null localAudioTrack = null
audioSource = null audioSource = null
peerConnection = null peerConnection = null
teardownE2EE()
} }
private fun flushBufferedRemoteCandidates() { private fun flushBufferedRemoteCandidates() {
@@ -604,7 +683,10 @@ object CallManager {
bufferedRemoteCandidates.clear() bufferedRemoteCandidates.clear()
} }
// ── E2EE (FrameCryptor AES-GCM) ───────────────────────────────── // ── E2EE (XChaCha20 — compatible with Desktop) ──────────────────
@Volatile
private var e2eeAvailable = true
private fun setupE2EE(sharedKeyHex: String) { private fun setupE2EE(sharedKeyHex: String) {
val keyBytes = sharedKeyHex.hexToBytes() val keyBytes = sharedKeyHex.hexToBytes()
@@ -612,53 +694,107 @@ object CallManager {
Log.e(TAG, "E2EE: invalid key (${keyBytes?.size ?: 0} bytes)") Log.e(TAG, "E2EE: invalid key (${keyBytes?.size ?: 0} bytes)")
return return
} }
val kp = FrameCryptorFactory.createFrameCryptorKeyProvider( sharedKeyBytes = keyBytes.copyOf(32)
/* sharedKey */ true, // Open native diagnostics file for frame-level logging
/* ratchetSalt */ ByteArray(0), try {
/* ratchetWindowSize */ 0, val dir = java.io.File(appContext!!.filesDir, "crash_reports")
/* uncryptedMagicBytes */ ByteArray(0), if (!dir.exists()) dir.mkdirs()
/* failureTolerance */ 0, val diagPath = java.io.File(dir, "e2ee_diag.txt").absolutePath
/* keyRingSize */ 1, XChaCha20E2EE.nativeOpenDiagFile(diagPath)
/* discardFrameWhenCryptorNotReady */ false } catch (_: Throwable) {}
) Log.i(TAG, "E2EE key ready (XChaCha20)")
kp.setSharedKey(0, keyBytes.copyOf(32)) }
keyProvider = kp
Log.i(TAG, "E2EE key provider created (AES-GCM)") /** Write a breadcrumb to crash_reports/e2ee_breadcrumb.txt — survives SIGSEGV */
private fun breadcrumb(step: String) {
try {
val dir = java.io.File(appContext!!.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
val f = java.io.File(dir, "e2ee_breadcrumb.txt")
// Reset file at start of key exchange
if (step.startsWith("KE:") && step.contains("agreement")) {
f.writeText("")
}
f.appendText("${System.currentTimeMillis()} $step\n")
} catch (_: Throwable) {}
}
/** Save a full crash report to crash_reports/ */
private fun saveCrashReport(title: String, error: Throwable) {
try {
val dir = java.io.File(appContext!!.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
val ts = java.text.SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", java.util.Locale.getDefault()).format(java.util.Date())
val f = java.io.File(dir, "crash_e2ee_$ts.txt")
val sw = java.io.StringWriter()
error.printStackTrace(java.io.PrintWriter(sw))
f.writeText("=== E2EE CRASH REPORT ===\n$title\n\nType: ${error.javaClass.name}\nMessage: ${error.message}\n\n$sw")
} catch (_: Throwable) {}
} }
private fun attachSenderE2EE(pc: PeerConnection) { private fun attachSenderE2EE(pc: PeerConnection) {
val factory = peerConnectionFactory ?: return if (!e2eeAvailable) return
val kp = keyProvider ?: return val key = sharedKeyBytes ?: return
val sender = pc.senders.firstOrNull() ?: return val sender = pc.senders.firstOrNull() ?: return
senderCryptor = FrameCryptorFactory.createFrameCryptorForRtpSender( try {
factory, sender, "caller", FrameCryptorAlgorithm.AES_GCM, kp breadcrumb("1. encryptor: nativeLoaded=${XChaCha20E2EE.nativeLoaded}")
) val enc = XChaCha20E2EE.Encryptor(key)
senderCryptor?.setEnabled(true) breadcrumb("2. encryptor created")
Log.i(TAG, "E2EE sender cryptor attached") val ptr = enc.getNativeFrameEncryptor()
breadcrumb("3. encryptor ptr=0x${ptr.toString(16)}")
if (ptr == 0L) {
saveCrashReport("Encryptor native ptr is 0", RuntimeException("null native ptr"))
return
}
breadcrumb("4. calling sender.setFrameEncryptor…")
sender.setFrameEncryptor(enc)
breadcrumb("5. setFrameEncryptor OK!")
senderEncryptor = enc
} catch (e: Throwable) {
saveCrashReport("attachSenderE2EE failed", e)
Log.e(TAG, "E2EE: sender encryptor failed", e)
e2eeAvailable = false
}
} }
private fun attachReceiverE2EE(transceiver: RtpTransceiver?) { private fun attachReceiverE2EE(transceiver: RtpTransceiver?) {
val factory = peerConnectionFactory ?: return if (!e2eeAvailable) return
val kp = keyProvider ?: return val key = sharedKeyBytes ?: return
val receiver = transceiver?.receiver ?: return val receiver = transceiver?.receiver ?: return
receiverCryptor = FrameCryptorFactory.createFrameCryptorForRtpReceiver( try {
factory, receiver, "callee", FrameCryptorAlgorithm.AES_GCM, kp breadcrumb("6. decryptor: creating…")
) val dec = XChaCha20E2EE.Decryptor(key)
receiverCryptor?.setEnabled(true) breadcrumb("7. decryptor created")
Log.i(TAG, "E2EE receiver cryptor attached") val ptr = dec.getNativeFrameDecryptor()
breadcrumb("8. decryptor ptr=0x${ptr.toString(16)}")
if (ptr == 0L) {
saveCrashReport("Decryptor native ptr is 0", RuntimeException("null native ptr"))
return
}
breadcrumb("9. calling receiver.setFrameDecryptor…")
receiver.setFrameDecryptor(dec)
breadcrumb("10. setFrameDecryptor OK!")
receiverDecryptor = dec
} catch (e: Throwable) {
saveCrashReport("attachReceiverE2EE failed", e)
Log.e(TAG, "E2EE: receiver decryptor failed", e)
e2eeAvailable = false
}
} }
private fun teardownE2EE() { private fun teardownE2EE() {
runCatching { senderCryptor?.setEnabled(false) } // Release our ref. WebRTC holds its own ref via scoped_refptr.
runCatching { senderCryptor?.dispose() } // After our Release: WebRTC ref remains. On peerConnection.close()
runCatching { receiverCryptor?.setEnabled(false) } // WebRTC releases its ref → ref=0 → native object deleted.
runCatching { receiverCryptor?.dispose() } runCatching { senderEncryptor?.dispose() }
runCatching { keyProvider?.dispose() } runCatching { receiverDecryptor?.dispose() }
senderCryptor = null senderEncryptor = null
receiverCryptor = null receiverDecryptor = null
keyProvider = null sharedKeyBytes?.let { it.fill(0) }
sharedKeyBytes = null
runCatching { XChaCha20E2EE.nativeCloseDiagFile() }
} }
private fun generateSessionKeys() { private fun generateSessionKeys() {
@@ -674,10 +810,27 @@ object CallManager {
val privateKey = localPrivateKey ?: return null val privateKey = localPrivateKey ?: return null
val peerPublic = peerPublicHex.hexToBytes() ?: return null val peerPublic = peerPublicHex.hexToBytes() ?: return null
if (peerPublic.size != 32) return null if (peerPublic.size != 32) return null
val shared = ByteArray(32) val rawDh = ByteArray(32)
val ok = X25519.calculateAgreement(privateKey, 0, peerPublic, 0, shared, 0) breadcrumb("KE: X25519 agreement…")
if (!ok) return null val ok = X25519.calculateAgreement(privateKey, 0, peerPublic, 0, rawDh, 0)
return shared.toHex() if (!ok) {
breadcrumb("KE: X25519 FAILED")
return null
}
breadcrumb("KE: X25519 OK, calling HSalsa20…")
return try {
val naclShared = XChaCha20E2EE.hsalsa20(rawDh)
rawDh.fill(0)
breadcrumb("KE: HSalsa20 OK, key ready")
naclShared.toHex()
} catch (e: Throwable) {
saveCrashReport("HSalsa20 failed", e)
breadcrumb("KE: HSalsa20 FAILED: ${e.message}")
e2eeAvailable = false
val hex = rawDh.toHex()
rawDh.fill(0)
hex
}
} }
private fun serializeSessionDescription(description: SessionDescription): String { private fun serializeSessionDescription(description: SessionDescription): String {

View File

@@ -0,0 +1,107 @@
package com.rosetta.messenger.network
import android.util.Log
import org.webrtc.FrameDecryptor
import org.webrtc.FrameEncryptor
/**
* XChaCha20-based E2EE compatible with Rosetta Desktop.
*
* Desktop encrypts audio frames using XChaCha20 (libsodium) with a nonce
* derived from the RTP timestamp. The shared key is computed as
* nacl.box.before(peerPub, ownSecret) = HSalsa20(zeros, X25519(sk, pk)).
*
* This class provides:
* - [hsalsa20] — applies HSalsa20 to a raw X25519 shared secret,
* producing the same key as nacl.box.before().
* - [Encryptor] / [Decryptor] — WebRTC FrameEncryptor / FrameDecryptor
* that use XChaCha20 matching the Desktop implementation.
*/
object XChaCha20E2EE {
private const val TAG = "XChaCha20E2EE"
var nativeLoaded: Boolean = false
private set
private var crashFilePath: String? = null
fun initWithContext(context: android.content.Context) {
if (!nativeLoaded) return
try {
val dir = java.io.File(context.filesDir, "crash_reports")
if (!dir.exists()) dir.mkdirs()
val path = java.io.File(dir, "native_crash.txt").absolutePath
crashFilePath = path
nativeInstallCrashHandler(path)
Log.i(TAG, "Native crash handler installed → $path")
} catch (e: Throwable) {
Log.e(TAG, "Failed to install native crash handler", e)
}
}
init {
try {
System.loadLibrary("rosetta_e2ee")
nativeLoaded = true
Log.i(TAG, "Native library loaded successfully")
} catch (e: UnsatisfiedLinkError) {
Log.e(TAG, "Failed to load native library rosetta_e2ee", e)
}
}
/**
* HSalsa20(zeros_16, rawDhShared, sigma) — converts a raw X25519
* shared secret into the NaCl box-before shared key.
*/
fun hsalsa20(rawDhShared: ByteArray): ByteArray {
require(nativeLoaded) { "Native library not loaded" }
require(rawDhShared.size >= 32) { "Raw DH shared secret must be >= 32 bytes" }
return nativeHSalsa20(rawDhShared)
}
/** WebRTC [FrameEncryptor] backed by native XChaCha20. */
class Encryptor(key: ByteArray) : FrameEncryptor {
private val nativePtr: Long
init {
require(nativeLoaded) { "Native library not loaded" }
nativePtr = nativeCreateEncryptor(key)
Log.i(TAG, "Encryptor created, ptr=0x${nativePtr.toString(16)}")
}
override fun getNativeFrameEncryptor(): Long = nativePtr
fun dispose() {
if (nativePtr != 0L) nativeReleaseEncryptor(nativePtr)
}
}
/** WebRTC [FrameDecryptor] backed by native XChaCha20. */
class Decryptor(key: ByteArray) : FrameDecryptor {
private val nativePtr: Long
init {
require(nativeLoaded) { "Native library not loaded" }
nativePtr = nativeCreateDecryptor(key)
Log.i(TAG, "Decryptor created, ptr=0x${nativePtr.toString(16)}")
}
override fun getNativeFrameDecryptor(): Long = nativePtr
fun dispose() {
if (nativePtr != 0L) nativeReleaseDecryptor(nativePtr)
}
}
/* ── JNI ─────────────────────────────────────────────────── */
@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 nativeCreateDecryptor(key: ByteArray): Long
@JvmStatic private external fun nativeReleaseDecryptor(ptr: Long)
@JvmStatic private external fun nativeInstallCrashHandler(path: String)
@JvmStatic external fun nativeOpenDiagFile(path: String)
@JvmStatic external fun nativeCloseDiagFile()
}

View File

@@ -17,6 +17,9 @@ import androidx.compose.material.icons.filled.MicOff
import androidx.compose.material.icons.filled.Videocam import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.icons.filled.VideocamOff import androidx.compose.material.icons.filled.VideocamOff
import androidx.compose.material.icons.filled.VolumeOff import androidx.compose.material.icons.filled.VolumeOff
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.QrCode2
import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material.icons.filled.VolumeUp
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -93,17 +96,28 @@ fun CallOverlay(
Brush.verticalGradient(listOf(GradientTop, GradientMid, GradientBottom)) Brush.verticalGradient(listOf(GradientTop, GradientMid, GradientBottom))
) )
) { ) {
// ── Encryption badge top center ── // ── Top bar: "Encrypted" left + QR icon right ──
if (state.phase == CallPhase.ACTIVE || state.phase == CallPhase.CONNECTING) { if (state.phase == CallPhase.ACTIVE || state.phase == CallPhase.CONNECTING) {
Text( Row(
text = "\uD83D\uDD12 Encrypted",
color = Color.White.copy(alpha = 0.4f),
fontSize = 13.sp,
modifier = Modifier modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter) .align(Alignment.TopCenter)
.statusBarsPadding() .statusBarsPadding()
.padding(top = 12.dp) .padding(horizontal = 16.dp, vertical = 12.dp),
) horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "\uD83D\uDD12 Encrypted",
color = Color.White.copy(alpha = 0.4f),
fontSize = 13.sp,
)
// QR grid icon — tap to show popover
if (state.keyCast.isNotBlank()) {
EncryptionKeyButton(keyHex = state.keyCast)
}
}
} }
// ── Center content: rings + avatar + name + status ── // ── Center content: rings + avatar + name + status ──
@@ -162,14 +176,6 @@ fun CallOverlay(
) )
} }
// Emoji key
if (state.keyCast.isNotBlank() && state.phase == CallPhase.ACTIVE) {
Spacer(modifier = Modifier.height(16.dp))
val emojis = remember(state.keyCast) { keyToEmojis(state.keyCast) }
if (emojis.isNotBlank()) {
Text(emojis, fontSize = 32.sp, letterSpacing = 4.sp, textAlign = TextAlign.Center)
}
}
} }
// ── Bottom buttons ── // ── Bottom buttons ──
@@ -430,16 +436,113 @@ private fun formatCallDuration(seconds: Int): String {
return if (h > 0) "%d:%02d:%02d".format(h, m, sec) else "%02d:%02d".format(m, sec) return if (h > 0) "%d:%02d:%02d".format(h, m, sec) else "%02d:%02d".format(m, sec)
} }
private fun keyToEmojis(keyCast: String): String { /**
val emojis = listOf( * QR icon in top-right corner — tap to show encryption key dropdown.
"\uD83D\uDE00", "\uD83D\uDE0E", "\uD83D\uDE80", "\uD83D\uDD12", * 1:1 match with Desktop's IconQrcode + Popover.
"\uD83C\uDF1F", "\uD83C\uDF08", "\uD83D\uDC8E", "\uD83C\uDF40", */
"\uD83D\uDD25", "\uD83C\uDF3A", "\uD83E\uDD8B", "\uD83C\uDF0D", @Composable
"\uD83C\uDF89", "\uD83E\uDD84", "\uD83C\uDF52", "\uD83D\uDCA1" private fun EncryptionKeyButton(keyHex: String) {
) var showPopup by remember { mutableStateOf(false) }
val hex = keyCast.replace(Regex("[^0-9a-fA-F]"), "").take(8)
if (hex.length < 8) return "" Box {
return (0 until 4).joinToString(" ") { i -> // QR code icon (matches Desktop IconQrcode size={24} color="white")
emojis[hex.substring(i * 2, i * 2 + 2).toInt(16) % emojis.size] Icon(
imageVector = Icons.Default.QrCode2,
contentDescription = "Encryption key",
tint = Color.White,
modifier = Modifier
.size(24.dp)
.clickable { showPopup = !showPopup }
)
// Dropdown popover (matches Desktop Popover width={300} withArrow)
androidx.compose.material3.DropdownMenu(
expanded = showPopup,
onDismissRequest = { showPopup = false },
modifier = Modifier
.widthIn(max = 300.dp)
.background(Color(0xFF1E293B)),
) {
Column(modifier = Modifier.padding(12.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "This call is secured by 256 bit end-to-end encryption. " +
"Only you and the recipient can read or listen to the content of this call.",
color = Color.White.copy(alpha = 0.6f),
fontSize = 11.sp,
lineHeight = 15.sp,
modifier = Modifier.weight(1f)
)
Canvas(modifier = Modifier.size(80.dp)) {
drawKeyGrid(keyHex, size.width, this)
}
}
Spacer(modifier = Modifier.height(8.dp))
val clipboardManager = androidx.compose.ui.platform.LocalClipboardManager.current
var copied by remember { mutableStateOf(false) }
Row(
modifier = Modifier
.fillMaxWidth()
.clip(androidx.compose.foundation.shape.RoundedCornerShape(8.dp))
.background(Color.White.copy(alpha = 0.08f))
.clickable {
clipboardManager.setText(androidx.compose.ui.text.AnnotatedString(keyHex))
copied = true
}
.padding(horizontal = 10.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
Icon(
imageVector = if (copied) Icons.Default.Check else Icons.Default.ContentCopy,
contentDescription = "Copy key",
tint = Color.White.copy(alpha = 0.5f),
modifier = Modifier.size(14.dp)
)
Text(
text = if (copied) "Copied!" else keyHex.take(16) + "..." + keyHex.takeLast(8),
color = Color.White.copy(alpha = 0.4f),
fontSize = 10.sp,
maxLines = 1,
fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace
)
}
}
}
}
}
/** Palette matching Desktop's Mantine theme.colors.blue[1..5] */
private val KeyGridPalette = listOf(
Color(0xFFDBE4FF), // blue[1]
Color(0xFFBAC8FF), // blue[2]
Color(0xFF91A7FF), // blue[3]
Color(0xFF748FFC), // blue[4]
Color(0xFF5C7CFA), // blue[5]
)
/**
* Draw 8x8 color grid — same algorithm as Desktop KeyImage.tsx:
* each character's charCode % palette.size determines the color.
*/
private fun drawKeyGrid(keyHex: String, totalSize: Float, scope: androidx.compose.ui.graphics.drawscope.DrawScope) {
val cells = 8
val cellSize = totalSize / cells
for (i in 0 until cells * cells) {
val color = if (i < keyHex.length) {
KeyGridPalette[keyHex[i].code % KeyGridPalette.size]
} else {
KeyGridPalette[0]
}
val row = i / cells
val col = i % cells
scope.drawRect(
color = color,
topLeft = Offset(col * cellSize, row * cellSize),
size = androidx.compose.ui.geometry.Size(cellSize, cellSize)
)
} }
} }

View File

@@ -44,8 +44,8 @@ class CrashReportManager private constructor(private val context: Context) : Thr
fun getCrashReports(context: Context): List<CrashReport> { fun getCrashReports(context: Context): List<CrashReport> {
val crashDir = File(context.filesDir, CRASH_DIR) val crashDir = File(context.filesDir, CRASH_DIR)
if (!crashDir.exists()) return emptyList() if (!crashDir.exists()) return emptyList()
return crashDir.listFiles() val reports = crashDir.listFiles()
?.filter { it.extension == "txt" } ?.filter { it.extension == "txt" }
?.sortedByDescending { it.lastModified() } ?.sortedByDescending { it.lastModified() }
?.map { file -> ?.map { file ->
@@ -54,7 +54,21 @@ class CrashReportManager private constructor(private val context: Context) : Thr
timestamp = file.lastModified(), timestamp = file.lastModified(),
content = file.readText() content = file.readText()
) )
} ?: emptyList() }?.toMutableList() ?: mutableListOf()
// Include native crash report if present
val nativeCrash = File(crashDir, "native_crash.txt")
if (nativeCrash.exists() && nativeCrash.length() > 0) {
reports.add(0, CrashReport(
fileName = "native_crash.txt",
timestamp = nativeCrash.lastModified(),
content = nativeCrash.readText()
))
// Delete after reading so it doesn't show up again
nativeCrash.delete()
}
return reports
} }
/** /**