diff --git a/BeaconCrypto.h b/BeaconCrypto.h index 6e3e41a..98c3a2b 100644 --- a/BeaconCrypto.h +++ b/BeaconCrypto.h @@ -155,5 +155,82 @@ static int beacon_crypto_encrypt(const uint8_t *plaintext, size_t pt_len, return 32 + 16 + (int)padded_len + 32; } +// Decrypt incoming RNS Token-format payload. +// +// Input layout: [ephemeral_pub:32][IV:16][ciphertext:var][HMAC:32] +// Returns plaintext length, or -1 on error (HMAC fail, decrypt fail, etc.) +// +// Crypto pipeline (reverse of beacon_crypto_encrypt): +// 1. Extract ephemeral public key +// 2. ECDH shared secret with OUR private key +// 3. HKDF-SHA256 → signing_key(32) + encryption_key(32) +// 4. Verify HMAC-SHA256(signing_key, IV || ciphertext) +// 5. AES-256-CBC decrypt +// 6. PKCS7 unpad +static int beacon_crypto_decrypt(const uint8_t *input, size_t input_len, + const uint8_t *our_x25519_sk, + const uint8_t *peer_identity_hash, + uint8_t *output, size_t output_cap) { + // Minimum: ephemeral(32) + IV(16) + 16-byte block + HMAC(32) = 96 + if (input_len < 96) return -1; + + const uint8_t *eph_pub = input; + const uint8_t *iv_pos = input + 32; + size_t ct_len = input_len - 32 - 16 - 32; // remove eph + IV + HMAC + const uint8_t *ct_pos = input + 32 + 16; + const uint8_t *hmac_in = input + input_len - 32; + + if (ct_len == 0 || ct_len % 16 != 0) return -1; + if (ct_len > output_cap) return -1; + + // 1. ECDH shared secret + uint8_t ss_bytes[32]; + if (crypto_scalarmult_curve25519(ss_bytes, our_x25519_sk, eph_pub) != 0) return -1; + + // 2. HKDF-SHA256: derive signing_key(32) + encryption_key(32) + uint8_t derived[64]; + int ret = rns_hkdf(ss_bytes, 32, peer_identity_hash, 16, derived); + if (ret != 0) return -1; + + uint8_t *signing_key = derived; + uint8_t *encryption_key = derived + 32; + + // 3. Verify HMAC-SHA256(signing_key, IV || ciphertext) + uint8_t computed_hmac[32]; + const mbedtls_md_info_t *md_info = mbedtls_md_info_from_type(MBEDTLS_MD_SHA256); + mbedtls_md_context_t md_ctx; + mbedtls_md_init(&md_ctx); + ret = mbedtls_md_setup(&md_ctx, md_info, 1); + if (ret == 0) ret = mbedtls_md_hmac_starts(&md_ctx, signing_key, 32); + if (ret == 0) ret = mbedtls_md_hmac_update(&md_ctx, iv_pos, 16); + if (ret == 0) ret = mbedtls_md_hmac_update(&md_ctx, ct_pos, ct_len); + if (ret == 0) ret = mbedtls_md_hmac_finish(&md_ctx, computed_hmac); + mbedtls_md_free(&md_ctx); + if (ret != 0) return -1; + + if (memcmp(computed_hmac, hmac_in, 32) != 0) return -2; // HMAC mismatch + + // 4. AES-256-CBC decrypt + uint8_t iv_copy[16]; + memcpy(iv_copy, iv_pos, 16); + mbedtls_aes_context aes; + mbedtls_aes_init(&aes); + ret = mbedtls_aes_setkey_dec(&aes, encryption_key, 256); + if (ret != 0) { mbedtls_aes_free(&aes); return -1; } + ret = mbedtls_aes_crypt_cbc(&aes, MBEDTLS_AES_DECRYPT, ct_len, + iv_copy, ct_pos, output); + mbedtls_aes_free(&aes); + if (ret != 0) return -1; + + // 5. PKCS7 unpad + uint8_t pad_val = output[ct_len - 1]; + if (pad_val == 0 || pad_val > 16) return -3; // invalid padding + for (size_t i = 0; i < pad_val; i++) { + if (output[ct_len - 1 - i] != pad_val) return -3; + } + + return (int)(ct_len - pad_val); +} + #endif #endif diff --git a/Gui.h b/Gui.h index b85d6d6..1302548 100644 --- a/Gui.h +++ b/Gui.h @@ -709,8 +709,13 @@ struct msg_entry_t { uint8_t pkt_type; uint8_t sender_hash[16]; char display_name[MSG_NAME_LEN]; + char content[64]; // LXMF message content (first 63 chars) + uint8_t sender_ed25519_pub[32]; // cached from announce for signature verification uint16_t pkt_len; bool is_announce; + bool is_lxmf; // true if decrypted LXMF message + bool verified; // signature verified + bool has_pubkey; // sender_ed25519_pub is valid }; extern msg_entry_t msg_log[]; extern uint8_t msg_log_head; @@ -1473,10 +1478,17 @@ static void gui_update_data() { lv_obj_clear_flag(gui_msg_row[shown], LV_OBJ_FLAG_HIDDEN); - // Name — amber for announces, white for data - lv_label_set_text(gui_msg_name[shown], m.display_name); - lv_obj_set_style_text_color(gui_msg_name[shown], - lv_color_hex(m.is_announce ? GUI_COL_AMBER : GUI_COL_WHITE), 0); + // Name — green for verified LXMF, amber for announces, white for data + if (m.is_lxmf && m.content[0]) { + // LXMF message: show content as primary text + lv_label_set_text(gui_msg_name[shown], m.content); + lv_obj_set_style_text_color(gui_msg_name[shown], + lv_color_hex(m.verified ? GUI_COL_GREEN : GUI_COL_WHITE), 0); + } else { + lv_label_set_text(gui_msg_name[shown], m.display_name); + lv_obj_set_style_text_color(gui_msg_name[shown], + lv_color_hex(m.is_announce ? GUI_COL_AMBER : GUI_COL_WHITE), 0); + } // RSSI with colour coding lv_label_set_text_fmt(gui_msg_rssi[shown], "%d dBm", m.rssi); @@ -1484,11 +1496,17 @@ static void gui_update_data() { (m.rssi > -100) ? GUI_COL_AMBER : GUI_COL_RED; lv_obj_set_style_text_color(gui_msg_rssi[shown], lv_color_hex(rssi_col), 0); - // Time ago + // Time ago + sender name for LXMF messages uint32_t ago = (millis() - m.timestamp) / 1000; - if (ago < 60) lv_label_set_text_fmt(gui_msg_time[shown], "%lus ago %dB", ago, m.pkt_len); - else if (ago < 3600) lv_label_set_text_fmt(gui_msg_time[shown], "%lum ago %dB", ago / 60, m.pkt_len); - else lv_label_set_text_fmt(gui_msg_time[shown], "%luh ago %dB", ago / 3600, m.pkt_len); + if (m.is_lxmf) { + if (ago < 60) lv_label_set_text_fmt(gui_msg_time[shown], "%s %lus ago", m.display_name, ago); + else if (ago < 3600) lv_label_set_text_fmt(gui_msg_time[shown], "%s %lum ago", m.display_name, ago / 60); + else lv_label_set_text_fmt(gui_msg_time[shown], "%s %luh ago", m.display_name, ago / 3600); + } else { + if (ago < 60) lv_label_set_text_fmt(gui_msg_time[shown], "%lus ago %dB", ago, m.pkt_len); + else if (ago < 3600) lv_label_set_text_fmt(gui_msg_time[shown], "%lum ago %dB", ago / 60, m.pkt_len); + else lv_label_set_text_fmt(gui_msg_time[shown], "%luh ago %dB", ago / 3600, m.pkt_len); + } shown++; } diff --git a/LxmfBeacon.h b/LxmfBeacon.h index a6d6cfd..9cd0db0 100644 --- a/LxmfBeacon.h +++ b/LxmfBeacon.h @@ -446,6 +446,155 @@ static int lxmf_build_message(uint8_t *out, size_t out_cap, return (int)total; } +// ---- Minimal Msgpack Reader ---- +// Read-only cursor over msgpack data. No allocation. +struct MsgpackReader { + const uint8_t *buf; + size_t pos; + size_t len; + + MsgpackReader(const uint8_t *data, size_t sz) : buf(data), pos(0), len(sz) {} + bool has(size_t n) const { return pos + n <= len; } + uint8_t peek() const { return has(1) ? buf[pos] : 0xFF; } + uint8_t read_u8() { return has(1) ? buf[pos++] : 0; } + + // Read fixarray length (0x90-0x9F) + int read_array_len() { + uint8_t b = read_u8(); + if ((b & 0xF0) == 0x90) return b & 0x0F; + if (b == 0xDC && has(2)) { uint16_t n = (buf[pos]<<8)|buf[pos+1]; pos+=2; return n; } + return -1; + } + + // Read uint (fixint, uint8, uint16, uint32) + uint32_t read_uint() { + uint8_t b = read_u8(); + if (b <= 0x7F) return b; + if (b == 0xCC && has(1)) return buf[pos++]; + if (b == 0xCD && has(2)) { uint16_t v=(buf[pos]<<8)|buf[pos+1]; pos+=2; return v; } + if (b == 0xCE && has(4)) { uint32_t v=(buf[pos]<<24)|(buf[pos+1]<<16)|(buf[pos+2]<<8)|buf[pos+3]; pos+=4; return v; } + return 0; + } + + // Read binary/string, returns pointer and sets length. Doesn't copy. + const uint8_t *read_bin(size_t *out_len) { + uint8_t b = read_u8(); + size_t n = 0; + if ((b & 0xE0) == 0xA0) n = b & 0x1F; // fixstr + else if (b == 0xC4 && has(1)) n = buf[pos++]; // bin8 + else if (b == 0xC5 && has(2)) { n=(buf[pos]<<8)|buf[pos+1]; pos+=2; } // bin16 + else if (b == 0xD9 && has(1)) n = buf[pos++]; // str8 + else { *out_len = 0; return NULL; } + if (!has(n)) { *out_len = 0; return NULL; } + const uint8_t *ptr = buf + pos; + pos += n; + *out_len = n; + return ptr; + } + + // Skip one msgpack value + void skip() { + uint8_t b = peek(); + if (b <= 0x7F || b >= 0xE0 || b == 0xC0 || b == 0xC2 || b == 0xC3) { pos++; return; } + if ((b & 0xF0) == 0x90) { int n = read_array_len(); for (int i=0;isource_hash, plaintext, 16); + // signature at plaintext+16, 64 bytes (verified later with sender's pubkey) + const uint8_t *msgpack_data = plaintext + 16 + 64; + size_t msgpack_len = pt_len - 16 - 64; + + MsgpackReader r(msgpack_data, msgpack_len); + + // Expect fixarray(4): [timestamp, title, content, fields] + int arr_len = r.read_array_len(); + if (arr_len < 3) return false; + + // [0] timestamp + out->timestamp = r.read_uint(); + + // [1] title + size_t title_len = 0; + const uint8_t *title_ptr = r.read_bin(&title_len); + if (title_ptr && title_len > 0) { + size_t n = title_len < sizeof(out->title)-1 ? title_len : sizeof(out->title)-1; + memcpy(out->title, title_ptr, n); + out->title[n] = '\0'; + } else { + out->title[0] = '\0'; + } + + // [2] content + size_t content_len = 0; + const uint8_t *content_ptr = r.read_bin(&content_len); + if (content_ptr && content_len > 0) { + size_t n = content_len < sizeof(out->content)-1 ? content_len : sizeof(out->content)-1; + memcpy(out->content, content_ptr, n); + out->content[n] = '\0'; + } else { + out->content[0] = '\0'; + } + + out->verified = false; // caller must verify with sender's pubkey + return true; +} + +// Verify LXMF signature using sender's Ed25519 public key +static bool lxmf_verify_signature(const uint8_t *plaintext, size_t pt_len, + const uint8_t *sender_ed25519_pub) { + if (pt_len < 16 + 64 + 4) return false; + const uint8_t *signature = plaintext + 16; + const uint8_t *msgpack_data = plaintext + 16 + 64; + size_t msgpack_len = pt_len - 16 - 64; + + // Reconstruct signed_part: dest_hash + source_hash + msgpack + SHA256(same) + // dest_hash = our lxmf_source_hash, source_hash = plaintext[0:16] + static uint8_t hashed_part[512]; + size_t hp_len = 16 + 16 + msgpack_len; + if (hp_len > sizeof(hashed_part) - 32) return false; + + memcpy(hashed_part, lxmf_source_hash, 16); // dest = us + memcpy(hashed_part + 16, plaintext, 16); // source + memcpy(hashed_part + 32, msgpack_data, msgpack_len); + + uint8_t message_hash[32]; + sha256_once(hashed_part, hp_len, message_hash); + + static uint8_t signed_part[512 + 32]; + memcpy(signed_part, hashed_part, hp_len); + memcpy(signed_part + hp_len, message_hash, 32); + size_t sp_len = hp_len + 32; + + return crypto_sign_ed25519_verify_detached(signature, signed_part, sp_len, + sender_ed25519_pub) == 0; +} + // ---- RNS Announce Packet Construction ---- // Builds a complete RNS announce packet in tbuf for transmission. // diff --git a/MessageLog.h b/MessageLog.h index 0244908..a2abf18 100644 --- a/MessageLog.h +++ b/MessageLog.h @@ -115,20 +115,11 @@ static void msg_log_parse_packet(const uint8_t *pkt, uint16_t len, const uint8_t *dest_hash = pkt + 2; // bytes 2-17 if (pkt_type == RNS_TYPE_ANNOUNCE && len > 167) { - // ANNOUNCE packet structure (HEADER_1, 19-byte header): - // [flags(1) hops(1) dest_hash(16) context(1)] - // [pub_keys(64) = x25519(32) + ed25519(32)] - // [name_hash(10)] - // [random_hash(10)] - // [signature(64)] - // [app_data(variable) = display name] - - // Compute identity hash from public keys - const uint8_t *pub_keys = pkt + 19; // 64 bytes + // ANNOUNCE: extract identity, display name, and cache public keys + const uint8_t *pub_keys = pkt + 19; // x25519(32) + ed25519(32) uint8_t identity_hash[32]; mbedtls_sha256(pub_keys, 64, identity_hash, 0); - // Extract display name from app_data char name[MSG_NAME_LEN] = {0}; int app_data_offset = 19 + 64 + 10 + 10 + 64; // = 167 int app_data_len = len - app_data_offset; @@ -138,8 +129,76 @@ static void msg_log_parse_packet(const uint8_t *pkt, uint16_t len, } msg_log_add(identity_hash, name, rssi, snr, pkt_type, len, true); + + // Cache sender's ed25519 public key for signature verification + int idx = msg_log_find_sender(identity_hash); + if (idx >= 0) { + memcpy(msg_log[idx].sender_ed25519_pub, pub_keys + 32, 32); + msg_log[idx].has_pubkey = true; + } + + } else if (pkt_type == RNS_TYPE_DATA && len > 19 + 96) { + // DATA packet — check if addressed to us (LXMF delivery) + extern uint8_t lxmf_source_hash[16]; + extern uint8_t lxmf_x25519_sk[32]; + extern bool lxmf_identity_configured; + + if (lxmf_identity_configured && memcmp(dest_hash, lxmf_source_hash, 16) == 0) { + // Addressed to our LXMF delivery destination — try to decrypt + const uint8_t *encrypted = pkt + 19; // after RNS header + size_t enc_len = len - 19; + + // We need the sender's identity hash for HKDF salt. + // For OPPORTUNISTIC messages, we use our OWN identity hash as salt + // (the sender encrypted TO us, using our identity hash as salt) + extern uint8_t lxmf_identity_hash[16]; + + static uint8_t plaintext[512]; + int pt_len = beacon_crypto_decrypt(encrypted, enc_len, + lxmf_x25519_sk, + lxmf_identity_hash, + plaintext, sizeof(plaintext)); + + if (pt_len > 0) { + // Decryption succeeded — parse LXMF message + LxmfParsed parsed; + if (lxmf_parse_received(plaintext, pt_len, &parsed)) { + // Look up sender's name and pubkey from announce cache + char *sender_name = NULL; + uint8_t *sender_pub = NULL; + int sender_idx = msg_log_find_sender(parsed.source_hash); + if (sender_idx >= 0) { + sender_name = msg_log[sender_idx].display_name; + if (msg_log[sender_idx].has_pubkey) + sender_pub = msg_log[sender_idx].sender_ed25519_pub; + } + + // Add as LXMF message entry + msg_log_add(parsed.source_hash, sender_name, rssi, snr, + pkt_type, len, false); + + // Store content in the new entry + int new_idx = msg_log_find_sender(parsed.source_hash); + if (new_idx >= 0) { + strncpy(msg_log[new_idx].content, parsed.content, 63); + msg_log[new_idx].content[63] = '\0'; + msg_log[new_idx].is_lxmf = true; + if (sender_pub) { + msg_log[new_idx].verified = + lxmf_verify_signature(plaintext, pt_len, sender_pub); + } + } + + Serial.printf("[lxmf_rx] from %02x%02x: \"%s\"\n", + parsed.source_hash[0], parsed.source_hash[1], parsed.content); + } + } + } else { + // DATA not for us — log generically + msg_log_add(dest_hash, NULL, rssi, snr, pkt_type, len, false); + } } else { - // DATA or short packet — log with dest_hash + // Other packet — log with dest_hash msg_log_add(dest_hash, NULL, rssi, snr, pkt_type, len, false); } }