mirror of
https://github.com/f4exb/sdrangel.git
synced 2026-06-26 21:43:25 -04:00
331 lines
10 KiB
C++
331 lines
10 KiB
C++
|
|
///////////////////////////////////////////////////////////////////////////////////
|
||
|
|
// Copyright (C) 2026 Tom Hensel <code@jitter.eu> //
|
||
|
|
// //
|
||
|
|
// This program is free software; you can redistribute it and/or modify //
|
||
|
|
// it under the terms of the GNU General Public License as published by //
|
||
|
|
// the Free Software Foundation as version 3 of the License, or //
|
||
|
|
// (at your option) any later version. //
|
||
|
|
// //
|
||
|
|
// MeshCore wire-packet builder implementations. //
|
||
|
|
///////////////////////////////////////////////////////////////////////////////////
|
||
|
|
|
||
|
|
#include "meshcore_builders.h"
|
||
|
|
|
||
|
|
#include "meshcorepacket.h"
|
||
|
|
#include "meshcore_crypto.h"
|
||
|
|
|
||
|
|
#include <QCryptographicHash>
|
||
|
|
#include <QtEndian>
|
||
|
|
|
||
|
|
#include <cmath>
|
||
|
|
#include <cstring>
|
||
|
|
|
||
|
|
namespace modemmeshcore
|
||
|
|
{
|
||
|
|
namespace builders
|
||
|
|
{
|
||
|
|
|
||
|
|
namespace
|
||
|
|
{
|
||
|
|
|
||
|
|
// little-endian uint32 -> 4 bytes
|
||
|
|
QByteArray u32LE(uint32_t v)
|
||
|
|
{
|
||
|
|
QByteArray out(4, '\0');
|
||
|
|
qToLittleEndian(v, reinterpret_cast<uchar*>(out.data()));
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray i32LE(int32_t v)
|
||
|
|
{
|
||
|
|
QByteArray out(4, '\0');
|
||
|
|
qToLittleEndian(v, reinterpret_cast<uchar*>(out.data()));
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray u16LE(uint16_t v)
|
||
|
|
{
|
||
|
|
QByteArray out(2, '\0');
|
||
|
|
qToLittleEndian(v, reinterpret_cast<uchar*>(out.data()));
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace
|
||
|
|
|
||
|
|
uint8_t makeHeader(uint8_t routeType, uint8_t payloadType, uint8_t version)
|
||
|
|
{
|
||
|
|
return static_cast<uint8_t>(
|
||
|
|
((version & kVersionMask) << kVersionShift)
|
||
|
|
| ((payloadType & kPayloadTypeMask) << kPayloadTypeShift)
|
||
|
|
| (routeType & kRouteMask));
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray buildWirePacket(uint8_t header, const QByteArray& payload, const WireOptions& opts)
|
||
|
|
{
|
||
|
|
if (opts.pathHashSize < 1 || opts.pathHashSize > 3) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
if (opts.path.size() % opts.pathHashSize != 0) {
|
||
|
|
return QByteArray(); // path length must be a multiple of hash size
|
||
|
|
}
|
||
|
|
|
||
|
|
const int hashCount = opts.path.size() / opts.pathHashSize;
|
||
|
|
if (hashCount > 63) {
|
||
|
|
return QByteArray(); // path_count field is 6 bits
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray out;
|
||
|
|
out.reserve(1 + (opts.transport ? 4 : 0) + 1 + opts.path.size() + payload.size());
|
||
|
|
|
||
|
|
// [0] header
|
||
|
|
out.append(static_cast<char>(header));
|
||
|
|
|
||
|
|
// [1..4] optional transport_codes (LE u16, u16)
|
||
|
|
if (opts.transport) {
|
||
|
|
out.append(u16LE(opts.transport->first));
|
||
|
|
out.append(u16LE(opts.transport->second));
|
||
|
|
}
|
||
|
|
|
||
|
|
// [N] path_len byte (encodes hash size + count)
|
||
|
|
out.append(static_cast<char>(encodePathLen(hashCount, opts.pathHashSize)));
|
||
|
|
|
||
|
|
// [N+1 .. N+P] path
|
||
|
|
out.append(opts.path);
|
||
|
|
|
||
|
|
// [N+1+P ..] payload
|
||
|
|
out.append(payload);
|
||
|
|
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray buildAdvert(const QByteArray& seed, const QByteArray& pubKey, const AdvertOptions& opts)
|
||
|
|
{
|
||
|
|
if (seed.size() != kPubKeySize || pubKey.size() != kPubKeySize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- compute flags ----
|
||
|
|
uint8_t flags = static_cast<uint8_t>(opts.nodeType & 0x0F);
|
||
|
|
if (opts.latLon) {
|
||
|
|
flags |= AdvertHasLocation;
|
||
|
|
}
|
||
|
|
if (!opts.name.isEmpty()) {
|
||
|
|
flags |= AdvertHasName;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- assemble app_data ----
|
||
|
|
QByteArray appData;
|
||
|
|
appData.reserve(1 + (opts.latLon ? 8 : 0) + opts.name.toUtf8().size());
|
||
|
|
appData.append(static_cast<char>(flags));
|
||
|
|
if (opts.latLon) {
|
||
|
|
const int32_t latI = static_cast<int32_t>(std::lround(opts.latLon->first * 1e6));
|
||
|
|
const int32_t lonI = static_cast<int32_t>(std::lround(opts.latLon->second * 1e6));
|
||
|
|
appData.append(i32LE(latI));
|
||
|
|
appData.append(i32LE(lonI));
|
||
|
|
}
|
||
|
|
if (!opts.name.isEmpty()) {
|
||
|
|
appData.append(opts.name.toUtf8());
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- sign over (pubkey || ts || app_data) ----
|
||
|
|
const QByteArray tsBytes = u32LE(opts.timestamp);
|
||
|
|
QByteArray message;
|
||
|
|
message.reserve(kPubKeySize + 4 + appData.size());
|
||
|
|
message.append(pubKey);
|
||
|
|
message.append(tsBytes);
|
||
|
|
message.append(appData);
|
||
|
|
|
||
|
|
const QByteArray signature = detail::signEd25519(seed, message);
|
||
|
|
if (signature.size() != kSignatureSize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- assemble ADVERT body: [pubkey(32)][ts(4)][signature(64)][app_data] ----
|
||
|
|
QByteArray body;
|
||
|
|
body.reserve(kPubKeySize + 4 + kSignatureSize + appData.size());
|
||
|
|
body.append(pubKey);
|
||
|
|
body.append(tsBytes);
|
||
|
|
body.append(signature);
|
||
|
|
body.append(appData);
|
||
|
|
|
||
|
|
// ---- wrap in wire envelope ----
|
||
|
|
WireOptions wireOpts;
|
||
|
|
wireOpts.transport = opts.transport;
|
||
|
|
return buildWirePacket(
|
||
|
|
makeHeader(opts.routeType, PayloadAdvert),
|
||
|
|
body,
|
||
|
|
wireOpts);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- TXT_MSG ------------------------------------------------------------------
|
||
|
|
|
||
|
|
QByteArray buildTxtMsg(const QByteArray& seed,
|
||
|
|
const QByteArray& destPub32,
|
||
|
|
const QByteArray& text,
|
||
|
|
const TxtMsgOptions& opts)
|
||
|
|
{
|
||
|
|
if (seed.size() != kPubKeySize || destPub32.size() != kPubKeySize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
const QByteArray secret = detail::sharedSecret(seed, destPub32);
|
||
|
|
if (secret.size() != kPubKeySize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
// plaintext: [ts(4 LE)] [txt_type<<2 | attempt(1)] [text]
|
||
|
|
QByteArray plaintext;
|
||
|
|
plaintext.reserve(4 + 1 + text.size());
|
||
|
|
plaintext.append(u32LE(opts.timestamp));
|
||
|
|
plaintext.append(static_cast<char>(((opts.txtType & 0x3F) << 2) | (opts.attempt & 0x03)));
|
||
|
|
plaintext.append(text);
|
||
|
|
|
||
|
|
const QByteArray encrypted = detail::encryptThenMac(secret, plaintext);
|
||
|
|
if (encrypted.isEmpty()) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
const QByteArray myPub = detail::derivePubKey(seed);
|
||
|
|
if (myPub.size() != kPubKeySize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
// body: [dest_hash(1)] [src_hash(1)] [MAC(2) || ciphertext]
|
||
|
|
QByteArray body;
|
||
|
|
body.reserve(1 + 1 + encrypted.size());
|
||
|
|
body.append(destPub32.left(1));
|
||
|
|
body.append(myPub.left(1));
|
||
|
|
body.append(encrypted);
|
||
|
|
|
||
|
|
WireOptions wireOpts;
|
||
|
|
wireOpts.transport = opts.transport;
|
||
|
|
return buildWirePacket(makeHeader(opts.routeType, PayloadTxt), body, wireOpts);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- ANON_REQ -----------------------------------------------------------------
|
||
|
|
|
||
|
|
QByteArray buildAnonReq(const QByteArray& seed,
|
||
|
|
const QByteArray& destPub32,
|
||
|
|
const QByteArray& data,
|
||
|
|
const AnonReqOptions& opts)
|
||
|
|
{
|
||
|
|
if (seed.size() != kPubKeySize || destPub32.size() != kPubKeySize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
const QByteArray secret = detail::sharedSecret(seed, destPub32);
|
||
|
|
if (secret.size() != kPubKeySize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
// plaintext: [ts(4)] [data]
|
||
|
|
QByteArray plaintext;
|
||
|
|
plaintext.reserve(4 + data.size());
|
||
|
|
plaintext.append(u32LE(opts.timestamp));
|
||
|
|
plaintext.append(data);
|
||
|
|
|
||
|
|
const QByteArray encrypted = detail::encryptThenMac(secret, plaintext);
|
||
|
|
if (encrypted.isEmpty()) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
const QByteArray myPub = detail::derivePubKey(seed);
|
||
|
|
if (myPub.size() != kPubKeySize) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
// body: [dest_hash(1)] [sender_pub(32)] [MAC(2) || ciphertext]
|
||
|
|
QByteArray body;
|
||
|
|
body.reserve(1 + kPubKeySize + encrypted.size());
|
||
|
|
body.append(destPub32.left(1));
|
||
|
|
body.append(myPub);
|
||
|
|
body.append(encrypted);
|
||
|
|
|
||
|
|
WireOptions wireOpts;
|
||
|
|
wireOpts.transport = opts.transport;
|
||
|
|
return buildWirePacket(makeHeader(opts.routeType, PayloadAnonReq), body, wireOpts);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- GroupChannel + GRP_TXT ---------------------------------------------------
|
||
|
|
|
||
|
|
GroupChannel GroupChannel::fromPsk(const QString& name, const QByteArray& pskRaw)
|
||
|
|
{
|
||
|
|
GroupChannel ch;
|
||
|
|
if (pskRaw.size() != 16 && pskRaw.size() != 32) {
|
||
|
|
return ch; // invalid -> isValid() == false
|
||
|
|
}
|
||
|
|
ch.name = name;
|
||
|
|
ch.pskRaw = pskRaw;
|
||
|
|
|
||
|
|
// Zero-pad PSK to 32 bytes (used as HMAC key).
|
||
|
|
ch.secret = pskRaw;
|
||
|
|
if (ch.secret.size() < kPubKeySize) {
|
||
|
|
ch.secret.append(kPubKeySize - ch.secret.size(), '\0');
|
||
|
|
}
|
||
|
|
|
||
|
|
// Channel hash = SHA-256(pskRaw)[0:1]
|
||
|
|
ch.hash = QCryptographicHash::hash(pskRaw, QCryptographicHash::Sha256).left(1);
|
||
|
|
return ch;
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray buildGrpTxt(const GroupChannel& channel,
|
||
|
|
const QByteArray& text,
|
||
|
|
const GrpTxtOptions& opts)
|
||
|
|
{
|
||
|
|
if (!channel.isValid()) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray plaintext;
|
||
|
|
plaintext.reserve(4 + 1 + text.size());
|
||
|
|
plaintext.append(u32LE(opts.timestamp));
|
||
|
|
plaintext.append(static_cast<char>(((opts.txtType & 0x3F) << 2) | (opts.attempt & 0x03)));
|
||
|
|
plaintext.append(text);
|
||
|
|
|
||
|
|
const QByteArray encrypted = detail::encryptThenMac(channel.secret, plaintext);
|
||
|
|
if (encrypted.isEmpty()) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
// body: [channel_hash(1)] [MAC(2) || ciphertext]
|
||
|
|
QByteArray body;
|
||
|
|
body.reserve(1 + encrypted.size());
|
||
|
|
body.append(channel.hash);
|
||
|
|
body.append(encrypted);
|
||
|
|
|
||
|
|
WireOptions wireOpts;
|
||
|
|
wireOpts.transport = opts.transport;
|
||
|
|
return buildWirePacket(makeHeader(opts.routeType, PayloadGrpTxt), body, wireOpts);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- ACK ----------------------------------------------------------------------
|
||
|
|
|
||
|
|
QByteArray buildAck(const QByteArray& destPub32,
|
||
|
|
const QByteArray& msgHash4,
|
||
|
|
const AckOptions& opts)
|
||
|
|
{
|
||
|
|
if (destPub32.size() != kPubKeySize || msgHash4.size() != 4) {
|
||
|
|
return QByteArray();
|
||
|
|
}
|
||
|
|
|
||
|
|
QByteArray body;
|
||
|
|
body.reserve(1 + 4);
|
||
|
|
body.append(destPub32.left(1));
|
||
|
|
body.append(msgHash4);
|
||
|
|
|
||
|
|
WireOptions wireOpts;
|
||
|
|
wireOpts.transport = opts.transport;
|
||
|
|
return buildWirePacket(makeHeader(opts.routeType, PayloadAck), body, wireOpts);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ---- Public channel PSK -------------------------------------------------------
|
||
|
|
|
||
|
|
QByteArray publicChannelPsk()
|
||
|
|
{
|
||
|
|
static const QByteArray kPsk = QByteArray::fromHex("8b3387e9c5cdea6ac9e5edbaa115cd72");
|
||
|
|
return kPsk;
|
||
|
|
}
|
||
|
|
|
||
|
|
} // namespace builders
|
||
|
|
} // namespace modemmeshcore
|