diff --git a/src/chat/invite_link.c b/src/chat/invite_link.c index e9f4bf0b..d36cb0fd 100644 --- a/src/chat/invite_link.c +++ b/src/chat/invite_link.c @@ -6,7 +6,7 @@ #include #include -#define INVITE_RAW_MAX 1157u +#define INVITE_RAW_MAX 1158u #define INVITE_PREFIX "utun://" static int reject(char* error, size_t size, const char* reason) { @@ -21,8 +21,15 @@ static int nonzero(const uint8_t* data, size_t len) { } int invite_link_validate(const struct InviteData* d) { - if (!d || !d->channelId || d->channelId > INT64_MAX || !d->join_key || !nonzero(d->pubkey, 32) || !sc_derive_node_id_from_pubkey(d->pubkey)) - return reject(NULL, 0, "missing channel, join key or node identity"); + if (!d || !nonzero(d->pubkey, 32) || !sc_derive_node_id_from_pubkey(d->pubkey)) + return reject(NULL, 0, "missing node identity"); + if (d->kind == INVITE_KIND_CHANNEL) { + if (!d->channelId || d->channelId > INT64_MAX || !d->join_key || nonzero(d->add_key, sizeof(d->add_key))) + return reject(NULL, 0, "invalid channel invitation credentials"); + } else if (d->kind == INVITE_KIND_SERVER) { + if (d->channelId || d->join_key || d->password_len || d->password[0] || !nonzero(d->add_key, sizeof(d->add_key))) + return reject(NULL, 0, "invalid server addition credentials"); + } else return reject(NULL, 0, "unknown link kind"); if (!d->addrCount || d->addrCount > INVITE_ADDR_MAX) return reject(NULL, 0, "invalid address count"); if (d->reality.has > 1) return reject(NULL, 0, "invalid REALITY marker"); if (d->reality.has && (!memchr(d->reality.server_name, 0, sizeof(d->reality.server_name)) || @@ -65,14 +72,21 @@ int invite_link_decode(const char* link, size_t len, struct InviteData* out, cha #define NEED(n) do { if ((size_t)(n) > count - pos) return reject(error, error_size, "truncated invitation"); } while (0) NEED(2); if (raw[pos++] != INVITE_LINK_VERSION) return reject(error, error_size, "unsupported invitation version"); - value.password_len = raw[pos++]; - if (value.password_len >= INVITE_PASS_MAX) return reject(error, error_size, "password too long"); - NEED(value.password_len); - if (memchr(raw + pos, 0, value.password_len)) return reject(error, error_size, "password contains NUL"); - memcpy(value.password, raw + pos, value.password_len); pos += value.password_len; - NEED(17); - for (unsigned i = 0; i < 8; i++) value.channelId = (value.channelId << 8) | raw[pos++]; - for (unsigned i = 0; i < 8; i++) value.join_key = (value.join_key << 8) | raw[pos++]; + value.kind = raw[pos++]; + if (value.kind == INVITE_KIND_CHANNEL) { + NEED(1); value.password_len = raw[pos++]; + if (value.password_len >= INVITE_PASS_MAX) return reject(error, error_size, "password too long"); + NEED(value.password_len); + if (memchr(raw + pos, 0, value.password_len)) return reject(error, error_size, "password contains NUL"); + memcpy(value.password, raw + pos, value.password_len); pos += value.password_len; + NEED(16); + for (unsigned i = 0; i < 8; i++) value.channelId = (value.channelId << 8) | raw[pos++]; + for (unsigned i = 0; i < 8; i++) value.join_key = (value.join_key << 8) | raw[pos++]; + } else if (value.kind == INVITE_KIND_SERVER) { + NEED(INVITE_ADD_KEY_SIZE); + memcpy(value.add_key, raw + pos, sizeof(value.add_key)); pos += sizeof(value.add_key); + } else return reject(error, error_size, "unknown link kind"); + NEED(1); value.reality.has = raw[pos++]; if (value.reality.has > 1) return reject(error, error_size, "invalid REALITY marker"); if (value.reality.has) { @@ -110,8 +124,8 @@ int invite_link_decode(const char* link, size_t len, struct InviteData* out, cha value.nodeId = sc_derive_node_id_from_pubkey(value.pubkey); *out = value; if (error && error_size) error[0] = 0; - DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "invite decoded channel=%llu node=%016llx addresses=%u", - (unsigned long long)value.channelId, (unsigned long long)value.nodeId, value.addrCount); + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "link decoded kind=%u channel=%llu node=%016llx addresses=%u", + value.kind, (unsigned long long)value.channelId, (unsigned long long)value.nodeId, value.addrCount); return 0; } @@ -143,11 +157,15 @@ int invite_link_encode(const struct InviteData* data, const char* password, char if (invite_link_validate(data)) return -1; size_t pass_len = password ? strnlen(password, INVITE_PASS_MAX) : 0; if (pass_len >= INVITE_PASS_MAX) return reject(NULL, 0, "password too long"); + if (data->kind == INVITE_KIND_SERVER && pass_len) return reject(NULL, 0, "server link cannot contain channel password"); uint8_t raw[INVITE_RAW_MAX]; size_t pos = 0; - raw[pos++] = INVITE_LINK_VERSION; raw[pos++] = (uint8_t)pass_len; - if (pass_len) { memcpy(raw + pos, password, pass_len); pos += pass_len; } - for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->channelId >> (8 * i)); - for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->join_key >> (8 * i)); + raw[pos++] = INVITE_LINK_VERSION; raw[pos++] = data->kind; + if (data->kind == INVITE_KIND_CHANNEL) { + raw[pos++] = (uint8_t)pass_len; + if (pass_len) { memcpy(raw + pos, password, pass_len); pos += pass_len; } + for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->channelId >> (8 * i)); + for (int i = 7; i >= 0; i--) raw[pos++] = (uint8_t)(data->join_key >> (8 * i)); + } else { memcpy(raw + pos, data->add_key, sizeof(data->add_key)); pos += sizeof(data->add_key); } uint8_t addresses[INVITE_RAW_MAX]; int address_len = invite_serialize_addrs(data, addresses, sizeof(addresses)); if (address_len < 0) return -1; diff --git a/src/chat/invite_link.h b/src/chat/invite_link.h index fb884767..f6e5f19f 100644 --- a/src/chat/invite_link.h +++ b/src/chat/invite_link.h @@ -1,9 +1,9 @@ /* * invite_link.h — invite-ссылки utun:// для каналов * - * encode/decode используют только версию 0x03. Проверка общая для всех клиентов. - * Формат 0x03: utun:// + обычный base64 с padding(version | pass_len/password | channel_id | join_key | - * reality_has/params | блоки pubkey/addrs). ID/port кодируются big-endian; nodeId выводится из pubkey. + * Версия 0x04: utun:// + base64(version | kind | реквизиты | reality | блоки pubkey/addrs). + * CHANNEL: pass_len/password | channel_id | join_key; SERVER: add_key[32], без полей канала. + * Проверка общая для всех клиентов. ID/port — big-endian; nodeId выводится из pubkey. * Сам кодек не подключает узел, не регистрирует join_key и не проверяет подпись/членство. * decode: 0/-1; encode: длина base64 без utun:// и NUL/-1; serialize_addrs: число байтов/-1. * Буферы предоставляет вызывающий; encode завершает строку NUL. @@ -14,7 +14,10 @@ #include #include -#define INVITE_LINK_VERSION 0x03 +#define INVITE_LINK_VERSION 0x04 +#define INVITE_KIND_CHANNEL 0 +#define INVITE_KIND_SERVER 1 +#define INVITE_ADD_KEY_SIZE 32 #define INVITE_LINK_MAX 1552 /* максимальная ссылка с NUL: 32 IPv6, REALITY и пароль */ #define INVITE_PUBKEY_SIZE 32 #define INVITE_ADDR_MAX 32 @@ -47,6 +50,8 @@ struct InviteReality { }; struct InviteData { + uint8_t kind; /* INVITE_KIND_*; SERVER не разрешает обычный JOIN */ + uint8_t add_key[INVITE_ADD_KEY_SIZE]; /* постоянный секрет добавления сервера; у CHANNEL нули */ uint64_t channelId; /* числовой ID канала */ uint64_t join_key; /* 64-битный ключ входа (генерируется инвайтером, хранится на connection-узле 10 мин) */ uint8_t pubkey[INVITE_PUBKEY_SIZE]; /* X25519 узла подключения */ diff --git a/tests/test_invite_codec.c b/tests/test_invite_codec.c index 2ab4c4e0..e8fbfded 100644 --- a/tests/test_invite_codec.c +++ b/tests/test_invite_codec.c @@ -80,14 +80,14 @@ int main(void) { raw_len -= link[strlen(link) - 1] == '='; raw_len -= link[strlen(link) - 2] == '='; uint8_t saved[1200]; memcpy(saved, raw, (size_t)raw_len); raw[0] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); - raw[18] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); - raw[62] = 64; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); - raw[63] = 0; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); - raw[126] |= 0x80; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); - size_t second = 126 + 33 + 4 * 20; raw[second + 1] ^= 1; + raw[19] = 2; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); + raw[63] = 64; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); + raw[64] = 0; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); + raw[127] |= 0x80; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); + size_t second = 127 + 33 + 4 * 20; raw[second + 1] ^= 1; rejected_raw(raw, (size_t)raw_len); memcpy(raw, saved, (size_t)raw_len); - rejected_raw(raw, 19); /* обязательный узел отсутствует */ - memcpy(raw, saved, (size_t)raw_len); raw[1] = 128; rejected_raw(raw, (size_t)raw_len); + rejected_raw(raw, 20); /* обязательный узел отсутствует */ + memcpy(raw, saved, (size_t)raw_len); raw[2] = 128; rejected_raw(raw, (size_t)raw_len); struct InviteData bad = input; bad.addrCount = 0; CHECK(invite_link_validate(&bad) < 0); bad = input; bad.channelId = UINT64_MAX; CHECK(invite_link_validate(&bad) < 0); bad = input; bad.addrCount = 33; CHECK(invite_link_validate(&bad) < 0); @@ -96,6 +96,20 @@ int main(void) { bad = input; bad.addrs[0].port = 0; CHECK(invite_link_validate(&bad) < 0); bad = input; bad.addrs[0].proto = 3; CHECK(invite_link_validate(&bad) < 0); bad = input; bad.reality.has = 0; CHECK(invite_link_validate(&bad) < 0); + struct InviteData server = input, decoded_server; + server.kind = INVITE_KIND_SERVER; server.channelId = 0; server.join_key = 0; + memset(server.add_key, 0x57, sizeof(server.add_key)); + CHECK(invite_link_encode(&server, NULL, link, sizeof(link)) > 0); + CHECK(!invite_link_decode(link, strlen(link), &decoded_server, NULL, 0)); + CHECK(decoded_server.kind == INVITE_KIND_SERVER && !decoded_server.channelId && !decoded_server.join_key); + CHECK(!memcmp(server.add_key, decoded_server.add_key, sizeof(server.add_key)) && decoded_server.addrCount == server.addrCount); + for (size_t cut = 0; cut < 34; cut++) { + uint8_t server_raw[34] = {INVITE_LINK_VERSION, INVITE_KIND_SERVER}; + rejected_raw(server_raw, cut); + } + server.channelId = 1; CHECK(invite_link_validate(&server) < 0); server.channelId = 0; + memset(server.add_key, 0, sizeof(server.add_key)); CHECK(invite_link_validate(&server) < 0); + bad = input; bad.kind = 2; CHECK(invite_link_validate(&bad) < 0); rejected("utun://", 7); rejected(NULL, 1); printf("invite codec: %u contract cases PASS\n", cases); return 0; }