Public Access
LoRa Scanner: MeshCore by default, and its public channel read
The MeshCore preset is the default for a new device, in the Settings and in the radio before the Scanner is opened. The setting now accepts it: it only took the 7 Meshtastic presets, so MeshCore picked in the App was not saved. Messages on MeshCore's public channel are decrypted with the channel's published key (AES-128, checked with 2 bytes of HMAC-SHA256): the row shows who says they sent it and the start of the text, the details the sender, the time and the whole text. Other channels and private messages stay encrypted. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
#include "aes128.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace roro::meshcore {
|
||||
|
||||
namespace {
|
||||
// The S-box and its inverse, worked out once rather than typed in.
|
||||
struct Tables {
|
||||
uint8_t s[256], inv[256];
|
||||
Tables() {
|
||||
auto rotl = [](uint8_t x, int n) { return static_cast<uint8_t>(x << n | x >> (8 - n)); };
|
||||
uint8_t p = 1, q = 1;
|
||||
do {
|
||||
p = static_cast<uint8_t>(p ^ (p << 1) ^ (p & 0x80 ? 0x1B : 0)); // p times 3
|
||||
q ^= static_cast<uint8_t>(q << 1); // q divided by 3
|
||||
q ^= static_cast<uint8_t>(q << 2);
|
||||
q ^= static_cast<uint8_t>(q << 4);
|
||||
if (q & 0x80) q ^= 0x09;
|
||||
uint8_t x = static_cast<uint8_t>(q ^ rotl(q, 1) ^ rotl(q, 2) ^ rotl(q, 3) ^ rotl(q, 4) ^ 0x63);
|
||||
s[p] = x;
|
||||
inv[x] = p;
|
||||
} while (p != 1);
|
||||
s[0] = 0x63;
|
||||
inv[0x63] = 0;
|
||||
}
|
||||
};
|
||||
const Tables& tables() {
|
||||
static const Tables t;
|
||||
return t;
|
||||
}
|
||||
|
||||
uint8_t mul(uint8_t a, uint8_t b) { // in GF(2^8)
|
||||
uint8_t r = 0;
|
||||
for (; b; b >>= 1) {
|
||||
if (b & 1) r ^= a;
|
||||
a = static_cast<uint8_t>(a << 1 ^ (a & 0x80 ? 0x1B : 0));
|
||||
}
|
||||
return r;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Aes128::Aes128(const uint8_t key[16]) {
|
||||
const Tables& t = tables();
|
||||
std::memcpy(roundKeys_, key, 16);
|
||||
uint8_t rcon = 1;
|
||||
for (int i = 16; i < 176; i += 4) {
|
||||
uint8_t w[4];
|
||||
std::memcpy(w, roundKeys_ + i - 4, 4);
|
||||
if (i % 16 == 0) {
|
||||
uint8_t first = w[0];
|
||||
w[0] = static_cast<uint8_t>(t.s[w[1]] ^ rcon);
|
||||
w[1] = t.s[w[2]];
|
||||
w[2] = t.s[w[3]];
|
||||
w[3] = t.s[first];
|
||||
rcon = static_cast<uint8_t>(rcon << 1 ^ (rcon & 0x80 ? 0x1B : 0));
|
||||
}
|
||||
for (int j = 0; j < 4; ++j) roundKeys_[i + j] = roundKeys_[i - 16 + j] ^ w[j];
|
||||
}
|
||||
}
|
||||
|
||||
void Aes128::decryptBlock(const uint8_t in[16], uint8_t out[16]) const {
|
||||
const Tables& t = tables();
|
||||
uint8_t s[16];
|
||||
for (int i = 0; i < 16; ++i) s[i] = in[i] ^ roundKeys_[160 + i];
|
||||
for (int round = 9; round >= 0; --round) {
|
||||
// Rows shifted back and bytes substituted back, in one go: column c, row r came from column c - r.
|
||||
uint8_t u[16];
|
||||
for (int c = 0; c < 4; ++c)
|
||||
for (int r = 0; r < 4; ++r) u[4 * c + r] = t.inv[s[4 * ((c - r + 4) % 4) + r]];
|
||||
for (int i = 0; i < 16; ++i) u[i] ^= roundKeys_[16 * round + i];
|
||||
if (round == 0) {
|
||||
std::memcpy(s, u, 16);
|
||||
break;
|
||||
}
|
||||
for (int c = 0; c < 4; ++c) {
|
||||
const uint8_t* a = u + 4 * c;
|
||||
s[4 * c + 0] = mul(a[0], 14) ^ mul(a[1], 11) ^ mul(a[2], 13) ^ mul(a[3], 9);
|
||||
s[4 * c + 1] = mul(a[0], 9) ^ mul(a[1], 14) ^ mul(a[2], 11) ^ mul(a[3], 13);
|
||||
s[4 * c + 2] = mul(a[0], 13) ^ mul(a[1], 9) ^ mul(a[2], 14) ^ mul(a[3], 11);
|
||||
s[4 * c + 3] = mul(a[0], 11) ^ mul(a[1], 13) ^ mul(a[2], 9) ^ mul(a[3], 14);
|
||||
}
|
||||
}
|
||||
std::memcpy(out, s, 16);
|
||||
}
|
||||
|
||||
} // namespace roro::meshcore
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace roro::meshcore {
|
||||
|
||||
// AES-128 (FIPS 197), one block at a time, decryption only: what reading a MeshCore message
|
||||
// takes. Small and dependency-free, so the same code runs in the PC tests and on the device.
|
||||
class Aes128 {
|
||||
public:
|
||||
explicit Aes128(const uint8_t key[16]);
|
||||
void decryptBlock(const uint8_t in[16], uint8_t out[16]) const;
|
||||
|
||||
private:
|
||||
uint8_t roundKeys_[176];
|
||||
};
|
||||
|
||||
} // namespace roro::meshcore
|
||||
@@ -0,0 +1,87 @@
|
||||
#include "meshcore_channel.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "aes128.h"
|
||||
#include "sha256.h"
|
||||
|
||||
namespace roro::meshcore {
|
||||
|
||||
namespace {
|
||||
// izOH6cXN6mrJ5e26oRXNcg== in base64, as the apps show it.
|
||||
const Channel kChannels[] = {
|
||||
{"Public", {0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a, 0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72}},
|
||||
};
|
||||
|
||||
void hmacSha256(const uint8_t key[16], const uint8_t* data, size_t len, uint8_t out[32]) {
|
||||
uint8_t pad[64], inner[32];
|
||||
std::memset(pad, 0x36, sizeof pad);
|
||||
for (int i = 0; i < 16; ++i) pad[i] ^= key[i];
|
||||
Sha256 in;
|
||||
in.update(pad, sizeof pad);
|
||||
in.update(data, len);
|
||||
in.finish(inner);
|
||||
std::memset(pad, 0x5C, sizeof pad);
|
||||
for (int i = 0; i < 16; ++i) pad[i] ^= key[i];
|
||||
Sha256 o;
|
||||
o.update(pad, sizeof pad);
|
||||
o.update(inner, sizeof inner);
|
||||
o.finish(out);
|
||||
}
|
||||
|
||||
// Bytes that aren't printable ASCII become '?': one for a whole UTF-8 character.
|
||||
std::string printable(const uint8_t* p, size_t n) {
|
||||
std::string out;
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (p[i] >= 0x20 && p[i] < 0x7F) out += static_cast<char>(p[i]);
|
||||
else if ((p[i] & 0xC0) != 0x80) out += '?';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
size_t channelCount() { return sizeof kChannels / sizeof kChannels[0]; }
|
||||
const Channel& channel(size_t i) { return kChannels[i]; }
|
||||
|
||||
uint8_t channelHash(const Channel& c) {
|
||||
uint8_t h[32];
|
||||
Sha256::hash(c.key, sizeof c.key, h);
|
||||
return h[0];
|
||||
}
|
||||
|
||||
bool readChannelText(const Packet& p, ChannelText& out) {
|
||||
// The channel's hash, 2 bytes of check, then whole AES blocks.
|
||||
if (p.type != Payload::GroupText || p.payloadLen < 3 + 16 || (p.payloadLen - 3) % 16) return false;
|
||||
const uint8_t* cipher = p.payload + 3;
|
||||
size_t len = p.payloadLen - 3;
|
||||
for (size_t i = 0; i < channelCount(); ++i) {
|
||||
const Channel& c = channel(i);
|
||||
if (channelHash(c) != p.payload[0]) continue;
|
||||
uint8_t mac[32];
|
||||
hmacSha256(c.key, cipher, len, mac);
|
||||
if (mac[0] != p.payload[1] || mac[1] != p.payload[2]) continue;
|
||||
uint8_t plain[192];
|
||||
Aes128 aes(c.key);
|
||||
for (size_t at = 0; at < len; at += 16) aes.decryptBlock(cipher + at, plain + at);
|
||||
// Timestamp, then a byte whose top 6 bits say what follows: 0 is plain text, "name: words".
|
||||
if (plain[4] >> 2) return false;
|
||||
ChannelText t;
|
||||
t.channel = c.name;
|
||||
t.timestamp = static_cast<uint32_t>(plain[0] | plain[1] << 8 | plain[2] << 16 | static_cast<uint32_t>(plain[3]) << 24);
|
||||
size_t end = 5;
|
||||
while (end < len && plain[end]) ++end; // padded with zeros
|
||||
std::string all = printable(plain + 5, end - 5);
|
||||
size_t colon = all.find(": ");
|
||||
if (colon == std::string::npos) {
|
||||
t.text = all;
|
||||
} else {
|
||||
t.sender = all.substr(0, colon);
|
||||
t.text = all.substr(colon + 2);
|
||||
}
|
||||
out = t;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace roro::meshcore
|
||||
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "meshcore_packet.h"
|
||||
|
||||
// MeshCore's channel messages: encrypted with a key everyone on the channel has. The public
|
||||
// channel's key is published, so what is said there can be read by anyone listening.
|
||||
namespace roro::meshcore {
|
||||
|
||||
struct Channel {
|
||||
const char* name;
|
||||
uint8_t key[16];
|
||||
};
|
||||
|
||||
// The channels whose key is known here: the public one, for now.
|
||||
size_t channelCount();
|
||||
const Channel& channel(size_t i);
|
||||
// What a message on the channel starts with: the first byte of the SHA-256 of its key.
|
||||
uint8_t channelHash(const Channel& c);
|
||||
|
||||
struct ChannelText {
|
||||
const char* channel = "";
|
||||
uint32_t timestamp = 0; // the sender's clock, seconds since 1970
|
||||
std::string sender; // what the sender calls itself: not signed, anyone can claim a name
|
||||
std::string text; // printable ASCII kept, the rest as '?'
|
||||
};
|
||||
|
||||
// True when the packet is a text on a known channel and its check (2 bytes of HMAC-SHA256) holds.
|
||||
bool readChannelText(const Packet& p, ChannelText& out);
|
||||
|
||||
} // namespace roro::meshcore
|
||||
@@ -4,8 +4,9 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// MeshCore's packets as they are on air (its docs/packet_format.md and docs/payloads.md). Only
|
||||
// what is sent in clear is read: the header, the path, and a node's advertisement.
|
||||
// MeshCore's packets as they are on air (its docs/packet_format.md and docs/payloads.md): what is
|
||||
// sent in clear, which is the header, the path, and a node's advertisement. meshcore_channel.h
|
||||
// reads the public channel's messages.
|
||||
namespace roro::meshcore {
|
||||
|
||||
// Radio settings: RadioLib's "private" sync word, and the preset its EU/UK networks use.
|
||||
|
||||
Reference in New Issue
Block a user