// MessageLog.h — Received packet log and RNS announce parser // Stores recent LoRa packets for the Messages screen viewer #ifndef MESSAGELOG_H #define MESSAGELOG_H #if BOARD_MODEL == BOARD_TWATCH_ULT #include "mbedtls/sha256.h" // RNS packet type bits (flags byte, bits 1-0) #define RNS_TYPE_DATA 0x00 #define RNS_TYPE_ANNOUNCE 0x01 // RNS header type (flags byte, bit 6) #define RNS_HEADER_1 0x00 #define RNS_HEADER_2 0x40 // IFAC flag (flags byte, bit 7) #define RNS_IFAC_FLAG 0x80 // msg_entry_t struct defined in Gui.h (included earlier) // MSG_LOG_SIZE and MSG_NAME_LEN also defined there #define MSG_DEDUP_MS 60000 // ignore same sender within 60s msg_entry_t msg_log[MSG_LOG_SIZE]; uint8_t msg_log_head = 0; uint8_t msg_log_count = 0; uint32_t msg_log_last_change = 0; // Find existing entry for this sender (for de-dup / update) static int msg_log_find_sender(const uint8_t *hash) { for (int i = 0; i < msg_log_count; i++) { int idx = (msg_log_head - 1 - i + MSG_LOG_SIZE) % MSG_LOG_SIZE; if (memcmp(msg_log[idx].sender_hash, hash, 16) == 0) return idx; } return -1; } // Add or update a message entry static void msg_log_add(const uint8_t *sender_hash, const char *name, int16_t rssi, int8_t snr, uint8_t pkt_type, uint16_t pkt_len, bool is_announce) { // De-dup: update existing entry for same sender int existing = msg_log_find_sender(sender_hash); if (existing >= 0) { msg_entry_t &e = msg_log[existing]; if (millis() - e.timestamp < MSG_DEDUP_MS && !is_announce) return; // Update existing e.timestamp = millis(); e.rssi = rssi; e.snr = snr; e.pkt_type = pkt_type; e.pkt_len = pkt_len; if (is_announce && name[0]) { strncpy(e.display_name, name, MSG_NAME_LEN - 1); e.display_name[MSG_NAME_LEN - 1] = '\0'; e.is_announce = true; } msg_log_last_change = millis(); return; } // New entry msg_entry_t &e = msg_log[msg_log_head]; e.timestamp = millis(); e.rssi = rssi; e.snr = snr; e.pkt_type = pkt_type; e.pkt_len = pkt_len; e.is_announce = is_announce; memcpy(e.sender_hash, sender_hash, 16); if (name && name[0]) { strncpy(e.display_name, name, MSG_NAME_LEN - 1); e.display_name[MSG_NAME_LEN - 1] = '\0'; } else { // Short hex of sender hash snprintf(e.display_name, MSG_NAME_LEN, "%02x%02x%02x..%02x%02x", sender_hash[0], sender_hash[1], sender_hash[2], sender_hash[14], sender_hash[15]); } msg_log_head = (msg_log_head + 1) % MSG_LOG_SIZE; if (msg_log_count < MSG_LOG_SIZE) msg_log_count++; msg_log_last_change = millis(); } // Parse an RNS packet and log it // Called from main loop after dequeue, before kiss_write_packet // pkt points to raw LoRa payload (after split reassembly), len is total length static void msg_log_parse_packet(const uint8_t *pkt, uint16_t len, int16_t rssi, int8_t snr) { if (len < 19) return; // too short for RNS header uint8_t flags = pkt[0]; bool has_ifac = (flags & RNS_IFAC_FLAG) != 0; uint8_t pkt_type = flags & 0x03; // IFAC packets: the real header starts after unmasking, but we can // still extract the dest_hash from bytes 2-17 (masked but deterministic position) // For announces, the IFAC is stripped by the receiver's IFAC handler. // For our purposes, we parse what we can. int hdr_offset = 0; // where the header starts after IFAC if (has_ifac) { // IFAC-wrapped: bytes 0-1 are masked header, bytes 2-9 are IFAC signature, // rest is masked payload. We can't parse without unmasking. // But we still log the packet with the dest_hash from bytes 2-17 // (these are the IFAC sig bytes, not the actual dest_hash) // For now, just log as generic packet with flags info uint8_t generic_hash[16]; memcpy(generic_hash, pkt + 2, 16); msg_log_add(generic_hash, NULL, rssi, snr, pkt_type, len, false); return; } // Non-IFAC packet — parse RNS header directly const uint8_t *dest_hash = pkt + 2; // bytes 2-17 if (pkt_type == RNS_TYPE_ANNOUNCE && len > 167) { // 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); char name[MSG_NAME_LEN] = {0}; int app_data_offset = 19 + 64 + 10 + 10 + 64; // = 167 int app_data_len = len - app_data_offset; if (app_data_len > 0 && app_data_len < MSG_NAME_LEN) { memcpy(name, pkt + app_data_offset, app_data_len); name[app_data_len] = '\0'; } 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 { // Other packet — log with dest_hash msg_log_add(dest_hash, NULL, rssi, snr, pkt_type, len, false); } } #endif // BOARD_MODEL == BOARD_TWATCH_ULT #endif // MESSAGELOG_H