Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ if(ENABLE_DOCTESTS)
${CMAKE_CURRENT_SOURCE_DIR}/locale
${CMAKE_CURRENT_SOURCE_DIR}/portduino
${CMAKE_CURRENT_SOURCE_DIR}/generated/${GENERATED_VIEW}
${GENERATED_FILES_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${nanopb_SOURCE_DIR}
)
set_target_properties(tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
add_test(NAME tests COMMAND tests)
endif()
endif()
17 changes: 17 additions & 0 deletions include/graphics/common/ChannelShareURL.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#pragma once

#include "mesh-pb-constants.h"
#include <cstddef>
#include <string>

enum class ChannelShareMode {
Replace,
Add,
};

namespace ChannelShareURL {

std::string make(const meshtastic_ChannelSettings *settings, size_t settingCount,
const meshtastic_Config_LoRaConfig &lora, ChannelShareMode mode);

} // namespace ChannelShareURL
2 changes: 1 addition & 1 deletion include/graphics/common/MeshtasticView.h
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ class MeshtasticView : public DeviceGUI
bool lastHeardToString(uint32_t lastHeard, char *buf);
const char *deviceRoleToString(enum eRole role);
std::string pskToBase64(uint8_t *bytes, uint32_t size);
bool base64ToPsk(const std::string &base64, uint8_t *bytes, uint16_t &size);
bool base64ToPsk(const std::string &base64, uint8_t *bytes, uint16_t &size, uint16_t capacity);

ViewController *controller;
ResponseHandler requests;
Expand Down
8 changes: 7 additions & 1 deletion include/graphics/view/TFT/TFTView_320x240.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <set>

class MapPanel;
enum class ChannelShareMode;

/**
* @brief GUI view for e.g. T-Deck
Expand Down Expand Up @@ -217,6 +218,8 @@ class TFTView_320x240 : public MeshtasticView
void showKeyboard(lv_obj_t *textArea);
void hideKeyboard(lv_obj_t *panel);
lv_obj_t *showQrCode(lv_obj_t *parent, const char *data);
void showChannelShareModePicker(void);
void showChannelShareQRCode(ChannelShareMode mode);

void enablePanel(lv_obj_t *panel);
void disablePanel(lv_obj_t *panel);
Expand Down Expand Up @@ -335,6 +338,8 @@ class TFTView_320x240 : public MeshtasticView
static void ui_event_modify_channel(lv_event_t *e);
static void ui_event_delete_channel(lv_event_t *e);
static void ui_event_generate_psk(lv_event_t *e);
static void ui_event_channel_share_mode(lv_event_t *e);
static void ui_event_channel_share_cancel(lv_event_t *e);
static void ui_event_qr_code(lv_event_t *e);

static void ui_event_screen_timeout_slider(lv_event_t *e);
Expand Down Expand Up @@ -428,6 +433,7 @@ class TFTView_320x240 : public MeshtasticView
std::array<lv_obj_t *, c_max_channels> ch_label; // indexable label list for settings
meshtastic_Channel *channel_scratch; // temporary scratch copy of channel db
lv_obj_t *qr; // qr code
lv_obj_t *channelShareModePanel = nullptr; // Replace/Add channel QR choice
MapPanel *map = nullptr; // map
std::unordered_map<uint32_t, lv_obj_t *> nodeObjects; // nodeObjects displayed on map
// extended default device profile struct with additional required data
Expand All @@ -444,4 +450,4 @@ class TFTView_320x240 : public MeshtasticView
};

meshtastic_DeviceProfile_full db{}; // full copy of the node's configuration db (except nodeinfos) plus ui data
};
};
111 changes: 98 additions & 13 deletions source/graphics/TFT/TFTView_320x240.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "graphics/view/TFT/TFTView_320x240.h"
#include "Arduino.h"
#include "graphics/common/BatteryLevel.h"
#include "graphics/common/ChannelShareURL.h"
#include "graphics/common/LoRaPresets.h"
#include "graphics/common/Ringtones.h"
#include "graphics/common/ViewController.h"
Expand Down Expand Up @@ -2178,18 +2179,101 @@ void TFTView_320x240::ui_event_generate_psk(lv_event_t *e)
lv_textarea_set_text(objects.settings_modify_channel_psk_textarea, base64.c_str());
}

std::string base64Https = base64;
for (char &c : base64Https) {
if (c == '+')
c = '-';
else if (c == '/')
c = '_';
else if (c == '=')
c = '\0'; // remove paddings at the end of the url
THIS->showChannelShareModePicker();
}

void TFTView_320x240::showChannelShareModePicker(void)
{
if (channelShareModePanel) {
lv_obj_delete(channelShareModePanel);
}

channelShareModePanel = lv_obj_create(objects.settings_modify_channel_panel);
lv_obj_set_size(channelShareModePanel, LV_PCT(92), 115);
lv_obj_center(channelShareModePanel);
lv_obj_clear_flag(channelShareModePanel, LV_OBJ_FLAG_SCROLLABLE);

lv_obj_t *title = lv_label_create(channelShareModePanel);
lv_label_set_text(title, _("Share channel"));
lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 0);

lv_obj_t *description = lv_label_create(channelShareModePanel);
lv_label_set_text(description, _("Replace sends channel and radio. Add keeps receiver channels and radio."));
lv_label_set_long_mode(description, LV_LABEL_LONG_WRAP);
lv_obj_set_width(description, LV_PCT(100));
lv_obj_align(description, LV_ALIGN_TOP_MID, 0, 24);
lv_obj_set_style_text_align(description, LV_TEXT_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);

lv_obj_t *replaceButton = lv_btn_create(channelShareModePanel);
lv_obj_set_size(replaceButton, 76, 30);
lv_obj_align(replaceButton, LV_ALIGN_BOTTOM_LEFT, 0, 0);
lv_obj_add_event_cb(replaceButton, ui_event_channel_share_mode, LV_EVENT_CLICKED, nullptr);
lv_obj_t *replaceLabel = lv_label_create(replaceButton);
lv_label_set_text(replaceLabel, _("Replace"));
lv_obj_center(replaceLabel);

lv_obj_t *addButton = lv_btn_create(channelShareModePanel);
lv_obj_set_size(addButton, 58, 30);
lv_obj_align(addButton, LV_ALIGN_BOTTOM_MID, 0, 0);
lv_obj_add_event_cb(addButton, ui_event_channel_share_mode, LV_EVENT_CLICKED, (void *)1);
lv_obj_t *addLabel = lv_label_create(addButton);
lv_label_set_text(addLabel, _("Add"));
lv_obj_center(addLabel);

lv_obj_t *cancelButton = lv_btn_create(channelShareModePanel);
lv_obj_set_size(cancelButton, 58, 30);
lv_obj_align(cancelButton, LV_ALIGN_BOTTOM_RIGHT, 0, 0);
lv_obj_add_event_cb(cancelButton, ui_event_channel_share_cancel, LV_EVENT_CLICKED, nullptr);
lv_obj_t *cancelLabel = lv_label_create(cancelButton);
lv_label_set_text(cancelLabel, _("Cancel"));
lv_obj_center(cancelLabel);

lv_group_focus_obj(replaceButton);
}

void TFTView_320x240::ui_event_channel_share_mode(lv_event_t *e)
{
if (THIS->channelShareModePanel) {
lv_obj_delete(THIS->channelShareModePanel);
THIS->channelShareModePanel = nullptr;
}
const auto mode = lv_event_get_user_data(e) ? ChannelShareMode::Add : ChannelShareMode::Replace;
THIS->showChannelShareQRCode(mode);
}

void TFTView_320x240::ui_event_channel_share_cancel(lv_event_t *e)
{
if (THIS->channelShareModePanel) {
lv_obj_delete(THIS->channelShareModePanel);
THIS->channelShareModePanel = nullptr;
}
}

void TFTView_320x240::showChannelShareQRCode(ChannelShareMode mode)
{
const uint8_t buttonId = (unsigned long)objects.settings_modify_channel_name_textarea->user_data;
const int8_t channelIndex = (signed long)ch_label[buttonId]->user_data;
if (channelIndex < 0 || !channel_scratch) {
messageAlert(_("Unable to create channel QR"), true);
return;
}

meshtastic_ChannelSettings settings = channel_scratch[channelIndex].settings;
snprintf(settings.name, sizeof(settings.name), "%s", lv_textarea_get_text(objects.settings_modify_channel_name_textarea));
if (!base64ToPsk(lv_textarea_get_text(objects.settings_modify_channel_psk_textarea), settings.psk.bytes, settings.psk.size,
sizeof(settings.psk.bytes))) {
messageAlert(_("Invalid channel key"), true);
return;
}
std::string qr = "https://meshtastic.org/e/#" + base64Https;

const std::string url = ChannelShareURL::make(&settings, 1, db.config.lora, mode);
if (url.empty()) {
messageAlert(_("Unable to create channel QR"), true);
return;
}

lv_obj_remove_flag(objects.settings_modify_channel_qr_panel, LV_OBJ_FLAG_HIDDEN);
THIS->qr = THIS->showQrCode(objects.settings_modify_channel_qr_panel, qr.c_str());
qr = showQrCode(objects.settings_modify_channel_qr_panel, url.c_str());
lv_obj_add_state(objects.keyboard_button_3, LV_STATE_DISABLED);
lv_obj_add_state(objects.keyboard_button_4, LV_STATE_DISABLED);
}
Expand Down Expand Up @@ -4204,7 +4288,8 @@ void TFTView_320x240::ui_event_ok(lv_event_t *e)
lv_textarea_add_text(objects.settings_modify_channel_psk_textarea, "=");
}

if (THIS->base64ToPsk(lv_textarea_get_text(objects.settings_modify_channel_psk_textarea), psk.bytes, psk.size)) {
if (THIS->base64ToPsk(lv_textarea_get_text(objects.settings_modify_channel_psk_textarea), psk.bytes, psk.size,
sizeof(psk.bytes))) {
if (strlen(name) || psk.size) {
// TODO: fill temp storage -> user data
lv_label_set_text(THIS->ch_label[btn_id], name);
Expand Down Expand Up @@ -6179,8 +6264,8 @@ void TFTView_320x240::restore(uint32_t option)
String b64pub = pubKey.substring(pubKey.lastIndexOf(":") + 1);
b64priv.trim();
b64pub.trim();
if (base64ToPsk(b64priv.c_str(), privkey.bytes, privkey.size) &&
base64ToPsk(b64pub.c_str(), pubkey.bytes, pubkey.size) &&
if (base64ToPsk(b64priv.c_str(), privkey.bytes, privkey.size, sizeof(privkey.bytes)) &&
base64ToPsk(b64pub.c_str(), pubkey.bytes, pubkey.size, sizeof(pubkey.bytes)) &&
controller->sendConfig(meshtastic_Config_SecurityConfig{db.config.security})) {
ILOG_INFO("restore pub/priv keys sent to radio");
} else {
Expand Down
49 changes: 49 additions & 0 deletions source/graphics/common/ChannelShareURL.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#include "graphics/common/ChannelShareURL.h"
#include "mesh/generated/meshtastic/apponly.pb.h"
#include "util/macaron_Base64.h"
#include <algorithm>
#include <pb_encode.h>

namespace {

std::string base64UrlEncode(const uint8_t *bytes, size_t size)
{
std::string encoded = macaron::Base64::Encode(bytes, size);
for (char &character : encoded) {
if (character == '+')
character = '-';
else if (character == '/')
character = '_';
}
while (!encoded.empty() && encoded.back() == '=') {
encoded.pop_back();
}
return encoded;
}

} // namespace

std::string ChannelShareURL::make(const meshtastic_ChannelSettings *settings, size_t settingCount,
const meshtastic_Config_LoRaConfig &lora, ChannelShareMode mode)
{
if (!settings || settingCount == 0 || settingCount > 8) {
return {};
}

meshtastic_ChannelSet channelSet = meshtastic_ChannelSet_init_zero;
channelSet.settings_count = static_cast<pb_size_t>(settingCount);
std::copy_n(settings, settingCount, channelSet.settings);
if (mode == ChannelShareMode::Replace) {
channelSet.has_lora_config = true;
channelSet.lora_config = lora;
}

uint8_t payload[meshtastic_ChannelSet_size];
pb_ostream_t stream = pb_ostream_from_buffer(payload, sizeof(payload));
if (!pb_encode(&stream, &meshtastic_ChannelSet_msg, &channelSet)) {
return {};
}

const char *prefix = mode == ChannelShareMode::Add ? "https://meshtastic.org/e/?add=true#" : "https://meshtastic.org/e/#";
return std::string(prefix) + base64UrlEncode(payload, stream.bytes_written);
}
13 changes: 9 additions & 4 deletions source/graphics/common/MeshtasticView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -209,16 +209,21 @@ std::string MeshtasticView::pskToBase64(uint8_t *bytes, uint32_t size)
}
}

bool MeshtasticView::base64ToPsk(const std::string &base64, uint8_t *bytes, uint16_t &size)
bool MeshtasticView::base64ToPsk(const std::string &base64, uint8_t *bytes, uint16_t &size, uint16_t capacity)
{
std::string out;
auto error = macaron::Base64::Decode(base64, out);
if (!error.empty()) {
ILOG_ERROR("Cannot decode '%s'", base64);
return false;
} else {
memcpy((char *)bytes, out.data(), out.size());
size = out.size();
}

if (out.size() > capacity) {
ILOG_ERROR("Decoded key is too large: %u", static_cast<unsigned int>(out.size()));
return false;
}

memcpy(bytes, out.data(), out.size());
size = out.size();
return true;
}
94 changes: 94 additions & 0 deletions tests/test_ChannelShareURL.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include "graphics/common/ChannelShareURL.h"
#include "mesh/generated/meshtastic/apponly.pb.h"
#include "util/macaron_Base64.h"
#include <doctest/doctest.h>
#include <pb_decode.h>
#include <cstdio>
#include <cstring>

namespace {

meshtastic_ChannelSettings channelSettings(const char *name, uint8_t seed)
{
meshtastic_ChannelSettings settings = meshtastic_ChannelSettings_init_zero;
snprintf(settings.name, sizeof(settings.name), "%s", name);
settings.psk.size = 16;
for (uint8_t i = 0; i < settings.psk.size; i++) {
settings.psk.bytes[i] = seed + i;
}
return settings;
}

bool decodeChannelSet(const std::string &url, meshtastic_ChannelSet &result)
{
size_t payloadStart = url.find('#');
if (payloadStart == std::string::npos) {
return false;
}

std::string payload = url.substr(payloadStart + 1);
for (char &character : payload) {
if (character == '-')
character = '+';
else if (character == '_')
character = '/';
}
payload.append((4 - payload.size() % 4) % 4, '=');

std::string encoded;
if (!macaron::Base64::Decode(payload, encoded).empty()) {
return false;
}

result = meshtastic_ChannelSet_init_zero;
pb_istream_t stream = pb_istream_from_buffer(reinterpret_cast<const pb_byte_t *>(encoded.data()), encoded.size());
return pb_decode(&stream, &meshtastic_ChannelSet_msg, &result);
}

} // namespace

TEST_CASE("Channel share Replace URL carries channels and LoRa")
{
meshtastic_ChannelSettings settings = channelSettings("Replace", 1);
meshtastic_Config_LoRaConfig lora = meshtastic_Config_LoRaConfig_init_zero;
lora.region = meshtastic_Config_LoRaConfig_RegionCode_US;
lora.modem_preset = meshtastic_Config_LoRaConfig_ModemPreset_LONG_FAST;
lora.channel_num = 7;

const std::string url = ChannelShareURL::make(&settings, 1, lora, ChannelShareMode::Replace);

CHECK(url.rfind("https://meshtastic.org/e/#", 0) == 0);
CHECK(url.find_first_of("+/=", url.find('#') + 1) == std::string::npos);

meshtastic_ChannelSet decoded = meshtastic_ChannelSet_init_zero;
REQUIRE(decodeChannelSet(url, decoded));
REQUIRE(decoded.settings_count == 1);
CHECK(strcmp(decoded.settings[0].name, "Replace") == 0);
CHECK(decoded.settings[0].psk.size == settings.psk.size);
CHECK(memcmp(decoded.settings[0].psk.bytes, settings.psk.bytes, settings.psk.size) == 0);
REQUIRE(decoded.has_lora_config);
CHECK(decoded.lora_config.region == lora.region);
CHECK(decoded.lora_config.modem_preset == lora.modem_preset);
CHECK(decoded.lora_config.channel_num == lora.channel_num);
}

TEST_CASE("Channel share Add URL omits LoRa for up to eight channels")
{
meshtastic_ChannelSettings settings[8] = {};
for (uint8_t i = 0; i < 8; i++) {
settings[i] = channelSettings("Channel", i);
snprintf(settings[i].name, sizeof(settings[i].name), "Channel%u", i);
}
meshtastic_Config_LoRaConfig lora = meshtastic_Config_LoRaConfig_init_zero;
lora.region = meshtastic_Config_LoRaConfig_RegionCode_EU_868;

const std::string url = ChannelShareURL::make(settings, 8, lora, ChannelShareMode::Add);

CHECK(url.rfind("https://meshtastic.org/e/?add=true#", 0) == 0);

meshtastic_ChannelSet decoded = meshtastic_ChannelSet_init_zero;
REQUIRE(decodeChannelSet(url, decoded));
REQUIRE(decoded.settings_count == 8);
CHECK(strcmp(decoded.settings[7].name, "Channel7") == 0);
CHECK_FALSE(decoded.has_lora_config);
}
Loading
Loading