#include "invite_link.h" #include "debug_ui.h" #include QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey, const QList& addrs, const QString& password) { if (pubkey.size() != 32) { GUI_ERROR("encodeInviteLink: pubkey must be 32 bytes (got %d)", pubkey.size()); return {}; } if (addrs.isEmpty()) { GUI_ERROR("encodeInviteLink: no addresses"); return {}; } QByteArray blob; int hasPass = !password.isEmpty() ? 1 : 0; blob.reserve(1 + (hasPass ? 1 + password.size() : 0) + 8 + addrs.size() * (1 + 32 + 20)); blob.append(static_cast(hasPass ? 0x02 : INVITE_LINK_VERSION)); if (hasPass) { QByteArray passBytes = password.toUtf8(); blob.append(static_cast(passBytes.size())); blob.append(passBytes); } /* channel_id: uint64 big-endian */ for (int i = 7; i >= 0; i--) blob.append(static_cast((channelId >> (i * 8)) & 0xFF)); int i = 0; while (i < addrs.size()) { int rem = addrs.size() - i; int cnt = (rem > 4) ? 4 : rem; /* header: bits 1-0 = count-1, bits 5-2 = family bits */ uint8_t header = (cnt - 1) & 0x03; for (int j = 0; j < cnt; j++) { if (addrs[i + j].family == 6) header |= (1 << (2 + j)); } blob.append(static_cast(header)); blob.append(pubkey); for (int j = 0; j < cnt; j++) { blob.append(static_cast(addrs[i + j].socketId)); blob.append(static_cast(addrs[i + j].protocol)); blob.append(addrs[i + j].address); quint16 p = addrs[i + j].port; blob.append(static_cast((p >> 8) & 0xFF)); blob.append(static_cast(p & 0xFF)); } i += cnt; } return QString::fromLatin1("utun://") + QString::fromLatin1(blob.toBase64()); } InviteData decodeInviteLink(const QString& link) { InviteData d; if (!link.startsWith(QLatin1String("utun://"))) { d.error = QStringLiteral("invalid prefix (expected utun://)"); GUI_ERROR("decodeInviteLink: invalid prefix"); return d; } QByteArray blob = QByteArray::fromBase64(link.mid(7).toLatin1()); if (blob.isEmpty()) { d.error = QStringLiteral("base64 decode failed"); GUI_ERROR("decodeInviteLink: base64 decode failed"); return d; } const auto* data = reinterpret_cast(blob.constData()); int len = blob.size(); if (len < 11) { d.error = QStringLiteral("blob too short (%1 bytes)").arg(len); GUI_ERROR("decodeInviteLink: blob too short (%d bytes)", len); return d; } int off = 0; uint8_t ver = data[off++]; if (ver != INVITE_LINK_VERSION && ver != 0x02 && ver != 0x01) { d.error = QStringLiteral("unsupported version 0x%1").arg(ver, 2, 16, QLatin1Char('0')); GUI_ERROR("decodeInviteLink: unsupported version %d", ver); return d; } /* v2/v3: password field before channel_id */ if (ver == 0x02 || ver == INVITE_LINK_VERSION) { if (off >= len) { d.error = QStringLiteral("truncated at pass_len"); return d; } uint8_t plen = data[off++]; if (plen > 0) { if (off + plen > len) { d.error = QStringLiteral("truncated at password"); return d; } d.password = QString::fromUtf8(reinterpret_cast(data + off), plen); off += plen; } } /* channel_id: uint64 big-endian */ if (off + 8 > len) { d.error = QStringLiteral("truncated at channel_id"); GUI_ERROR("decodeInviteLink: truncated at channel_id"); return d; } uint64_t chId = 0; for (int i = 0; i < 8; i++) chId = (chId << 8) | data[off++]; d.channelId = chId; /* join_key: uint64 big-endian */ if (off + 8 > len) { d.error = QStringLiteral("truncated at join_key"); GUI_ERROR("decodeInviteLink: truncated at join_key"); return d; } uint64_t jk = 0; for (int i = 0; i < 8; i++) jk = (jk << 8) | data[off++]; d.joinKey = jk; /* v3: reality-блок (node-global параметры) */ if (ver == INVITE_LINK_VERSION) { if (off >= len) { d.error = QStringLiteral("truncated at reality"); return d; } if (data[off++] != 0) { if (off + 8 + 32 + 3 + 1 > len) { d.error = QStringLiteral("truncated at reality params"); return d; } d.reality.shortId = QByteArray(reinterpret_cast(data + off), 8); off += 8; d.reality.serverPubkey = QByteArray(reinterpret_cast(data + off), 32); off += 32; d.reality.version = QByteArray(reinterpret_cast(data + off), 3); off += 3; uint8_t snlen = data[off++]; if (snlen > INVITE_SNAME_MAX - 1) snlen = INVITE_SNAME_MAX - 1; if (off + snlen > len) { d.error = QStringLiteral("truncated at reality server_name"); return d; } d.reality.serverName = QString::fromUtf8(reinterpret_cast(data + off), snlen); off += snlen; d.reality.has = true; } } bool firstBlock = true; while (off + 1 <= len) { uint8_t header = data[off++]; int cnt = (header & 0x03) + 1; if (cnt < 1 || cnt > 4) { d.error = QStringLiteral("invalid addr count %1").arg(cnt); GUI_ERROR("decodeInviteLink: invalid addr count %d", cnt); return d; } if (off + 32 > len) { d.error = QStringLiteral("truncated at pubkey"); GUI_ERROR("decodeInviteLink: truncated at pubkey"); return d; } if (firstBlock) { d.pubkey = QByteArray(reinterpret_cast(data + off), 32); firstBlock = false; } off += 32; for (int j = 0; j < cnt; j++) { bool isV6 = header & (1 << (2 + j)); int ipLen = isV6 ? 16 : 4; if (off + 2 + ipLen + 2 > len) { d.error = QStringLiteral("truncated at addr %1 of block").arg(j); GUI_ERROR("decodeInviteLink: truncated at addr %d of block", j); return d; } InviteAddr a; a.socketId = data[off++]; a.proto = data[off++]; a.family = isV6 ? 6 : 4; a.address = QByteArray(reinterpret_cast(data + off), ipLen); off += ipLen; a.port = (static_cast(data[off]) << 8) | data[off + 1]; off += 2; d.addrs.append(a); } } return d; } QByteArray serializeInviteAddrs(const InviteData& d) { QByteArray buf; buf.append(static_cast(d.reality.has ? 1 : 0)); if (d.reality.has) { buf.append(d.reality.shortId.leftJustified(8, '\0', true)); buf.append(d.reality.serverPubkey.leftJustified(32, '\0', true)); buf.append(d.reality.version.leftJustified(3, '\0', true)); QByteArray sn = d.reality.serverName.toUtf8().left(INVITE_SNAME_MAX - 1); buf.append(static_cast(sn.size())); buf.append(sn); } for (const auto& a : d.addrs) { buf.append(static_cast(a.family)); buf.append(static_cast(a.socketId)); buf.append(static_cast(a.proto)); buf.append(a.address); buf.append(static_cast((a.port >> 8) & 0xFF)); buf.append(static_cast(a.port & 0xFF)); } return buf; }