You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
210 lines
7.6 KiB
210 lines
7.6 KiB
#include "invite_link.h" |
|
#include "debug_ui.h" |
|
#include <QByteArray> |
|
|
|
QString encodeInviteLink(uint64_t channelId, const QByteArray& pubkey, |
|
const QList<NodeAddr>& 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<char>(hasPass ? 0x02 : INVITE_LINK_VERSION)); |
|
|
|
if (hasPass) { |
|
QByteArray passBytes = password.toUtf8(); |
|
blob.append(static_cast<char>(passBytes.size())); |
|
blob.append(passBytes); |
|
} |
|
|
|
/* channel_id: uint64 big-endian */ |
|
for (int i = 7; i >= 0; i--) |
|
blob.append(static_cast<char>((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<char>(header)); |
|
blob.append(pubkey); |
|
|
|
for (int j = 0; j < cnt; j++) { |
|
blob.append(static_cast<char>(addrs[i + j].socketId)); |
|
blob.append(static_cast<char>(addrs[i + j].protocol)); |
|
blob.append(addrs[i + j].address); |
|
quint16 p = addrs[i + j].port; |
|
blob.append(static_cast<char>((p >> 8) & 0xFF)); |
|
blob.append(static_cast<char>(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<const uint8_t*>(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<const char*>(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<const char*>(data + off), 8); off += 8; |
|
d.reality.serverPubkey = QByteArray(reinterpret_cast<const char*>(data + off), 32); off += 32; |
|
d.reality.version = QByteArray(reinterpret_cast<const char*>(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<const char*>(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<const char*>(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<const char*>(data + off), ipLen); |
|
off += ipLen; |
|
a.port = (static_cast<quint16>(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<char>(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<char>(sn.size())); |
|
buf.append(sn); |
|
} |
|
for (const auto& a : d.addrs) { |
|
buf.append(static_cast<char>(a.family)); |
|
buf.append(static_cast<char>(a.socketId)); |
|
buf.append(static_cast<char>(a.proto)); |
|
buf.append(a.address); |
|
buf.append(static_cast<char>((a.port >> 8) & 0xFF)); |
|
buf.append(static_cast<char>(a.port & 0xFF)); |
|
} |
|
return buf; |
|
}
|
|
|