From bc2ac140a8bc8f93e49e4c54ea08e883ef1f6aa6 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 3 Nov 2026 01:31:47 +0100 Subject: [PATCH] Require reciprocal PIN confirmation before device key transfer --- src/chat/chat_devgroup.h | 10 +- src/chat/chat_devgroup_invite.c | 171 ++++++++++++++++++++------------ tests/test_devgroup_network.c | 43 +++++++- 3 files changed, 153 insertions(+), 71 deletions(-) diff --git a/src/chat/chat_devgroup.h b/src/chat/chat_devgroup.h index 56b8208cf..ee93ad748 100644 --- a/src/chat/chat_devgroup.h +++ b/src/chat/chat_devgroup.h @@ -8,6 +8,11 @@ extern "C" { #endif enum chat_devgroup_action { CHAT_DEVGROUP_STATUS, CHAT_DEVGROUP_INVITE, CHAT_DEVGROUP_ACCEPT, CHAT_DEVGROUP_RENAME, CHAT_DEVGROUP_LEAVE }; +/* Этапы приглашения; 8..10 продолжают согласование до передачи ключей. Терминальные: 4..7. */ +enum chat_devgroup_state { + DG_WAIT_PIN = 1, DG_PIN_SENT, DG_KEYS_SENT, DG_JOINED, DG_REJECTED, DG_EXPIRED, DG_UNKNOWN, + DG_WAIT_RETURN, DG_RETURN_SENT, DG_WAIT_KEYS +}; /* Запрос UI: text=имя/ID приглашения, pin только для ACCEPT; group/peer только для INVITE. * Все массивы NUL-terminated. Трамплин забирает u_malloc-блок; RESULT=request_id LE8,success1,JSON UTF-8. * JSON результата INVITE содержит PIN: показывать только локально, не логировать. */ @@ -27,9 +32,10 @@ void chat_devgroup_cancel_pending(struct UTUN_INSTANCE* inst); void chat_devgroup_destroy(struct UTUN_INSTANCE* inst); /* Пригласить известный узел через CHAT-группу; out_id[33] hex, out_pin[5]. 0/-1, PIN только для локального UI. */ int chat_devgroup_invite(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t peer, char out_id[33], char out_pin[5]); -/* Ввести PIN для входящего приглашения; id — 32 hex, pin — ровно четыре цифры. 0/-1. */ +/* Ввести первый PIN на получателе или ответный PIN на инициаторе; id — 32 hex, pin — четыре цифры. 0/-1. */ int chat_devgroup_accept(struct UTUN_INSTANCE* inst, const char* id, const char* pin); -/* Снимок группы/приглашений в JSON; caller освобождает через u_free. PIN/секретов в снимке нет. */ +/* Локальный JSON для UI: pin — свой показываемый PIN, input=1/2 — ожидаемый ввод, pin_error — ошибка попытки. + * Не логировать снимок и не передавать пиру. Caller освобождает через u_free. */ char* chat_devgroup_status(struct UTUN_INSTANCE* inst); /* Создать группу либо переименовать свою. Имя UTF-8,1..63 байта; no-op не повышает версию. */ int chat_devgroup_rename(struct UTUN_INSTANCE* inst, const char* name); diff --git a/src/chat/chat_devgroup_invite.c b/src/chat/chat_devgroup_invite.c index b10c85f99..4a94605af 100644 --- a/src/chat/chat_devgroup_invite.c +++ b/src/chat/chat_devgroup_invite.c @@ -18,13 +18,17 @@ #define DG_PIN 2 #define DG_KEYS 3 #define DG_RESULT 4 -/* state: 1=ожидается PIN,2=отправлен PIN,3=переданы ключи,4=успех,5=отказ,6=истёк,7=результат неизвестен. */ +#define DG_RETURN_PIN 5 +#define DG_PHASE_FIRST 1 +#define DG_PHASE_RETURN 2 +#define DG_PHASE_IMPORT 3 struct dg_invite { uint8_t id[16], x[32], ed[32], group_ed[32]; uint64_t group, peer, expires, sent; - unsigned state, outgoing, attempts; + unsigned state, outgoing, attempts, return_attempts; uint8_t last_pin[4], last_result; /* Повторы одной попытки получают прежний результат. */ - char pin[5]; /* Только локальная память; никогда не выводится в журнал. */ + uint8_t last_return_pin[4], return_result; + char pin[5], entered_pin[5]; /* Свой PIN и введённый PIN пира; никогда не выводятся в журнал. */ }; struct chat_devgroup { struct UTUN_INSTANCE* inst; @@ -33,6 +37,22 @@ struct chat_devgroup { unsigned ticks; }; static struct chat_devgroup* dg_ctx(struct UTUN_INSTANCE* inst) { return inst && CC(inst) ? CC(inst)->devgroup : NULL; } +static int dg_pending(unsigned state) { return state == DG_WAIT_PIN || state == DG_PIN_SENT || state == DG_KEYS_SENT || state >= DG_WAIT_RETURN; } +static void dg_clear_pins(struct dg_invite* p) { + OPENSSL_cleanse(p->pin, sizeof(p->pin)); OPENSSL_cleanse(p->entered_pin, sizeof(p->entered_pin)); +} +static int dg_random_pin(char pin[5], const char* different) { + uint32_t random; + do { + do { + if (RAND_bytes((uint8_t*)&random, sizeof(random)) != 1) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup: PIN randomness failed"); return -1; + } + } while (random >= UINT32_MAX - UINT32_MAX % 10000); + snprintf(pin, 5, "%04u", random % 10000); + } while (different && !memcmp(pin, different, 4)); + return 0; +} static void dg_hex(const uint8_t* in, size_t n, char* out) { for (size_t i = 0; islots[i]; - if (p->state && p->state<4 && p->peer == peer) { DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: peer already has pending invitation"); return NULL; } - if (!p->state || (p->state >= 4 && get_time_tb()>p->expires)) free_slot = p; + if (dg_pending(p->state) && p->peer == peer) { DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: peer already has pending invitation"); return NULL; } + if (!p->state || (!dg_pending(p->state) && get_time_tb()>p->expires)) free_slot = p; } if (!free_slot) DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: invitation capacity reached"); if (free_slot) OPENSSL_cleanse(free_slot, sizeof(*free_slot)); @@ -92,13 +112,28 @@ static int dg_keys(struct chat_devgroup* ctx, struct dg_invite* p) { } OPENSSL_cleanse(ep, 32); OPENSSL_cleanse(xp, 32); OPENSSL_cleanse(data, sizeof(data)); return rc; } -static void dg_result(struct chat_devgroup* ctx, struct dg_invite* p, uint8_t code) { - uint8_t result[2] = {(uint8_t)p->attempts, code}; +static void dg_result(struct chat_devgroup* ctx, struct dg_invite* p, uint8_t phase, uint8_t code) { + uint8_t result[3] = {phase, (uint8_t)(phase == DG_PHASE_RETURN ? p->return_attempts : p->attempts), code}; dg_send(ctx, p, DG_RESULT, result, sizeof(result)); } static void dg_send_pin(struct chat_devgroup* ctx, struct dg_invite* p) { - uint8_t body[5] = {(uint8_t)p->attempts}; memcpy(body + 1, p->pin, 4); - dg_send(ctx, p, DG_PIN, body, sizeof(body)); OPENSSL_cleanse(body, sizeof(body)); + uint8_t body[5] = {(uint8_t)(p->outgoing ? p->return_attempts : p->attempts)}; memcpy(body + 1, p->entered_pin, 4); + dg_send(ctx, p, p->outgoing ? DG_RETURN_PIN : DG_PIN, body, sizeof(body)); OPENSSL_cleanse(body, sizeof(body)); +} +/* Создание группы откладывается до двух ручных проверок. Пустая привязка допускает только создание новой группы. */ +static int dg_prepare_keys(struct chat_devgroup* ctx, struct dg_invite* p) { + struct devgroup_record own; uint8_t ep[32], xp[32], empty[32] = {0}; + int has = devgroup_load_local(ctx->inst, &own, ep, xp), pinned = memcmp(p->group_ed, empty, 32) != 0; + OPENSSL_cleanse(ep, 32); OPENSSL_cleanse(xp, 32); + if (!has && !pinned) { + if (chat_devgroup_rename(ctx->inst, ctx->inst->name[0] ? ctx->inst->name : "My devices")) has = -1; + else has = devgroup_load_local(ctx->inst, &own, ep, xp); + OPENSSL_cleanse(ep, 32); OPENSSL_cleanse(xp, 32); + } + if (has != 1 || (pinned && memcmp(p->group_ed, own.ed, 32))) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup: invitation cancelled after membership changed"); return -1; + } + memcpy(p->group_ed, own.ed, 32); return 0; } static void dg_receive(struct ETCP_CONN* conn, struct ll_entry* entry) { struct chat_devgroup* ctx = conn ? dg_ctx(conn->instance) : NULL; @@ -119,7 +154,7 @@ static void dg_receive(struct ETCP_CONN* conn, struct ll_entry* entry) { if (receipt<0) goto out; if (receipt) { struct dg_invite reply = {.group = group, .peer = peer}; memcpy(reply.id, id, 16); memcpy(reply.x, x, 32); memcpy(reply.ed, ed, 32); - dg_result(ctx, &reply, 0); goto out; + dg_result(ctx, &reply, DG_PHASE_IMPORT, 0); goto out; } } if (kind == DG_OFFER) { @@ -134,56 +169,54 @@ static void dg_receive(struct ETCP_CONN* conn, struct ll_entry* entry) { goto out; } if (!p || get_time_tb()>p->expires) { DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: expired/unknown invitation packet kind=%u", kind); goto out; } - if (kind == DG_PIN && p->outgoing) { + if ((kind == DG_PIN && p->outgoing) || (kind == DG_RETURN_PIN && !p->outgoing)) { if (n != 5 || data[0] < 1 || data[0] > 3) goto bad; - if (p->state == 1 || p->state == 5) { - if (data[0] == p->attempts) { - if (CRYPTO_memcmp(data + 1, p->last_pin, 4)) goto bad; - dg_result(ctx, p, p->last_result); goto out; - } - if (data[0] != p->attempts + 1) goto bad; + const int first = kind == DG_PIN; + const uint8_t phase = first ? DG_PHASE_FIRST : DG_PHASE_RETURN; + unsigned* attempts = first ? &p->attempts : &p->return_attempts; + uint8_t* last_pin = first ? p->last_pin : p->last_return_pin; + uint8_t* result = first ? &p->last_result : &p->return_result; + if (data[0] == *attempts) { + if (CRYPTO_memcmp(data + 1, last_pin, 4)) goto bad; + dg_result(ctx, p, phase, *result); goto out; } - if (p->state == 3) { dg_keys(ctx, p); goto out; } - if (p->state == 4) { dg_result(ctx, p, 0); goto out; } - if (p->state != 1) { dg_result(ctx, p, 2); goto out; } - p->attempts = data[0]; memcpy(p->last_pin, data + 1, 4); + if (data[0] != *attempts + 1 || p->state != (first ? DG_WAIT_PIN : DG_WAIT_RETURN)) goto bad; + *attempts = data[0]; memcpy(last_pin, data + 1, 4); if (CRYPTO_memcmp(data + 1, p->pin, 4)) { - if (p->attempts >= 3) p->state = 5; - p->last_result = p->state == 5 ? 2 : 1; - DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: PIN rejected peer=%016llx attempt=%u", (unsigned long long)peer, p->attempts); - dg_result(ctx, p, p->state == 5 ? 2 : 1); dg_changed(ctx); goto out; + if (*attempts >= 3) { p->state = DG_REJECTED; dg_clear_pins(p); } + *result = p->state == DG_REJECTED ? 2 : 1; + DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: PIN rejected peer=%016llx phase=%u attempt=%u", + (unsigned long long)peer, phase, *attempts); + } else { + p->state = first ? DG_WAIT_RETURN : DG_WAIT_KEYS; *result = 0; + dg_clear_pins(p); + DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup: PIN verified peer=%016llx phase=%u state=%u", + (unsigned long long)peer, phase, p->state); } - struct devgroup_record own; uint8_t ep[32], xp[32]; int has = devgroup_load_local(ctx->inst, &own, ep, xp); - OPENSSL_cleanse(ep, 32); OPENSSL_cleanse(xp, 32); - const uint8_t empty[32] = {0}; - int pinned = memcmp(p->group_ed, empty, 32) != 0; - if (!has && !pinned) { - if (chat_devgroup_rename(ctx->inst, ctx->inst->name[0] ? ctx->inst->name : "My devices")) has = -1; - else has = devgroup_load_local(ctx->inst, &own, ep, xp); - OPENSSL_cleanse(ep, 32); OPENSSL_cleanse(xp, 32); - } - if (has != 1 || (pinned && memcmp(p->group_ed, own.ed, 32))) { - DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup: invitation cancelled after membership changed"); - p->state = 5; dg_result(ctx, p, 2); dg_changed(ctx); goto out; - } - memcpy(p->group_ed, own.ed, 32); - DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup: PIN verified, transferring existing group keys peer=%016llx", - (unsigned long long)peer); - p->state = 3; OPENSSL_cleanse(p->pin, sizeof(p->pin)); OPENSSL_cleanse(p->last_pin, sizeof(p->last_pin)); - dg_keys(ctx, p); dg_changed(ctx); - } else if (kind == DG_KEYS && !p->outgoing && p->state == 2) { + dg_result(ctx, p, phase, *result); dg_changed(ctx); + } else if (kind == DG_KEYS && !p->outgoing && p->state == DG_WAIT_KEYS) { if (n <= 64) goto bad; struct devgroup_record r; if (devgroup_decode(&r, data + 64, n-64)) goto bad; int rc = chat_devgroup_import(ctx->inst, &r, data, data + 32, p->id, p->peer); - p->state = rc ? 5 : 4; OPENSSL_cleanse(p->pin, sizeof(p->pin)); dg_result(ctx, p, rc ? 2 : 0); dg_changed(ctx); + p->state = rc ? DG_REJECTED : DG_JOINED; dg_clear_pins(p); + dg_result(ctx, p, DG_PHASE_IMPORT, rc ? 2 : 0); dg_changed(ctx); } else if (kind == DG_RESULT) { - if (n != 2 || data[1] > 2) goto bad; - if (p->outgoing && p->state == 3) p->state = data[1] ? 7 : 4; - else if (!p->outgoing && p->state == 2 && data[0] == p->attempts && data[1]) { - p->state = data[1] == 1 ? 1 : 5; OPENSSL_cleanse(p->pin, sizeof(p->pin)); - } - else goto out; + if (n != 3 || data[0] < DG_PHASE_FIRST || data[0] > DG_PHASE_IMPORT || data[2] > 2) goto bad; + if (data[0] == DG_PHASE_FIRST && !p->outgoing && p->state == DG_PIN_SENT && data[1] == p->attempts) { + p->state = data[2] == 0 ? DG_WAIT_RETURN : data[2] == 1 ? DG_WAIT_PIN : DG_REJECTED; + OPENSSL_cleanse(p->entered_pin, sizeof(p->entered_pin)); + if (data[2]) dg_clear_pins(p); + } else if (data[0] == DG_PHASE_RETURN && p->outgoing && p->state == DG_RETURN_SENT && data[1] == p->return_attempts) { + dg_clear_pins(p); + if (data[2]) p->state = data[2] == 1 ? DG_WAIT_RETURN : DG_REJECTED; + else if (dg_prepare_keys(ctx, p)) { p->state = DG_REJECTED; dg_result(ctx, p, DG_PHASE_IMPORT, 2); } + else { p->state = DG_KEYS_SENT; dg_keys(ctx, p); } + } else if (data[0] == DG_PHASE_IMPORT && p->outgoing && p->state == DG_KEYS_SENT) { + p->state = data[2] ? DG_UNKNOWN : DG_JOINED; dg_clear_pins(p); + } else if (data[0] == DG_PHASE_IMPORT && !p->outgoing && p->state == DG_WAIT_KEYS && data[2] == 2) { + p->state = DG_REJECTED; dg_clear_pins(p); + } else goto out; DEBUG_INFO(DEBUG_CATEGORY_CHAT,"devgroup: invitation result peer=%016llx state=%u", (unsigned long long)peer, p->state); dg_changed(ctx); } else goto bad; goto out; @@ -195,9 +228,9 @@ out: static void dg_tick(void* arg) { struct chat_devgroup* ctx = arg; ctx->timer = NULL; uint64_t now = get_time_tb(); for (unsigned i = 0; islots[i]; if (!p->state || p->state >= 4) continue; + struct dg_invite* p = &ctx->slots[i]; if (!dg_pending(p->state)) continue; if (now>p->expires) { - p->state = p->state == 3 ? 7 : 6; OPENSSL_cleanse(p->pin, sizeof(p->pin)); + p->state = p->state == DG_KEYS_SENT ? DG_UNKNOWN : DG_EXPIRED; dg_clear_pins(p); DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: invitation timeout peer=%016llx state=%u", (unsigned long long)p->peer, p->state); dg_changed(ctx); continue; } @@ -205,6 +238,8 @@ static void dg_tick(void* arg) { if (p->outgoing && p->state == 1) dg_send(ctx, p, DG_OFFER, NULL, 0); else if (p->outgoing && p->state == 3) dg_keys(ctx, p); else if (!p->outgoing && p->state == 2) dg_send_pin(ctx, p); + else if (p->outgoing && p->state == DG_RETURN_SENT) dg_send_pin(ctx, p); + else if (!p->outgoing && p->state == DG_WAIT_KEYS) dg_result(ctx, p, DG_PHASE_RETURN, 0); } if (++ctx->ticks%5 == 0) chat_devgroup_refresh(ctx->inst); ctx->timer = uasync_set_timeout(ctx->inst->ua, 10000, ctx, dg_tick,"devgroup"); @@ -224,9 +259,9 @@ fail: void chat_devgroup_cancel_pending(struct UTUN_INSTANCE* inst) { struct chat_devgroup* ctx = dg_ctx(inst); if (!ctx) return; for (unsigned i = 0; i < DG_SLOTS; i++) { - struct dg_invite* p = &ctx->slots[i]; if (!p->state || p->state >= 4) continue; + struct dg_invite* p = &ctx->slots[i]; if (!dg_pending(p->state)) continue; p->state = p->state == 3 ? 7 : 5; - OPENSSL_cleanse(p->pin, sizeof(p->pin)); OPENSSL_cleanse(p->last_pin, sizeof(p->last_pin)); + dg_clear_pins(p); DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup: invitation cancelled peer=%016llx state=%u", (unsigned long long)p->peer, p->state); } dg_changed(ctx); @@ -248,11 +283,9 @@ int chat_devgroup_invite(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t pe if (has < 0) return -1; if (has) memcpy(p->group_ed, own.ed, 32); // Нулевая привязка: группа создаётся после проверки PIN. - uint32_t random; - do { if (RAND_bytes((uint8_t*)&random, sizeof(random)) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT,"devgroup: PIN randomness failed"); return -1; } } - while (random >= UINT32_MAX-UINT32_MAX%10000); + if (dg_random_pin(p->pin, NULL)) return -1; if (RAND_bytes(p->id, 16) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT,"devgroup: invitation randomness failed"); return -1; } - snprintf(p->pin, sizeof(p->pin),"%04u", random%10000); p->group = group; p->peer = peer; p->outgoing = 1; p->state = 1; p->expires = get_time_tb() + DG_TTL; + p->group = group; p->peer = peer; p->outgoing = 1; p->state = DG_WAIT_PIN; p->expires = get_time_tb() + DG_TTL; memcpy(p->x, x, 32); memcpy(p->ed, ed, 32); dg_hex(p->id, 16, out_id); memcpy(out_pin, p->pin, 5); dg_send(ctx, p, DG_OFFER, NULL, 0); DEBUG_INFO(DEBUG_CATEGORY_CHAT,"devgroup: invitation created peer=%016llx", (unsigned long long)peer); dg_changed(ctx); return 0; @@ -263,9 +296,15 @@ int chat_devgroup_accept(struct UTUN_INSTANCE* inst, const char* id, const char* DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: invalid acceptance arguments"); return -1; } struct dg_invite* p = dg_find(ctx, binary); - if (!p || p->outgoing || p->state != 1 || get_time_tb()>p->expires) { DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: acceptance unavailable"); return -1; } - if (p->attempts >= 3) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup: PIN attempts exhausted"); return -1; } - memcpy(p->pin, pin, 5); p->attempts++; p->state = 2; + if (!p || get_time_tb()>p->expires || p->state != (p->outgoing ? DG_WAIT_RETURN : DG_WAIT_PIN)) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT,"devgroup: acceptance unavailable"); return -1; + } + unsigned* attempts = p->outgoing ? &p->return_attempts : &p->attempts; + if (*attempts >= 3) { DEBUG_WARN(DEBUG_CATEGORY_CHAT, "devgroup: PIN attempts exhausted"); return -1; } + if (!p->outgoing && dg_random_pin(p->pin, pin)) return -1; + memcpy(p->entered_pin, pin, 5); (*attempts)++; p->state = p->outgoing ? DG_RETURN_SENT : DG_PIN_SENT; + DEBUG_INFO(DEBUG_CATEGORY_CHAT, "devgroup: PIN submitted peer=%016llx phase=%u attempt=%u", + (unsigned long long)p->peer, p->outgoing ? DG_PHASE_RETURN : DG_PHASE_FIRST, *attempts); dg_send_pin(ctx, p); dg_changed(ctx); return 0; } char* chat_devgroup_status(struct UTUN_INSTANCE* inst) { @@ -278,8 +317,12 @@ char* chat_devgroup_status(struct UTUN_INSTANCE* inst) { unsigned count = 0; for (unsigned i = 0; islots[i]; if (!p->state) continue; dg_hex(p->id, 16, id); - if (strbuf_addf(&json,"%s{\"id\":\"%s\",\"peer\":\"%016llx\",\"channel\":\"%llu\",\"outgoing\":%u,\"state\":%u}", - count++ ? "," : "", id, (unsigned long long)p->peer, (unsigned long long)p->group, p->outgoing, p->state)<0) goto fail; + int input = !p->outgoing && p->state == DG_WAIT_PIN ? 1 : p->outgoing && p->state == DG_WAIT_RETURN ? 2 : 0; + int show_pin = p->outgoing ? p->state == DG_WAIT_PIN : p->state == DG_WAIT_RETURN || p->state == DG_PIN_SENT; + int error = input && (p->outgoing ? p->return_attempts : p->attempts); + if (strbuf_addf(&json,"%s{\"id\":\"%s\",\"peer\":\"%016llx\",\"channel\":\"%llu\",\"outgoing\":%u,\"state\":%u," + "\"pin\":\"%s\",\"input\":%d,\"pin_error\":%d}", count++ ? "," : "", id, (unsigned long long)p->peer, + (unsigned long long)p->group, p->outgoing, p->state, show_pin ? p->pin : "", input, error)<0) goto fail; } if (strbuf_addf(&json,"]}")<0) goto fail; return strbuf_detach(&json); diff --git a/tests/test_devgroup_network.c b/tests/test_devgroup_network.c index 3235f70d0..6dbd9046d 100644 --- a/tests/test_devgroup_network.c +++ b/tests/test_devgroup_network.c @@ -98,8 +98,9 @@ int __wrap_etcp_route_send(struct UTUN_INSTANCE* inst, uint64_t group, uint64_t key_sends++; if (hold_keys && !held_keys) { held_keys = entry; return 0; } } - if (entry->dgram[1] == 4 && entry->len == 20 && - ((entry->dgram[19] == 1 && drop_pin_result-- > 0) || (entry->dgram[19] == 0 && drop_key_ack-- > 0))) { + if (entry->dgram[1] == 4 && entry->len == 21 && + ((entry->dgram[18] == 1 && entry->dgram[20] == 1 && drop_pin_result-- > 0) || + (entry->dgram[18] == 3 && entry->dgram[20] == 0 && drop_key_ack-- > 0))) { queue_dgram_free(entry); queue_entry_free(entry); return 0; } } @@ -119,6 +120,20 @@ static void wait_state(int node, const char* id, int expected) { while (get_time_tb() < until) { if (state(node, id) == expected) return; uasync_poll(ua, 5); } DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "devgroup test: timeout node=%d state=%d expected=%d", node, state(node, id), expected); abort(); } +static void return_pin(const char* id, char pin[5]) { + char* json = chat_devgroup_status(nodes[2]); assert(json); sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(nodes[2]->topo_sqlite_db, + "SELECT json_extract(value,'$.pin') FROM json_each(?,'$.invitations') WHERE json_extract(value,'$.id')=?", + -1, &st, NULL) == SQLITE_OK); + sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); sqlite3_bind_text(st, 2, id, -1, SQLITE_STATIC); + assert(sqlite3_step(st) == SQLITE_ROW && sqlite3_column_bytes(st, 0) == 4); + memcpy(pin, sqlite3_column_text(st, 0), 4); pin[4] = 0; sqlite3_finalize(st); u_free(json); +} +static void confirm_both(const char* id, const char* pin) { + assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, DG_WAIT_RETURN); wait_state(0, id, DG_WAIT_RETURN); + char reply[5]; return_pin(id, reply); assert(strcmp(reply, pin)); + assert(!chat_devgroup_accept(nodes[0], id, reply)); +} static void wait_profile(int node, uint64_t peer, const char* name) { uint64_t until = get_time_tb() + 150000; while (get_time_tb() < until) { @@ -167,7 +182,16 @@ int main(void) { assert(!chat_devgroup_accept(nodes[2], id, wrong)); wait_state(2, id, 1); assert(duplicate_pin >= 2 && state(0, id) == 1); assert(devgroup_load_local(nodes[0], &before, before_ed, before_x) == 0); - assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, 4); wait_state(0, id, 4); + int previous_keys = key_sends; + assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, DG_WAIT_RETURN); wait_state(0, id, DG_WAIT_RETURN); + assert(key_sends == previous_keys && devgroup_load_local(nodes[2], &before, before_ed, before_x) == 0); + assert(devgroup_load_local(nodes[0], &before, before_ed, before_x) == 0); + char reply[5]; return_pin(id, reply); assert(strcmp(reply, pin)); + strcpy(wrong, reply); wrong[0] = reply[0] == '0' ? '1' : '0'; + assert(!chat_devgroup_accept(nodes[0], id, wrong)); wait_state(0, id, DG_WAIT_RETURN); + assert(state(2, id) == DG_WAIT_RETURN && key_sends == previous_keys); + assert(devgroup_load_local(nodes[2], &before, before_ed, before_x) == 0); + assert(!chat_devgroup_accept(nodes[0], id, reply)); wait_state(2, id, 4); wait_state(0, id, 4); assert(key_sends >= 2); struct devgroup_record r[3]; uint8_t ep[3][32], xp[3][32]; assert(devgroup_load_local(nodes[0], &r[0], ep[0], xp[0]) == 1); @@ -188,7 +212,7 @@ int main(void) { /* После успешного импорта новый ACK может потеряться прямо перед выходом. */ drop_key_ack = 1; assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); - assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, 4); + confirm_both(id, pin); wait_state(2, id, 4); assert(devgroup_load_local(nodes[2], &r[2], ep[2], xp[2]) == 1); assert(!memcmp(ep[0], ep[2], 32) && !memcmp(xp[0], xp[2], 32)); assert(!chat_devgroup_leave(nodes[2])); wait_state(0, id, 4); @@ -198,10 +222,19 @@ int main(void) { strcpy(wrong, pin); wrong[0] = pin[0] == '0' ? '1' : '0'; for (int i = 0; i < 3; i++) { assert(!chat_devgroup_accept(nodes[2], id, wrong)); wait_state(2, id, i == 2 ? 5 : 1); } assert(state(0, id) == 5 && chat_devgroup_accept(nodes[2], id, pin) < 0); + /* Три неверных ответных PIN тоже завершают операцию без импорта ключей. */ + assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); + assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(2, id, DG_WAIT_RETURN); wait_state(0, id, DG_WAIT_RETURN); + return_pin(id, reply); strcpy(wrong, reply); wrong[0] = reply[0] == '0' ? '1' : '0'; previous_keys = key_sends; + for (int i = 0; i < 3; i++) { + assert(!chat_devgroup_accept(nodes[0], id, wrong)); wait_state(0, id, i == 2 ? DG_REJECTED : DG_WAIT_RETURN); + } + assert(state(2, id) == DG_REJECTED && key_sends == previous_keys); + assert(devgroup_load_local(nodes[2], &before, before_ed, before_x) == 0); /* Выход до первого commit тоже отменяет согласие: поздние ключи не должны присоединить C. */ hold_keys = 1; assert(!chat_devgroup_invite(nodes[0], gid, nodes[2]->node_id, id, pin)); wait_state(2, id, 1); - assert(!chat_devgroup_accept(nodes[2], id, pin)); wait_state(0, id, 3); assert(held_keys); + confirm_both(id, pin); wait_state(0, id, 3); assert(held_keys); assert(!chat_devgroup_leave(nodes[2]) && state(2, id) == 5); hold_keys = 0; assert(!__real_etcp_route_send(nodes[0], gid, nodes[2]->node_id, held_keys, 0, ROUTE_CRYPTO_SIGN | ROUTE_CRYPTO_ENCRYPT)); held_keys = NULL;