Browse Source

android: voice messages + attachments architecture plan, build fixes

topo_upd
evgeny 2 months ago
parent
commit
e751e6ee69
  1. 2
      lib/Makefile.am
  2. 158
      lib/sha256.c
  3. 41
      lib/sha256.h
  4. 32
      lib/sha256_doc.md
  5. 31
      src/chat/chat_sync.c
  6. 9
      src/chat/db_sync.c
  7. 50
      src/transport_layer/secure_channel.c
  8. 22
      src/utun_instance.c
  9. 103
      tools/chatgui-android/AGENTS.md
  10. 1
      tools/chatgui-android/app/src/main/AndroidManifest.xml
  11. 4
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/components/MessageBubble.kt
  12. 57
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChannelListScreen.kt
  13. 45
      tools/chatgui-android/app/src/main/java/com/utun/chat/ui/screens/ChatScreen.kt
  14. 51
      tools/chatgui-android/jni_bridge/android_jni_bridge.c
  15. 11
      tools/chatgui-android/libutun_lite/instance_lite.c
  16. 1
      tools/chatgui/src/joindialog.cpp
  17. 8
      tools/chatgui/transport/node_config.cpp

2
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 \

158
lib/sha256.c

@ -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 <stdlib.h>
#include <memory.h>
#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;
}
}

41
lib/sha256.h

@ -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 <stddef.h>
#include <stdint.h>
/****************************** 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

32
lib/sha256_doc.md

@ -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) — размер выходного дайджеста в байтах.

31
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);
}

9
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 <openssl/sha.h>
#include "../../lib/sqlite3.h"
#include "../../lib/platform_compat.h"
#include <string.h>
@ -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); }

50
src/transport_layer/secure_channel.c

@ -17,7 +17,6 @@
#include <stdio.h>
#include <fcntl.h>
#include "crc32.h"
#include "../lib/sha256.h"
#include <openssl/evp.h>
#include <openssl/sha.h>
@ -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;
}

22
src/utun_instance.c

@ -21,7 +21,6 @@
#include "../lib/u_async.h"
#include "../lib/debug_config.h"
#include "../lib/sha256.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
@ -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) {

103
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:<listen_port>
[allowed_keys]
allow_all=yes
[ntp]
enabled=no
[debug]
console_level=info
# Опционально — UDP-лог
[log_udp]
ip=<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/`.

1
tools/chatgui-android/app/src/main/AndroidManifest.xml

@ -16,6 +16,7 @@
<activity
android:name=".MainActivity"
android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

4
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)

57
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)
}
}
}

45
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()
)
}
}

51
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 <stdio.h>
#include <stdlib.h>
#include <string.h>
@ -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);

11
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");
}

1
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,

8
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 <QHostInfo>
extern "C" {
#include <openssl/evp.h>
@ -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";

Loading…
Cancel
Save