From e751e6ee69dbe0533c369cf2c961a5b240765cd6 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 28 Jul 2026 23:07:23 +0300 Subject: [PATCH] android: voice messages + attachments architecture plan, build fixes --- lib/Makefile.am | 2 - lib/sha256.c | 158 ------------------ lib/sha256.h | 41 ----- lib/sha256_doc.md | 32 ---- src/chat/chat_sync.c | 31 +++- src/chat/db_sync.c | 9 +- src/transport_layer/secure_channel.c | 50 +++--- src/utun_instance.c | 22 ++- tools/chatgui-android/AGENTS.md | 103 +++++++++++- .../app/src/main/AndroidManifest.xml | 1 + .../utun/chat/ui/components/MessageBubble.kt | 4 +- .../utun/chat/ui/screens/ChannelListScreen.kt | 57 +++++-- .../com/utun/chat/ui/screens/ChatScreen.kt | 45 +++-- .../jni_bridge/android_jni_bridge.c | 51 +++++- .../libutun_lite/instance_lite.c | 11 ++ tools/chatgui/src/joindialog.cpp | 1 - tools/chatgui/transport/node_config.cpp | 8 +- 17 files changed, 298 insertions(+), 328 deletions(-) delete mode 100644 lib/sha256.c delete mode 100644 lib/sha256.h delete mode 100644 lib/sha256_doc.md diff --git a/lib/Makefile.am b/lib/Makefile.am index b5a7ed18..6e23aa65 100644 --- a/lib/Makefile.am +++ b/lib/Makefile.am @@ -11,8 +11,6 @@ libuasync_a_SOURCES = \ timeout_heap.h \ memory_pool.c \ memory_pool.h \ - sha256.c \ - sha256.h \ socket_compat.c \ socket_compat.h \ platform_compat.c \ diff --git a/lib/sha256.c b/lib/sha256.c deleted file mode 100644 index 84691b36..00000000 --- a/lib/sha256.c +++ /dev/null @@ -1,158 +0,0 @@ -/********************************************************************* -* Filename: sha256.c -* Author: Brad Conte (brad AT bradconte.com) -* Copyright: -* Disclaimer: This code is presented "as is" without any guarantees. -* Details: Implementation of the SHA-256 hashing algorithm. - SHA-256 is one of the three algorithms in the SHA2 - specification. The others, SHA-384 and SHA-512, are not - offered in this implementation. - Algorithm specification can be found here: - * http://csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf - This implementation uses little endian byte order. -*********************************************************************/ - -/*************************** HEADER FILES ***************************/ -#include -#include -#include "sha256.h" - -/****************************** MACROS ******************************/ -#define ROTLEFT(a,b) (((a) << (b)) | ((a) >> (32-(b)))) -#define ROTRIGHT(a,b) (((a) >> (b)) | ((a) << (32-(b)))) - -#define CH(x,y,z) (((x) & (y)) ^ (~(x) & (z))) -#define MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) -#define EP0(x) (ROTRIGHT(x,2) ^ ROTRIGHT(x,13) ^ ROTRIGHT(x,22)) -#define EP1(x) (ROTRIGHT(x,6) ^ ROTRIGHT(x,11) ^ ROTRIGHT(x,25)) -#define SIG0(x) (ROTRIGHT(x,7) ^ ROTRIGHT(x,18) ^ ((x) >> 3)) -#define SIG1(x) (ROTRIGHT(x,17) ^ ROTRIGHT(x,19) ^ ((x) >> 10)) - -/**************************** VARIABLES *****************************/ -static const uint32_t k[64] = { - 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5, - 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174, - 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da, - 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967, - 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85, - 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070, - 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3, - 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2 -}; - -/*********************** FUNCTION DEFINITIONS ***********************/ -static void sc_sha256_transform(SC_SHA256_CTX *ctx, const uint8_t data[]) -{ - uint32_t a, b, c, d, e, f, g, h, i, j, t1, t2, m[64]; - - for (i = 0, j = 0; i < 16; ++i, j += 4) - m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]); - for ( ; i < 64; ++i) - m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16]; - - a = ctx->state[0]; - b = ctx->state[1]; - c = ctx->state[2]; - d = ctx->state[3]; - e = ctx->state[4]; - f = ctx->state[5]; - g = ctx->state[6]; - h = ctx->state[7]; - - for (i = 0; i < 64; ++i) { - t1 = h + EP1(e) + CH(e,f,g) + k[i] + m[i]; - t2 = EP0(a) + MAJ(a,b,c); - h = g; - g = f; - f = e; - e = d + t1; - d = c; - c = b; - b = a; - a = t1 + t2; - } - - ctx->state[0] += a; - ctx->state[1] += b; - ctx->state[2] += c; - ctx->state[3] += d; - ctx->state[4] += e; - ctx->state[5] += f; - ctx->state[6] += g; - ctx->state[7] += h; -} - -void sc_sha256_init(SC_SHA256_CTX *ctx) -{ - ctx->datalen = 0; - ctx->bitlen = 0; - ctx->state[0] = 0x6a09e667; - ctx->state[1] = 0xbb67ae85; - ctx->state[2] = 0x3c6ef372; - ctx->state[3] = 0xa54ff53a; - ctx->state[4] = 0x510e527f; - ctx->state[5] = 0x9b05688c; - ctx->state[6] = 0x1f83d9ab; - ctx->state[7] = 0x5be0cd19; -} - -void sc_sha256_update(SC_SHA256_CTX *ctx, const uint8_t data[], size_t len) -{ - uint32_t i; - - for (i = 0; i < len; ++i) { - ctx->data[ctx->datalen] = data[i]; - ctx->datalen++; - if (ctx->datalen == 64) { - sc_sha256_transform(ctx, ctx->data); - ctx->bitlen += 512; - ctx->datalen = 0; - } - } -} - -void sc_sha256_final(SC_SHA256_CTX *ctx, uint8_t hash[]) -{ - uint32_t i; - - i = ctx->datalen; - - // Pad whatever data is left in the buffer. - if (ctx->datalen < 56) { - ctx->data[i++] = 0x80; - while (i < 56) - ctx->data[i++] = 0x00; - } - else { - ctx->data[i++] = 0x80; - while (i < 64) - ctx->data[i++] = 0x00; - sc_sha256_transform(ctx, ctx->data); - memset(ctx->data, 0, 56); - } - - // Append to the padding the total message's length in bits and transform. - ctx->bitlen += ctx->datalen * 8; - ctx->data[63] = ctx->bitlen; - ctx->data[62] = ctx->bitlen >> 8; - ctx->data[61] = ctx->bitlen >> 16; - ctx->data[60] = ctx->bitlen >> 24; - ctx->data[59] = ctx->bitlen >> 32; - ctx->data[58] = ctx->bitlen >> 40; - ctx->data[57] = ctx->bitlen >> 48; - ctx->data[56] = ctx->bitlen >> 56; - sc_sha256_transform(ctx, ctx->data); - - // Since this implementation uses little endian byte ordering and SHA uses big endian, - // reverse all the bytes when copying the final state to the output hash. - for (i = 0; i < 4; ++i) { - hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff; - hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff; - hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff; - hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff; - hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff; - hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff; - hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff; - hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff; - } -} diff --git a/lib/sha256.h b/lib/sha256.h deleted file mode 100644 index d6fd600a..00000000 --- a/lib/sha256.h +++ /dev/null @@ -1,41 +0,0 @@ -/********************************************************************* -* Filename: sha256.h -* Author: Brad Conte (brad AT bradconte.com) -* Copyright: -* Disclaimer: This code is presented "as is" without any guarantees. -* Details: Defines the API for the corresponding SHA1 implementation. -*********************************************************************/ - -#ifndef SC_SHA256_H -#define SC_SHA256_H - -#ifdef __cplusplus -extern "C" { -#endif - - -/*************************** HEADER FILES ***************************/ -#include -#include - -/****************************** MACROS ******************************/ -#define SC_SHA256_BLOCK_SIZE 32 // SHA256 outputs a 32 byte digest - -/**************************** DATA TYPES ****************************/ -typedef struct { - uint8_t data[64]; - uint32_t datalen; - unsigned long long bitlen; - uint32_t state[8]; -} SC_SHA256_CTX; - -/*********************** FUNCTION DECLARATIONS **********************/ -void sc_sha256_init(SC_SHA256_CTX *ctx); -void sc_sha256_update(SC_SHA256_CTX *ctx, const uint8_t data[], size_t len); -void sc_sha256_final(SC_SHA256_CTX *ctx, uint8_t hash[]); - - -#ifdef __cplusplus -} -#endif -#endif // SC_SHA256_H diff --git a/lib/sha256_doc.md b/lib/sha256_doc.md deleted file mode 100644 index 7dfabbe9..00000000 --- a/lib/sha256_doc.md +++ /dev/null @@ -1,32 +0,0 @@ -# SHA-256 (sha256.h) - -## 1. Назначение -Чистая C-реализация SHA-256 по FIPS 180-2, без внешних зависимостей. Используется как fallback при отключенной OpenSSL (`USE_OPENSSL` не определён) — в `secure_channel.c` для деривации session key, stream nonce, хеширования pubkey для обфускации и в `db_sync.c` для контрольных сумм БД. - -## 2. Как пользоваться -Трёхфазный API: init → update (можно много раз) → final. - -```c -SC_SHA256_CTX ctx; -uint8_t hash[SC_SHA256_BLOCK_SIZE]; // 32 байта - -sc_sha256_init(&ctx); -sc_sha256_update(&ctx, data1, len1); -sc_sha256_update(&ctx, data2, len2); // можно добавлять данные порциями -sc_sha256_final(&ctx, hash); // hash — 32 байта, после этого ctx непригоден -``` - -После `sc_sha256_final` контекст нельзя переиспользовать без повторного `sc_sha256_init`. - -## 3. API - -### Структура -- **`SC_SHA256_CTX`** — контекст хеширования: буфер 64 байта, счётчик данных, счётчик бит, 8 слов состояния. - -### Функции -- **`sc_sha256_init(ctx)`** — инициализирует контекст начальными значениями IV (первые 32 бита дробных частей квадратных корней первых 8 простых чисел). -- **`sc_sha256_update(ctx, data, len)`** — добавляет данные в хеш; обрабатывает блоки по 64 байта через `sc_sha256_transform` (внутренняя, раунды SHA-256). -- **`sc_sha256_final(ctx, hash)`** — завершает хеширование: дополняет данные до кратных 512 бит (padding с 0x80 и длиной сообщения), выполняет трансформацию, выдаёт 32-байтовый дайджест в big-endian порядке. - -### Константа -- **`SC_SHA256_BLOCK_SIZE`** (32) — размер выходного дайджеста в байтах. diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c index 6d5d0d95..9ff49d64 100644 --- a/src/chat/chat_sync.c +++ b/src/chat/chat_sync.c @@ -226,6 +226,27 @@ static struct ETCP_CONN* cs_find_conn_for_node(struct UTUN_INSTANCE* inst, uint6 return NULL; } +/* ── helper: find active ETCP_CONN by pubkey (for invite: same physical node, different node_id) ── */ + +static struct ETCP_CONN* cs_find_conn_by_pubkey(struct UTUN_INSTANCE* inst, + const uint8_t* pubkey_bin, + uint64_t* out_node_id) { + struct ll_entry* entry = inst->connections ? inst->connections->head : NULL; + while (entry) { + struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; + struct ETCP_CONN* conn = ce->conn; + if (conn && conn->initialized && conn->links_up + && memcmp(conn->crypto_ctx.peer_public_key, pubkey_bin, 32) == 0) { + *out_node_id = conn->peer_node_id; + DEBUG_INFO(DEBUG_CATEGORY_DB_SYNC, "%s: find_conn_by_pubkey MATCH conn=%s peer=0x%016llx", + CS_ID, conn->log_name, (unsigned long long)conn->peer_node_id); + return conn; + } + entry = entry->next; + } + return NULL; +} + /* ── helper: check if peer has active ETCP link ── */ static int cs_is_peer_online(struct UTUN_INSTANCE* inst, uint64_t peer_id) { @@ -642,11 +663,17 @@ static void cm_invite_trampoline(void* arg) { } struct ETCP_CONN* existing = instance_find_conn(inst, node_id); + if (!existing) existing = cs_find_conn_by_pubkey(inst, inv->pubkey, &node_id); + if (existing) { if (existing->links_up) { - DEBUG_INFO(DEBUG_CATEGORY_DB_SYNC, "%s: invite conn already UP node=0x%016llx, reusing", CS_ID, (unsigned long long)node_id); - if (g_cs && g_cs->pending_invite_ch_id != 0 && !g_cs->info_req_timer) + if (g_cs && g_cs->pending_invite_ch_id != 0 && !g_cs->info_req_timer) { + if (node_id != existing->peer_node_id) + DEBUG_WARN(DEBUG_CATEGORY_DB_SYNC, + "%s: invite node_id mismatch: derived=%016llx ETCP=%016llx — using ETCP", + CS_ID, (unsigned long long)node_id, (unsigned long long)existing->peer_node_id); cs_on_conn_up(existing, NULL); + } } else { DEBUG_INFO(DEBUG_CATEGORY_DB_SYNC, "%s: invite conn in progress node=0x%016llx, waiting UP", CS_ID, (unsigned long long)node_id); } diff --git a/src/chat/db_sync.c b/src/chat/db_sync.c index 6352017a..11d37024 100644 --- a/src/chat/db_sync.c +++ b/src/chat/db_sync.c @@ -10,7 +10,7 @@ #include "../../lib/debug_config.h" #include "../../lib/mem.h" #include "../../lib/u_async.h" -#include "../../lib/sha256.h" +#include #include "../../lib/sqlite3.h" #include "../../lib/platform_compat.h" #include @@ -131,10 +131,7 @@ static void db_sqlite_close(struct DB_SYNC* db) static void db_sha256(const uint8_t* data, size_t len, uint8_t hash[32]) { - SC_SHA256_CTX ctx; - sc_sha256_init(&ctx); - sc_sha256_update(&ctx, data, len); - sc_sha256_final(&ctx, hash); + SHA256(data, len, hash); } static uint64_t db_hash64(const uint8_t* data, size_t len) @@ -1220,7 +1217,7 @@ static void db_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) const char* tbl = (const char*)sqlite3_column_text(st, 0); const char* ch_id = (const char*)sqlite3_column_text(st, 1); if (!ch_id || !ch_id[0]) continue; - uint64_t h; { uint8_t sh[32]; SC_SHA256_CTX ctx; sc_sha256_init(&ctx); sc_sha256_update(&ctx, (const uint8_t*)ch_id, strlen(ch_id)); sc_sha256_final(&ctx, sh); memcpy(&h, sh, 8); } + uint64_t h; { uint8_t sh[32]; SHA256((const uint8_t*)ch_id, strlen(ch_id), sh); memcpy(&h, sh, 8); } if (h == hash) { si = db_sync_instance_add(db->inst, tbl, hash, 1); if (si) { si_peer_add(si, src); } diff --git a/src/transport_layer/secure_channel.c b/src/transport_layer/secure_channel.c index d650c290..19a15aee 100644 --- a/src/transport_layer/secure_channel.c +++ b/src/transport_layer/secure_channel.c @@ -17,7 +17,6 @@ #include #include #include "crc32.h" -#include "../lib/sha256.h" #include #include @@ -99,29 +98,29 @@ static sc_status_t verify_and_strip_crc32(uint8_t *plaintext_with_crc, size_t to } static void sc_derive_session_key(const uint8_t *shared_secret, uint8_t *session_key) { - SC_SHA256_CTX sha_ctx; + SHA256_CTX sha_ctx; uint8_t hash[SC_HASH_SIZE]; - sc_sha256_init(&sha_ctx); - sc_sha256_update(&sha_ctx, shared_secret, SC_SHARED_SECRET_SIZE); - sc_sha256_update(&sha_ctx, (const uint8_t *)"uTun-v3-session", 15); - sc_sha256_final(&sha_ctx, hash); + SHA256_Init(&sha_ctx); + SHA256_Update(&sha_ctx, shared_secret, SC_SHARED_SECRET_SIZE); + SHA256_Update(&sha_ctx, (const uint8_t *)"uTun-v3-session", 15); + SHA256_Final(hash, &sha_ctx); memcpy(session_key, hash, SC_SESSION_KEY_SIZE); } // Derive 12-byte nonce for stream cipher from session key + stream_id static void sc_stream_derive_nonce(const uint8_t *session_key, uint32_t stream_id, uint8_t *nonce_out) { - SC_SHA256_CTX sha_ctx; + SHA256_CTX sha_ctx; uint8_t hash[SC_HASH_SIZE]; uint8_t stream_id_le[4]; stream_id_le[0] = (uint8_t)(stream_id >> 0); stream_id_le[1] = (uint8_t)(stream_id >> 8); stream_id_le[2] = (uint8_t)(stream_id >> 16); stream_id_le[3] = (uint8_t)(stream_id >> 24); - sc_sha256_init(&sha_ctx); - sc_sha256_update(&sha_ctx, session_key, SC_SESSION_KEY_SIZE); - sc_sha256_update(&sha_ctx, stream_id_le, sizeof(stream_id_le)); - sc_sha256_update(&sha_ctx, (const uint8_t *)"uTun3-stream", 12); - sc_sha256_final(&sha_ctx, hash); + SHA256_Init(&sha_ctx); + SHA256_Update(&sha_ctx, session_key, SC_SESSION_KEY_SIZE); + SHA256_Update(&sha_ctx, stream_id_le, sizeof(stream_id_le)); + SHA256_Update(&sha_ctx, (const uint8_t *)"uTun3-stream", 12); + SHA256_Final(hash, &sha_ctx); memcpy(nonce_out, hash, SC_STREAM_NONCE_SIZE); } @@ -424,8 +423,8 @@ sc_status_t sc_derive_ed25519_pubkey(const uint8_t *x25519_privkey, uint8_t *ed2 uint64_t sc_derive_node_id_from_pubkey(const uint8_t *public_key) { if (!public_key) { DEBUG_ERROR(DEBUG_CATEGORY_CRYPTO, "sc_derive_node_id_from_pubkey: NULL public_key"); return 0; } - uint8_t sha_hash[SC_HASH_SIZE]; SC_SHA256_CTX ctx; - sc_sha256_init(&ctx); sc_sha256_update(&ctx, public_key, SC_PUBKEY_SIZE); sc_sha256_final(&ctx, sha_hash); + uint8_t sha_hash[SC_HASH_SIZE]; + SHA256(public_key, SC_PUBKEY_SIZE, sha_hash); uint64_t node_id; memcpy(&node_id, sha_hash, 8); node_id &= 0x7FFFFFFFFFFFFFFFULL; @@ -572,10 +571,7 @@ sc_status_t sc_ed25519_verify(const uint8_t pubkey[32], const uint8_t* msg, size sc_status_t sc_sha_transcode(const uint8_t *key, size_t key_len, uint8_t *data, size_t data_len) { if (!key || !data || key_len == 0 || data_len == 0) return SC_ERR_INVALID_ARG; uint8_t sha_hash[SC_HASH_SIZE]; - SC_SHA256_CTX ctx; - sc_sha256_init(&ctx); - sc_sha256_update(&ctx, key, key_len); - sc_sha256_final(&ctx, sha_hash); + SHA256(key, key_len, sha_hash); for (size_t i = 0; i < data_len; i++) data[i] ^= sha_hash[i % SC_HASH_SIZE]; return SC_OK; } @@ -583,15 +579,15 @@ sc_status_t sc_sha_transcode(const uint8_t *key, size_t key_len, uint8_t *data, sc_status_t sc_obfuscate_pubkey(const uint8_t *salt, const uint8_t *peer_pubkey, const uint8_t *pubkey, uint8_t *output) { if (!salt || !peer_pubkey || !pubkey || !output) return SC_ERR_INVALID_ARG; uint8_t sha[SC_HASH_SIZE*2]; - SC_SHA256_CTX ctx; - sc_sha256_init(&ctx); - sc_sha256_update(&ctx, salt, SC_PUBKEY_ENC_SALT_SIZE); - sc_sha256_update(&ctx, peer_pubkey, SC_PUBKEY_SIZE); - sc_sha256_final(&ctx, sha); - sc_sha256_init(&ctx); - sc_sha256_update(&ctx, peer_pubkey, SC_PUBKEY_SIZE); - sc_sha256_update(&ctx, salt, SC_PUBKEY_ENC_SALT_SIZE); - sc_sha256_final(&ctx, sha+SC_HASH_SIZE); + SHA256_CTX ctx; + SHA256_Init(&ctx); + SHA256_Update(&ctx, salt, SC_PUBKEY_ENC_SALT_SIZE); + SHA256_Update(&ctx, peer_pubkey, SC_PUBKEY_SIZE); + SHA256_Final(sha, &ctx); + SHA256_Init(&ctx); + SHA256_Update(&ctx, peer_pubkey, SC_PUBKEY_SIZE); + SHA256_Update(&ctx, salt, SC_PUBKEY_ENC_SALT_SIZE); + SHA256_Final(sha + SC_HASH_SIZE, &ctx); for (size_t i = 0; i < SC_PUBKEY_SIZE; i++) output[i] = pubkey[i] ^ sha[i]; return SC_OK; } diff --git a/src/utun_instance.c b/src/utun_instance.c index a26f8e62..60e5ba9d 100644 --- a/src/utun_instance.c +++ b/src/utun_instance.c @@ -21,7 +21,6 @@ #include "../lib/u_async.h" #include "../lib/debug_config.h" -#include "../lib/sha256.h" #include #include #include @@ -101,12 +100,21 @@ static int instance_init_common(struct UTUN_INSTANCE* instance, struct UASYNC* u return -1; } - // Derive node_id from pubkey if not set in config - if (!instance->node_id) { - uint8_t pubkey[SC_PUBKEY_SIZE]; - sc_compute_public_key_from_private(instance->my_keys.private_key, pubkey); - instance->node_id = sc_derive_node_id_from_pubkey(pubkey); - DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "node_id derived from pubkey: %016llx", (unsigned long long)instance->node_id); + /* Always verify node_id matches pubkey, fix if mismatched */ + { + uint8_t pub_bin[SC_PUBKEY_SIZE]; + sc_compute_public_key_from_private(instance->my_keys.private_key, pub_bin); + uint64_t derived = sc_derive_node_id_from_pubkey(pub_bin); + DEBUG_INFO(DEBUG_CATEGORY_CONFIG, "node_id check: config=%016llx derived=%016llx match=%d", + (unsigned long long)instance->node_id, (unsigned long long)derived, + (instance->node_id == derived)); + if (!instance->node_id || instance->node_id != derived) { + if (instance->node_id) + DEBUG_WARN(DEBUG_CATEGORY_CONFIG, "node_id mismatch: config=%016llx derived=%016llx — fixing", + (unsigned long long)instance->node_id, (unsigned long long)derived); + instance->node_id = derived; + config->global.my_node_id = derived; + } } if (sc_derive_ed25519_pubkey(instance->my_keys.private_key, instance->my_ed25519_pubkey) != SC_OK) { diff --git a/tools/chatgui-android/AGENTS.md b/tools/chatgui-android/AGENTS.md index 0f6ed770..9531db04 100644 --- a/tools/chatgui-android/AGENTS.md +++ b/tools/chatgui-android/AGENTS.md @@ -71,16 +71,64 @@ tools/chatgui-android/ | Сборка C | CMake 3.16+ | GCC (Linux) / Clang (Android NDK) | | Сборка APK | Gradle 8.x | AGP 8.x, Kotlin 1.9 | -## Сборка +## Зависимости -### Headless (Linux, для отладки сетевого стека) +### Для сборки APK (Android) + +| Компонент | Версия | Установка | +|-----------|--------|-----------| +| JDK | 17+ | `apt install openjdk-17-jdk` | +| CMake | 3.22+ | `apt install cmake` | +| Android SDK | API 36 | через [sdkmanager](https://developer.android.com/studio#command-line-tools-only) или Android Studio | +| Android NDK | 29.0.14206865 | `sdkmanager "ndk;29.0.14206865"` | +| Gradle | 8.5 (wrapper) | `./gradlew` скачает сам | +| Kotlin | 1.9.20 | встроен в Gradle плагин | +| adb | platform-tools | `apt install adb` (для прошивки) | + +OpenSSL для APK **предсобран** под `arm64-v8a` и `x86_64`, лежит в `app/src/main/cpp/openssl/`. +Собирать отдельно не нужно. + +### Для Headless (Linux, отладка без телефона) + +| Компонент | Установка | +|-----------|-----------| +| CMake 3.16+ | `apt install cmake` | +| GCC / Clang | `apt install build-essential` | +| OpenSSL dev | `apt install libssl-dev` | +| pthread | встроен в libc | + +### Настройка окружения + +Создать `local.properties` в корне `tools/chatgui-android/`: + +```properties +sdk.dir=/home/user/Android/Sdk +``` + +Путь `ndk.dir` можно не указывать — Gradle скачает NDK сам (версия из `ndkVersion` в `app/build.gradle.kts`). +Если NDK уже установлен отдельно: + +```properties +ndk.dir=/home/user/Android/Sdk/ndk/29.0.14206865 +``` + +### Установка Android SDK (без Android Studio) ```bash -cd tools/chatgui-android && mkdir -p build && cd build -cmake .. && make -j4 -./headless/headless +cd /tmp && wget https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip +unzip commandlinetools-linux-*.zip -d ~/Android +mkdir -p ~/Android/Sdk/cmdline-tools +mv ~/Android/cmdline-tools ~/Android/Sdk/cmdline-tools/latest + +export ANDROID_HOME=~/Android/Sdk +export PATH=$ANDROID_HOME/cmdline-tools/latest/bin:$PATH + +yes | sdkmanager --licenses +sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865" ``` +## Сборка + ### APK (Android) ```bash @@ -90,6 +138,14 @@ export ANDROID_HOME=/home/user/Android/Sdk # APK: app/build/outputs/apk/debug/app-debug.apk ``` +### Headless (Linux, для отладки сетевого стека) + +```bash +cd tools/chatgui-android && mkdir -p build && cd build +cmake .. && make -j4 +./headless/headless +``` + ## Прошивка на телефон ```bash @@ -109,6 +165,43 @@ nc localhost 9999 > {"id":2,"cmd":"join","link":"utun://..."} ``` +## Конфиг + +Конфиг хранится в Android DataStore (`ConfigProvider.kt`) и передаётся в C-ядро +при старте как INI-текст через `nativeStart(configText)`. + +Формат (генерируется автоматически, пользователь не редактирует): + +```ini +[global] +my_node_name=MyDevice +my_public_key=<64 hex chars X25519> +my_private_key=<64 hex chars X25519> +db_path=/data/data/com.utun.chat/files +db_sync_enabled=1 + +[server:main] +addr=0.0.0.0: + +[allowed_keys] +allow_all=yes + +[ntp] +enabled=no + +[debug] +console_level=info + +# Опционально — UDP-лог +[log_udp] +ip= +port=9999 +``` + +Ключи (`my_public_key`, `my_private_key`) генерируются автоматически при первом запуске +и сохраняются в `keys.conf` в `db_path`. Имя узла (`my_node_name`) берётся из +настроек DataStore или `Build.MODEL`. + ## Логирование через logreceiver (только Android) Debug-сообщения с телефона дублируются через UDP на приёмник `tools/logreceiver/`. diff --git a/tools/chatgui-android/app/src/main/AndroidManifest.xml b/tools/chatgui-android/app/src/main/AndroidManifest.xml index 527ed4f7..c8c87e7c 100644 --- a/tools/chatgui-android/app/src/main/AndroidManifest.xml +++ b/tools/chatgui-android/app/src/main/AndroidManifest.xml @@ -16,6 +16,7 @@ diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt index c61f0baf..0eea27c2 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt @@ -9,6 +9,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -19,6 +20,7 @@ fun MessageBubble(message: Message) { val bgColor = if (message.isOutgoing) Color(0xFF4A90D9) else Color(0xFFE8E8E8) val textColor = if (message.isOutgoing) Color.White else Color.Black val alignment = if (message.isOutgoing) Arrangement.End else Arrangement.Start + val minBubbleWidth = (LocalConfiguration.current.screenWidthDp / 3).dp Row( modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 2.dp), @@ -26,7 +28,7 @@ fun MessageBubble(message: Message) { ) { Column( modifier = Modifier - .widthIn(max = 300.dp) + .widthIn(min = minBubbleWidth, max = 300.dp) .clip(RoundedCornerShape(12.dp)) .background(bgColor) .padding(12.dp) diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt index 49d787e9..50b54ce9 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.QrCode @@ -15,8 +16,10 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.utun.chat.data.Channel @@ -72,26 +75,48 @@ fun ChannelListScreen( } else { LazyColumn { items(channels) { ch -> + val barColor = if (ch.peersOnline > 0) Color(0xFF4CAF50) else Color(0xFF9E9E9E) + val badgeText = when { + ch.peersOnline > 99 -> "99+" + ch.peersOnline > 0 -> ch.peersOnline.toString() + else -> "" + } + Surface(modifier = Modifier.clickable { onSelectChannel(ch) }) { - Row(Modifier.fillMaxWidth().padding(16.dp)) { - Box(Modifier.size(48.dp), contentAlignment = androidx.compose.ui.Alignment.Center) { - Surface(shape = MaterialTheme.shapes.small) { - Text("#", fontSize = 24.sp, modifier = Modifier.padding(8.dp)) + Row(Modifier.fillMaxWidth().height(IntrinsicSize.Min)) { + Box(Modifier.width(4.dp).fillMaxHeight().background(barColor)) + Row(Modifier.fillMaxWidth().padding(12.dp)) { + Box(Modifier.size(44.dp), contentAlignment = Alignment.Center) { + Surface(shape = MaterialTheme.shapes.small) { + Text("#", fontSize = 22.sp, modifier = Modifier.padding(8.dp)) + } + if (badgeText.isNotEmpty()) { + val badgeSize = 18.dp + Box( + Modifier + .align(Alignment.TopStart) + .offset(x = (-4).dp, y = (-6).dp) + .size(badgeSize) + .clip(CircleShape) + .background(Color(0xFF4CAF50)), + contentAlignment = Alignment.Center + ) { + Text( + badgeText, fontSize = 9.sp, color = Color.White, + fontWeight = FontWeight.Bold, textAlign = TextAlign.Center + ) + } + } } - } - Spacer(Modifier.width(12.dp)) - Column { - Row { - Text(ch.name, fontWeight = FontWeight.Bold, fontSize = 16.sp) - if (ch.peersOnline > 0) { - Text(" (${ch.peersOnline})", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(start = 4.dp)) + Spacer(Modifier.width(10.dp)) + Column { + Row(verticalAlignment = Alignment.CenterVertically) { + Text(ch.name, fontWeight = FontWeight.Bold, fontSize = 16.sp) } + if (ch.lastMsg.isNotEmpty()) + Text(ch.lastMsg, fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) } - if (ch.lastMsg.isNotEmpty()) - Text(ch.lastMsg, fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) } } } diff --git a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt index 77ba2bb6..8c80a3e4 100644 --- a/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt +++ b/tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt @@ -5,7 +5,7 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -31,30 +31,17 @@ fun ChatScreen( if (messages.isNotEmpty()) listState.animateScrollToItem(messages.size - 1) } - Scaffold( - topBar = { - TopAppBar( - title = { Text(channel.name) }, - navigationIcon = { - IconButton(onClick = onBack) { Icon(Icons.Default.ArrowBack, "Back") } - } - ) - }, - bottomBar = { - InputBar( - text = text, - onTextChange = { text = it }, - onSend = { - onSendMessage(text) - text = "" - }, - enabled = true, - modifier = Modifier.navigationBarsPadding() - ) - } - ) { padding -> + Column( + modifier = Modifier.fillMaxSize().imePadding() + ) { + TopAppBar( + title = { Text(channel.name) }, + navigationIcon = { + IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, "Back") } + } + ) LazyColumn( - modifier = Modifier.fillMaxSize().padding(padding).imePadding(), + modifier = Modifier.weight(1f).fillMaxWidth(), state = listState ) { items(messages.size) { idx -> @@ -62,5 +49,15 @@ fun ChatScreen( } item { Spacer(Modifier.height(8.dp)) } } + InputBar( + text = text, + onTextChange = { text = it }, + onSend = { + onSendMessage(text) + text = "" + }, + enabled = true, + modifier = Modifier.navigationBarsPadding() + ) } } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index f06c9b61..5a44d74b 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -15,6 +15,7 @@ #include "../../../lib/u_async.h" #include "../../../lib/mem.h" #include "../../../lib/sqlite3.h" +#include "../../../src/chat/chat_core.h" #include #include #include @@ -168,6 +169,24 @@ void utun_bridge_poll(void) { void utun_bridge_send_message(const char* channel_id, const char* text) { bridge_log(BLEV_DEBUG, "send ch=%s text=%s", channel_id ? channel_id : "", text ? text : ""); + if (!channel_id || !text) return; + + struct UASYNC* ua = instance_lite_get_uasync(); + if (!ua) { bridge_log(BLEV_ERROR, "send: ua not ready"); return; } + + size_t datalen = strlen(text); + struct chat_msg_submit* req = u_calloc(1, sizeof(struct chat_msg_submit) + datalen); + if (!req) return; + + strncpy(req->channel_id, channel_id, sizeof(req->channel_id) - 1); + strncpy(req->content_type, "text/plain", sizeof(req->content_type) - 1); + req->data = (uint8_t*)(req + 1); + req->data_len = (uint32_t)datalen; + memcpy(req->data, text, datalen); + req->timestamp = 0; + + uasync_post(ua, chat_core_submit_trampoline, req); + bridge_log(BLEV_DEBUG, "send queued ch=%s len=%zu", channel_id, datalen); } void utun_bridge_create_channel(const char* name, const char* channel_id) { @@ -232,6 +251,19 @@ static char* json_escape_alloc(const char* str) { return out; } +static void bridge_get_node_name(sqlite3* db, uint64_t node_id, char* out, size_t sz) { + out[0] = '\0'; + if (!db || node_id == 0) return; + sqlite3_stmt* st = NULL; + if (sqlite3_prepare_v2(db, "SELECT name FROM nodes WHERE node_id=?", -1, &st, NULL) != SQLITE_OK) return; + sqlite3_bind_int64(st, 1, (sqlite3_int64)node_id); + if (sqlite3_step(st) == SQLITE_ROW) { + const char* n = (const char*)sqlite3_column_text(st, 0); + if (n && n[0]) snprintf(out, sz, "%s", n); + } + sqlite3_finalize(st); +} + char* utun_bridge_get_channels_json(void) { sqlite3* db = chat_core_get_db(); if (!chat_core_is_initialized() || !db) { @@ -274,7 +306,11 @@ char* utun_bridge_get_channels_json(void) { const char* ds = strstr(dp, "\"d\":\""); if (ds) { ds += 5; char* de = strchr((char*)ds, '"'); if (de) { size_t tl = (size_t)(de - ds); if (tl > 120) tl = 120; memcpy(txt, ds, tl); txt[tl] = '\0'; } } } - snprintf(last_msg, sizeof(last_msg), "0x%016llx: %s", (unsigned long long)author, txt); + char author_name[128]; + bridge_get_node_name(db, (uint64_t)author, author_name, sizeof(author_name)); + if (!author_name[0]) + snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)author); + snprintf(last_msg, sizeof(last_msg), "%s: %s", author_name, txt); } sqlite3_finalize(lm); } @@ -344,12 +380,17 @@ char* utun_bridge_get_messages_json(const char* channel_id, int limit) { const char* sep = first ? "" : ","; first = 0; - size_t needed = snprintf(NULL, 0, "%s{\"id\":%lld,\"author\":\"0x%016llx\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s}", - sep, (long long)msg_id, (unsigned long long)node_id, esc_txt, (long long)ts, is_out ? "true" : "false"); + char author_name[128]; + bridge_get_node_name(db, (uint64_t)node_id, author_name, sizeof(author_name)); + if (!author_name[0]) + snprintf(author_name, sizeof(author_name), "0x%016llx", (unsigned long long)node_id); + + size_t needed = snprintf(NULL, 0, "%s{\"id\":%lld,\"author\":\"%s\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s}", + sep, (long long)msg_id, author_name, esc_txt, (long long)ts, is_out ? "true" : "false"); while (pos + needed + 2 > cap) { cap *= 2; char* tmp = u_realloc(json, cap); if (!tmp) break; json = tmp; } if (pos + needed + 2 <= cap) - pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":%lld,\"author\":\"0x%016llx\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s}", - sep, (long long)msg_id, (unsigned long long)node_id, esc_txt, (long long)ts, is_out ? "true" : "false"); + pos += (size_t)snprintf(json + pos, cap - pos, "%s{\"id\":%lld,\"author\":\"%s\",\"text\":\"%s\",\"ts\":%lld,\"isOutgoing\":%s}", + sep, (long long)msg_id, author_name, esc_txt, (long long)ts, is_out ? "true" : "false"); u_free(esc_txt); } sqlite3_finalize(st); diff --git a/tools/chatgui-android/libutun_lite/instance_lite.c b/tools/chatgui-android/libutun_lite/instance_lite.c index 25113dce..52521f9f 100644 --- a/tools/chatgui-android/libutun_lite/instance_lite.c +++ b/tools/chatgui-android/libutun_lite/instance_lite.c @@ -198,6 +198,12 @@ static void* instance_thread(void* arg) { /* Bind chat sync via etcp_router */ etcp_router_bind(g_inst, ETCP_RT_ID_CHAT_SYNC, NULL); + /* Set my_name from config */ + if (g_inst->config->global.name[0]) { + chat_core_update_my_name(g_inst->config->global.name); + IL_LOGI("my_name set to '%s'", g_inst->config->global.name); + } + IL_LOGI("all init done, entering poll loop"); /* Notify GUI of current public key (initial generation or existing) */ @@ -331,6 +337,11 @@ static void restart_trampoline(void* arg) { return; } + if (g_inst->config->global.name[0]) { + chat_core_update_my_name(g_inst->config->global.name); + IL_LOGI("restart: my_name set to '%s'", g_inst->config->global.name); + } + IL_LOGI("restart: done"); } diff --git a/tools/chatgui/src/joindialog.cpp b/tools/chatgui/src/joindialog.cpp index 31fdbf62..153d2292 100644 --- a/tools/chatgui/src/joindialog.cpp +++ b/tools/chatgui/src/joindialog.cpp @@ -15,7 +15,6 @@ extern "C" { #include "secure_channel.h" -#include "../../../lib/sha256.h" } JoinDialog::JoinDialog(UtunNode* node, DbManager* db, QWidget* parent, diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 43f9ec81..50fea78e 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -1,6 +1,7 @@ // node_config.cpp — Simplified INI config manager for embedded uTun node #include "node_config.h" #include "config_updater.h" +#include extern "C" { #include @@ -165,7 +166,12 @@ void NodeConfig::generateIdentity() { } bool NodeConfig::create(const QString& nodeName, quint16 listenPort) { - m_nodeName = nodeName.isEmpty() ? "chatgui" : nodeName; + if (nodeName.isEmpty()) { + QString host = QHostInfo::localHostName(); + m_nodeName = host.isEmpty() ? "chatgui" : host; + } else { + m_nodeName = nodeName; + } if (m_pubKey.isEmpty()) generateIdentity(); if (m_controlAddr.isEmpty()) m_controlAddr = "127.0.0.1:9999";