From 7dcb90032d9fe4fc21ff794a4b3d8fef0bccdf6c Mon Sep 17 00:00:00 2001 From: liu weikai Date: Sat, 20 Jun 2026 16:56:53 +0800 Subject: [PATCH] Implement MeshCore V2 multibyte paths --- docs/meshcore/packet_structure.md | 4 +- docs/meshcore/payloads.md | 20 +- docs/meshcore/v2_multibyte_paths.md | 473 ++++++++++ .../include/chat/domain/chat_types.h | 17 + .../infra/meshcore/meshcore_ble_backend.h | 7 + .../infra/meshcore/meshcore_payload_helpers.h | 13 + .../meshcore/meshcore_protocol_helpers.h | 39 + .../meshcore/meshcore_payload_helpers.cpp | 71 +- .../meshcore/meshcore_protocol_helpers.cpp | 138 ++- .../meshcore_self_announcement_core.cpp | 12 +- .../mesh/protocol/mesh_protocol_strategy.h | 4 + .../meshcore/meshcore_protocol_strategy.cpp | 150 +++- .../tests/test_meshcore_protocol_strategy.cpp | 62 ++ .../include/phone/common/phone_app_facade.h | 10 + .../phone/meshcore/meshcore_phone_core.h | 35 + .../src/meshcore/meshcore_phone_core.cpp | 84 +- modules/core_sys/include/app/app_config.h | 2 + .../ui/screens/settings/settings_state.h | 2 + .../settings/settings_page_components.cpp | 35 + .../include/ble/app_phone_facade.h | 6 + .../arduino_common/include/ble/meshcore_ble.h | 4 + .../chat/infra/meshcore/meshcore_adapter.h | 42 + .../arduino_common/src/app_config_store.cpp | 8 + .../src/ble/app_phone_facade.cpp | 11 + .../arduino_common/src/ble/meshcore_ble.cpp | 149 +++- .../src/ble/meshcore_ble_owner_hooks.cpp | 57 +- .../chat/infra/meshcore/meshcore_adapter.cpp | 820 ++++++++++++++---- .../platform_ui_settings_backup_runtime.cpp | 10 + .../infra/meshcore/meshcore_radio_adapter.h | 5 + .../infra/meshcore/meshcore_radio_adapter.cpp | 88 +- 30 files changed, 2076 insertions(+), 302 deletions(-) create mode 100644 docs/meshcore/v2_multibyte_paths.md diff --git a/docs/meshcore/packet_structure.md b/docs/meshcore/packet_structure.md index 92c410be..f5ffd3f8 100644 --- a/docs/meshcore/packet_structure.md +++ b/docs/meshcore/packet_structure.md @@ -9,6 +9,8 @@ | payload | up to 184 (`MAX_PACKET_PAYLOAD`) | The actual data being transmitted. | Note: see the [payloads doc](./payloads.md) for more information about the content of payload. +Trail Mate's complete V2 multibyte path/profile rules are defined in +[V2 multibyte paths](./v2_multibyte_paths.md). ## Header Breakdown @@ -55,6 +57,6 @@ bit 0 means the lowest bit (1s place) | Value | Version | Description | |--------|---------|---------------------------------------------------| | `0x00` | 1 | 1-byte src/dest hashes, 2-byte MAC. | -| `0x01` | 2 | Future version (e.g., 2-byte hashes, 4-byte MAC). | +| `0x01` | 2 | 2-byte hashes, 4-byte MAC; paths use 2 bytes per hop. | | `0x02` | 3 | Future version. | | `0x03` | 4 | Future version. | diff --git a/docs/meshcore/payloads.md b/docs/meshcore/payloads.md index f86a70bc..1d065792 100644 --- a/docs/meshcore/payloads.md +++ b/docs/meshcore/payloads.md @@ -17,10 +17,16 @@ Inside of each [meshcore packet](./packet_structure.md) is a payload, identified This document defines the structure of each of these payload types. NOTE: all 16 and 32-bit integer fields are Little Endian. +Trail Mate's complete V2 multibyte path/profile rules are defined in +[V2 multibyte paths](./v2_multibyte_paths.md). ## Important concepts: -* Node hash: the first byte of the node's public key +* V1 node hash: the first byte of the node's public key. +* V2 node hash: the first two bytes of the node's public key. +* V1 cipher MAC: 2 bytes. +* V2 cipher MAC: 4 bytes. +* Path length fields are byte counts. V1 paths use one byte per hop; V2 paths use two bytes per hop. # Node advertisement This kind of payload notifies receivers that a node exists, and gives information about the node @@ -76,6 +82,10 @@ Returned path, request, response, and plain text messages are all formatted in t | cipher MAC | 2 | MAC for encrypted data in next field | | ciphertext | rest of payload | encrypted message, see subsections below for details | +For payload version 2, the same encrypted peer payload shape uses a 2-byte +destination hash, 2-byte source hash, and 4-byte cipher MAC before the +ciphertext. + ## Returned path Returned path messages provide a description of the route a packet took from the original author. Receivers will send returned path messages to the author of the original message. @@ -83,7 +93,7 @@ Returned path messages provide a description of the route a packet took from the | Field | Size (bytes) | Description | |-------------|--------------|----------------------------------------------------------------------------------------------| | path length | 1 | length of next field | -| path | see above | a list of node hashes (one byte each) | +| path | see above | a list of node hashes (one byte each in V1, two bytes each in V2) | | extra type | 1 | extra, bundled payload type, eg., acknowledgement or response. Same values as in [packet structure](./packet_structure.md) | | extra | rest of data | extra, bundled payload content, follows same format as main content defined by this document | @@ -164,6 +174,9 @@ txt_type | cipher MAC | 2 | MAC for encrypted data in next field | | ciphertext | rest of payload | encrypted message, see below for details | +For payload version 2, anonymous request uses a 2-byte destination hash and a +4-byte cipher MAC. The sender public key remains 32 bytes. + ## Room server login | Field | Size (bytes) | Description | @@ -187,6 +200,9 @@ txt_type | cipher MAC | 2 | MAC for encrypted data in next field | | ciphertext | rest of payload | encrypted message, see below for details | +For payload version 2, group payloads use a 2-byte channel hash and a 4-byte +cipher MAC. + The plaintext contained in the ciphertext matches the format described in [plain text message](#plain-text-message). Specifically, it consists of a four byte timestamp, a flags byte, and the message. The flags byte will generally be `0x00` because it is a "plain text message". The message will be of the form `: ` (eg., `user123: I'm on my way`). diff --git a/docs/meshcore/v2_multibyte_paths.md b/docs/meshcore/v2_multibyte_paths.md new file mode 100644 index 00000000..1a80f9a4 --- /dev/null +++ b/docs/meshcore/v2_multibyte_paths.md @@ -0,0 +1,473 @@ +# MeshCore V2 Multibyte Paths Specification + +This document defines Trail Mate's full implementation target for MeshCore +issue #60: multibyte paths plus a repeater policy that can refuse one-byte +legacy traffic. This is not an MVP. The implementation may be delivered in +vertical layers, but the accepted protocol surface is the complete mechanism +defined here. + +## User Requirement + +The GitHub issue asks for "multi byte paths" and a repeater option to "only +repeat multi byte messages." The stated reason is path hash collision pressure: +when more than 140 repeaters are visible, the one-byte MeshCore node hash is no +longer usable enough, and adding repeaters makes the situation worse unless +repeaters can stop amplifying one-byte messages. + +For Trail Mate this means: + +1. MeshCore packet and payload handling must support a multibyte path profile. +2. The multibyte profile is MeshCore payload version 2. +3. A repeater can be configured to forward only multibyte packets. +4. Phone/BLE protocol surfaces must carry the profile, otherwise the path cannot + be displayed, cached, or reused correctly. + +## Upstream MeshCore Analysis + +The source under `.tmp/MeshCore` reserves the protocol shape for V2, but does not +implement V2 data routing. Trail Mate must therefore implement the missing +mechanism deliberately rather than simply toggling a constant. + +### Packet Version And Wire Envelope + +Upstream `src/Packet.h` defines the packet header as route bits, payload type +bits, and payload version bits. The version values are: + +| Version Bits | Name | Upstream Meaning | +|--------------|------|------------------| +| `0x00` | `PAYLOAD_VER_1` | 1-byte source/destination hashes, 2-byte MAC | +| `0x01` | `PAYLOAD_VER_2` | Future, with upstream comment suggesting 2-byte hashes and 4-byte MAC | +| `0x02` | `PAYLOAD_VER_3` | Future | +| `0x03` | `PAYLOAD_VER_4` | Future | + +Upstream `src/Packet.cpp` writes the packet envelope as: + +1. `header` +2. optional 4 bytes of transport codes +3. one byte `path_len` +4. `path_len` bytes of path +5. remaining payload bytes + +The important point is that `path_len` is already a byte count, not a hop count. +This lets V2 use two bytes per hop without changing the packet envelope. + +### V1 Hash And MAC Constants + +Upstream `src/MeshCore.h` defines: + +| Constant | Value | Meaning | +|----------|-------|---------| +| `PATH_HASH_SIZE` | `1` | Node hash is one byte | +| `CIPHER_MAC_SIZE` | `2` | Encrypted payload MAC is two bytes | +| `MAX_PATH_SIZE` | `64` | Path byte capacity | +| `MAX_PACKET_PAYLOAD` | `184` | Payload byte capacity | + +Upstream `src/Identity.h` implements `copyHashTo()` by copying +`PATH_HASH_SIZE` bytes from the public key prefix. `isHashMatch()` compares the +same fixed width unless a caller explicitly passes a length. + +### V2 Is Rejected For Normal Data + +Upstream `src/Mesh.cpp` starts `Mesh::onRecvPacket()` by rejecting all packets +with `getPayloadVer() > PAYLOAD_VER_1`. Therefore all normal request, response, +text, ACK, group, anonymous request, returned path, and direct/flood routing +logic is V1-only today. + +### TRACE Has A Local Variable-Width Hash Mechanism + +Upstream direct `PAYLOAD_TYPE_TRACE` is the only partial precedent for +multibyte path hashes. Its flags use the lower two bits as `path_sz`; the next +hash width is `1 << path_sz` bytes, and the receiver checks +`Identity::isHashMatch(hash, width)`. + +This is not the V2 data mechanism: + +1. TRACE still rides in a packet that is otherwise accepted only by the + V1 gate. +2. TRACE appends the route path to the payload and uses the packet `path` field + for SNR collection. +3. Normal direct routing, flood routing, path returns, and payload crypto do not + use this flag. + +Trail Mate should reuse the idea that public-key prefixes can be compared at +variable widths, but V2 data packets must be represented by the packet payload +version, not by TRACE flags. + +### Direct Routing Is Fixed To One-Byte Hops + +Upstream direct routing checks whether the first `PATH_HASH_SIZE` bytes of +`packet->path` match this node. If so, it removes itself from the front of the +path and retransmits. Because `PATH_HASH_SIZE` is `1`, `removeSelfFromPath()` +contains a byte-shuffle implementation and has a compile-time error for wider +hash sizes. + +Trail Mate cannot implement V2 by increasing a global hash size. It must make +direct routing profile-aware: + +1. V1 direct packets consume one byte from the front of the path. +2. V2 direct packets consume two bytes from the front of the path. +3. `path_len` remains the path byte length. +4. A direct path is valid only when `path_len` is a multiple of the profile hash + width. + +### Flood Routing Is Fixed To One-Byte Appends + +Upstream flood routing appends this node's hash by calling `copyHashTo()` and +increasing `path_len` by `PATH_HASH_SIZE`. Because `PATH_HASH_SIZE` is `1`, +V1 flood packets accumulate one byte per hop. + +Trail Mate must make flood routing profile-aware: + +1. V1 flood packets append one byte of the local public-key prefix. +2. V2 flood packets append two bytes of the local public-key prefix. +3. Flood duplicate/self-path detection must scan hops by the profile hash + width, not by raw bytes. +4. Flood max checks must compare hop count when the setting is a hop limit, and + also enforce `MAX_PATH_SIZE` as a byte capacity. + +### Path Return Payloads Are Fixed To One-Byte Hashes + +Upstream `createPathReturn()` builds a `PAYLOAD_TYPE_PATH` packet with: + +1. destination hash prefix of `PATH_HASH_SIZE` +2. source hash prefix of `PATH_HASH_SIZE` +3. encrypted plaintext containing `path_len`, path bytes, `extra_type`, and + optional `extra` + +Because the packet payload version is V1 and `PATH_HASH_SIZE` is one, both the +outer peer-addressing hashes and the returned path are V1-only. + +Trail Mate V2 path returns must set payload version 2 and use: + +1. two-byte destination hash +2. two-byte source hash +3. four-byte cipher MAC +4. plaintext `path_len` as a byte count +5. returned path bytes that are a multiple of two +6. any bundled `extra` payload interpreted under the same profile unless that + payload has its own explicit envelope + +### Repeater Policy Exists But Is Not Profile-Aware + +Upstream repeaters implement `allowPacketForward()` with: + +1. disabled forwarding check (`disable_fwd`) +2. flood max check (`packet->path_len >= flood_max`) +3. optional region/transport checks + +The CLI exposes this as `repeat on/off` and `flood.max`. There is no upstream +profile filter and no "repeat only multibyte" option. + +Trail Mate must add this as an explicit forwarding policy. The user's required +behavior is strict: `MultibyteOnly` limits both flood repeat and direct +forwarding. A repeater in this mode must not be used as a V1 relay. + +### Collision Model + +Upstream documentation acknowledges one-byte public-key-prefix collision as a +known MeshCore limitation. The FAQ suggests generating a different private key +to obtain a desired first byte. That workaround does not scale to dense repeater +visibility. V2 reduces this pressure by moving both routing paths and encrypted +peer addressing from an 8-bit prefix to a 16-bit prefix. + +## Trail Mate Current Baseline + +Trail Mate currently mirrors the upstream V1 limitation in several layers: + +| Area | Current Behavior | +|------|------------------| +| Packet parsing | Extracts payload version but runtime accepts only version 1 | +| Identity | `selfHash()` returns only `pub_[0]` | +| Peer payload helpers | Assume `dest_hash(1) + src_hash(1) + MAC(2)` | +| Group payload helpers | Assume `channel_hash(1) + MAC(2)` | +| Anonymous request helpers | Assume `dest_hash(1) + pubkey(32) + MAC(2)` | +| Flood routing | Appends one local hash byte and scans path byte-by-byte | +| Direct routing | Compares/removes one leading path byte | +| Path return | Stores and reuses path bytes without a profile | +| Route cache | Caches peer hash and path bytes without hash-width metadata | +| UI/settings | Exposes repeat and flood max but no path profile or repeat profile | +| BLE/phone protocol | Sends path bytes and path length without profile/hash width | + +Any implementation that only sends V2 but keeps route cache, direct forwarding, +or BLE as V1-shaped is incomplete. + +## Trail Mate V2 Definition + +Trail Mate formally defines MeshCore payload version 2 as: + +| Property | V1 | V2 | +|----------|----|----| +| Payload version bits | `0` | `1` | +| Path hash width | 1 byte | 2 bytes | +| Peer destination hash | 1 byte | 2 bytes | +| Peer source hash | 1 byte | 2 bytes | +| Group/channel hash | 1 byte | 2 bytes | +| Anonymous destination hash | 1 byte | 2 bytes | +| Cipher MAC size | 2 bytes | 4 bytes | +| `path_len` meaning | byte count | byte count | +| Max path bytes | 64 | 64 | +| Max path hops | 64 | 32 | + +The V2 hash bytes are the first two bytes of the Ed25519 public key for node +identity, and the first two bytes of the existing channel hash for group +payloads. V1 remains first byte only. + +## Profiles + +Trail Mate uses two separate concepts: + +1. Payload profile: the wire shape of a packet already received or about to be + transmitted. +2. Send profile preference: the local policy for choosing a payload profile. + +### Payload Profiles + +| Name | Version | Hash Bytes | MAC Bytes | +|------|---------|------------|-----------| +| `V1` | `0` | `1` | `2` | +| `V2` | `1` | `2` | `4` | + +The payload profile is derived from the packet version bits for received frames. +Code must not infer profile from path length alone. + +### Send Profile Preferences + +| Name | Meaning | +|------|---------| +| `AutoPreferV2` | Default. Send V2 when the destination, route cache, and local protocol surface can represent V2. Fall back to V1 only when a known constraint requires it. | +| `V1Only` | Send legacy V1 packets. This is compatibility mode. | +| `V2Only` | Send only V2 packets. If the destination cannot be represented as V2, sending fails or rediscovery is required. | + +The default must be `AutoPreferV2`. + +### Forwarding Policies + +| Name | Meaning | +|------|---------| +| `Any` | Forward V1 and V2 according to normal repeat, flood max, duplicate, and route checks. | +| `MultibyteOnly` | Forward only packets whose payload profile is V2 or later with hash width greater than one. This applies to both flood repeat and direct forwarding. | + +`MultibyteOnly` is a forwarding policy, not merely a flood option. If a V1 +direct packet has this node as next hop, a MultibyteOnly repeater must drop it. + +## Wire Format + +### Packet Envelope + +The packet envelope is unchanged: + +| Field | Size | V2 Interpretation | +|-------|------|-------------------| +| `header` | 1 byte | Version bits are `PAYLOAD_VER_2` | +| `transport_codes` | optional 4 bytes | unchanged | +| `path_len` | 1 byte | path byte count | +| `path` | `path_len` bytes | two bytes per hop | +| `payload` | remaining bytes | payload type-specific V2 layout | + +The parser must reject malformed V2 paths when `path_len % 2 != 0`. + +### Peer Encrypted Payloads + +For `REQ`, `RESPONSE`, `TXT_MSG`, and `PATH`, V2 payload bytes are: + +| Field | Size | +|-------|------| +| destination hash | 2 bytes | +| source hash | 2 bytes | +| cipher MAC | 4 bytes | +| ciphertext | remaining bytes | + +The encrypted plaintext structures remain type-specific and unchanged except +where they contain path bytes. Returned path plaintext keeps its one-byte +`path_len` field as a byte count. + +### Anonymous Request + +For `ANON_REQ`, V2 payload bytes are: + +| Field | Size | +|-------|------| +| destination hash | 2 bytes | +| sender public key | 32 bytes | +| cipher MAC | 4 bytes | +| ciphertext | remaining bytes | + +### Group Payloads + +For `GRP_TXT` and `GRP_DATA`, V2 payload bytes are: + +| Field | Size | +|-------|------| +| channel hash | 2 bytes | +| cipher MAC | 4 bytes | +| ciphertext | remaining bytes | + +### ACK, Advert, Control, Trace, Multipart, Custom + +Payload version still belongs to the packet envelope, but not every payload type +has embedded peer hashes or MACs. + +| Payload Type | V2 Handling | +|--------------|-------------| +| `ACK` | Version may be V1 or V2. Packet hash and duplicate semantics remain unchanged. | +| `ADVERT` | Advertisement body remains unchanged. The route path, if any, follows the packet profile. | +| `CONTROL` | Body remains unchanged. Profile matters for routing only. | +| `TRACE` | Existing TRACE hash-size flags remain TRACE-specific. V2 route paths still use the packet profile. | +| `MULTIPART` | Outer packet profile determines route/hash/MAC interpretation of the embedded or reconstructed payload. | +| `RAW_CUSTOM` | Body is application-defined. Routing path still follows the packet profile. | + +## Routing Semantics + +### Profile-Aware Path Helpers + +All routing code must use helpers equivalent to: + +1. `hashBytes(profile)` +2. `macBytes(profile)` +3. `version(profile)` +4. `pathHopCount(profile, path_len)` +5. `pathIsWellFormed(profile, path_len)` +6. `copySelfHash(profile, dest)` +7. `hashMatches(profile, hash, public_key)` + +Raw constants such as `1`, `2`, or `4` must not be spread through routing, +payload, or BLE code as protocol truth. + +### Flood Routing + +On V2 flood receive: + +1. Reject if the path byte length is not a multiple of two. +2. Reject if this node's two-byte hash already appears in the path. +3. If the packet is for this node, process it and mark it not to retransmit. +4. If forwarding is allowed, append this node's two-byte hash. +5. Enforce `MAX_PATH_SIZE` as bytes. +6. Enforce the configured flood max as hops. +7. Retransmit with the same payload version. + +### Direct Routing + +On V2 direct receive: + +1. Reject if `path_len` is non-zero and not a multiple of two. +2. If `path_len == 0`, this is a zero-hop direct packet for local processing. +3. If the first two path bytes do not match this node, drop. +4. If forwarding policy is `MultibyteOnly`, allow this V2 path; if the packet is + V1, drop. +5. Remove the leading two bytes from the path. +6. Retransmit with the same payload version. + +### Returned Path Reuse + +A returned path must be cached with: + +1. payload profile +2. hash width +3. peer hash bytes +4. outbound path bytes +5. path byte length +6. hop count derived from profile +7. quality/age metadata + +When sending direct, Trail Mate must only reuse a cached path whose profile is +compatible with the selected send profile. `AutoPreferV2` should prefer V2 +cached routes and fall back to V1 routes only when no V2 route exists and the +destination has not been marked V2-required. + +## Settings And Persistence + +Trail Mate must persist: + +1. send profile preference, default `AutoPreferV2` +2. forwarding policy, default `MultibyteOnly` +3. existing repeat enabled/disabled setting +4. existing flood max setting + +The old repeat switch still controls whether this node repeats at all. The new +forwarding policy controls which packet profiles may be repeated when repeat is +enabled. + +Settings backup/restore must include the new values with stable JSON keys. + +## BLE And Phone Protocol Extension + +BLE/phone external protocol must be extended. A path without profile metadata is +ambiguous because the same byte array has a different hop count and different +hash display depending on profile. + +Every contact/path transfer that currently contains `out_path_len` and +`out_path` must also contain: + +1. path profile/version +2. hash width +3. peer hash width/profile for displayed short hash +4. enough versioning or feature bits for older phone clients to reject or ignore + the extension safely + +The phone-visible representation must display V2 paths as two-byte hop hashes +and must send V2 route reuse commands back with the same profile. A phone command +that sends a path without profile metadata is V1 by definition and must not be +used to transmit V2 paths. + +## Compatibility + +V1 is still accepted unless local forwarding policy says otherwise. This keeps +Trail Mate compatible with existing MeshCore nodes. + +V2 packets should be emitted by default for Trail Mate-originated peer/group +traffic when enough local information is available. Because upstream MeshCore +currently rejects `PAYLOAD_VER_2`, `AutoPreferV2` may fall back to V1 for known +legacy peers and legacy phone-provided paths. The fallback must be explicit in +the selected send profile logic, not an accidental consequence of missing V2 +code. + +## Implementation Plan + +The implementation should be layered, but each layer must preserve the final +model: + +1. Add protocol profile primitives and unit tests for V1/V2 hash width, MAC + width, version, path validation, hop count, and hash formatting. +2. Make payload helpers parse and build V1 and V2 peer, anonymous, and group + shapes. +3. Make identity helpers provide one-byte and two-byte public-key prefixes. +4. Make packet receive, flood forwarding, direct forwarding, duplicate/self path + checks, timeout/hop estimates, and path return creation profile-aware. +5. Extend route cache and peer info to store route profile and peer hash bytes. +6. Add send profile selection with default `AutoPreferV2`. +7. Add forwarding policy with `MultibyteOnly` applying to flood repeat and direct + forwarding. +8. Make self-announcement/ADVERT frame generation profile-aware so default + device announcements no longer remain an accidental V1 island. +9. Extend platform backends that expose MeshCore raw/phone-facing sends. The + ESP adapter owns full V2 route/cache/forwarding semantics; the nRF52 radio + adapter must at least use explicit profile headers, V2 peer/anonymous + request hashes/MACs when pubkeys are available, and `sendRawDataEx` for + external V2 path reuse. +10. Extend settings persistence, UI settings, and backup/restore. +11. Extend BLE/phone path/contact protocol so V2 paths can be displayed and + reused. +12. Update docs and tests. + +## Burn-Down Criteria + +The issue is not complete until all of these are true: + +1. V2 packets with `2-byte hash + 4-byte MAC` are parsed, built, received, and + transmitted. +2. V2 flood paths append two-byte hashes and enforce both byte capacity and hop + flood max. +3. V2 direct paths match/remove two-byte leading hops. +4. V1 forwarding is blocked for both flood and direct paths when + `MultibyteOnly` is configured. +5. Route/path cache entries retain profile metadata and direct sends do not + reuse V1/V2 paths ambiguously. +6. `AutoPreferV2` is the default send preference. +7. BLE/phone protocol can display and reuse V2 paths correctly. +8. Existing V1 compatibility tests continue to pass. +9. New unit tests cover V1/V2 payload shape, routing path math, forwarding + policy, route cache profile separation, and BLE profile serialization. +10. The implementation has passed the repository's relevant build/test commands. + GitNexus `detect_changes` must run before any commit when the index is + available; if GitNexus is unavailable after an attempted refresh, record + that explicitly and proceed under the user-approved bypass. diff --git a/modules/core_chat/include/chat/domain/chat_types.h b/modules/core_chat/include/chat/domain/chat_types.h index 5d38cbc2..cf7c796a 100644 --- a/modules/core_chat/include/chat/domain/chat_types.h +++ b/modules/core_chat/include/chat/domain/chat_types.h @@ -378,6 +378,19 @@ struct MeshIncomingData /** * @brief Mesh configuration */ +enum class MeshCorePayloadSendProfile : uint8_t +{ + V1Only = 0, + AutoPreferV2 = 1, + V2Only = 2, +}; + +enum class MeshCoreForwardProfile : uint8_t +{ + Any = 0, + MultibyteOnly = 1, +}; + struct MeshConfig { // Meshtastic radio configuration @@ -419,6 +432,8 @@ struct MeshConfig float meshcore_airtime_factor; uint8_t meshcore_flood_max; bool meshcore_multi_acks; + MeshCorePayloadSendProfile meshcore_send_profile; + MeshCoreForwardProfile meshcore_forward_profile; uint8_t meshcore_channel_slot; char meshcore_channel_name[32]; @@ -453,6 +468,8 @@ struct MeshConfig meshcore_airtime_factor(1.0f), meshcore_flood_max(16), meshcore_multi_acks(false), + meshcore_send_profile(MeshCorePayloadSendProfile::AutoPreferV2), + meshcore_forward_profile(MeshCoreForwardProfile::MultibyteOnly), meshcore_channel_slot(0) { strncpy(primary_channel_name, "LongFast", sizeof(primary_channel_name) - 1); diff --git a/modules/core_chat/include/chat/infra/meshcore/meshcore_ble_backend.h b/modules/core_chat/include/chat/infra/meshcore/meshcore_ble_backend.h index 936065fe..cd2a066b 100644 --- a/modules/core_chat/include/chat/infra/meshcore/meshcore_ble_backend.h +++ b/modules/core_chat/include/chat/infra/meshcore/meshcore_ble_backend.h @@ -38,6 +38,13 @@ class IMeshCoreBleBackend virtual bool sendRawData(const uint8_t* path, size_t path_len, const uint8_t* payload, size_t payload_len, uint32_t* out_est_timeout) = 0; + virtual bool sendRawDataEx(uint8_t profile, const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout) + { + (void)profile; + return sendRawData(path, path_len, payload, payload_len, out_est_timeout); + } virtual void setFloodScopeKey(const uint8_t* key, size_t len) = 0; }; diff --git a/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h b/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h index e2b85f51..523ad12b 100644 --- a/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h +++ b/modules/core_chat/include/chat/infra/meshcore/meshcore_payload_helpers.h @@ -6,6 +6,7 @@ #pragma once #include "chat/domain/chat_types.h" +#include "chat/infra/meshcore/meshcore_protocol_helpers.h" #include #include @@ -128,15 +129,27 @@ void xorCrypt(uint8_t* data, size_t len, const uint8_t* key, size_t key_len); const uint8_t* selectChannelKey(const chat::MeshConfig& cfg, size_t* out_len); bool isPeerPayloadType(uint8_t payload_type); bool isPeerCipherShape(size_t payload_len); +bool isPeerCipherShape(PayloadProfile profile, size_t payload_len); bool isAnonReqCipherShape(size_t payload_len); +bool isAnonReqCipherShape(PayloadProfile profile, size_t payload_len); bool buildFrameNoTransport(uint8_t route_type, uint8_t payload_type, const uint8_t* path, size_t path_len, const uint8_t* payload, size_t payload_len, uint8_t* out_frame, size_t out_cap, size_t* out_len); +bool buildFrameNoTransport(PayloadProfile profile, + uint8_t route_type, uint8_t payload_type, + const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint8_t* out_frame, size_t out_cap, size_t* out_len); bool buildPeerDatagramPayload(uint8_t dest_hash, uint8_t src_hash, const uint8_t key16[16], const uint8_t key32[32], const uint8_t* plain, size_t plain_len, uint8_t* out_payload, size_t out_cap, size_t* out_len); +bool buildPeerDatagramPayload(PayloadProfile profile, + const uint8_t* dest_hash, const uint8_t* src_hash, + const uint8_t key16[16], const uint8_t key32[32], + const uint8_t* plain, size_t plain_len, + uint8_t* out_payload, size_t out_cap, size_t* out_len); bool decodeDirectAppPayload(const uint8_t* plain, size_t plain_len, DecodedDirectAppPayload* out); bool decodeGroupAppPayload(const uint8_t* plain, size_t plain_len, DecodedGroupAppPayload* out); bool hasControlPrefix(const uint8_t* payload, size_t len, uint8_t kind); diff --git a/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h b/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h index b2d0fe84..a863f0cf 100644 --- a/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h +++ b/modules/core_chat/include/chat/infra/meshcore/meshcore_protocol_helpers.h @@ -14,6 +14,20 @@ namespace chat namespace meshcore { +enum class PayloadProfile : uint8_t +{ + V1 = 0, + V2 = 1, +}; + +constexpr uint8_t kMeshCorePayloadVer1 = 0x00; +constexpr uint8_t kMeshCorePayloadVer2 = 0x01; +constexpr uint8_t kMeshCoreMaxPayloadVer = kMeshCorePayloadVer2; +constexpr size_t kMeshCoreV1HashBytes = 1; +constexpr size_t kMeshCoreV2HashBytes = 2; +constexpr size_t kMeshCoreV1CipherMacSize = 2; +constexpr size_t kMeshCoreV2CipherMacSize = 4; + struct ParsedPacket { uint8_t route_type = 0; @@ -46,6 +60,23 @@ struct DecodedTracePayload std::string toHex(const uint8_t* data, size_t len, size_t max_len = 128); uint8_t buildHeader(uint8_t route_type, uint8_t payload_type, uint8_t payload_ver); +PayloadProfile payloadProfileFromVersion(uint8_t payload_ver); +uint8_t payloadVersion(PayloadProfile profile); +bool isSupportedPayloadVersion(uint8_t payload_ver); +size_t payloadHashBytes(PayloadProfile profile); +size_t payloadMacBytes(PayloadProfile profile); +bool pathIsWellFormed(PayloadProfile profile, size_t path_len); +size_t pathHopCount(PayloadProfile profile, size_t path_len); +bool copyPublicHash(PayloadProfile profile, + const uint8_t* pubkey, + size_t pubkey_len, + uint8_t* out_hash, + size_t out_cap); +bool hashesEqual(PayloadProfile profile, + const uint8_t* lhs, + size_t lhs_len, + const uint8_t* rhs, + size_t rhs_len); bool parsePacket(const uint8_t* data, size_t len, ParsedPacket* out); size_t tracePathHashSize(uint8_t flags); bool isValidTracePathHashBytes(uint8_t flags, size_t path_hashes_len, size_t max_hops); @@ -70,13 +101,21 @@ void toHmacKey32(const uint8_t* key16, uint8_t* out_key32); void sharedSecretToKeys(const uint8_t* secret, uint8_t* out_key16, uint8_t* out_key32); void sha256Trunc(uint8_t* out_hash, size_t out_len, const uint8_t* msg, size_t msg_len); uint8_t computeChannelHash(const uint8_t* key16); +bool computeChannelHashBytes(const uint8_t* key16, uint8_t* out_hash, size_t hash_len); size_t aesEncrypt(const uint8_t* key16, uint8_t* dest, const uint8_t* src, size_t src_len); size_t aesDecrypt(const uint8_t* key16, uint8_t* dest, const uint8_t* src, size_t src_len); size_t encryptThenMac(const uint8_t* key16, const uint8_t* key32, uint8_t* out, size_t out_cap, const uint8_t* plain, size_t plain_len); +size_t encryptThenMac(const uint8_t* key16, const uint8_t* key32, + uint8_t* out, size_t out_cap, + const uint8_t* plain, size_t plain_len, + size_t mac_len); bool macThenDecrypt(const uint8_t* key16, const uint8_t* key32, const uint8_t* src, size_t src_len, uint8_t* out_plain, size_t* out_plain_len); +bool macThenDecrypt(const uint8_t* key16, const uint8_t* key32, + const uint8_t* src, size_t src_len, uint8_t* out_plain, + size_t* out_plain_len, size_t mac_len); size_t trimTrailingZeros(const uint8_t* buf, size_t len); } // namespace meshcore diff --git a/modules/core_chat/src/infra/meshcore/meshcore_payload_helpers.cpp b/modules/core_chat/src/infra/meshcore/meshcore_payload_helpers.cpp index d20d4e83..e5082a99 100644 --- a/modules/core_chat/src/infra/meshcore/meshcore_payload_helpers.cpp +++ b/modules/core_chat/src/infra/meshcore/meshcore_payload_helpers.cpp @@ -25,11 +25,9 @@ constexpr uint8_t kPayloadTypeGrpTxt = 0x05; constexpr uint8_t kPayloadTypeAnonReq = 0x07; constexpr uint8_t kPayloadTypeDirectData = kPayloadTypeAnonReq; constexpr uint8_t kPayloadTypePath = 0x08; -constexpr uint8_t kPayloadVer1 = 0x00; constexpr size_t kMeshcoreMaxPathSize = 64; constexpr size_t kMeshcoreMaxFrameSize = 255; constexpr size_t kCipherBlockSize = 16; -constexpr size_t kCipherMacSize = 2; constexpr size_t kMeshcorePubKeySize = 32; constexpr size_t kMeshcorePubKeyPrefixSize = 8; constexpr NodeId kSyntheticNodePrefix = 0x4D430000UL; @@ -172,21 +170,37 @@ bool isPeerPayloadType(uint8_t payload_type) bool isPeerCipherShape(size_t payload_len) { - if (payload_len <= (2 + kCipherMacSize)) + return isPeerCipherShape(PayloadProfile::V1, payload_len); +} + +bool isPeerCipherShape(PayloadProfile profile, size_t payload_len) +{ + const size_t hash_bytes = payloadHashBytes(profile); + const size_t mac_bytes = payloadMacBytes(profile); + const size_t prefix_len = hash_bytes * 2U; + if (payload_len <= (prefix_len + mac_bytes)) { return false; } - const size_t enc_len = payload_len - 2 - kCipherMacSize; + const size_t enc_len = payload_len - prefix_len - mac_bytes; return (enc_len % kCipherBlockSize) == 0; } bool isAnonReqCipherShape(size_t payload_len) { - if (payload_len <= (1 + kMeshcorePubKeySize + kCipherMacSize)) + return isAnonReqCipherShape(PayloadProfile::V1, payload_len); +} + +bool isAnonReqCipherShape(PayloadProfile profile, size_t payload_len) +{ + const size_t hash_bytes = payloadHashBytes(profile); + const size_t mac_bytes = payloadMacBytes(profile); + const size_t prefix_len = hash_bytes + kMeshcorePubKeySize; + if (payload_len <= (prefix_len + mac_bytes)) { return false; } - const size_t enc_len = payload_len - 1 - kMeshcorePubKeySize - kCipherMacSize; + const size_t enc_len = payload_len - prefix_len - mac_bytes; return (enc_len % kCipherBlockSize) == 0; } @@ -194,10 +208,24 @@ bool buildFrameNoTransport(uint8_t route_type, uint8_t payload_type, const uint8_t* path, size_t path_len, const uint8_t* payload, size_t payload_len, uint8_t* out_frame, size_t out_cap, size_t* out_len) +{ + return buildFrameNoTransport(PayloadProfile::V1, + route_type, payload_type, + path, path_len, + payload, payload_len, + out_frame, out_cap, out_len); +} + +bool buildFrameNoTransport(PayloadProfile profile, + uint8_t route_type, uint8_t payload_type, + const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint8_t* out_frame, size_t out_cap, size_t* out_len) { if (!out_frame || !out_len || !payload || payload_len == 0 || out_cap == 0 || path_len > kMeshcoreMaxPathSize || - (path_len > 0 && !path)) + (path_len > 0 && !path) || + !pathIsWellFormed(profile, path_len)) { return false; } @@ -211,7 +239,7 @@ bool buildFrameNoTransport(uint8_t route_type, uint8_t payload_type, } size_t index = 0; - out_frame[index++] = buildHeader(route_type, payload_type, kPayloadVer1); + out_frame[index++] = buildHeader(route_type, payload_type, payloadVersion(profile)); out_frame[index++] = static_cast(path_len); if (path_len > 0) { @@ -233,17 +261,36 @@ bool buildPeerDatagramPayload(uint8_t dest_hash, uint8_t src_hash, const uint8_t* plain, size_t plain_len, uint8_t* out_payload, size_t out_cap, size_t* out_len) { - if (!key16 || !key32 || !plain || plain_len == 0 || !out_payload || !out_len || out_cap < 3) + return buildPeerDatagramPayload(PayloadProfile::V1, + &dest_hash, &src_hash, + key16, key32, + plain, plain_len, + out_payload, out_cap, out_len); +} + +bool buildPeerDatagramPayload(PayloadProfile profile, + const uint8_t* dest_hash, const uint8_t* src_hash, + const uint8_t key16[16], const uint8_t key32[32], + const uint8_t* plain, size_t plain_len, + uint8_t* out_payload, size_t out_cap, size_t* out_len) +{ + const size_t hash_bytes = payloadHashBytes(profile); + const size_t prefix_len = hash_bytes * 2U; + if (!dest_hash || !src_hash || !key16 || !key32 || !plain || plain_len == 0 || + !out_payload || !out_len || out_cap <= prefix_len) { return false; } size_t index = 0; - out_payload[index++] = dest_hash; - out_payload[index++] = src_hash; + memcpy(&out_payload[index], dest_hash, hash_bytes); + index += hash_bytes; + memcpy(&out_payload[index], src_hash, hash_bytes); + index += hash_bytes; const size_t encrypted_len = encryptThenMac(key16, key32, &out_payload[index], out_cap - index, - plain, plain_len); + plain, plain_len, + payloadMacBytes(profile)); if (encrypted_len == 0) { return false; diff --git a/modules/core_chat/src/infra/meshcore/meshcore_protocol_helpers.cpp b/modules/core_chat/src/infra/meshcore/meshcore_protocol_helpers.cpp index 9a34ac08..66f52bd9 100644 --- a/modules/core_chat/src/infra/meshcore/meshcore_protocol_helpers.cpp +++ b/modules/core_chat/src/infra/meshcore/meshcore_protocol_helpers.cpp @@ -38,7 +38,6 @@ constexpr float kFloodSendTimeoutFactor = 16.0f; constexpr float kDirectSendPerhopFactor = 6.0f; constexpr uint32_t kDirectSendPerhopExtraMs = 250; constexpr size_t kCipherBlockSize = 16; -constexpr size_t kCipherMacSize = 2; constexpr size_t kCipherKeySize = 16; constexpr size_t kCipherHmacKeySize = 32; @@ -313,6 +312,76 @@ uint8_t buildHeader(uint8_t route_type, uint8_t payload_type, uint8_t payload_ve return (route_type & 0x03) | ((payload_type & 0x0F) << 2) | ((payload_ver & 0x03) << 6); } +PayloadProfile payloadProfileFromVersion(uint8_t payload_ver) +{ + return payload_ver == kMeshCorePayloadVer2 ? PayloadProfile::V2 : PayloadProfile::V1; +} + +uint8_t payloadVersion(PayloadProfile profile) +{ + return profile == PayloadProfile::V2 ? kMeshCorePayloadVer2 : kMeshCorePayloadVer1; +} + +bool isSupportedPayloadVersion(uint8_t payload_ver) +{ + return payload_ver == kMeshCorePayloadVer1 || payload_ver == kMeshCorePayloadVer2; +} + +size_t payloadHashBytes(PayloadProfile profile) +{ + return profile == PayloadProfile::V2 ? kMeshCoreV2HashBytes : kMeshCoreV1HashBytes; +} + +size_t payloadMacBytes(PayloadProfile profile) +{ + return profile == PayloadProfile::V2 ? kMeshCoreV2CipherMacSize : kMeshCoreV1CipherMacSize; +} + +bool pathIsWellFormed(PayloadProfile profile, size_t path_len) +{ + const size_t hash_bytes = payloadHashBytes(profile); + return hash_bytes != 0 && (path_len % hash_bytes) == 0; +} + +size_t pathHopCount(PayloadProfile profile, size_t path_len) +{ + const size_t hash_bytes = payloadHashBytes(profile); + if (hash_bytes == 0 || (path_len % hash_bytes) != 0) + { + return 0; + } + return path_len / hash_bytes; +} + +bool copyPublicHash(PayloadProfile profile, + const uint8_t* pubkey, + size_t pubkey_len, + uint8_t* out_hash, + size_t out_cap) +{ + const size_t hash_bytes = payloadHashBytes(profile); + if (!pubkey || pubkey_len < hash_bytes || !out_hash || out_cap < hash_bytes) + { + return false; + } + memcpy(out_hash, pubkey, hash_bytes); + return true; +} + +bool hashesEqual(PayloadProfile profile, + const uint8_t* lhs, + size_t lhs_len, + const uint8_t* rhs, + size_t rhs_len) +{ + const size_t hash_bytes = payloadHashBytes(profile); + if (!lhs || !rhs || lhs_len < hash_bytes || rhs_len < hash_bytes) + { + return false; + } + return memcmp(lhs, rhs, hash_bytes) == 0; +} + bool parsePacket(const uint8_t* data, size_t len, ParsedPacket* out) { if (!data || len < 2 || !out) @@ -677,14 +746,20 @@ void sha256Trunc(uint8_t* out_hash, size_t out_len, const uint8_t* msg, size_t m uint8_t computeChannelHash(const uint8_t* key16) { uint8_t hash = 0; - if (!key16) - { - return hash; - } - sha256Trunc(&hash, 1, key16, kCipherKeySize); + computeChannelHashBytes(key16, &hash, 1); return hash; } +bool computeChannelHashBytes(const uint8_t* key16, uint8_t* out_hash, size_t hash_len) +{ + if (!key16 || !out_hash || hash_len == 0 || hash_len > 32) + { + return false; + } + sha256Trunc(out_hash, hash_len, key16, kCipherKeySize); + return true; +} + size_t aesEncrypt(const uint8_t* key16, uint8_t* dest, const uint8_t* src, size_t src_len) { if (!key16 || !dest || !src || src_len == 0) @@ -782,19 +857,28 @@ size_t encryptThenMac(const uint8_t* key16, const uint8_t* key32, uint8_t* out, size_t out_cap, const uint8_t* plain, size_t plain_len) { - if (!key16 || !key32 || !out || !plain || plain_len == 0 || out_cap <= kCipherMacSize) + return encryptThenMac(key16, key32, out, out_cap, plain, plain_len, kMeshCoreV1CipherMacSize); +} + +size_t encryptThenMac(const uint8_t* key16, const uint8_t* key32, + uint8_t* out, size_t out_cap, + const uint8_t* plain, size_t plain_len, + size_t mac_len) +{ + if (!key16 || !key32 || !out || !plain || plain_len == 0 || + mac_len == 0 || mac_len > 32 || out_cap <= mac_len) { return 0; } - size_t max_cipher = out_cap - kCipherMacSize; + size_t max_cipher = out_cap - mac_len; size_t cipher_len = ((plain_len + (kCipherBlockSize - 1)) / kCipherBlockSize) * kCipherBlockSize; if (cipher_len == 0 || cipher_len > max_cipher) { return 0; } - size_t enc_len = aesEncrypt(key16, out + kCipherMacSize, plain, plain_len); + size_t enc_len = aesEncrypt(key16, out + mac_len, plain, plain_len); if (enc_len != cipher_len) { return 0; @@ -802,57 +886,65 @@ size_t encryptThenMac(const uint8_t* key16, const uint8_t* key32, #if defined(ESP_PLATFORM) || defined(TRAIL_MATE_HAS_OPENSSL) uint8_t full_hash[32] = {}; - if (!hmacSha256(key32, kCipherHmacKeySize, out + kCipherMacSize, enc_len, full_hash)) + if (!hmacSha256(key32, kCipherHmacKeySize, out + mac_len, enc_len, full_hash)) { return 0; } - memcpy(out, full_hash, kCipherMacSize); + memcpy(out, full_hash, mac_len); #elif defined(ARDUINO) SHA256 sha; sha.resetHMAC(key32, kCipherHmacKeySize); - sha.update(out + kCipherMacSize, enc_len); - sha.finalizeHMAC(key32, kCipherHmacKeySize, out, kCipherMacSize); + sha.update(out + mac_len, enc_len); + sha.finalizeHMAC(key32, kCipherHmacKeySize, out, mac_len); #else return 0; #endif - return kCipherMacSize + enc_len; + return mac_len + enc_len; } bool macThenDecrypt(const uint8_t* key16, const uint8_t* key32, const uint8_t* src, size_t src_len, uint8_t* out_plain, size_t* out_plain_len) { - if (!key16 || !key32 || !src || src_len <= kCipherMacSize || !out_plain || !out_plain_len) + return macThenDecrypt(key16, key32, src, src_len, out_plain, out_plain_len, kMeshCoreV1CipherMacSize); +} + +bool macThenDecrypt(const uint8_t* key16, const uint8_t* key32, + const uint8_t* src, size_t src_len, uint8_t* out_plain, + size_t* out_plain_len, size_t mac_len) +{ + if (!key16 || !key32 || !src || src_len <= mac_len || !out_plain || !out_plain_len || + mac_len == 0 || mac_len > 32) { return false; } - size_t cipher_len = src_len - kCipherMacSize; + size_t cipher_len = src_len - mac_len; if ((cipher_len % kCipherBlockSize) != 0) { return false; } - uint8_t expected[kCipherMacSize]; + uint8_t expected[32] = {}; #if defined(ESP_PLATFORM) || defined(TRAIL_MATE_HAS_OPENSSL) uint8_t full_hash[32] = {}; - if (!hmacSha256(key32, kCipherHmacKeySize, src + kCipherMacSize, cipher_len, full_hash)) + if (!hmacSha256(key32, kCipherHmacKeySize, src + mac_len, cipher_len, full_hash)) { return false; } - memcpy(expected, full_hash, sizeof(expected)); + memcpy(expected, full_hash, mac_len); #elif defined(ARDUINO) SHA256 sha; sha.resetHMAC(key32, kCipherHmacKeySize); - sha.update(src + kCipherMacSize, cipher_len); - sha.finalizeHMAC(key32, kCipherHmacKeySize, expected, kCipherMacSize); + sha.update(src + mac_len, cipher_len); + sha.finalizeHMAC(key32, kCipherHmacKeySize, expected, mac_len); #else return false; #endif - if (memcmp(expected, src, kCipherMacSize) != 0) + if (memcmp(expected, src, mac_len) != 0) { return false; } - size_t plain_len = aesDecrypt(key16, out_plain, src + kCipherMacSize, cipher_len); + size_t plain_len = aesDecrypt(key16, out_plain, src + mac_len, cipher_len); if (plain_len == 0) { return false; diff --git a/modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp b/modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp index 82252b3c..800d3b1b 100644 --- a/modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp +++ b/modules/core_chat/src/runtime/meshcore_self_announcement_core.cpp @@ -2,6 +2,7 @@ #include "chat/infra/meshcore/meshcore_identity_crypto.h" #include "chat/infra/meshcore/meshcore_payload_helpers.h" +#include "chat/infra/meshcore/meshcore_protocol_helpers.h" #include #include @@ -19,6 +20,13 @@ constexpr uint8_t kAdvertTypeRepeater = 0x02; constexpr uint8_t kAdvertFlagHasLocation = 0x10; constexpr uint8_t kAdvertFlagHasName = 0x80; +chat::meshcore::PayloadProfile selectPayloadProfile(const MeshConfig& config) +{ + return config.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only + ? chat::meshcore::PayloadProfile::V1 + : chat::meshcore::PayloadProfile::V2; +} + } // namespace bool MeshCoreSelfAnnouncementCore::buildAdvertPacket(const MeshCoreAnnouncementRequest& request, @@ -115,7 +123,9 @@ bool MeshCoreSelfAnnouncementCore::buildAdvertPacket(const MeshCoreAnnouncementR } size_t frame_size = sizeof(out_packet->frame); - if (!chat::meshcore::buildFrameNoTransport(request.broadcast ? kRouteTypeFlood : kRouteTypeDirect, + const chat::meshcore::PayloadProfile profile = selectPayloadProfile(request.mesh_config); + if (!chat::meshcore::buildFrameNoTransport(profile, + request.broadcast ? kRouteTypeFlood : kRouteTypeDirect, kPayloadTypeAdvert, nullptr, 0, diff --git a/modules/core_mesh/include/mesh/protocol/mesh_protocol_strategy.h b/modules/core_mesh/include/mesh/protocol/mesh_protocol_strategy.h index 8c243596..874693a4 100644 --- a/modules/core_mesh/include/mesh/protocol/mesh_protocol_strategy.h +++ b/modules/core_mesh/include/mesh/protocol/mesh_protocol_strategy.h @@ -21,6 +21,10 @@ struct ProtocolBuildContext uint32_t now_ms = 0; uint32_t packet_id = 0; uint8_t channel_hash = 0; + uint8_t meshcore_payload_ver = 0; + ByteView meshcore_local_hash{}; + ByteView meshcore_peer_hash{}; + ByteView meshcore_channel_hash{}; uint8_t hop_limit = 2; uint8_t route_type = 0; ByteView route_path{}; diff --git a/modules/core_mesh/src/protocol/meshcore/meshcore_protocol_strategy.cpp b/modules/core_mesh/src/protocol/meshcore/meshcore_protocol_strategy.cpp index ffee9c17..fbdb24ea 100644 --- a/modules/core_mesh/src/protocol/meshcore/meshcore_protocol_strategy.cpp +++ b/modules/core_mesh/src/protocol/meshcore/meshcore_protocol_strategy.cpp @@ -21,14 +21,12 @@ constexpr uint8_t kRouteTypeFlood = 0x01; constexpr uint8_t kPayloadTypeAdvert = 0x04; constexpr uint8_t kPayloadTypeGrpData = 0x06; constexpr uint8_t kPayloadTypeDirectData = 0x07; -constexpr uint8_t kPayloadVer1 = 0x00; constexpr uint8_t kDirectAppMagic0 = 0xDA; constexpr uint8_t kDirectAppMagic1 = 0x7A; constexpr uint8_t kDirectAppFlagWantAck = 0x01; constexpr uint8_t kGroupDataMagic0 = 0x47; constexpr uint8_t kGroupDataMagic1 = 0x44; constexpr uint8_t kLoraSyncWordPrivate = 0x12; -constexpr size_t kCipherMacSize = 2; constexpr size_t kCipherBlockSize = 16; constexpr size_t kMeshCoreMaxFrameSize = 255; constexpr size_t kMeshCoreMaxPayloadSize = 184; @@ -169,6 +167,10 @@ ProtocolResult MeshCoreProtocolStrategy::buildDirectMessage( uint8_t key16[16] = {}; uint8_t key32[32] = {}; + const chat::meshcore::PayloadProfile profile = + chat::meshcore::payloadProfileFromVersion(context.meshcore_payload_ver); + const size_t hash_bytes = chat::meshcore::payloadHashBytes(profile); + const size_t mac_len = chat::meshcore::payloadMacBytes(profile); if (!group_packet) { @@ -185,32 +187,51 @@ ProtocolResult MeshCoreProtocolStrategy::buildDirectMessage( } constexpr size_t kDirectPlainPrefix = 2 + 1 + sizeof(command.application_port); - constexpr size_t kDirectCipherBudget = - ((kMeshCoreMaxPayloadSize - 2 - kCipherMacSize) / kCipherBlockSize) * kCipherBlockSize; - uint8_t plain[kDirectCipherBudget] = {}; + const size_t kDirectCipherBudget = + ((kMeshCoreMaxPayloadSize - (hash_bytes * 2U) - mac_len) / kCipherBlockSize) * + kCipherBlockSize; + uint8_t plain[kMeshCoreMaxPayloadSize] = {}; size_t plain_len = 0; plain[0] = kDirectAppMagic0; plain[1] = kDirectAppMagic1; plain[2] = command.request_ack ? kDirectAppFlagWantAck : 0x00; std::memcpy(plain + 3, &command.application_port, sizeof(command.application_port)); - if (!copyPayload(command, plain, sizeof(plain), kDirectPlainPrefix, plain_len)) + if (!copyPayload(command, plain, kDirectCipherBudget, kDirectPlainPrefix, plain_len)) { return ProtocolResult::fail(ProtocolFailure::EncodeFailed); } uint8_t payload[kMeshCoreMaxPayloadSize] = {}; size_t payload_len = 0; - const uint8_t dest_hash = lowNodeHash(command.to); - const uint8_t src_hash = lowNodeHash(context.local_node); - if (src_hash == 0 || !chat::meshcore::buildPeerDatagramPayload(dest_hash, - src_hash, - key16, - key32, - plain, - plain_len, - payload, - sizeof(payload), - &payload_len)) + uint8_t dest_hash[chat::meshcore::kMeshCoreV2HashBytes] = {}; + uint8_t src_hash[chat::meshcore::kMeshCoreV2HashBytes] = {}; + bool hashes_ok = false; + if (profile == chat::meshcore::PayloadProfile::V1) + { + dest_hash[0] = lowNodeHash(command.to); + src_hash[0] = lowNodeHash(context.local_node); + hashes_ok = src_hash[0] != 0; + } + else if (context.meshcore_peer_hash.data && + context.meshcore_local_hash.data && + context.meshcore_peer_hash.size >= hash_bytes && + context.meshcore_local_hash.size >= hash_bytes) + { + std::memcpy(dest_hash, context.meshcore_peer_hash.data, hash_bytes); + std::memcpy(src_hash, context.meshcore_local_hash.data, hash_bytes); + hashes_ok = true; + } + if (!hashes_ok || + !chat::meshcore::buildPeerDatagramPayload(profile, + dest_hash, + src_hash, + key16, + key32, + plain, + plain_len, + payload, + sizeof(payload), + &payload_len)) { return ProtocolResult::fail(ProtocolFailure::EncodeFailed); } @@ -218,7 +239,8 @@ ProtocolResult MeshCoreProtocolStrategy::buildDirectMessage( const uint8_t route_type = context.route_type != 0 ? context.route_type : kRouteTypeFlood; const uint8_t* route_path = context.route_path.empty() ? nullptr : context.route_path.data; const size_t route_path_len = context.route_path.empty() ? 0 : context.route_path.size; - if (!chat::meshcore::buildFrameNoTransport(route_type, + if (!chat::meshcore::buildFrameNoTransport(profile, + route_type, kPayloadTypeDirectData, route_path, route_path_len, @@ -247,9 +269,9 @@ ProtocolResult MeshCoreProtocolStrategy::buildDirectMessage( constexpr size_t kGroupPlainPrefix = 2 + sizeof(context.local_node.value) + sizeof(command.application_port); - constexpr size_t kGroupCipherBudget = - ((kMeshCoreMaxPayloadSize - 1 - kCipherMacSize) / kCipherBlockSize) * kCipherBlockSize; - uint8_t plain[kGroupCipherBudget] = {}; + const size_t kGroupCipherBudget = + ((kMeshCoreMaxPayloadSize - hash_bytes - mac_len) / kCipherBlockSize) * kCipherBlockSize; + uint8_t plain[kMeshCoreMaxPayloadSize] = {}; size_t plain_len = 0; plain[0] = kGroupDataMagic0; plain[1] = kGroupDataMagic1; @@ -257,7 +279,7 @@ ProtocolResult MeshCoreProtocolStrategy::buildDirectMessage( std::memcpy(plain + 2 + sizeof(context.local_node.value), &command.application_port, sizeof(command.application_port)); - if (!copyPayload(command, plain, sizeof(plain), kGroupPlainPrefix, plain_len)) + if (!copyPayload(command, plain, kGroupCipherBudget, kGroupPlainPrefix, plain_len)) { return ProtocolResult::fail(ProtocolFailure::EncodeFailed); } @@ -268,21 +290,36 @@ ProtocolResult MeshCoreProtocolStrategy::buildDirectMessage( encrypted, sizeof(encrypted), plain, - plain_len); - if (encrypted_len == 0 || encrypted_len > (kMeshCoreMaxPayloadSize - 1)) + plain_len, + mac_len); + if (encrypted_len == 0 || encrypted_len > (kMeshCoreMaxPayloadSize - hash_bytes)) { return ProtocolResult::fail(ProtocolFailure::CryptoFailed); } uint8_t payload[kMeshCoreMaxPayloadSize] = {}; size_t payload_len = 0; - payload[payload_len++] = context.channel_hash != 0 - ? context.channel_hash - : chat::meshcore::computeChannelHash(key16); + uint8_t channel_hash[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (context.meshcore_channel_hash.data && + context.meshcore_channel_hash.size >= hash_bytes) + { + std::memcpy(channel_hash, context.meshcore_channel_hash.data, hash_bytes); + } + else if (profile == chat::meshcore::PayloadProfile::V1 && context.channel_hash != 0) + { + channel_hash[0] = context.channel_hash; + } + else if (!chat::meshcore::computeChannelHashBytes(key16, channel_hash, hash_bytes)) + { + return ProtocolResult::fail(ProtocolFailure::CryptoFailed); + } + std::memcpy(payload + payload_len, channel_hash, hash_bytes); + payload_len += hash_bytes; std::memcpy(payload + payload_len, encrypted, encrypted_len); payload_len += encrypted_len; - if (!chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + if (!chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeFlood, kPayloadTypeGrpData, nullptr, 0, @@ -309,11 +346,15 @@ ProtocolResult MeshCoreProtocolStrategy::parseRadioPacket(const RadioRxPacket& p chat::meshcore::ParsedPacket parsed; if (!chat::meshcore::parsePacket(packet.bytes, packet.size, &parsed) || - parsed.payload_ver != kPayloadVer1 || + !chat::meshcore::isSupportedPayloadVersion(parsed.payload_ver) || !parsed.payload || parsed.payload_len == 0) { return ProtocolResult::fail(ProtocolFailure::DecodeFailed); } + const chat::meshcore::PayloadProfile profile = + chat::meshcore::payloadProfileFromVersion(parsed.payload_ver); + const size_t hash_bytes = chat::meshcore::payloadHashBytes(profile); + const size_t mac_len = chat::meshcore::payloadMacBytes(profile); if (parsed.payload_type == kPayloadTypeAdvert && parsed.payload_len >= kMeshCorePubKeySize) { @@ -386,22 +427,23 @@ ProtocolResult MeshCoreProtocolStrategy::parseRadioPacket(const RadioRxPacket& p if (parsed.payload_type == kPayloadTypeDirectData) { - if (!has_direct_secret_ || parsed.payload_len <= (2 + kCipherMacSize)) + if (!has_direct_secret_ || parsed.payload_len <= ((hash_bytes * 2U) + mac_len)) { return ProtocolResult::fail(ProtocolFailure::MissingPeerKey); } - const uint8_t dest_hash = parsed.payload[0]; - const uint8_t src_hash = parsed.payload[1]; - if (local_public_hash_ != 0 && dest_hash != local_public_hash_) + const uint8_t* dest_hash = parsed.payload; + const uint8_t* src_hash = parsed.payload + hash_bytes; + if (local_public_hash_ != 0 && dest_hash[0] != local_public_hash_) { return ProtocolResult::fail(ProtocolFailure::InvalidInput); } if (!chat::meshcore::macThenDecrypt(direct_key16_, direct_key32_, - parsed.payload + 2, - parsed.payload_len - 2, + parsed.payload + (hash_bytes * 2U), + parsed.payload_len - (hash_bytes * 2U), plain, - &plain_len)) + &plain_len, + mac_len)) { return ProtocolResult::fail(ProtocolFailure::CryptoFailed); } @@ -414,7 +456,7 @@ ProtocolResult MeshCoreProtocolStrategy::parseRadioPacket(const RadioRxPacket& p } out.kind = MeshProtocolEventKind::MessageReceived; - out.peer = NodeId{src_hash}; + out.peer = NodeId{src_hash[0]}; out.packet_id = chat::meshcore::packetSignature(parsed.payload_type, parsed.path_len, parsed.payload, @@ -425,16 +467,31 @@ ProtocolResult MeshCoreProtocolStrategy::parseRadioPacket(const RadioRxPacket& p if (parsed.payload_type == kPayloadTypeGrpData) { - if (parsed.payload_len <= (1 + kCipherMacSize)) + if (parsed.payload_len <= (hash_bytes + mac_len)) { return ProtocolResult::fail(ProtocolFailure::DecodeFailed); } - const uint8_t channel_hash = parsed.payload[0]; + const uint8_t* channel_hash = parsed.payload; uint8_t rx_key16[16] = {}; uint8_t rx_key32[32] = {}; const uint8_t* decrypt_key16 = group_key16_; const uint8_t* decrypt_key32 = group_key32_; - uint8_t expected_hash = group_channel_hash_; + uint8_t expected_hash[chat::meshcore::kMeshCoreV2HashBytes] = {}; + bool has_expected_hash = false; + if (has_group_key_) + { + if (profile == chat::meshcore::PayloadProfile::V1 && group_channel_hash_ != 0) + { + expected_hash[0] = group_channel_hash_; + has_expected_hash = true; + } + else + { + has_expected_hash = chat::meshcore::computeChannelHashBytes(group_key16_, + expected_hash, + hash_bytes); + } + } if (!has_group_key_) { if (!mapSecretToKeys(ByteView{chat::meshcore::publicGroupPsk(), @@ -446,9 +503,11 @@ ProtocolResult MeshCoreProtocolStrategy::parseRadioPacket(const RadioRxPacket& p } decrypt_key16 = rx_key16; decrypt_key32 = rx_key32; - expected_hash = chat::meshcore::computeChannelHash(rx_key16); + has_expected_hash = chat::meshcore::computeChannelHashBytes(rx_key16, + expected_hash, + hash_bytes); } - if (expected_hash != 0 && channel_hash != expected_hash) + if (has_expected_hash && memcmp(channel_hash, expected_hash, hash_bytes) != 0) { return ProtocolResult::fail(has_group_key_ ? ProtocolFailure::InvalidInput @@ -456,10 +515,11 @@ ProtocolResult MeshCoreProtocolStrategy::parseRadioPacket(const RadioRxPacket& p } if (!chat::meshcore::macThenDecrypt(decrypt_key16, decrypt_key32, - parsed.payload + 1, - parsed.payload_len - 1, + parsed.payload + hash_bytes, + parsed.payload_len - hash_bytes, plain, - &plain_len)) + &plain_len, + mac_len)) { return ProtocolResult::fail(ProtocolFailure::CryptoFailed); } diff --git a/modules/core_mesh/tests/test_meshcore_protocol_strategy.cpp b/modules/core_mesh/tests/test_meshcore_protocol_strategy.cpp index 861ced43..b0baed4d 100644 --- a/modules/core_mesh/tests/test_meshcore_protocol_strategy.cpp +++ b/modules/core_mesh/tests/test_meshcore_protocol_strategy.cpp @@ -79,6 +79,37 @@ int main() assert(event.payload.size == sizeof(text) - 1); assert(std::memcmp(event.payload.data, text, sizeof(text) - 1) == 0); + uint8_t v2_dest_hash[2] = {0x44, 0x33}; + uint8_t v2_src_hash[2] = {0x22, 0x11}; + context.meshcore_payload_ver = chat::meshcore::kMeshCorePayloadVer2; + context.meshcore_peer_hash = mesh::ByteView{v2_dest_hash, sizeof(v2_dest_hash)}; + context.meshcore_local_hash = mesh::ByteView{v2_src_hash, sizeof(v2_src_hash)}; + auto v2_direct_built = strategy.buildDirectMessage(context, command, encoded); + assert(v2_direct_built.ok); + chat::meshcore::ParsedPacket parsed_v2_direct{}; + assert(chat::meshcore::parsePacket(encoded.bytes, encoded.size, &parsed_v2_direct)); + assert(parsed_v2_direct.payload_ver == chat::meshcore::kMeshCorePayloadVer2); + assert(parsed_v2_direct.payload_len > 4); + assert(std::memcmp(parsed_v2_direct.payload, v2_dest_hash, sizeof(v2_dest_hash)) == 0); + assert(std::memcmp(parsed_v2_direct.payload + sizeof(v2_dest_hash), + v2_src_hash, + sizeof(v2_src_hash)) == 0); + + packet = mesh::RadioRxPacket{}; + std::memcpy(packet.bytes, encoded.bytes, encoded.size); + packet.size = encoded.size; + event = mesh::MeshProtocolEvent{}; + auto v2_direct_parsed = receiver.parseRadioPacket(packet, event); + assert(v2_direct_parsed.ok); + assert(event.kind == mesh::MeshProtocolEventKind::MessageReceived); + assert(event.peer == mesh::NodeId{0x22}); + assert(event.payload.size == sizeof(text) - 1); + assert(std::memcmp(event.payload.data, text, sizeof(text) - 1) == 0); + + context.meshcore_payload_ver = chat::meshcore::kMeshCorePayloadVer1; + context.meshcore_peer_hash = mesh::ByteView{}; + context.meshcore_local_hash = mesh::ByteView{}; + uint8_t group_key[16] = {}; for (size_t index = 0; index < sizeof(group_key); ++index) { @@ -132,6 +163,37 @@ int main() assert(event.payload.size == sizeof(text) - 1); assert(std::memcmp(event.payload.data, text, sizeof(text) - 1) == 0); + context.meshcore_payload_ver = chat::meshcore::kMeshCorePayloadVer2; + context.meshcore_channel_hash = mesh::ByteView{}; + auto v2_group_built = strategy.buildDirectMessage(context, group_command, encoded); + assert(v2_group_built.ok); + chat::meshcore::ParsedPacket parsed_v2_group{}; + assert(chat::meshcore::parsePacket(encoded.bytes, encoded.size, &parsed_v2_group)); + assert(parsed_v2_group.payload_ver == chat::meshcore::kMeshCorePayloadVer2); + assert(parsed_v2_group.payload_len > (chat::meshcore::kMeshCoreV2HashBytes + + chat::meshcore::kMeshCoreV2CipherMacSize)); + uint8_t expected_group_hash[2] = {}; + assert(chat::meshcore::computeChannelHashBytes(group_key, + expected_group_hash, + sizeof(expected_group_hash))); + assert(std::memcmp(parsed_v2_group.payload, + expected_group_hash, + sizeof(expected_group_hash)) == 0); + + packet = mesh::RadioRxPacket{}; + std::memcpy(packet.bytes, encoded.bytes, encoded.size); + packet.size = encoded.size; + event = mesh::MeshProtocolEvent{}; + auto v2_group_parsed = receiver.parseRadioPacket(packet, event); + assert(v2_group_parsed.ok); + assert(event.kind == mesh::MeshProtocolEventKind::MessageReceived); + assert(event.peer == context.local_node); + assert(event.payload.size == sizeof(text) - 1); + assert(std::memcmp(event.payload.data, text, sizeof(text) - 1) == 0); + + context.meshcore_payload_ver = chat::meshcore::kMeshCorePayloadVer1; + context.meshcore_channel_hash = mesh::ByteView{}; + mesh::meshcore::McIdentityFlow identity; uint8_t seed[mesh::meshcore::kMeshCoreSeedSize] = {}; uint8_t public_key[mesh::meshcore::kMeshCorePublicKeySize] = {}; diff --git a/modules/core_phone/include/phone/common/phone_app_facade.h b/modules/core_phone/include/phone/common/phone_app_facade.h index d21989fb..1b74cf17 100644 --- a/modules/core_phone/include/phone/common/phone_app_facade.h +++ b/modules/core_phone/include/phone/common/phone_app_facade.h @@ -148,6 +148,16 @@ class IPhoneAppFacade const uint8_t* payload, std::size_t payload_len, uint32_t* out_est_timeout) = 0; + virtual bool meshCoreSendRawDataEx(uint8_t profile, + const uint8_t* path, + std::size_t path_len, + const uint8_t* payload, + std::size_t payload_len, + uint32_t* out_est_timeout) + { + (void)profile; + return meshCoreSendRawData(path, path_len, payload, payload_len, out_est_timeout); + } virtual void meshCoreSetFloodScopeKey(const uint8_t* key, std::size_t len) = 0; }; diff --git a/modules/core_phone/include/phone/meshcore/meshcore_phone_core.h b/modules/core_phone/include/phone/meshcore/meshcore_phone_core.h index fc691a71..469d5100 100644 --- a/modules/core_phone/include/phone/meshcore/meshcore_phone_core.h +++ b/modules/core_phone/include/phone/meshcore/meshcore_phone_core.h @@ -63,6 +63,21 @@ struct MeshCorePhoneTuningParams uint32_t airtime_factor_milli = 0; }; +struct MeshCorePhonePathMetadata +{ + uint8_t profile = 0; + uint8_t hash_bytes = 1; + uint8_t hop_count = 0; +}; + +struct MeshCorePhoneAdvertPath +{ + uint32_t timestamp = 0; + uint8_t path_len = 0; + uint8_t path[64] = {}; + MeshCorePhonePathMetadata meta{}; +}; + class MeshCorePhoneHooks { public: @@ -96,6 +111,26 @@ class MeshCorePhoneHooks virtual void onPendingPathDiscoveryRequest(uint32_t) {} virtual void onSentRoute(bool) {} virtual bool lookupAdvertPath(const uint8_t*, size_t, uint32_t*, uint8_t*, size_t*) const { return false; } + virtual bool lookupAdvertPathEx(const uint8_t* pubkey, size_t len, + MeshCorePhoneAdvertPath* out) const + { + if (!out) + { + return false; + } + uint32_t ts = 0; + size_t path_len = sizeof(out->path); + if (!lookupAdvertPath(pubkey, len, &ts, out->path, &path_len)) + { + return false; + } + out->timestamp = ts; + out->path_len = static_cast(path_len > sizeof(out->path) ? sizeof(out->path) : path_len); + out->meta.profile = 0; + out->meta.hash_bytes = 1; + out->meta.hop_count = out->path_len; + return true; + } virtual bool hasActiveConnection(const uint8_t*, size_t) const { return false; } virtual void logoutActiveConnection(const uint8_t*, size_t) {} virtual bool getRadioStats(MeshCorePhoneRadioStats*) const { return false; } diff --git a/modules/core_phone/src/meshcore/meshcore_phone_core.cpp b/modules/core_phone/src/meshcore/meshcore_phone_core.cpp index e49e9817..b8433e2d 100644 --- a/modules/core_phone/src/meshcore/meshcore_phone_core.cpp +++ b/modules/core_phone/src/meshcore/meshcore_phone_core.cpp @@ -105,10 +105,27 @@ constexpr size_t kPrefixSize = 6; constexpr size_t kMaxFrameSize = 172; constexpr size_t kPubKeySize = chat::meshcore::kMeshCorePubKeySize; constexpr size_t kMaxPathSize = 64; +constexpr uint8_t kPathMetadataExtMarker = 0xE2; +constexpr uint8_t kPathMetadataExtLen = 3; constexpr uint8_t STATS_TYPE_CORE = 0; constexpr uint8_t STATS_TYPE_RADIO = 1; constexpr uint8_t STATS_TYPE_PACKETS = 2; +bool appendPathMetadataExt(uint8_t* out, size_t out_cap, size_t& index, + const MeshCorePhonePathMetadata& meta) +{ + if (!out || index + 2 + kPathMetadataExtLen > out_cap) + { + return false; + } + out[index++] = kPathMetadataExtMarker; + out[index++] = kPathMetadataExtLen; + out[index++] = meta.profile; + out[index++] = meta.hash_bytes == 0 ? 1 : meta.hash_bytes; + out[index++] = meta.hop_count; + return true; +} + void copyBounded(char* dst, size_t dst_len, const char* src) { if (!dst || dst_len == 0) @@ -989,8 +1006,31 @@ void MeshCorePhoneCore::handleCmdFrame(const uint8_t* data, size_t len) if (cmd == CMD_SEND_RAW_DATA && len >= 6) { size_t index = 1; - const int8_t path_len = static_cast(data[index++]); - if (path_len < 0 || (index + static_cast(path_len) + 4U) > len) + uint8_t profile = 0; + uint8_t path_len = 0; + if (data[index] == 0x80) + { + if (len < 1 + 1 + 1 + 1 + 4) + { + enqueueErr(ERR_CODE_UNSUPPORTED_CMD); + return; + } + ++index; + profile = data[index++]; + path_len = data[index++]; + } + else + { + const int8_t legacy_path_len = static_cast(data[index++]); + if (legacy_path_len < 0) + { + enqueueErr(ERR_CODE_UNSUPPORTED_CMD); + return; + } + path_len = static_cast(legacy_path_len); + } + const uint8_t hash_bytes = profile == 1 ? 2 : 1; + if ((path_len % hash_bytes) != 0 || (index + static_cast(path_len) + 4U) > len) { enqueueErr(ERR_CODE_UNSUPPORTED_CMD); return; @@ -1000,7 +1040,8 @@ void MeshCorePhoneCore::handleCmdFrame(const uint8_t* data, size_t len) const uint8_t* payload = &data[index]; const size_t payload_len = len - index; uint32_t est_timeout = 0; - if (!app_.meshCoreSendRawData(path, static_cast(path_len), payload, payload_len, &est_timeout)) + if (!app_.meshCoreSendRawDataEx(profile, path, static_cast(path_len), + payload, payload_len, &est_timeout)) { enqueueErr(ERR_CODE_TABLE_FULL); return; @@ -1172,14 +1213,29 @@ void MeshCorePhoneCore::handleCmdFrame(const uint8_t* data, size_t len) uint32_t ts = 0; uint8_t path_len = 0; uint8_t path[kMaxPathSize] = {}; + MeshCorePhonePathMetadata path_meta{}; + path_meta.profile = 0; + path_meta.hash_bytes = 1; bool found = false; if (hooks_) { - size_t lookup_len = kMaxPathSize; - found = hooks_->lookupAdvertPath(pubkey, kPubKeySize, &ts, path, &lookup_len); + MeshCorePhoneAdvertPath advert_path{}; + found = hooks_->lookupAdvertPathEx(pubkey, kPubKeySize, &advert_path); if (found) { - path_len = static_cast(std::min(lookup_len, static_cast(kMaxPathSize))); + ts = advert_path.timestamp; + path_len = advert_path.path_len; + std::memcpy(path, advert_path.path, std::min(path_len, kMaxPathSize)); + path_meta = advert_path.meta; + } + else + { + size_t lookup_len = kMaxPathSize; + found = hooks_->lookupAdvertPath(pubkey, kPubKeySize, &ts, path, &lookup_len); + if (found) + { + path_len = static_cast(std::min(lookup_len, static_cast(kMaxPathSize))); + } } } PhoneNodeView entry{}; @@ -1222,6 +1278,16 @@ void MeshCorePhoneCore::handleCmdFrame(const uint8_t* data, size_t len) std::memcpy(&out[index], path, path_len); index += path_len; } + if (path_meta.hash_bytes == 0) + { + path_meta.hash_bytes = 1; + } + if (path_meta.hop_count == 0 && path_len > 0) + { + path_meta.hop_count = static_cast( + path_len / std::max(path_meta.hash_bytes, 1)); + } + appendPathMetadataExt(out, sizeof(out), index, path_meta); enqueueFrame(out, index); return; } @@ -1717,6 +1783,12 @@ bool MeshCorePhoneCore::buildContactFromNode(const PhoneNodeView& entry, uint8_t std::memcpy(&out.buf[index], &last_adv, sizeof(last_adv)); index += sizeof(last_adv); + MeshCorePhonePathMetadata meta{}; + meta.profile = 0; + meta.hash_bytes = 1; + meta.hop_count = 0; + appendPathMetadataExt(out.buf.data(), out.buf.size(), index, meta); + out.len = index; return true; } diff --git a/modules/core_sys/include/app/app_config.h b/modules/core_sys/include/app/app_config.h index 8cf9ec39..d639b0fa 100644 --- a/modules/core_sys/include/app/app_config.h +++ b/modules/core_sys/include/app/app_config.h @@ -223,6 +223,8 @@ struct AppConfig meshcore_config.meshcore_airtime_factor = 1.0f; meshcore_config.meshcore_flood_max = 16; meshcore_config.meshcore_multi_acks = false; + meshcore_config.meshcore_send_profile = chat::MeshCorePayloadSendProfile::AutoPreferV2; + meshcore_config.meshcore_forward_profile = chat::MeshCoreForwardProfile::MultibyteOnly; meshcore_config.meshcore_channel_slot = 0; strncpy(meshcore_config.meshcore_channel_name, "Public", sizeof(meshcore_config.meshcore_channel_name) - 1); diff --git a/modules/ui_shared/include/ui/screens/settings/settings_state.h b/modules/ui_shared/include/ui/screens/settings/settings_state.h index 185ef79a..5b55b5da 100644 --- a/modules/ui_shared/include/ui/screens/settings/settings_state.h +++ b/modules/ui_shared/include/ui/screens/settings/settings_state.h @@ -105,6 +105,8 @@ struct SettingsData char mc_airtime[16] = "1"; int mc_flood_max = 16; bool mc_multi_acks = false; + int mc_send_profile = 1; + int mc_forward_profile = 1; int mc_channel_slot = 0; char mc_channel_name[32] = "Public"; char mc_channel_key[65] = {}; diff --git a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp index 298364a0..8369f0c2 100644 --- a/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp +++ b/modules/ui_shared/src/ui/screens/settings/settings_page_components.cpp @@ -1130,6 +1130,8 @@ static void reset_mesh_settings() "mc_airtime", "mc_flood_max", "mc_multi_acks", + "mc_send_prof", + "mc_fwd_prof", "mc_channel_slot", "mc_ch_slot", }; @@ -1373,6 +1375,8 @@ static void settings_load() float_to_text(mc_cfg.meshcore_airtime_factor, g_settings.mc_airtime, sizeof(g_settings.mc_airtime), 3); g_settings.mc_flood_max = mc_cfg.meshcore_flood_max; g_settings.mc_multi_acks = mc_cfg.meshcore_multi_acks; + g_settings.mc_send_profile = static_cast(static_cast(mc_cfg.meshcore_send_profile)); + g_settings.mc_forward_profile = static_cast(static_cast(mc_cfg.meshcore_forward_profile)); g_settings.mc_channel_slot = mc_cfg.meshcore_channel_slot; strncpy(g_settings.mc_channel_name, mc_cfg.meshcore_channel_name, sizeof(g_settings.mc_channel_name) - 1); g_settings.mc_channel_name[sizeof(g_settings.mc_channel_name) - 1] = '\0'; @@ -2622,6 +2626,22 @@ static void on_option_clicked(lv_event_t* e) app_ctx.saveConfig(); app_ctx.applyMeshConfig(); } + if (payload->item->pref_key && strcmp(payload->item->pref_key, "mc_send_prof") == 0) + { + app::IAppFacade& app_ctx = app::appFacade(); + app_ctx.getConfig().meshcore_config.meshcore_send_profile = + static_cast(payload->value); + app_ctx.saveConfig(); + app_ctx.applyMeshConfig(); + } + if (payload->item->pref_key && strcmp(payload->item->pref_key, "mc_fwd_prof") == 0) + { + app::IAppFacade& app_ctx = app::appFacade(); + app_ctx.getConfig().meshcore_config.meshcore_forward_profile = + static_cast(payload->value); + app_ctx.saveConfig(); + app_ctx.applyMeshConfig(); + } if (payload->item->pref_key && strcmp(payload->item->pref_key, "mc_channel_slot") == 0) { app::IAppFacade& app_ctx = app::appFacade(); @@ -3450,6 +3470,15 @@ static const settings::ui::SettingOption kMeshCoreFloodOptions[] = { {"48", 48}, {"64", 64}, }; +static const settings::ui::SettingOption kMeshCoreSendProfileOptions[] = { + {"V1 Only", 0}, + {"Auto Prefer V2", 1}, + {"V2 Only", 2}, +}; +static const settings::ui::SettingOption kMeshCoreForwardProfileOptions[] = { + {"Any", 0}, + {"Multibyte Only", 1}, +}; // Tx power options are populated dynamically based on board limits. static const settings::ui::SettingOption kNetUtilOptions[] = { {"Auto", 0}, @@ -3568,6 +3597,8 @@ static settings::ui::SettingItem kNetworkItems[] = { {"MC Airtime Factor", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.mc_airtime, sizeof(g_settings.mc_airtime), false, "mc_airtime"}, {"MC Flood Max", settings::ui::SettingType::Enum, kMeshCoreFloodOptions, sizeof(kMeshCoreFloodOptions) / sizeof(kMeshCoreFloodOptions[0]), &g_settings.mc_flood_max, nullptr, nullptr, 0, false, "mc_flood_max"}, {"MC Multi ACKs", settings::ui::SettingType::Toggle, nullptr, 0, nullptr, &g_settings.mc_multi_acks, nullptr, 0, false, "mc_multi_acks"}, + {"MC Send Profile", settings::ui::SettingType::Enum, kMeshCoreSendProfileOptions, sizeof(kMeshCoreSendProfileOptions) / sizeof(kMeshCoreSendProfileOptions[0]), &g_settings.mc_send_profile, nullptr, nullptr, 0, false, "mc_send_prof"}, + {"MC Forward Profile", settings::ui::SettingType::Enum, kMeshCoreForwardProfileOptions, sizeof(kMeshCoreForwardProfileOptions) / sizeof(kMeshCoreForwardProfileOptions[0]), &g_settings.mc_forward_profile, nullptr, nullptr, 0, false, "mc_fwd_prof"}, {"MC Channel Slot", settings::ui::SettingType::Enum, kMeshCoreChannelSlotOptions, sizeof(kMeshCoreChannelSlotOptions) / sizeof(kMeshCoreChannelSlotOptions[0]), &g_settings.mc_channel_slot, nullptr, nullptr, 0, false, "mc_channel_slot"}, {"MC Channel Name", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.mc_channel_name, sizeof(g_settings.mc_channel_name), false, "mc_channel_name"}, {"MC Channel Key", settings::ui::SettingType::Text, nullptr, 0, nullptr, nullptr, g_settings.mc_channel_key, sizeof(g_settings.mc_channel_key), true, "mc_channel_key"}, @@ -3892,6 +3923,8 @@ static bool should_show_item(const settings::ui::SettingItem& item) if (has_pref_key(item, "mc_airtime")) return false; if (has_pref_key(item, "mc_flood_max")) return false; if (has_pref_key(item, "mc_multi_acks")) return false; + if (has_pref_key(item, "mc_send_prof")) return false; + if (has_pref_key(item, "mc_fwd_prof")) return false; if (has_pref_key(item, "mc_channel_slot")) return false; if (has_pref_key(item, "mc_channel_name")) return false; if (has_pref_key(item, "mc_channel_key")) return false; @@ -3909,6 +3942,8 @@ static bool should_show_item(const settings::ui::SettingItem& item) if (has_pref_key(item, "mc_airtime")) return false; if (has_pref_key(item, "mc_flood_max")) return false; if (has_pref_key(item, "mc_multi_acks")) return false; + if (has_pref_key(item, "mc_send_prof")) return false; + if (has_pref_key(item, "mc_fwd_prof")) return false; if (has_pref_key(item, "mc_channel_slot")) return false; if (has_pref_key(item, "mc_channel_name")) return false; if (has_pref_key(item, "mc_channel_key")) return false; diff --git a/platform/esp/arduino_common/include/ble/app_phone_facade.h b/platform/esp/arduino_common/include/ble/app_phone_facade.h index a520407e..90633dde 100644 --- a/platform/esp/arduino_common/include/ble/app_phone_facade.h +++ b/platform/esp/arduino_common/include/ble/app_phone_facade.h @@ -134,6 +134,12 @@ class AppPhoneFacade final : public phone::IPhoneAppFacade, const uint8_t* payload, std::size_t payload_len, uint32_t* out_est_timeout) override; + bool meshCoreSendRawDataEx(uint8_t profile, + const uint8_t* path, + std::size_t path_len, + const uint8_t* payload, + std::size_t payload_len, + uint32_t* out_est_timeout) override; void meshCoreSetFloodScopeKey(const uint8_t* key, std::size_t len) override; private: diff --git a/platform/esp/arduino_common/include/ble/meshcore_ble.h b/platform/esp/arduino_common/include/ble/meshcore_ble.h index c753ecf9..d667c3e6 100644 --- a/platform/esp/arduino_common/include/ble/meshcore_ble.h +++ b/platform/esp/arduino_common/include/ble/meshcore_ble.h @@ -53,6 +53,8 @@ class MeshCoreBleService : public BleService, uint8_t type = 0; uint8_t flags = 0; uint8_t out_path_len = 0; + uint8_t out_path_profile = 0; + uint8_t out_path_hash_bytes = 1; uint8_t out_path[64] = {}; char name[32] = {}; uint32_t last_advert = 0; @@ -191,6 +193,8 @@ class MeshCoreBleService : public BleService, void onSentRoute(bool sent_flood) override; bool lookupAdvertPath(const uint8_t* pubkey, size_t len, uint32_t* out_ts, uint8_t* out_path, size_t* inout_len) const override; + bool lookupAdvertPathEx(const uint8_t* pubkey, size_t len, + phone::meshcore::MeshCorePhoneAdvertPath* out) const override; bool hasActiveConnection(const uint8_t* prefix, size_t len) const override; void logoutActiveConnection(const uint8_t* prefix, size_t len) override; bool getRadioStats(phone::meshcore::MeshCorePhoneRadioStats* out) const override; diff --git a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h index b03abf76..8e3ff1ed 100644 --- a/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h +++ b/platform/esp/arduino_common/include/platform/esp/arduino_common/chat/infra/meshcore/meshcore_adapter.h @@ -7,6 +7,7 @@ #include "board/LoraBoard.h" #include "chat/infra/meshcore/meshcore_ble_backend.h" +#include "chat/infra/meshcore/meshcore_protocol_helpers.h" #include "chat/ports/i_mesh_adapter.h" #include "chat/runtime/meshcore_runtime.h" #include "chat/runtime/protocol_runtime_factory.h" @@ -106,10 +107,13 @@ class MeshCoreAdapter : public IMeshAdapter, struct PeerInfo { uint8_t peer_hash = 0; + uint8_t peer_hash_len = chat::meshcore::kMeshCoreV1HashBytes; + uint8_t peer_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; NodeId node_id = 0; bool has_pubkey = false; bool pubkey_verified = false; uint8_t pubkey[MeshCoreIdentity::kPubKeySize] = {}; + PayloadProfile out_path_profile = PayloadProfile::V1; uint8_t out_path_len = 0; uint8_t out_path[kMaxPeerPathLen] = {}; uint32_t last_seen_ms = 0; @@ -157,6 +161,7 @@ class MeshCoreAdapter : public IMeshAdapter, uint32_t auth = 0; uint32_t trip_ms = 0; std::vector payload; + PayloadProfile path_profile = PayloadProfile::V1; std::vector in_path; std::vector out_path; std::vector trace_hashes; @@ -192,6 +197,9 @@ class MeshCoreAdapter : public IMeshAdapter, bool sendRawData(const uint8_t* path, size_t path_len, const uint8_t* payload, size_t payload_len, uint32_t* out_est_timeout); + bool sendRawDataEx(uint8_t profile, const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout) override; bool sendTracePath(const uint8_t* path, size_t path_len, uint32_t tag, uint32_t auth, uint8_t flags, uint32_t* out_est_timeout); @@ -234,6 +242,7 @@ class MeshCoreAdapter : public IMeshAdapter, { uint8_t path[kMaxPeerPathLen] = {}; uint8_t path_len = 0; + PayloadProfile profile = PayloadProfile::V1; ChannelId channel = ChannelId::PRIMARY; int16_t snr_x10 = std::numeric_limits::min(); uint8_t sample_count = 0; @@ -243,9 +252,12 @@ class MeshCoreAdapter : public IMeshAdapter, }; uint8_t peer_hash = 0; + uint8_t peer_hash_len = chat::meshcore::kMeshCoreV1HashBytes; + uint8_t peer_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; NodeId node_id_guess = 0; uint8_t out_path[kMaxPeerPathLen] = {}; uint8_t out_path_len = 0; + PayloadProfile out_path_profile = PayloadProfile::V1; bool has_out_path = false; bool has_pubkey = false; bool pubkey_verified = false; @@ -338,12 +350,23 @@ class MeshCoreAdapter : public IMeshAdapter, bool isDiscoverRxGuardActive(uint32_t now_ms) const; bool resolveGroupSecret(ChannelId channel, uint8_t out_key16[16], uint8_t out_key32[32], uint8_t* out_hash) const; + bool resolveGroupSecret(ChannelId channel, uint8_t out_key16[16], + uint8_t out_key32[32], PayloadProfile profile, + uint8_t* out_hash, size_t out_hash_cap) const; ChannelId resolveChannelFromHash(uint8_t channel_hash, bool* out_match) const; + ChannelId resolveChannelFromHash(PayloadProfile profile, const uint8_t* channel_hash, + bool* out_match) const; bool deriveIdentitySecret(uint8_t peer_hash, uint8_t out_key16[16], uint8_t out_key32[32]) const; + bool deriveIdentitySecret(PayloadProfile profile, const uint8_t* peer_hash, + uint8_t out_key16[16], uint8_t out_key32[32]) const; bool deriveDirectSecret(ChannelId channel, uint8_t peer_hash, uint8_t out_key16[16], uint8_t out_key32[32]) const; + bool deriveDirectSecret(ChannelId channel, PayloadProfile profile, const uint8_t* peer_hash, + uint8_t out_key16[16], uint8_t out_key32[32]) const; bool lookupPeerPubKey(uint8_t peer_hash, uint8_t out_pubkey[MeshCoreIdentity::kPubKeySize]) const; + bool lookupPeerPubKey(PayloadProfile profile, const uint8_t* peer_hash, + uint8_t out_pubkey[MeshCoreIdentity::kPubKeySize]) const; void rememberPeerPubKey(const uint8_t pubkey[MeshCoreIdentity::kPubKeySize], uint32_t now_ms, bool verified); void loadPeerPubKeysFromPrefs(); @@ -352,14 +375,27 @@ class MeshCoreAdapter : public IMeshAdapter, bool tryDecryptPeerPayload(uint8_t src_hash, const uint8_t* cipher, size_t cipher_len, uint8_t* out_plain, size_t* out_plain_len, ChannelId* out_channel) const; + bool tryDecryptPeerPayload(PayloadProfile profile, const uint8_t* src_hash, + const uint8_t* cipher, size_t cipher_len, + uint8_t* out_plain, size_t* out_plain_len, + ChannelId* out_channel) const; PeerRouteEntry* findPeerRouteByHash(uint8_t peer_hash); const PeerRouteEntry* findPeerRouteByHash(uint8_t peer_hash) const; + PeerRouteEntry* findPeerRouteByHash(PayloadProfile profile, const uint8_t* peer_hash); + const PeerRouteEntry* findPeerRouteByHash(PayloadProfile profile, const uint8_t* peer_hash) const; PeerRouteEntry* selectPeerRouteByHash(uint8_t peer_hash, uint32_t now_ms); const PeerRouteEntry* selectPeerRouteByHash(uint8_t peer_hash, uint32_t now_ms) const; + PeerRouteEntry* selectPeerRouteByHash(PayloadProfile profile, const uint8_t* peer_hash, + uint32_t now_ms); + const PeerRouteEntry* selectPeerRouteByHash(PayloadProfile profile, const uint8_t* peer_hash, + uint32_t now_ms) const; PeerRouteEntry& upsertPeerRoute(uint8_t peer_hash, uint32_t now_ms); + PeerRouteEntry& upsertPeerRoute(PayloadProfile profile, const uint8_t* peer_hash, + uint32_t now_ms); void prunePeerRoutes(uint32_t now_ms); void refreshBestPeerRoute(PeerRouteEntry& entry, uint32_t now_ms); void rememberPeerPathCandidate(PeerRouteEntry& entry, + PayloadProfile profile, const uint8_t* path, size_t path_len, ChannelId channel, int16_t snr_x10, uint32_t now_ms); @@ -370,6 +406,12 @@ class MeshCoreAdapter : public IMeshAdapter, NodeId resolvePeerNodeId(uint8_t peer_hash) const; void rememberPeerPath(uint8_t peer_hash, const uint8_t* path, size_t path_len, ChannelId channel, uint32_t now_ms); + void rememberPeerPath(PayloadProfile profile, const uint8_t* peer_hash, + const uint8_t* path, size_t path_len, + ChannelId channel, uint32_t now_ms); + PayloadProfile selectSendProfile(const PeerRouteEntry* route) const; + bool allowsForwardingProfile(PayloadProfile profile) const; + bool selfHash(PayloadProfile profile, uint8_t* out_hash, size_t out_cap) const; void pruneSeen(uint32_t now_ms); bool hasSeenSignature(uint32_t signature, uint32_t now_ms); void handleRawPacketInternal(const uint8_t* data, size_t size, bool allow_duplicate); diff --git a/platform/esp/arduino_common/src/app_config_store.cpp b/platform/esp/arduino_common/src/app_config_store.cpp index 6a169f68..12d82b12 100644 --- a/platform/esp/arduino_common/src/app_config_store.cpp +++ b/platform/esp/arduino_common/src/app_config_store.cpp @@ -903,6 +903,12 @@ bool loadAppConfigFromPreferences(AppConfig& config, meshcore_config.meshcore_airtime_factor = get_float("mc_airtime", meshcore_config.meshcore_airtime_factor); meshcore_config.meshcore_flood_max = get_uchar("mc_flood_max", meshcore_config.meshcore_flood_max); meshcore_config.meshcore_multi_acks = get_bool("mc_multi_acks", false); + meshcore_config.meshcore_send_profile = static_cast( + get_uchar("mc_send_prof", + static_cast(meshcore_config.meshcore_send_profile))); + meshcore_config.meshcore_forward_profile = static_cast( + get_uchar("mc_fwd_prof", + static_cast(meshcore_config.meshcore_forward_profile))); meshcore_config.meshcore_channel_slot = get_uchar("mc_ch_slot", 0); meshcore_config.tx_enabled = get_bool("mc_tx_en", meshcore_config.tx_enabled); String mc_name = get_string("mc_ch_name", meshcore_config.meshcore_channel_name); @@ -1251,6 +1257,8 @@ bool saveAppConfigToPreferences(AppConfig& config, put_float("mc_airtime", meshcore_config.meshcore_airtime_factor); put_uchar("mc_flood_max", meshcore_config.meshcore_flood_max); put_bool("mc_multi_acks", meshcore_config.meshcore_multi_acks); + put_uchar("mc_send_prof", static_cast(meshcore_config.meshcore_send_profile)); + put_uchar("mc_fwd_prof", static_cast(meshcore_config.meshcore_forward_profile)); put_uchar("mc_ch_slot", meshcore_config.meshcore_channel_slot); put_bool("mc_tx_en", meshcore_config.tx_enabled); put_string("mc_ch_name", meshcore_config.meshcore_channel_name); diff --git a/platform/esp/arduino_common/src/ble/app_phone_facade.cpp b/platform/esp/arduino_common/src/ble/app_phone_facade.cpp index f230b921..a044c75b 100644 --- a/platform/esp/arduino_common/src/ble/app_phone_facade.cpp +++ b/platform/esp/arduino_common/src/ble/app_phone_facade.cpp @@ -741,6 +741,17 @@ bool AppPhoneFacade::meshCoreSendRawData(const uint8_t* path, return backend && backend->sendRawData(path, path_len, payload, payload_len, out_est_timeout); } +bool AppPhoneFacade::meshCoreSendRawDataEx(uint8_t profile, + const uint8_t* path, + std::size_t path_len, + const uint8_t* payload, + std::size_t payload_len, + uint32_t* out_est_timeout) +{ + auto* backend = meshCoreBackend(); + return backend && backend->sendRawDataEx(profile, path, path_len, payload, payload_len, out_est_timeout); +} + void AppPhoneFacade::meshCoreSetFloodScopeKey(const uint8_t* key, std::size_t len) { if (auto* backend = meshCoreBackend()) diff --git a/platform/esp/arduino_common/src/ble/meshcore_ble.cpp b/platform/esp/arduino_common/src/ble/meshcore_ble.cpp index 48e54a9a..4b05cdee 100644 --- a/platform/esp/arduino_common/src/ble/meshcore_ble.cpp +++ b/platform/esp/arduino_common/src/ble/meshcore_ble.cpp @@ -135,6 +135,8 @@ constexpr size_t kMaxFrameSize = 172; constexpr size_t kPubKeySize = chat::meshcore::MeshCoreIdentity::kPubKeySize; constexpr size_t kPubKeyPrefixSize = 6; constexpr size_t kMaxPathSize = 64; +constexpr uint8_t kPathMetadataExtMarker = 0xE2; +constexpr uint8_t kPathMetadataExtLen = 3; constexpr uint32_t kBleWriteMinIntervalMs = 60; constexpr bool kMeshCoreBleSecurityEnabled = true; constexpr uint32_t kMeshCoreDefaultBlePin = 123456; @@ -161,6 +163,56 @@ int8_t encodeSnrQdb(int16_t snr_x10) return static_cast(qdb); } +bool appendPathMetadataExt(uint8_t* out, size_t out_cap, size_t& index, + uint8_t profile, uint8_t hash_bytes, uint8_t hop_count) +{ + if (!out || index + 2 + kPathMetadataExtLen > out_cap) + { + return false; + } + out[index++] = kPathMetadataExtMarker; + out[index++] = kPathMetadataExtLen; + out[index++] = profile; + out[index++] = hash_bytes == 0 ? 1 : hash_bytes; + out[index++] = hop_count; + return true; +} + +void decodePathMetadataExt(const uint8_t* data, size_t len, + uint8_t* out_profile, uint8_t* out_hash_bytes, uint8_t* out_hop_count) +{ + if (out_profile) + { + *out_profile = 0; + } + if (out_hash_bytes) + { + *out_hash_bytes = 1; + } + if (out_hop_count) + { + *out_hop_count = 0; + } + if (!data || len < 2 + kPathMetadataExtLen || + data[0] != kPathMetadataExtMarker || + data[1] != kPathMetadataExtLen) + { + return; + } + if (out_profile) + { + *out_profile = data[2]; + } + if (out_hash_bytes) + { + *out_hash_bytes = data[3] == 0 ? 1 : data[3]; + } + if (out_hop_count) + { + *out_hop_count = data[4]; + } +} + int8_t encodeRssiDbm(int16_t rssi_x10) { if (rssi_x10 == std::numeric_limits::min()) @@ -1112,6 +1164,17 @@ void MeshCoreBleService::update() memcpy(&out[i], ev.in_path.data(), in_len); i += in_len; } + { + size_t index = static_cast(i); + const uint8_t profile = static_cast(ev.path_profile); + const uint8_t hash_bytes = static_cast( + chat::meshcore::payloadHashBytes(ev.path_profile)); + const uint8_t hop_count = (out_len > 0 && hash_bytes > 0) + ? static_cast(out_len / hash_bytes) + : 0; + appendPathMetadataExt(out, sizeof(out), index, profile, hash_bytes, hop_count); + i = static_cast(index); + } enqueueFrame(out, i); break; } @@ -1534,14 +1597,59 @@ void MeshCoreBleService::loadManualContacts() return; } size_t len = prefs.getBytesLength("blob"); - if (len == 0 || (len % sizeof(ContactRecord)) != 0) + if (len == 0) { prefs.end(); return; } - size_t count = len / sizeof(ContactRecord); - manual_contacts_.resize(count); - prefs.getBytes("blob", manual_contacts_.data(), len); + if ((len % sizeof(ContactRecord)) == 0) + { + size_t count = len / sizeof(ContactRecord); + manual_contacts_.resize(count); + prefs.getBytes("blob", manual_contacts_.data(), len); + } + else + { + struct LegacyContactRecord + { + uint8_t pubkey[chat::meshcore::MeshCoreIdentity::kPubKeySize]; + uint8_t type; + uint8_t flags; + uint8_t out_path_len; + uint8_t out_path[64]; + char name[32]; + uint32_t last_advert; + int32_t lat; + int32_t lon; + uint32_t lastmod; + } __attribute__((packed)); + + if ((len % sizeof(LegacyContactRecord)) != 0) + { + prefs.end(); + return; + } + std::vector legacy(len / sizeof(LegacyContactRecord)); + prefs.getBytes("blob", legacy.data(), len); + manual_contacts_.reserve(legacy.size()); + for (const LegacyContactRecord& old : legacy) + { + ContactRecord rec{}; + memcpy(rec.pubkey, old.pubkey, sizeof(rec.pubkey)); + rec.type = old.type; + rec.flags = old.flags; + rec.out_path_len = old.out_path_len; + rec.out_path_profile = 0; + rec.out_path_hash_bytes = 1; + memcpy(rec.out_path, old.out_path, sizeof(rec.out_path)); + memcpy(rec.name, old.name, sizeof(rec.name)); + rec.last_advert = old.last_advert; + rec.lat = old.lat; + rec.lon = old.lon; + rec.lastmod = old.lastmod; + manual_contacts_.push_back(rec); + } + } prefs.end(); } @@ -1716,6 +1824,14 @@ void MeshCoreBleService::buildContactsSnapshot(uint32_t filter_since) i += 4; memcpy(&frame.buf[i], &record.lastmod, 4); i += 4; + const uint8_t hash_bytes = record.out_path_hash_bytes == 0 ? 1 : record.out_path_hash_bytes; + const uint8_t hop_count = (record.out_path_len > 0 && hash_bytes > 0) + ? static_cast(record.out_path_len / hash_bytes) + : 0; + size_t index = static_cast(i); + appendPathMetadataExt(frame.buf.data(), frame.buf.size(), + index, record.out_path_profile, hash_bytes, hop_count); + i = static_cast(index); frame.len = static_cast(i); contacts_frames_.push_back(frame); contacts_most_recent_ = std::max(contacts_most_recent_, record.lastmod); @@ -1782,6 +1898,8 @@ bool MeshCoreBleService::decodeContactPayload(const uint8_t* frame, size_t len, out->type = frame[i++]; out->flags = frame[i++]; out->out_path_len = frame[i++]; + out->out_path_profile = 0; + out->out_path_hash_bytes = 1; memset(out->out_path, 0, sizeof(out->out_path)); if (out->out_path_len > 0) { @@ -1807,9 +1925,15 @@ bool MeshCoreBleService::decodeContactPayload(const uint8_t* frame, size_t len, if (len >= i + 4) { memcpy(&lastmod, &frame[i], 4); + i += 4; } } + decodePathMetadataExt(frame + i, len > i ? len - i : 0, + &out->out_path_profile, + &out->out_path_hash_bytes, + nullptr); + if (out_lastmod) { *out_lastmod = lastmod; @@ -1874,6 +1998,18 @@ bool MeshCoreBleService::buildContactFrame(const chat::meshcore::MeshCoreAdapter i += 4; memcpy(&out.buf[i], &last_adv, 4); i += 4; + { + size_t index = static_cast(i); + const uint8_t profile = static_cast(peer.out_path_profile); + const uint8_t hash_bytes = static_cast( + chat::meshcore::payloadHashBytes(peer.out_path_profile)); + const uint8_t hop_count = (peer.out_path_len > 0 && hash_bytes > 0) + ? static_cast(peer.out_path_len / hash_bytes) + : 0; + appendPathMetadataExt(out.buf.data(), out.buf.size(), index, + profile, hash_bytes, hop_count); + i = static_cast(index); + } out.len = static_cast(i); return true; @@ -1914,6 +2050,11 @@ bool MeshCoreBleService::buildContactFromNode(const chat::contacts::NodeEntry& e i += 4; memcpy(&out.buf[i], &last_adv, 4); i += 4; + { + size_t index = static_cast(i); + appendPathMetadataExt(out.buf.data(), out.buf.size(), index, 0, 1, 0); + i = static_cast(index); + } out.len = static_cast(i); return true; diff --git a/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp b/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp index 2dbb9dbf..184c4306 100644 --- a/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp +++ b/platform/esp/arduino_common/src/ble/meshcore_ble_owner_hooks.cpp @@ -159,35 +159,72 @@ void MeshCoreBleService::onSentRoute(bool sent_flood) bool MeshCoreBleService::lookupAdvertPath(const uint8_t* pubkey, size_t len, uint32_t* out_ts, uint8_t* out_path, size_t* inout_len) const { - if (!pubkey || len < kPubKeySize || !out_ts || !out_path || !inout_len) + if (!out_ts || !out_path || !inout_len) + { + return false; + } + phone::meshcore::MeshCorePhoneAdvertPath path{}; + if (!lookupAdvertPathEx(pubkey, len, &path)) + { + return false; + } + *out_ts = path.timestamp; + const size_t copy_len = std::min(static_cast(path.path_len), *inout_len); + if (copy_len > 0) + { + std::memcpy(out_path, path.path, copy_len); + } + *inout_len = copy_len; + return true; +} + +bool MeshCoreBleService::lookupAdvertPathEx(const uint8_t* pubkey, size_t len, + phone::meshcore::MeshCorePhoneAdvertPath* out) const +{ + if (!pubkey || len < kPubKeySize || !out) { return false; } if (const ContactRecord* rec = const_cast(this)->findManualContact(pubkey)) { - *out_ts = rec->last_advert; - const size_t copy_len = std::min(static_cast(rec->out_path_len), *inout_len); + *out = {}; + out->timestamp = rec->last_advert; + const size_t copy_len = std::min(static_cast(rec->out_path_len), sizeof(out->path)); if (copy_len > 0) { - std::memcpy(out_path, rec->out_path, copy_len); + std::memcpy(out->path, rec->out_path, copy_len); } - *inout_len = copy_len; + out->path_len = static_cast(copy_len); + out->meta.profile = rec->out_path_profile; + out->meta.hash_bytes = rec->out_path_hash_bytes == 0 ? 1 : rec->out_path_hash_bytes; + out->meta.hop_count = copy_len > 0 + ? static_cast(copy_len / out->meta.hash_bytes) + : 0; return true; } if (const auto* adapter = meshCoreAdapter()) { chat::meshcore::MeshCoreAdapter::PeerInfo peer{}; - if (adapter->lookupPeerByHash(pubkey[0], &peer)) + if (adapter->lookupPeerByHash(pubkey[0], &peer) && + peer.has_pubkey && + std::memcmp(peer.pubkey, pubkey, kPubKeySize) == 0) { - *out_ts = peer.last_seen_ms / 1000U; - const size_t copy_len = std::min(static_cast(peer.out_path_len), *inout_len); + *out = {}; + out->timestamp = peer.last_seen_ms / 1000U; + const size_t copy_len = std::min(static_cast(peer.out_path_len), sizeof(out->path)); if (copy_len > 0) { - std::memcpy(out_path, peer.out_path, copy_len); + std::memcpy(out->path, peer.out_path, copy_len); } - *inout_len = copy_len; + out->path_len = static_cast(copy_len); + out->meta.profile = static_cast(peer.out_path_profile); + out->meta.hash_bytes = static_cast( + chat::meshcore::payloadHashBytes(peer.out_path_profile)); + out->meta.hop_count = copy_len > 0 + ? static_cast(copy_len / out->meta.hash_bytes) + : 0; return true; } } diff --git a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp index f6adc7ff..28f22623 100644 --- a/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp +++ b/platform/esp/arduino_common/src/chat/infra/meshcore/meshcore_adapter.cpp @@ -56,8 +56,6 @@ constexpr uint8_t kPayloadTypePath = 0x08; constexpr uint8_t kPayloadTypeTrace = chat::meshcore::kMeshCorePayloadTypeTrace; constexpr uint8_t kPayloadTypeMultipart = 0x0A; constexpr uint8_t kPayloadTypeControl = chat::meshcore::kMeshCorePayloadTypeControl; -constexpr uint8_t kPayloadVer1 = 0x00; -constexpr size_t kMeshcorePathHashSize = 1; constexpr size_t kMeshcoreMaxPathSize = 64; constexpr size_t kMeshcoreMaxFrameSize = 255; constexpr size_t kMeshcoreMaxPayloadSize = 184; @@ -72,7 +70,6 @@ constexpr uint32_t kAckDelayMs = 120; constexpr uint32_t kAckSpacingMs = 300; constexpr uint32_t kDiscoverRxGuardMs = 5000; constexpr size_t kCipherBlockSize = 16; -constexpr size_t kCipherMacSize = 2; constexpr size_t kCipherKeySize = 16; constexpr size_t kCipherHmacKeySize = 32; constexpr size_t kGroupPlainPrefixSize = 5; @@ -138,6 +135,32 @@ T clampValue(T value, T min_value, T max_value) return value; } +uint8_t lowHashByte(PayloadProfile profile, const uint8_t* hash) +{ + (void)profile; + return hash ? hash[0] : 0; +} + +void copyPeerHashBytes(PayloadProfile profile, const uint8_t* hash, + uint8_t out_hash[chat::meshcore::kMeshCoreV2HashBytes], + uint8_t* out_len) +{ + if (!out_hash) + { + return; + } + memset(out_hash, 0, chat::meshcore::kMeshCoreV2HashBytes); + const size_t hash_bytes = payloadHashBytes(profile); + if (hash && hash_bytes <= chat::meshcore::kMeshCoreV2HashBytes) + { + memcpy(out_hash, hash, hash_bytes); + } + if (out_len) + { + *out_len = static_cast(hash_bytes); + } +} + #ifndef MESHCORE_LOG_ENABLE #define MESHCORE_LOG_ENABLE 0 #endif @@ -397,6 +420,14 @@ bool MeshCoreAdapter::sendPeerRequestPayload(const uint8_t* pubkey, size_t len, ensurePeerPublicKey(pubkey, len, false); const uint8_t peer_hash = pubkey[0]; + uint8_t peer_hash_v2[chat::meshcore::kMeshCoreV2HashBytes] = {}; + copyPublicHash(PayloadProfile::V2, pubkey, len, peer_hash_v2, sizeof(peer_hash_v2)); + const PeerRouteEntry* v2_route = selectPeerRouteByHash(PayloadProfile::V2, peer_hash_v2, now_ms); + const PeerRouteEntry* v1_route = selectPeerRouteByHash(peer_hash, now_ms); + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; uint8_t shared_secret[kCipherHmacKeySize] = {}; if (!identity_.deriveSharedSecret(pubkey, shared_secret)) @@ -427,7 +458,11 @@ bool MeshCoreAdapter::sendPeerRequestPayload(const uint8_t* pubkey, size_t len, uint8_t datagram[kMeshcoreMaxPayloadSize] = {}; size_t datagram_len = 0; - if (!buildPeerDatagramPayload(peer_hash, self_hash_, + uint8_t peer_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + uint8_t self_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!copyPublicHash(profile, pubkey, len, peer_hash_bytes, sizeof(peer_hash_bytes)) || + !selfHash(profile, self_hash_bytes, sizeof(self_hash_bytes)) || + !buildPeerDatagramPayload(profile, peer_hash_bytes, self_hash_bytes, key16, key32, plain, plain_len, datagram, sizeof(datagram), &datagram_len)) @@ -435,7 +470,7 @@ bool MeshCoreAdapter::sendPeerRequestPayload(const uint8_t* pubkey, size_t len, return false; } - const PeerRouteEntry* route = selectPeerRouteByHash(peer_hash, now_ms); + const PeerRouteEntry* route = (profile == PayloadProfile::V2) ? v2_route : v1_route; uint8_t route_type = kRouteTypeFlood; const uint8_t* out_path = nullptr; size_t out_path_len = 0; @@ -448,7 +483,8 @@ bool MeshCoreAdapter::sendPeerRequestPayload(const uint8_t* pubkey, size_t len, uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(route_type, kPayloadTypeReq, + if (!buildFrameNoTransport(profile, + route_type, kPayloadTypeReq, out_path, out_path_len, datagram, datagram_len, frame, sizeof(frame), &frame_len)) @@ -473,7 +509,9 @@ bool MeshCoreAdapter::sendPeerRequestPayload(const uint8_t* pubkey, size_t len, if (out_est_timeout) { *out_est_timeout = estimateSendTimeoutMs(frame_len, - (route_type == kRouteTypeDirect) ? out_path_len : 0, + (route_type == kRouteTypeDirect) + ? pathHopCount(profile, out_path_len) + : 0, route_type == kRouteTypeFlood, config_.meshcore_bw_khz, config_.meshcore_sf, @@ -506,6 +544,14 @@ bool MeshCoreAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t len, ensurePeerPublicKey(pubkey, len, false); const uint8_t peer_hash = pubkey[0]; + uint8_t peer_hash_v2[chat::meshcore::kMeshCoreV2HashBytes] = {}; + copyPublicHash(PayloadProfile::V2, pubkey, len, peer_hash_v2, sizeof(peer_hash_v2)); + const PeerRouteEntry* v2_route = selectPeerRouteByHash(PayloadProfile::V2, peer_hash_v2, now_ms); + const PeerRouteEntry* v1_route = selectPeerRouteByHash(peer_hash, now_ms); + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; uint8_t shared_secret[kCipherHmacKeySize] = {}; if (!identity_.deriveSharedSecret(pubkey, shared_secret)) @@ -525,7 +571,8 @@ bool MeshCoreAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t len, uint8_t cipher[kMeshcoreMaxPayloadSize] = {}; size_t cipher_len = encryptThenMac(key16, key32, cipher, sizeof(cipher), - payload, payload_len); + payload, payload_len, + payloadMacBytes(profile)); if (cipher_len == 0) { return false; @@ -533,17 +580,24 @@ bool MeshCoreAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t len, uint8_t datagram[kMeshcoreMaxPayloadSize] = {}; size_t datagram_len = 0; - if (1 + kMeshcorePubKeySize + cipher_len > sizeof(datagram)) + uint8_t peer_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!copyPublicHash(profile, pubkey, len, peer_hash_bytes, sizeof(peer_hash_bytes))) { return false; } - datagram[datagram_len++] = peer_hash; + const size_t hash_bytes = payloadHashBytes(profile); + if (hash_bytes + kMeshcorePubKeySize + cipher_len > sizeof(datagram)) + { + return false; + } + memcpy(datagram + datagram_len, peer_hash_bytes, hash_bytes); + datagram_len += hash_bytes; memcpy(datagram + datagram_len, identity_.publicKey(), kMeshcorePubKeySize); datagram_len += kMeshcorePubKeySize; memcpy(datagram + datagram_len, cipher, cipher_len); datagram_len += cipher_len; - const PeerRouteEntry* route = selectPeerRouteByHash(peer_hash, now_ms); + const PeerRouteEntry* route = (profile == PayloadProfile::V2) ? v2_route : v1_route; uint8_t route_type = kRouteTypeFlood; const uint8_t* out_path = nullptr; size_t out_path_len = 0; @@ -556,7 +610,8 @@ bool MeshCoreAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t len, uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(route_type, kPayloadTypeAnonReq, + if (!buildFrameNoTransport(profile, + route_type, kPayloadTypeAnonReq, out_path, out_path_len, datagram, datagram_len, frame, sizeof(frame), &frame_len)) @@ -577,7 +632,9 @@ bool MeshCoreAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t len, if (out_est_timeout) { *out_est_timeout = estimateSendTimeoutMs(frame_len, - (route_type == kRouteTypeDirect) ? out_path_len : 0, + (route_type == kRouteTypeDirect) + ? pathHopCount(profile, out_path_len) + : 0, route_type == kRouteTypeFlood, config_.meshcore_bw_khz, config_.meshcore_sf, @@ -589,12 +646,27 @@ bool MeshCoreAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t len, bool MeshCoreAdapter::sendRawData(const uint8_t* path, size_t path_len, const uint8_t* payload, size_t payload_len, uint32_t* out_est_timeout) +{ + return sendRawDataEx(chat::meshcore::kMeshCorePayloadVer1, + path, path_len, payload, payload_len, out_est_timeout); +} + +bool MeshCoreAdapter::sendRawDataEx(uint8_t raw_profile, const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout) { if (!payload || payload_len == 0 || path_len > kMeshcoreMaxPathSize || (path_len > 0 && !path)) { return false; } + const PayloadProfile profile = raw_profile == chat::meshcore::kMeshCorePayloadVer2 + ? PayloadProfile::V2 + : PayloadProfile::V1; + if (!pathIsWellFormed(profile, path_len)) + { + return false; + } const uint32_t now_ms = millis(); const TxGateReason tx_gate = checkTxGate(now_ms); @@ -605,7 +677,8 @@ bool MeshCoreAdapter::sendRawData(const uint8_t* path, size_t path_len, uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeRawCustom, + if (!buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeRawCustom, path, path_len, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -620,7 +693,7 @@ bool MeshCoreAdapter::sendRawData(const uint8_t* path, size_t path_len, } if (out_est_timeout) { - *out_est_timeout = estimateSendTimeoutMs(frame_len, path_len, false, + *out_est_timeout = estimateSendTimeoutMs(frame_len, pathHopCount(profile, path_len), false, config_.meshcore_bw_khz, config_.meshcore_sf, config_.meshcore_cr); @@ -655,7 +728,12 @@ bool MeshCoreAdapter::sendTracePath(const uint8_t* path, size_t path_len, uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeTrace, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + if (!buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeTrace, nullptr, 0, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -694,7 +772,12 @@ bool MeshCoreAdapter::sendControlData(const uint8_t* payload, size_t payload_len uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeControl, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + if (!buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeControl, nullptr, 0, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -748,7 +831,12 @@ bool MeshCoreAdapter::sendStoredAdvert(const uint8_t* pubkey, size_t len) uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeAdvert, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + if (!buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeAdvert, nullptr, 0, entry->last_advert, entry->last_advert_len, frame, sizeof(frame), &frame_len)) @@ -778,6 +866,14 @@ bool MeshCoreAdapter::sendIdentityAdvertWithLocation(bool broadcast, bool includ bool MeshCoreAdapter::resolveGroupSecret(ChannelId channel, uint8_t out_key16[16], uint8_t out_key32[32], uint8_t* out_hash) const +{ + return resolveGroupSecret(channel, out_key16, out_key32, + PayloadProfile::V1, out_hash, out_hash ? 1 : 0); +} + +bool MeshCoreAdapter::resolveGroupSecret(ChannelId channel, uint8_t out_key16[16], + uint8_t out_key32[32], PayloadProfile profile, + uint8_t* out_hash, size_t out_hash_cap) const { if (!out_key16 || !out_key32) { @@ -815,20 +911,39 @@ bool MeshCoreAdapter::resolveGroupSecret(ChannelId channel, uint8_t out_key16[16 toHmacKey32(out_key16, out_key32); if (out_hash) { - *out_hash = computeChannelHash(out_key16); + if (out_hash_cap < payloadHashBytes(profile) || + !computeChannelHashBytes(out_key16, out_hash, payloadHashBytes(profile))) + { + return false; + } } return true; } ChannelId MeshCoreAdapter::resolveChannelFromHash(uint8_t channel_hash, bool* out_match) const +{ + return resolveChannelFromHash(PayloadProfile::V1, &channel_hash, out_match); +} + +ChannelId MeshCoreAdapter::resolveChannelFromHash(PayloadProfile profile, + const uint8_t* channel_hash, + bool* out_match) const { if (out_match) { *out_match = false; } + if (!channel_hash) + { + return ChannelId::PRIMARY; + } + + const size_t hash_bytes = payloadHashBytes(profile); + uint8_t expected[chat::meshcore::kMeshCoreV2HashBytes] = {}; if (!isZeroKey(config_.primary_key, chat::kMeshCoreChannelKeyLen) && - computeChannelHash(config_.primary_key) == channel_hash) + computeChannelHashBytes(config_.primary_key, expected, hash_bytes) && + memcmp(expected, channel_hash, hash_bytes) == 0) { if (out_match) { @@ -837,7 +952,8 @@ ChannelId MeshCoreAdapter::resolveChannelFromHash(uint8_t channel_hash, bool* ou return ChannelId::PRIMARY; } if (!isZeroKey(config_.secondary_key, chat::kMeshCoreChannelKeyLen) && - computeChannelHash(config_.secondary_key) == channel_hash) + computeChannelHashBytes(config_.secondary_key, expected, hash_bytes) && + memcmp(expected, channel_hash, hash_bytes) == 0) { if (out_match) { @@ -847,7 +963,8 @@ ChannelId MeshCoreAdapter::resolveChannelFromHash(uint8_t channel_hash, bool* ou } if (shouldUsePublicChannelFallback(config_) && - computeChannelHash(publicGroupPsk()) == channel_hash) + computeChannelHashBytes(publicGroupPsk(), expected, hash_bytes) && + memcmp(expected, channel_hash, hash_bytes) == 0) { if (out_match) { @@ -861,32 +978,81 @@ ChannelId MeshCoreAdapter::resolveChannelFromHash(uint8_t channel_hash, bool* ou MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::findPeerRouteByHash(uint8_t peer_hash) { - for (auto& entry : peer_routes_) - { - if (entry.peer_hash == peer_hash) - { - return &entry; - } - } - return nullptr; + return findPeerRouteByHash(PayloadProfile::V1, &peer_hash); } const MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::findPeerRouteByHash(uint8_t peer_hash) const { - for (const auto& entry : peer_routes_) + return findPeerRouteByHash(PayloadProfile::V1, &peer_hash); +} + +MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::findPeerRouteByHash(PayloadProfile profile, + const uint8_t* peer_hash) +{ + if (!peer_hash) { - if (entry.peer_hash == peer_hash) + return nullptr; + } + const size_t hash_bytes = payloadHashBytes(profile); + PeerRouteEntry* low_match = nullptr; + for (auto& entry : peer_routes_) + { + if (entry.peer_hash != lowHashByte(profile, peer_hash)) + { + continue; + } + if (entry.peer_hash_len >= hash_bytes && + memcmp(entry.peer_hash_bytes, peer_hash, hash_bytes) == 0) { return &entry; } + if (!low_match && profile == PayloadProfile::V1) + { + low_match = &entry; + } } - return nullptr; + return low_match; +} + +const MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::findPeerRouteByHash(PayloadProfile profile, + const uint8_t* peer_hash) const +{ + if (!peer_hash) + { + return nullptr; + } + const size_t hash_bytes = payloadHashBytes(profile); + const PeerRouteEntry* low_match = nullptr; + for (const auto& entry : peer_routes_) + { + if (entry.peer_hash != lowHashByte(profile, peer_hash)) + { + continue; + } + if (entry.peer_hash_len >= hash_bytes && + memcmp(entry.peer_hash_bytes, peer_hash, hash_bytes) == 0) + { + return &entry; + } + if (!low_match && profile == PayloadProfile::V1) + { + low_match = &entry; + } + } + return low_match; } MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::selectPeerRouteByHash(uint8_t peer_hash, uint32_t now_ms) { - PeerRouteEntry* entry = findPeerRouteByHash(peer_hash); + return selectPeerRouteByHash(PayloadProfile::V1, &peer_hash, now_ms); +} + +MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::selectPeerRouteByHash(PayloadProfile profile, + const uint8_t* peer_hash, + uint32_t now_ms) +{ + PeerRouteEntry* entry = findPeerRouteByHash(profile, peer_hash); if (!entry) { return nullptr; @@ -900,13 +1066,25 @@ MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::selectPeerRouteByHash(uint8_t { return nullptr; } + if (entry->out_path_profile != profile || + !pathIsWellFormed(profile, entry->out_path_len)) + { + return nullptr; + } return entry; } const MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::selectPeerRouteByHash(uint8_t peer_hash, uint32_t now_ms) const { - const PeerRouteEntry* entry = findPeerRouteByHash(peer_hash); + return selectPeerRouteByHash(PayloadProfile::V1, &peer_hash, now_ms); +} + +const MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::selectPeerRouteByHash(PayloadProfile profile, + const uint8_t* peer_hash, + uint32_t now_ms) const +{ + const PeerRouteEntry* entry = findPeerRouteByHash(profile, peer_hash); if (!entry) { return nullptr; @@ -920,13 +1098,27 @@ const MeshCoreAdapter::PeerRouteEntry* MeshCoreAdapter::selectPeerRouteByHash(ui { return nullptr; } + if (entry->out_path_profile != profile || + !pathIsWellFormed(profile, entry->out_path_len)) + { + return nullptr; + } return entry; } MeshCoreAdapter::PeerRouteEntry& MeshCoreAdapter::upsertPeerRoute(uint8_t peer_hash, uint32_t now_ms) { - if (PeerRouteEntry* found = findPeerRouteByHash(peer_hash)) + return upsertPeerRoute(PayloadProfile::V1, &peer_hash, now_ms); +} + +MeshCoreAdapter::PeerRouteEntry& MeshCoreAdapter::upsertPeerRoute(PayloadProfile profile, + const uint8_t* peer_hash, + uint32_t now_ms) +{ + if (PeerRouteEntry* found = findPeerRouteByHash(profile, peer_hash)) { + copyPeerHashBytes(profile, peer_hash, found->peer_hash_bytes, &found->peer_hash_len); + found->peer_hash = lowHashByte(profile, peer_hash); found->last_seen_ms = now_ms; return *found; } @@ -943,7 +1135,8 @@ MeshCoreAdapter::PeerRouteEntry& MeshCoreAdapter::upsertPeerRoute(uint8_t peer_h peer_routes_.erase(oldest); } PeerRouteEntry entry; - entry.peer_hash = peer_hash; + entry.peer_hash = lowHashByte(profile, peer_hash); + copyPeerHashBytes(profile, peer_hash, entry.peer_hash_bytes, &entry.peer_hash_len); entry.last_seen_ms = now_ms; peer_routes_.push_back(entry); return peer_routes_.back(); @@ -985,6 +1178,7 @@ void MeshCoreAdapter::refreshBestPeerRoute(PeerRouteEntry& entry, uint32_t now_m entry.best_candidate = 0; entry.has_out_path = false; entry.out_path_len = 0; + entry.out_path_profile = PayloadProfile::V1; return; } @@ -994,7 +1188,9 @@ void MeshCoreAdapter::refreshBestPeerRoute(PeerRouteEntry& entry, uint32_t now_m { PeerRouteEntry::PathCandidate& candidate = entry.candidates[i]; const uint32_t age_ms = now_ms - candidate.last_seen_ms; - candidate.quality = computePathQuality(candidate.path_len, + candidate.quality = computePathQuality( + static_cast(pathHopCount(candidate.profile, + candidate.path_len)), candidate.snr_x10, candidate.sample_count, age_ms); @@ -1012,6 +1208,7 @@ void MeshCoreAdapter::refreshBestPeerRoute(PeerRouteEntry& entry, uint32_t now_m memcpy(entry.out_path, best.path, best.path_len); } entry.out_path_len = best.path_len; + entry.out_path_profile = best.profile; entry.has_out_path = true; entry.preferred_channel = best.channel; } @@ -1053,15 +1250,22 @@ void MeshCoreAdapter::prunePeerRoutes(uint32_t now_ms) } void MeshCoreAdapter::rememberPeerPathCandidate(PeerRouteEntry& entry, + PayloadProfile profile, const uint8_t* path, size_t path_len, ChannelId channel, int16_t snr_x10, uint32_t now_ms) { + if (!pathIsWellFormed(profile, path_len)) + { + return; + } uint8_t found_idx = entry.candidate_count; for (uint8_t i = 0; i < entry.candidate_count; ++i) { const PeerRouteEntry::PathCandidate& candidate = entry.candidates[i]; - if (candidate.channel != channel || candidate.path_len != path_len) + if (candidate.channel != channel || + candidate.profile != profile || + candidate.path_len != path_len) { continue; } @@ -1095,6 +1299,7 @@ void MeshCoreAdapter::rememberPeerPathCandidate(PeerRouteEntry& entry, PeerRouteEntry::PathCandidate fresh{}; fresh.path_len = static_cast(path_len); + fresh.profile = profile; if (path_len > 0) { memcpy(fresh.path, path, path_len); @@ -1104,7 +1309,8 @@ void MeshCoreAdapter::rememberPeerPathCandidate(PeerRouteEntry& entry, fresh.sample_count = 1; fresh.first_seen_ms = now_ms; fresh.last_seen_ms = now_ms; - fresh.quality = computePathQuality(fresh.path_len, fresh.snr_x10, fresh.sample_count, 0); + fresh.quality = computePathQuality(static_cast(pathHopCount(profile, fresh.path_len)), + fresh.snr_x10, fresh.sample_count, 0); entry.candidates[found_idx] = fresh; } else @@ -1112,6 +1318,7 @@ void MeshCoreAdapter::rememberPeerPathCandidate(PeerRouteEntry& entry, PeerRouteEntry::PathCandidate& candidate = entry.candidates[found_idx]; candidate.channel = channel; candidate.path_len = static_cast(path_len); + candidate.profile = profile; if (path_len > 0) { memcpy(candidate.path, path, path_len); @@ -1133,7 +1340,8 @@ void MeshCoreAdapter::rememberPeerPathCandidate(PeerRouteEntry& entry, } } candidate.last_seen_ms = now_ms; - candidate.quality = computePathQuality(candidate.path_len, candidate.snr_x10, candidate.sample_count, 0); + candidate.quality = computePathQuality(static_cast(pathHopCount(profile, candidate.path_len)), + candidate.snr_x10, candidate.sample_count, 0); } entry.last_seen_ms = now_ms; @@ -1207,14 +1415,26 @@ NodeId MeshCoreAdapter::resolvePeerNodeId(uint8_t peer_hash) const void MeshCoreAdapter::rememberPeerPath(uint8_t peer_hash, const uint8_t* path, size_t path_len, ChannelId channel, uint32_t now_ms) +{ + rememberPeerPath(PayloadProfile::V1, &peer_hash, path, path_len, channel, now_ms); +} + +void MeshCoreAdapter::rememberPeerPath(PayloadProfile profile, const uint8_t* peer_hash, + const uint8_t* path, size_t path_len, + ChannelId channel, uint32_t now_ms) { if (path_len > kMaxPeerPathLen || (path_len > 0 && !path)) { return; } - PeerRouteEntry& entry = upsertPeerRoute(peer_hash, now_ms); + if (!peer_hash || !pathIsWellFormed(profile, path_len)) + { + return; + } + PeerRouteEntry& entry = upsertPeerRoute(profile, peer_hash, now_ms); uint8_t prev_path[kMaxPeerPathLen] = {}; const uint8_t prev_len = entry.has_out_path ? entry.out_path_len : 0; + const PayloadProfile prev_profile = entry.out_path_profile; const bool had_path = entry.has_out_path; if (had_path && prev_len > 0) { @@ -1223,30 +1443,78 @@ void MeshCoreAdapter::rememberPeerPath(uint8_t peer_hash, const uint8_t* path, s const int16_t snr_x10 = std::isfinite(last_rx_snr_) ? static_cast(std::lround(last_rx_snr_ * 10.0f)) : std::numeric_limits::min(); - rememberPeerPathCandidate(entry, path, path_len, channel, snr_x10, now_ms); + rememberPeerPathCandidate(entry, profile, path, path_len, channel, snr_x10, now_ms); const bool has_path = entry.has_out_path; const uint8_t new_len = entry.out_path_len; const bool changed = (had_path != has_path) || + (entry.out_path_profile != prev_profile) || (new_len != prev_len) || (new_len > 0 && memcmp(entry.out_path, prev_path, new_len) != 0); if (changed) { Event ev{}; ev.type = Event::Type::PathUpdated; - ev.peer_hash = peer_hash; - ev.peer_node = resolvePeerNodeId(peer_hash); + ev.peer_hash = lowHashByte(profile, peer_hash); + ev.peer_node = resolvePeerNodeId(ev.peer_hash); + ev.path_profile = entry.out_path_profile; pushEvent(std::move(ev)); } } +PayloadProfile MeshCoreAdapter::selectSendProfile(const PeerRouteEntry* route) const +{ + switch (config_.meshcore_send_profile) + { + case MeshCorePayloadSendProfile::V1Only: + return PayloadProfile::V1; + case MeshCorePayloadSendProfile::V2Only: + return PayloadProfile::V2; + case MeshCorePayloadSendProfile::AutoPreferV2: + default: + if (route && route->has_out_path && route->out_path_profile == PayloadProfile::V2) + { + return PayloadProfile::V2; + } + return PayloadProfile::V2; + } +} + +bool MeshCoreAdapter::allowsForwardingProfile(PayloadProfile profile) const +{ + switch (config_.meshcore_forward_profile) + { + case MeshCoreForwardProfile::Any: + return true; + case MeshCoreForwardProfile::MultibyteOnly: + default: + return payloadHashBytes(profile) > chat::meshcore::kMeshCoreV1HashBytes; + } +} + +bool MeshCoreAdapter::selfHash(PayloadProfile profile, uint8_t* out_hash, size_t out_cap) const +{ + if (!out_hash || out_cap < payloadHashBytes(profile) || !identity_.isReady()) + { + return false; + } + return copyPublicHash(profile, identity_.publicKey(), MeshCoreIdentity::kPubKeySize, + out_hash, out_cap); +} + bool MeshCoreAdapter::lookupPeerPubKey(uint8_t peer_hash, uint8_t out_pubkey[MeshCoreIdentity::kPubKeySize]) const +{ + return lookupPeerPubKey(PayloadProfile::V1, &peer_hash, out_pubkey); +} + +bool MeshCoreAdapter::lookupPeerPubKey(PayloadProfile profile, const uint8_t* peer_hash, + uint8_t out_pubkey[MeshCoreIdentity::kPubKeySize]) const { if (!out_pubkey) { return false; } - const PeerRouteEntry* entry = findPeerRouteByHash(peer_hash); + const PeerRouteEntry* entry = findPeerRouteByHash(profile, peer_hash); // MeshCore peers can start using ECDH direct payloads immediately after DISCOVER_RESP // (before a signed ADVERT is observed). Accept any learned full pubkey here to remain // interoperable with upstream behavior. @@ -1272,7 +1540,21 @@ void MeshCoreAdapter::rememberPeerPubKey(const uint8_t pubkey[MeshCoreIdentity:: return; } - PeerRouteEntry& entry = upsertPeerRoute(peer_hash, now_ms); + uint8_t peer_hash2[chat::meshcore::kMeshCoreV2HashBytes] = {}; + copyPublicHash(PayloadProfile::V2, pubkey, MeshCoreIdentity::kPubKeySize, + peer_hash2, sizeof(peer_hash2)); + PeerRouteEntry* entry_ptr = findPeerRouteByHash(peer_hash); + if (!entry_ptr || (entry_ptr->has_pubkey && + memcmp(entry_ptr->pubkey, pubkey, MeshCoreIdentity::kPubKeySize) != 0)) + { + entry_ptr = &upsertPeerRoute(PayloadProfile::V2, peer_hash2, now_ms); + } + else + { + copyPeerHashBytes(PayloadProfile::V2, peer_hash2, + entry_ptr->peer_hash_bytes, &entry_ptr->peer_hash_len); + } + PeerRouteEntry& entry = *entry_ptr; const bool changed = !entry.has_pubkey || (memcmp(entry.pubkey, pubkey, MeshCoreIdentity::kPubKeySize) != 0) || (!entry.pubkey_verified && verified); @@ -1336,7 +1618,10 @@ void MeshCoreAdapter::loadPeerPubKeysFromPrefs() continue; } - PeerRouteEntry& entry = upsertPeerRoute(persisted.peer_hash, now_ms); + uint8_t peer_hash2[chat::meshcore::kMeshCoreV2HashBytes] = {}; + copyPublicHash(PayloadProfile::V2, persisted.pubkey, sizeof(persisted.pubkey), + peer_hash2, sizeof(peer_hash2)); + PeerRouteEntry& entry = upsertPeerRoute(PayloadProfile::V2, peer_hash2, now_ms); entry.has_pubkey = true; entry.pubkey_verified = (persisted.flags & kPersistedPeerFlagVerified) != 0; memcpy(entry.pubkey, persisted.pubkey, sizeof(entry.pubkey)); @@ -1460,6 +1745,13 @@ void MeshCoreAdapter::maybeAutoDiscoverMissingPeer(uint8_t peer_hash, uint32_t n bool MeshCoreAdapter::deriveIdentitySecret(uint8_t peer_hash, uint8_t out_key16[16], uint8_t out_key32[32]) const +{ + return deriveIdentitySecret(PayloadProfile::V1, &peer_hash, out_key16, out_key32); +} + +bool MeshCoreAdapter::deriveIdentitySecret(PayloadProfile profile, const uint8_t* peer_hash, + uint8_t out_key16[16], + uint8_t out_key32[32]) const { if (!out_key16 || !out_key32 || !identity_.isReady()) { @@ -1467,7 +1759,7 @@ bool MeshCoreAdapter::deriveIdentitySecret(uint8_t peer_hash, } uint8_t peer_pubkey[MeshCoreIdentity::kPubKeySize] = {}; - if (!lookupPeerPubKey(peer_hash, peer_pubkey)) + if (!lookupPeerPubKey(profile, peer_hash, peer_pubkey)) { return false; } @@ -1485,15 +1777,31 @@ bool MeshCoreAdapter::deriveIdentitySecret(uint8_t peer_hash, bool MeshCoreAdapter::deriveDirectSecret(ChannelId channel, uint8_t peer_hash, uint8_t out_key16[16], uint8_t out_key32[32]) const +{ + return deriveDirectSecret(channel, PayloadProfile::V1, &peer_hash, out_key16, out_key32); +} + +bool MeshCoreAdapter::deriveDirectSecret(ChannelId channel, PayloadProfile profile, + const uint8_t* peer_hash, + uint8_t out_key16[16], uint8_t out_key32[32]) const { (void)channel; - return deriveIdentitySecret(peer_hash, out_key16, out_key32); + return deriveIdentitySecret(profile, peer_hash, out_key16, out_key32); } bool MeshCoreAdapter::tryDecryptPeerPayload(uint8_t src_hash, const uint8_t* cipher, size_t cipher_len, uint8_t* out_plain, size_t* out_plain_len, ChannelId* out_channel) const +{ + return tryDecryptPeerPayload(PayloadProfile::V1, &src_hash, cipher, cipher_len, + out_plain, out_plain_len, out_channel); +} + +bool MeshCoreAdapter::tryDecryptPeerPayload(PayloadProfile profile, const uint8_t* src_hash, + const uint8_t* cipher, size_t cipher_len, + uint8_t* out_plain, size_t* out_plain_len, + ChannelId* out_channel) const { if (!cipher || cipher_len == 0 || !out_plain || !out_plain_len) { @@ -1502,7 +1810,7 @@ bool MeshCoreAdapter::tryDecryptPeerPayload(uint8_t src_hash, ChannelId order[3] = {ChannelId::PRIMARY, ChannelId::SECONDARY, ChannelId::PRIMARY}; size_t order_len = 0; - const PeerRouteEntry* known = findPeerRouteByHash(src_hash); + const PeerRouteEntry* known = findPeerRouteByHash(profile, src_hash); if (known) { order[order_len++] = known->preferred_channel; @@ -1542,7 +1850,8 @@ bool MeshCoreAdapter::tryDecryptPeerPayload(uint8_t src_hash, ++tried; size_t plain_len = 0; - if (!macThenDecrypt(key16, key32, cipher, cipher_len, out_plain, &plain_len)) + if (!macThenDecrypt(key16, key32, cipher, cipher_len, out_plain, &plain_len, + payloadMacBytes(profile))) { return false; } @@ -1559,7 +1868,7 @@ bool MeshCoreAdapter::tryDecryptPeerPayload(uint8_t src_hash, { uint8_t key16[16] = {}; uint8_t key32[32] = {}; - if (deriveDirectSecret(order[i], src_hash, key16, key32) && + if (deriveDirectSecret(order[i], profile, src_hash, key16, key32) && tryCandidate(order[i], key16, key32)) { return true; @@ -1671,7 +1980,7 @@ MeshActionResult MeshCoreAdapter::transmitFrameNowDetailed(const uint8_t* data, if (state == RADIOLIB_ERR_NONE) { ParsedPacket parsed; - if (parsePacket(data, len, &parsed) && parsed.payload_ver == kPayloadVer1) + if (parsePacket(data, len, &parsed) && isSupportedPayloadVersion(parsed.payload_ver)) { const uint32_t packet_sig = packetSignature(parsed.payload_type, parsed.path_len, parsed.payload, parsed.payload_len); @@ -2136,7 +2445,12 @@ bool MeshCoreAdapter::executeProtocolEffect(const runtime::ProtocolEffect& effec { uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - ok = buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeControl, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + ok = buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeControl, nullptr, 0, resp_payload, resp_len, frame, sizeof(frame), &frame_len); @@ -2381,7 +2695,12 @@ MeshActionResult MeshCoreAdapter::sendDiscoverRequestLocalDetailed(const runtime uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeDirect, kPayloadTypeControl, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + if (!buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeControl, nullptr, 0, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -2512,7 +2831,12 @@ MeshActionResult MeshCoreAdapter::sendIdentityAdvertDetailed(bool broadcast, boo uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; const uint8_t route_type = broadcast ? kRouteTypeFlood : kRouteTypeDirect; - if (!buildFrameNoTransport(route_type, kPayloadTypeAdvert, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + if (!buildFrameNoTransport(profile, + route_type, kPayloadTypeAdvert, nullptr, 0, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -2636,7 +2960,12 @@ bool MeshCoreAdapter::exportAdvertFrame(const uint8_t* pubkey, size_t len, uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeFlood, kPayloadTypeAdvert, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + if (!buildFrameNoTransport(profile, + kRouteTypeFlood, kPayloadTypeAdvert, nullptr, 0, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -2659,7 +2988,12 @@ bool MeshCoreAdapter::exportAdvertFrame(const uint8_t* pubkey, size_t len, uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeFlood, kPayloadTypeAdvert, + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + if (!buildFrameNoTransport(profile, + kRouteTypeFlood, kPayloadTypeAdvert, nullptr, 0, entry->last_advert, entry->last_advert_len, frame, sizeof(frame), &frame_len)) @@ -2681,7 +3015,7 @@ bool MeshCoreAdapter::importAdvertFrame(const uint8_t* frame, size_t len) { return false; } - if (parsed.payload_ver != kPayloadVer1 || + if (!isSupportedPayloadVersion(parsed.payload_ver) || parsed.payload_type != kPayloadTypeAdvert || parsed.payload_len < kAdvertMinPayloadSize) { @@ -3034,8 +3368,13 @@ MeshSendResult MeshCoreAdapter::sendTextDetailed(ChannelId channel, const std::s uint8_t channel_key16[16]; uint8_t channel_key32[32]; - uint8_t channel_hash = 0; - if (!resolveGroupSecret(channel, channel_key16, channel_key32, &channel_hash)) + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + uint8_t channel_hash[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!resolveGroupSecret(channel, channel_key16, channel_key32, + profile, channel_hash, sizeof(channel_hash))) { MESHCORE_LOG("[MESHCORE] TX text dropped (no channel secret)\n"); return MeshSendResult::fail(MeshOperationFailure::ChannelKeyMissing); @@ -3051,18 +3390,20 @@ MeshSendResult MeshCoreAdapter::sendTextDetailed(ChannelId channel, const std::s decorated = user_long_name_ + ": " + text; } - constexpr size_t kGroupCipherBudget = - ((kMeshcoreMaxPayloadSize - 1 - kCipherMacSize) / kCipherBlockSize) * kCipherBlockSize; + const size_t channel_hash_len = payloadHashBytes(profile); + const size_t mac_len = payloadMacBytes(profile); + const size_t kGroupCipherBudget = + ((kMeshcoreMaxPayloadSize - channel_hash_len - mac_len) / kCipherBlockSize) * kCipherBlockSize; constexpr size_t kGroupPlainPrefix = kGroupPlainPrefixSize; // ts(4) + txt_type(1) - constexpr size_t kGroupTextBudget = (kGroupCipherBudget > kGroupPlainPrefix) - ? (kGroupCipherBudget - kGroupPlainPrefix) - : 0; + const size_t kGroupTextBudget = (kGroupCipherBudget > kGroupPlainPrefix) + ? (kGroupCipherBudget - kGroupPlainPrefix) + : 0; if (decorated.size() > kGroupTextBudget) { decorated.resize(kGroupTextBudget); } - uint8_t plain[kGroupCipherBudget]; + uint8_t plain[kMeshcoreMaxPayloadSize]; size_t plain_len = 0; uint32_t msg_ts = now_message_timestamp(); memcpy(&plain[plain_len], &msg_ts, sizeof(msg_ts)); @@ -3075,8 +3416,9 @@ MeshSendResult MeshCoreAdapter::sendTextDetailed(ChannelId channel, const std::s uint8_t encrypted[kMeshcoreMaxPayloadSize]; size_t encrypted_len = encryptThenMac(channel_key16, channel_key32, encrypted, sizeof(encrypted), - plain, plain_len); - if (encrypted_len == 0 || encrypted_len > (kMeshcoreMaxPayloadSize - 1)) + plain, plain_len, + mac_len); + if (encrypted_len == 0 || encrypted_len > (kMeshcoreMaxPayloadSize - channel_hash_len)) { MESHCORE_LOG("[MESHCORE] TX text dropped (encrypt fail) ch=%u plain_len=%u\n", static_cast(channel), @@ -3086,9 +3428,10 @@ MeshSendResult MeshCoreAdapter::sendTextDetailed(ChannelId channel, const std::s uint8_t buffer[256]; size_t index = 0; - buffer[index++] = buildHeader(kRouteTypeFlood, kPayloadTypeGrpTxt, kPayloadVer1); + buffer[index++] = buildHeader(kRouteTypeFlood, kPayloadTypeGrpTxt, payloadVersion(profile)); buffer[index++] = 0; // path_len = 0 - buffer[index++] = channel_hash; + memcpy(buffer + index, channel_hash, channel_hash_len); + index += channel_hash_len; memcpy(&buffer[index], encrypted, encrypted_len); index += encrypted_len; @@ -3130,7 +3473,24 @@ MeshActionResult MeshCoreAdapter::sendDirectTextDetailed(ChannelId channel, cons rememberPeerNodeId(peer_hash, peer, now_ms); const PeerRouteEntry* peer_route = findPeerRouteByHash(peer_hash); - const PeerRouteEntry* selected_route = selectPeerRouteByHash(peer_hash, now_ms); + const PayloadProfile profile = selectSendProfile(peer_route); + uint8_t peer_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (profile == PayloadProfile::V2) + { + if (peer_route && peer_route->has_pubkey) + { + copyPublicHash(profile, peer_route->pubkey, sizeof(peer_route->pubkey), + peer_hash_bytes, sizeof(peer_hash_bytes)); + } + } + else + { + peer_hash_bytes[0] = peer_hash; + } + const PeerRouteEntry* selected_route = + (profile == PayloadProfile::V2 && peer_route && peer_route->has_pubkey) + ? selectPeerRouteByHash(profile, peer_hash_bytes, now_ms) + : selectPeerRouteByHash(peer_hash, now_ms); chat::runtime::MeshCoreDirectRouteFacts route_facts{}; route_facts.identity_ready = identity_.isReady(); route_facts.has_peer_route = peer_route != nullptr; @@ -3157,17 +3517,19 @@ MeshActionResult MeshCoreAdapter::sendDirectTextDetailed(ChannelId channel, cons uint8_t peer_key16[16]; uint8_t peer_key32[32]; - if (!deriveDirectSecret(route_decision.tx_channel, peer_hash, peer_key16, peer_key32)) + if (!deriveDirectSecret(route_decision.tx_channel, profile, peer_hash_bytes, peer_key16, peer_key32)) { return MeshActionResult::fail(MeshOperationFailure::PeerKeyMissing); } constexpr size_t kDirectPlainPrefixSize = 5; // ts(4) + flags(1) - constexpr size_t kDirectCipherBudget = - ((kMeshcoreMaxPayloadSize - 2 - kCipherMacSize) / kCipherBlockSize) * kCipherBlockSize; - constexpr size_t kDirectTextBudget = (kDirectCipherBudget > kDirectPlainPrefixSize) - ? (kDirectCipherBudget - kDirectPlainPrefixSize) - : 0; + const size_t hash_bytes = payloadHashBytes(profile); + const size_t mac_len = payloadMacBytes(profile); + const size_t kDirectCipherBudget = + ((kMeshcoreMaxPayloadSize - (hash_bytes * 2U) - mac_len) / kCipherBlockSize) * kCipherBlockSize; + const size_t kDirectTextBudget = (kDirectCipherBudget > kDirectPlainPrefixSize) + ? (kDirectCipherBudget - kDirectPlainPrefixSize) + : 0; std::string body = text; if (body.size() > kDirectTextBudget) @@ -3175,7 +3537,7 @@ MeshActionResult MeshCoreAdapter::sendDirectTextDetailed(ChannelId channel, cons body.resize(kDirectTextBudget); } - uint8_t plain[kDirectCipherBudget]; + uint8_t plain[kMeshcoreMaxPayloadSize]; size_t plain_len = 0; uint32_t msg_ts = now_message_timestamp(); memcpy(&plain[plain_len], &msg_ts, sizeof(msg_ts)); @@ -3195,7 +3557,9 @@ MeshActionResult MeshCoreAdapter::sendDirectTextDetailed(ChannelId channel, cons uint8_t payload[kMeshcoreMaxPayloadSize]; size_t payload_len = 0; - if (!buildPeerDatagramPayload(peer_hash, self_hash_, + uint8_t self_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!selfHash(profile, self_hash_bytes, sizeof(self_hash_bytes)) || + !buildPeerDatagramPayload(profile, peer_hash_bytes, self_hash_bytes, peer_key16, peer_key32, plain, plain_len, payload, sizeof(payload), &payload_len)) @@ -3206,7 +3570,8 @@ MeshActionResult MeshCoreAdapter::sendDirectTextDetailed(ChannelId channel, cons uint8_t frame[kMeshcoreMaxFrameSize]; size_t frame_len = 0; - if (!buildFrameNoTransport(route_type, kPayloadTypeTxtMsg, + if (!buildFrameNoTransport(profile, + route_type, kPayloadTypeTxtMsg, out_path, out_path_len, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -3234,7 +3599,9 @@ MeshActionResult MeshCoreAdapter::sendDirectTextDetailed(ChannelId channel, cons if (out_timeout) { *out_timeout = estimateSendTimeoutMs(frame_len, - (route_type == kRouteTypeDirect) ? out_path_len : 0, + (route_type == kRouteTypeDirect) + ? pathHopCount(profile, out_path_len) + : 0, route_type == kRouteTypeFlood, config_.meshcore_bw_khz, config_.meshcore_sf, @@ -3305,7 +3672,24 @@ bool MeshCoreAdapter::sendAppData(ChannelId channel, uint32_t portnum, rememberPeerNodeId(peer_hash, dest, now_ms); const PeerRouteEntry* peer_route = findPeerRouteByHash(peer_hash); - const PeerRouteEntry* selected_route = selectPeerRouteByHash(peer_hash, now_ms); + const PayloadProfile profile = selectSendProfile(peer_route); + uint8_t peer_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (profile == PayloadProfile::V2) + { + if (peer_route && peer_route->has_pubkey) + { + copyPublicHash(profile, peer_route->pubkey, sizeof(peer_route->pubkey), + peer_hash_bytes, sizeof(peer_hash_bytes)); + } + } + else + { + peer_hash_bytes[0] = peer_hash; + } + const PeerRouteEntry* selected_route = + (profile == PayloadProfile::V2 && peer_route && peer_route->has_pubkey) + ? selectPeerRouteByHash(profile, peer_hash_bytes, now_ms) + : selectPeerRouteByHash(peer_hash, now_ms); chat::runtime::MeshCoreDirectRouteFacts route_facts{}; route_facts.identity_ready = identity_.isReady(); route_facts.has_peer_route = peer_route != nullptr; @@ -3336,7 +3720,7 @@ bool MeshCoreAdapter::sendAppData(ChannelId channel, uint32_t portnum, uint8_t peer_key16[16]; uint8_t peer_key32[32]; - if (!deriveDirectSecret(route_decision.tx_channel, peer_hash, peer_key16, peer_key32)) + if (!deriveDirectSecret(route_decision.tx_channel, profile, peer_hash_bytes, peer_key16, peer_key32)) { MESHCORE_LOG("[MESHCORE] TX direct app-data dropped (no peer secret) peer=%08lX port=%u\n", static_cast(dest), @@ -3351,6 +3735,16 @@ bool MeshCoreAdapter::sendAppData(ChannelId channel, uint32_t portnum, core_context.channel_key = ::mesh::ByteView{peer_key32, sizeof(peer_key32)}; core_context.route_type = route_type; core_context.route_path = ::mesh::ByteView{out_path, out_path_len}; + core_context.meshcore_payload_ver = payloadVersion(profile); + core_context.meshcore_peer_hash = + ::mesh::ByteView{peer_hash_bytes, payloadHashBytes(profile)}; + uint8_t self_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!selfHash(profile, self_hash_bytes, sizeof(self_hash_bytes))) + { + return false; + } + core_context.meshcore_local_hash = + ::mesh::ByteView{self_hash_bytes, payloadHashBytes(profile)}; ::mesh::DirectMessageCommand core_command{ ::mesh::NodeId{dest}, @@ -3397,8 +3791,13 @@ bool MeshCoreAdapter::sendAppData(ChannelId channel, uint32_t portnum, uint8_t channel_key16[16]; uint8_t channel_key32[32]; - uint8_t channel_hash = 0; - if (!resolveGroupSecret(channel, channel_key16, channel_key32, &channel_hash)) + const PayloadProfile profile = + (config_.meshcore_send_profile == MeshCorePayloadSendProfile::V1Only) + ? PayloadProfile::V1 + : PayloadProfile::V2; + uint8_t channel_hash[chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!resolveGroupSecret(channel, channel_key16, channel_key32, + profile, channel_hash, sizeof(channel_hash))) { MESHCORE_LOG("[MESHCORE] TX group app-data dropped (no channel secret) ch=%u port=%u\n", static_cast(channel), @@ -3411,7 +3810,10 @@ bool MeshCoreAdapter::sendAppData(ChannelId channel, uint32_t portnum, ::mesh::ProtocolBuildContext core_context{}; core_context.local_node = ::mesh::NodeId{node_id_}; core_context.channel_key = ::mesh::ByteView{channel_key32, sizeof(channel_key32)}; - core_context.channel_hash = channel_hash; + core_context.channel_hash = channel_hash[0]; + core_context.meshcore_payload_ver = payloadVersion(profile); + core_context.meshcore_channel_hash = + ::mesh::ByteView{channel_hash, payloadHashBytes(profile)}; ::mesh::DirectMessageCommand core_command{ ::mesh::NodeId{}, @@ -3456,6 +3858,16 @@ void MeshCoreAdapter::applyConfig(const MeshConfig& config) config_.meshcore_airtime_factor = clampValue(config_.meshcore_airtime_factor, 0.0f, 9.0f); config_.meshcore_flood_max = clampValue(config_.meshcore_flood_max, 0, 64); config_.meshcore_channel_slot = clampValue(config_.meshcore_channel_slot, 0, 14); + if (static_cast(config_.meshcore_send_profile) > + static_cast(MeshCorePayloadSendProfile::V2Only)) + { + config_.meshcore_send_profile = MeshCorePayloadSendProfile::AutoPreferV2; + } + if (static_cast(config_.meshcore_forward_profile) > + static_cast(MeshCoreForwardProfile::MultibyteOnly)) + { + config_.meshcore_forward_profile = MeshCoreForwardProfile::MultibyteOnly; + } if (config_.meshcore_channel_name[0] == '\0') { @@ -3487,7 +3899,7 @@ void MeshCoreAdapter::applyConfig(const MeshConfig& config) const uint8_t secondary_hash = has_secondary_key ? computeChannelHash(config_.secondary_key) : 0xFF; const bool has_public = shouldUsePublicChannelFallback(config_); const uint8_t public_hash = has_public ? computeChannelHash(publicGroupPsk()) : 0xFF; - MESHCORE_LOG("[MESHCORE] apply cfg preset=%u freq=%.3f bw=%.3f sf=%u cr=%u(4/%u) txp=%d tx_en=%u repeat=%u flood_max=%u multi_acks=%u slot=%u ch='%s' hash[p=%02X s=%02X pub=%02X] identity[ready=%u self=%02X]\n", + MESHCORE_LOG("[MESHCORE] apply cfg preset=%u freq=%.3f bw=%.3f sf=%u cr=%u(4/%u) txp=%d tx_en=%u repeat=%u flood_max=%u multi_acks=%u send_profile=%u fwd_profile=%u slot=%u ch='%s' hash[p=%02X s=%02X pub=%02X] identity[ready=%u self=%02X]\n", static_cast(config_.meshcore_region_preset), static_cast(config_.meshcore_freq_mhz), static_cast(config_.meshcore_bw_khz), @@ -3499,6 +3911,8 @@ void MeshCoreAdapter::applyConfig(const MeshConfig& config) config_.meshcore_client_repeat ? 1U : 0U, static_cast(config_.meshcore_flood_max), config_.meshcore_multi_acks ? 1U : 0U, + static_cast(static_cast(config_.meshcore_send_profile)), + static_cast(static_cast(config_.meshcore_forward_profile)), static_cast(config_.meshcore_channel_slot), config_.meshcore_channel_name, static_cast(primary_hash), @@ -3655,16 +4069,30 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, toHex(data, size).c_str()); return; } - if (parsed.payload_ver != kPayloadVer1) + if (!isSupportedPayloadVersion(parsed.payload_ver)) { - MESHCORE_LOG("[MESHCORE] RX drop payload ver=%u (want=%u) len=%u type=%u route=%u\n", + MESHCORE_LOG("[MESHCORE] RX drop payload ver=%u len=%u type=%u route=%u\n", static_cast(parsed.payload_ver), - static_cast(kPayloadVer1), static_cast(size), static_cast(parsed.payload_type), static_cast(parsed.route_type)); return; } + const PayloadProfile profile = payloadProfileFromVersion(parsed.payload_ver); + const size_t hash_bytes = payloadHashBytes(profile); + const size_t mac_len = payloadMacBytes(profile); + const bool trace_path = (parsed.payload_type == kPayloadTypeTrace); + if (!trace_path && !pathIsWellFormed(profile, parsed.path_len)) + { + MESHCORE_LOG("[MESHCORE] RX drop malformed path ver=%u path_len=%u hash_bytes=%u type=%u route=%u\n", + static_cast(parsed.payload_ver), + static_cast(parsed.path_len), + static_cast(hash_bytes), + static_cast(parsed.payload_type), + static_cast(parsed.route_type)); + return; + } + const size_t path_hops = trace_path ? parsed.path_len : pathHopCount(profile, parsed.path_len); const uint32_t now_ms = millis(); prunePendingAppAcks(now_ms); @@ -3725,7 +4153,8 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, { uint8_t frame[kMeshcoreMaxFrameSize]; size_t frame_len = 0; - if (!buildFrameNoTransport(kRouteTypeDirect, payload_type, + if (!buildFrameNoTransport(profile, + kRouteTypeDirect, payload_type, path, path_len, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -3967,6 +4396,9 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, return; } + uint8_t self_hash_bytes[chat::meshcore::kMeshCoreV2HashBytes] = {}; + const bool have_self_hash = selfHash(profile, self_hash_bytes, sizeof(self_hash_bytes)); + // Direct routing hop forwarding: only the addressed next-hop should retransmit. if (is_direct_route && parsed.path_len > 0) { @@ -3986,18 +4418,20 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, } } - if (parsed.path[0] != self_hash_) + if (!have_self_hash || + parsed.path_len < hash_bytes || + memcmp(parsed.path, self_hash_bytes, hash_bytes) != 0) { return; } - if (!config_.meshcore_client_repeat || size <= 2) + if (!config_.meshcore_client_repeat || !allowsForwardingProfile(profile) || size <= 2) { return; } const size_t payload_start = static_cast(parsed.payload - data); - const size_t new_path_len = parsed.path_len - 1; + const size_t new_path_len = parsed.path_len - hash_bytes; auto copyForwardPath = [&](uint8_t* out_path) -> void { @@ -4005,7 +4439,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, { return; } - memcpy(out_path, parsed.path + 1, new_path_len); + memcpy(out_path, parsed.path + hash_bytes, new_path_len); }; if (parsed.payload_type == kPayloadTypeAck && parsed.payload_len >= sizeof(uint32_t)) @@ -4038,12 +4472,12 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, return; } - std::vector fwd(size - 1); + std::vector fwd(size - hash_bytes); memcpy(fwd.data(), data, parsed.path_len_index); fwd[parsed.path_len_index] = static_cast(new_path_len); if (new_path_len > 0) { - memcpy(&fwd[parsed.path_len_index + 1], parsed.path + 1, new_path_len); + memcpy(&fwd[parsed.path_len_index + 1], parsed.path + hash_bytes, new_path_len); } const size_t new_payload_start = parsed.path_len_index + 1 + new_path_len; memcpy(&fwd[new_payload_start], &data[payload_start], size - payload_start); @@ -4076,35 +4510,38 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, }; const bool is_anon_req_payload = (parsed.payload_type == kPayloadTypeAnonReq) && - isAnonReqCipherShape(parsed.payload_len) && - !isPeerCipherShape(parsed.payload_len); + isAnonReqCipherShape(profile, parsed.payload_len) && + !isPeerCipherShape(profile, parsed.payload_len); const bool is_peer_payload_candidate = isPeerPayloadType(parsed.payload_type) && - isPeerCipherShape(parsed.payload_len) && + isPeerCipherShape(profile, parsed.payload_len) && !is_anon_req_payload; const bool flood_peer_for_self = is_flood_route && is_peer_payload_candidate && - parsed.payload_len >= 1 && - parsed.payload[0] == self_hash_; + parsed.payload_len >= hash_bytes && + have_self_hash && + memcmp(parsed.payload, self_hash_bytes, hash_bytes) == 0; const bool flood_anon_for_self = is_flood_route && is_anon_req_payload && - parsed.payload_len >= 1 && - parsed.payload[0] == self_hash_; + parsed.payload_len >= hash_bytes && + have_self_hash && + memcmp(parsed.payload, self_hash_bytes, hash_bytes) == 0; const bool is_multipart_ack = (parsed.payload_type == kPayloadTypeMultipart && parsed.payload_len > 0 && ((parsed.payload[0] & 0x0F) == kPayloadTypeAck)); if (config_.meshcore_client_repeat && is_flood_route && + allowsForwardingProfile(profile) && !flood_peer_for_self && !flood_anon_for_self && !is_multipart_ack && shouldFloodRepeatPayload(parsed.payload_type) && - parsed.path_len < config_.meshcore_flood_max && - (parsed.path_len + kMeshcorePathHashSize) <= kMeshcoreMaxPathSize && - (size + kMeshcorePathHashSize) <= kMeshcoreMaxFrameSize) + path_hops < config_.meshcore_flood_max && + (parsed.path_len + hash_bytes) <= kMeshcoreMaxPathSize && + (size + hash_bytes) <= kMeshcoreMaxFrameSize) { bool self_in_path = false; - for (size_t i = 0; i < parsed.path_len; ++i) + for (size_t i = 0; i + hash_bytes <= parsed.path_len; i += hash_bytes) { - if (parsed.path[i] == self_hash_) + if (memcmp(parsed.path + i, self_hash_bytes, hash_bytes) == 0) { self_in_path = true; break; @@ -4116,11 +4553,11 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, const size_t path_start = parsed.path_len_index + 1; const size_t payload_start = static_cast(parsed.payload - data); - std::vector fwd(size + kMeshcorePathHashSize); + std::vector fwd(size + hash_bytes); memcpy(fwd.data(), data, path_start + parsed.path_len); - fwd[parsed.path_len_index] = static_cast(parsed.path_len + 1); - fwd[path_start + parsed.path_len] = self_hash_; - memcpy(&fwd[path_start + parsed.path_len + 1], + fwd[parsed.path_len_index] = static_cast(parsed.path_len + hash_bytes); + memcpy(&fwd[path_start + parsed.path_len], self_hash_bytes, hash_bytes); + memcpy(&fwd[path_start + parsed.path_len + hash_bytes], &data[payload_start], size - payload_start); @@ -4140,34 +4577,34 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, enqueueScheduled(fwd.data(), fwd.size(), total_delay); MESHCORE_LOG("[MESHCORE] REPEAT queued path=%u->%u flood_max=%u delay=%lu\n", - static_cast(parsed.path_len), - static_cast(parsed.path_len + 1), + static_cast(path_hops), + static_cast(path_hops + 1), static_cast(config_.meshcore_flood_max), static_cast(total_delay)); } } const bool is_peer_payload = isPeerPayloadType(parsed.payload_type) && - isPeerCipherShape(parsed.payload_len) && + isPeerCipherShape(profile, parsed.payload_len) && !is_anon_req_payload; const bool is_group_text_payload = (parsed.payload_type == kPayloadTypeGrpTxt && - parsed.payload_len > (1 + kCipherMacSize)); + parsed.payload_len > (hash_bytes + mac_len)); const bool is_group_data_payload = (parsed.payload_type == kPayloadTypeGrpData && - parsed.payload_len > (1 + kCipherMacSize)); + parsed.payload_len > (hash_bytes + mac_len)); const bool is_raw_payload = (parsed.payload_type == kPayloadTypeRawCustom && parsed.payload_len > sizeof(uint32_t)); if (config_.tx_enabled && is_raw_payload) { uint8_t ack_frame[6] = {}; - ack_frame[0] = buildHeader(kRouteTypeFlood, kPayloadTypeAck, kPayloadVer1); + ack_frame[0] = buildHeader(kRouteTypeFlood, kPayloadTypeAck, payloadVersion(profile)); ack_frame[1] = 0; memcpy(&ack_frame[2], &frame_sig, sizeof(frame_sig)); if (config_.meshcore_multi_acks) { uint8_t multi_ack[7] = {}; - multi_ack[0] = buildHeader(kRouteTypeFlood, kPayloadTypeMultipart, kPayloadVer1); + multi_ack[0] = buildHeader(kRouteTypeFlood, kPayloadTypeMultipart, payloadVersion(profile)); multi_ack[1] = 0; multi_ack[2] = static_cast((1U << 4) | kPayloadTypeAck); memcpy(&multi_ack[3], &frame_sig, sizeof(frame_sig)); @@ -4196,13 +4633,16 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, } meta.origin = RxOrigin::Mesh; meta.direct = direct; - meta.hop_count = (parsed.path_len <= 255) ? static_cast(parsed.path_len) : 0xFF; + meta.hop_count = (path_hops <= 255) ? static_cast(path_hops) : 0xFF; meta.hop_limit = 0xFF; meta.wire_flags = data[0]; if (parsed.path_len > 0) { meta.next_hop = parsed.path[0]; - meta.relay_node = parsed.path[parsed.path_len - 1]; + const size_t relay_offset = !trace_path && parsed.path_len >= hash_bytes + ? parsed.path_len - hash_bytes + : parsed.path_len - 1; + meta.relay_node = parsed.path[relay_offset]; } if (std::isfinite(last_rx_rssi_)) { @@ -4214,7 +4654,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, } }; - auto sendPeerDatagram = [&](uint8_t payload_type, uint8_t dest_hash, ChannelId channel, + auto sendPeerDatagram = [&](uint8_t payload_type, const uint8_t* dest_hash, ChannelId channel, const uint8_t* plain, size_t plain_len, uint8_t route_type, const uint8_t* route_path, size_t route_path_len, uint32_t delay_ms) -> bool @@ -4226,14 +4666,15 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, uint8_t key16[16]; uint8_t key32[32]; - if (!deriveDirectSecret(channel, dest_hash, key16, key32)) + if (!deriveDirectSecret(channel, profile, dest_hash, key16, key32)) { return false; } uint8_t payload[kMeshcoreMaxPayloadSize]; size_t payload_len = 0; - if (!buildPeerDatagramPayload(dest_hash, self_hash_, + if (!have_self_hash || + !buildPeerDatagramPayload(profile, dest_hash, self_hash_bytes, key16, key32, plain, plain_len, payload, sizeof(payload), &payload_len)) @@ -4243,7 +4684,8 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, uint8_t frame[kMeshcoreMaxFrameSize]; size_t frame_len = 0; - if (!buildFrameNoTransport(route_type, payload_type, + if (!buildFrameNoTransport(profile, + route_type, payload_type, route_path, route_path_len, payload, payload_len, frame, sizeof(frame), &frame_len)) @@ -4264,7 +4706,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, return enqueueScheduled(frame, frame_len, 50); }; - auto sendPathReturn = [&](uint8_t dest_hash, ChannelId channel, + auto sendPathReturn = [&](const uint8_t* dest_hash, ChannelId channel, const uint8_t* return_path, size_t return_path_len, uint8_t route_type, const uint8_t* route_path, size_t route_path_len, uint8_t extra_type, const uint8_t* extra, size_t extra_len, @@ -4284,7 +4726,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, delay_ms); }; - auto sendPeerAck = [&](uint8_t src_hash, ChannelId channel, uint32_t ack_value) -> void + auto sendPeerAck = [&](const uint8_t* src_hash, ChannelId channel, uint32_t ack_value) -> void { if (!config_.tx_enabled) { @@ -4304,7 +4746,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, return; } - const PeerRouteEntry* route = selectPeerRouteByHash(src_hash, now_ms); + const PeerRouteEntry* route = selectPeerRouteByHash(profile, src_hash, now_ms); uint8_t route_type = kRouteTypeFlood; const uint8_t* route_path = nullptr; size_t route_path_len = 0; @@ -4324,8 +4766,8 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, if (is_anon_req_payload) { - const uint8_t dest_hash = parsed.payload[0]; - if (dest_hash != self_hash_) + const uint8_t* dest_hash = parsed.payload; + if (!have_self_hash || memcmp(dest_hash, self_hash_bytes, hash_bytes) != 0) { return; } @@ -4337,16 +4779,17 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, return; } - const uint8_t* sender_pubkey = parsed.payload + 1; - const uint8_t src_hash = sender_pubkey[0]; - const uint8_t* cipher = parsed.payload + 1 + kMeshcorePubKeySize; - const size_t cipher_len = parsed.payload_len - 1 - kMeshcorePubKeySize; + const uint8_t* sender_pubkey = parsed.payload + hash_bytes; + uint8_t src_hash[chat::meshcore::kMeshCoreV2HashBytes] = {}; + copyPublicHash(profile, sender_pubkey, kMeshcorePubKeySize, src_hash, sizeof(src_hash)); + const uint8_t* cipher = parsed.payload + hash_bytes + kMeshcorePubKeySize; + const size_t cipher_len = parsed.payload_len - hash_bytes - kMeshcorePubKeySize; uint8_t shared_secret[kCipherHmacKeySize] = {}; if (!identity_.deriveSharedSecret(sender_pubkey, shared_secret)) { MESHCORE_LOG("[MESHCORE] RX ANON_REQ drop (shared secret failed) src=%02X\n", - static_cast(src_hash)); + static_cast(src_hash[0])); return; } @@ -4357,16 +4800,16 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, key32, sizeof(key32))) { MESHCORE_LOG("[MESHCORE] RX ANON_REQ drop (shared secret expand failed) src=%02X\n", - static_cast(src_hash)); + static_cast(src_hash[0])); return; } uint8_t plain[kMeshcoreMaxPayloadSize] = {}; size_t plain_len = 0; - if (!macThenDecrypt(key16, key32, cipher, cipher_len, plain, &plain_len)) + if (!macThenDecrypt(key16, key32, cipher, cipher_len, plain, &plain_len, mac_len)) { MESHCORE_LOG("[MESHCORE] RX ANON_REQ decrypt failed src=%02X len=%u\n", - static_cast(src_hash), + static_cast(src_hash[0]), static_cast(parsed.payload_len)); return; } @@ -4374,7 +4817,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, const NodeId sender_node = deriveNodeIdFromPubkey(sender_pubkey, kMeshcorePubKeySize); rememberPeerPubKey(sender_pubkey, now_ms, true); - rememberPeerNodeId(src_hash, sender_node, now_ms); + rememberPeerNodeId(src_hash[0], sender_node, now_ms); uint32_t tag = 0; if (plain_len >= sizeof(tag)) @@ -4382,7 +4825,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, memcpy(&tag, plain, sizeof(tag)); } MESHCORE_LOG("[MESHCORE] RX ANON_REQ src=%02X node=%08lX len=%u route=%s tag=%08lX\n", - static_cast(src_hash), + static_cast(src_hash[0]), static_cast(sender_node), static_cast(plain_len), is_flood_route ? "flood" : "direct", @@ -4392,9 +4835,9 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, if (is_peer_payload) { - const uint8_t dest_hash = parsed.payload[0]; - const uint8_t src_hash = parsed.payload[1]; - if (dest_hash != self_hash_) + const uint8_t* dest_hash = parsed.payload; + const uint8_t* src_hash = parsed.payload + hash_bytes; + if (!have_self_hash || memcmp(dest_hash, self_hash_bytes, hash_bytes) != 0) { return; } @@ -4402,29 +4845,30 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, uint8_t plain[kMeshcoreMaxPayloadSize]; size_t plain_len = 0; ChannelId peer_channel = ChannelId::PRIMARY; - if (!tryDecryptPeerPayload(src_hash, - parsed.payload + 2, parsed.payload_len - 2, + if (!tryDecryptPeerPayload(profile, src_hash, + parsed.payload + (hash_bytes * 2U), + parsed.payload_len - (hash_bytes * 2U), plain, &plain_len, &peer_channel)) { - const PeerRouteEntry* route = findPeerRouteByHash(src_hash); + const PeerRouteEntry* route = findPeerRouteByHash(profile, src_hash); const bool has_pubkey = (route && route->has_pubkey); MESHCORE_LOG("[MESHCORE] RX peer decrypt failed type=%u src=%02X has_pubkey=%u\n", static_cast(parsed.payload_type), - static_cast(src_hash), + static_cast(src_hash[0]), has_pubkey ? 1U : 0U); if (!has_pubkey) { - maybeAutoDiscoverMissingPeer(src_hash, now_ms); + maybeAutoDiscoverMissingPeer(src_hash[0], now_ms); } return; } plain_len = trimTrailingZeros(plain, plain_len); // Do not infer zero-hop route from direct packets that already traversed a path. // Route candidates should be learned from PATH payloads (or flood-derived returns). - PeerRouteEntry& route = upsertPeerRoute(src_hash, now_ms); + PeerRouteEntry& route = upsertPeerRoute(profile, src_hash, now_ms); route.preferred_channel = peer_channel; - const NodeId from_node = resolvePeerNodeId(src_hash); + const NodeId from_node = resolvePeerNodeId(src_hash[0]); if (parsed.payload_type == kPayloadTypeTxtMsg && plain_len > 5) { @@ -4449,7 +4893,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, uint32_t ack_value = packet_sig; uint8_t sender_pubkey[kMeshcorePubKeySize] = {}; - if (lookupPeerPubKey(src_hash, sender_pubkey)) + if (lookupPeerPubKey(profile, src_hash, sender_pubkey)) { size_t text_len = 0; const size_t text_cap = plain_len - 5; @@ -4466,7 +4910,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, else { MESHCORE_LOG("[MESHCORE] RX text ACK fallback src=%02X reason=no_pubkey\n", - static_cast(src_hash)); + static_cast(src_hash[0])); } sendPeerAck(src_hash, peer_channel, ack_value); return; @@ -4509,7 +4953,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, else { MESHCORE_LOG("[MESHCORE] RX signed text ACK fallback src=%02X reason=no_identity\n", - static_cast(src_hash)); + static_cast(src_hash[0])); } sendPeerAck(src_hash, peer_channel, ack_value); return; @@ -4563,7 +5007,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, } else { - const PeerRouteEntry* route = selectPeerRouteByHash(src_hash, now_ms); + const PeerRouteEntry* route = selectPeerRouteByHash(profile, src_hash, now_ms); uint8_t route_type = kRouteTypeFlood; const uint8_t* route_path = nullptr; size_t route_path_len = 0; @@ -4584,12 +5028,12 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, if (parsed.payload_type == kPayloadTypeResponse) { MESHCORE_LOG("[MESHCORE] RX RESPONSE src=%02X len=%u route=%s\n", - static_cast(src_hash), + static_cast(src_hash[0]), static_cast(plain_len), is_flood_route ? "flood" : "direct"); Event ev{}; ev.type = Event::Type::Response; - ev.peer_hash = src_hash; + ev.peer_hash = src_hash[0]; ev.peer_node = from_node; ev.payload.assign(plain, plain + plain_len); pushEvent(std::move(ev)); @@ -4608,9 +5052,9 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, const uint8_t* extra = &plain[index]; const size_t extra_len = plain_len - index; - rememberPeerPath(src_hash, out_path, out_path_len, peer_channel, now_ms); + rememberPeerPath(profile, src_hash, out_path, out_path_len, peer_channel, now_ms); MESHCORE_LOG("[MESHCORE] RX PATH src=%02X out_path_len=%u extra_type=%u extra_len=%u route=%s\n", - static_cast(src_hash), + static_cast(src_hash[0]), static_cast(out_path_len), static_cast(extra_type), static_cast(extra_len), @@ -4627,11 +5071,11 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, else if (extra_type == kPayloadTypeResponse && extra_len > 0) { MESHCORE_LOG("[MESHCORE] RX PATH/RESPONSE src=%02X len=%u\n", - static_cast(src_hash), + static_cast(src_hash[0]), static_cast(extra_len)); Event ev{}; ev.type = Event::Type::PathResponse; - ev.peer_hash = src_hash; + ev.peer_hash = src_hash[0]; ev.peer_node = from_node; if (extra_len >= sizeof(uint32_t)) { @@ -4659,7 +5103,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, } } - auto logUnknownGroupHash = [&](const char* kind, uint8_t channel_hash) -> void + auto logUnknownGroupHash = [&](const char* kind, const uint8_t* channel_hash) -> void { const bool has_primary_key = !isZeroKey(config_.primary_key, chat::kMeshCoreChannelKeyLen); const bool has_secondary_key = !isZeroKey(config_.secondary_key, chat::kMeshCoreChannelKeyLen); @@ -4667,9 +5111,9 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, const uint8_t secondary_hash = has_secondary_key ? computeChannelHash(config_.secondary_key) : 0xFF; const bool has_public = shouldUsePublicChannelFallback(config_); const uint8_t public_hash = has_public ? computeChannelHash(publicGroupPsk()) : 0xFF; - MESHCORE_LOG("[MESHCORE] RX group %s drop unknown hash=%02X local[p=%02X s=%02X pub=%02X]\n", + MESHCORE_LOG("[MESHCORE] RX group %s drop unknown hash=%s local[p=%02X s=%02X pub=%02X]\n", kind, - static_cast(channel_hash), + toHex(channel_hash, hash_bytes).c_str(), static_cast(primary_hash), static_cast(secondary_hash), static_cast(public_hash)); @@ -4677,9 +5121,9 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, if (is_group_text_payload) { - const uint8_t channel_hash = parsed.payload[0]; + const uint8_t* channel_hash = parsed.payload; bool channel_match = false; - const ChannelId rx_channel = resolveChannelFromHash(channel_hash, &channel_match); + const ChannelId rx_channel = resolveChannelFromHash(profile, channel_hash, &channel_match); if (!channel_match) { logUnknownGroupHash("text", channel_hash); @@ -4696,11 +5140,11 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, uint8_t plain[kMeshcoreMaxPayloadSize]; size_t plain_len = 0; if (!macThenDecrypt(key16, key32, - parsed.payload + 1, parsed.payload_len - 1, - plain, &plain_len)) + parsed.payload + hash_bytes, parsed.payload_len - hash_bytes, + plain, &plain_len, mac_len)) { - MESHCORE_LOG("[MESHCORE] RX group text decrypt fail hash=%02X len=%u ch=%u\n", - static_cast(channel_hash), + MESHCORE_LOG("[MESHCORE] RX group text decrypt fail hash=%s len=%u ch=%u\n", + toHex(channel_hash, hash_bytes).c_str(), static_cast(parsed.payload_len), static_cast(rx_channel)); return; @@ -4742,7 +5186,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, incoming.hop_limit = 0; incoming.encrypted = true; fill_rx_meta(incoming.rx_meta, false); - incoming.rx_meta.channel_hash = channel_hash; + incoming.rx_meta.channel_hash = channel_hash[0]; if (incoming.from != 0) { rememberPeerNodeId(static_cast(incoming.from & 0xFFU), incoming.from, now_ms); @@ -4751,9 +5195,9 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, } else if (is_group_data_payload) { - const uint8_t channel_hash = parsed.payload[0]; + const uint8_t* channel_hash = parsed.payload; bool channel_match = false; - const ChannelId rx_channel = resolveChannelFromHash(channel_hash, &channel_match); + const ChannelId rx_channel = resolveChannelFromHash(profile, channel_hash, &channel_match); if (!channel_match) { logUnknownGroupHash("data", channel_hash); @@ -4770,11 +5214,11 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, uint8_t plain[kMeshcoreMaxPayloadSize]; size_t plain_len = 0; if (!macThenDecrypt(key16, key32, - parsed.payload + 1, parsed.payload_len - 1, - plain, &plain_len)) + parsed.payload + hash_bytes, parsed.payload_len - hash_bytes, + plain, &plain_len, mac_len)) { - MESHCORE_LOG("[MESHCORE] RX group data decrypt fail hash=%02X len=%u ch=%u\n", - static_cast(channel_hash), + MESHCORE_LOG("[MESHCORE] RX group data decrypt fail hash=%s len=%u ch=%u\n", + toHex(channel_hash, hash_bytes).c_str(), static_cast(parsed.payload_len), static_cast(rx_channel)); return; @@ -4797,11 +5241,11 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, incoming.to = 0xFFFFFFFF; incoming.packet_id = next_msg_id_++; incoming.channel = rx_channel; - incoming.channel_hash = channel_hash; + incoming.channel_hash = channel_hash[0]; incoming.want_response = false; incoming.payload.assign(decoded.payload, decoded.payload + decoded.payload_len); fill_rx_meta(incoming.rx_meta, false); - incoming.rx_meta.channel_hash = channel_hash; + incoming.rx_meta.channel_hash = channel_hash[0]; if (incoming.from != 0) { rememberPeerNodeId(static_cast(incoming.from & 0xFFU), incoming.from, now_ms); @@ -4866,7 +5310,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, return; } - const uint8_t hops = (parsed.path_len <= 255) ? static_cast(parsed.path_len) : 0xFF; + const uint8_t hops = (path_hops <= 255) ? static_cast(path_hops) : 0xFF; const float snr = std::isfinite(last_rx_snr_) ? last_rx_snr_ : NAN; const float rssi = std::isfinite(last_rx_rssi_) ? last_rx_rssi_ : NAN; const uint32_t ts = is_valid_epoch(advert_ts) ? advert_ts : now_message_timestamp(); @@ -4899,6 +5343,7 @@ void MeshCoreAdapter::handleRawPacketInternal(const uint8_t* data, size_t size, ev.type = Event::Type::Advert; ev.peer_hash = peer_hash; ev.peer_node = node; + ev.path_profile = profile; ev.advert_is_new = is_new_peer; pushEvent(std::move(ev)); @@ -5072,6 +5517,8 @@ bool MeshCoreAdapter::getPeerInfos(std::vector& out) const { PeerInfo info; info.peer_hash = entry.peer_hash; + info.peer_hash_len = entry.peer_hash_len; + memcpy(info.peer_hash_bytes, entry.peer_hash_bytes, sizeof(info.peer_hash_bytes)); info.node_id = entry.node_id_guess; info.has_pubkey = entry.has_pubkey; info.pubkey_verified = entry.pubkey_verified; @@ -5079,6 +5526,7 @@ bool MeshCoreAdapter::getPeerInfos(std::vector& out) const { memcpy(info.pubkey, entry.pubkey, sizeof(info.pubkey)); } + info.out_path_profile = entry.out_path_profile; info.out_path_len = entry.has_out_path ? entry.out_path_len : 0; if (info.out_path_len > 0) { @@ -5104,6 +5552,8 @@ bool MeshCoreAdapter::lookupPeerByHash(uint8_t hash, PeerInfo* out) const } PeerInfo info; info.peer_hash = entry->peer_hash; + info.peer_hash_len = entry->peer_hash_len; + memcpy(info.peer_hash_bytes, entry->peer_hash_bytes, sizeof(info.peer_hash_bytes)); info.node_id = entry->node_id_guess; info.has_pubkey = entry->has_pubkey; info.pubkey_verified = entry->pubkey_verified; @@ -5111,6 +5561,7 @@ bool MeshCoreAdapter::lookupPeerByHash(uint8_t hash, PeerInfo* out) const { memcpy(info.pubkey, entry->pubkey, sizeof(info.pubkey)); } + info.out_path_profile = entry->out_path_profile; info.out_path_len = entry->has_out_path ? entry->out_path_len : 0; if (info.out_path_len > 0) { @@ -5134,6 +5585,8 @@ bool MeshCoreAdapter::lookupPeerByNodeId(NodeId node_id, PeerInfo* out) const { PeerInfo info; info.peer_hash = entry.peer_hash; + info.peer_hash_len = entry.peer_hash_len; + memcpy(info.peer_hash_bytes, entry.peer_hash_bytes, sizeof(info.peer_hash_bytes)); info.node_id = entry.node_id_guess; info.has_pubkey = entry.has_pubkey; info.pubkey_verified = entry.pubkey_verified; @@ -5141,6 +5594,7 @@ bool MeshCoreAdapter::lookupPeerByNodeId(NodeId node_id, PeerInfo* out) const { memcpy(info.pubkey, entry.pubkey, sizeof(info.pubkey)); } + info.out_path_profile = entry.out_path_profile; info.out_path_len = entry.has_out_path ? entry.out_path_len : 0; if (info.out_path_len > 0) { @@ -5227,12 +5681,14 @@ bool MeshCoreAdapter::clearPeerPath(uint8_t peer_hash) return false; } entry->out_path_len = 0; + entry->out_path_profile = PayloadProfile::V1; entry->has_out_path = false; memset(entry->out_path, 0, sizeof(entry->out_path)); Event ev{}; ev.type = Event::Type::PathUpdated; ev.peer_hash = peer_hash; ev.peer_node = resolvePeerNodeId(peer_hash); + ev.path_profile = PayloadProfile::V1; pushEvent(std::move(ev)); return true; } diff --git a/platform/esp/arduino_common/src/platform_ui_settings_backup_runtime.cpp b/platform/esp/arduino_common/src/platform_ui_settings_backup_runtime.cpp index 32bddaba..06fde479 100644 --- a/platform/esp/arduino_common/src/platform_ui_settings_backup_runtime.cpp +++ b/platform/esp/arduino_common/src/platform_ui_settings_backup_runtime.cpp @@ -381,6 +381,10 @@ void add_mesh_config(cJSON* parent, add_float(object, "meshcore_airtime_factor", config.meshcore_airtime_factor); add_int(object, "meshcore_flood_max", config.meshcore_flood_max); add_bool(object, "meshcore_multi_acks", config.meshcore_multi_acks); + add_int(object, "meshcore_send_profile", + static_cast(static_cast(config.meshcore_send_profile))); + add_int(object, "meshcore_forward_profile", + static_cast(static_cast(config.meshcore_forward_profile))); add_int(object, "meshcore_channel_slot", config.meshcore_channel_slot); add_string(object, "meshcore_channel_name", config.meshcore_channel_name); add_blob_hex(object, "meshcore_channel_key", config.secondary_key, chat::kMeshCoreChannelKeyLen); @@ -438,6 +442,12 @@ void restore_mesh_config(cJSON* object, config.meshcore_airtime_factor = json_float(object, "meshcore_airtime_factor", config.meshcore_airtime_factor); config.meshcore_flood_max = static_cast(json_int(object, "meshcore_flood_max", config.meshcore_flood_max)); config.meshcore_multi_acks = json_bool(object, "meshcore_multi_acks", config.meshcore_multi_acks); + config.meshcore_send_profile = static_cast( + json_int(object, "meshcore_send_profile", + static_cast(static_cast(config.meshcore_send_profile)))); + config.meshcore_forward_profile = static_cast( + json_int(object, "meshcore_forward_profile", + static_cast(static_cast(config.meshcore_forward_profile)))); config.meshcore_channel_slot = static_cast( json_int(object, "meshcore_channel_slot", config.meshcore_channel_slot)); copy_json_string(object, "meshcore_channel_name", config.meshcore_channel_name, sizeof(config.meshcore_channel_name)); diff --git a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h index f5e68ea7..ea4ffeb0 100644 --- a/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h +++ b/platform/nrf52/arduino_common/include/platform/nrf52/arduino_common/chat/infra/meshcore/meshcore_radio_adapter.h @@ -2,6 +2,7 @@ #include "chat/infra/meshcore/meshcore_ble_backend.h" #include "chat/infra/meshcore/meshcore_identity_crypto.h" +#include "chat/infra/meshcore/meshcore_protocol_helpers.h" #include "chat/ports/i_mesh_adapter.h" #include "chat/runtime/meshcore_runtime.h" #include "chat/runtime/protocol_runtime_factory.h" @@ -90,6 +91,9 @@ class MeshCoreRadioAdapter final : public ::chat::IMeshAdapter, bool sendRawData(const uint8_t* path, size_t path_len, const uint8_t* payload, size_t payload_len, uint32_t* out_est_timeout); + bool sendRawDataEx(uint8_t profile, const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout) override; void setFloodScopeKey(const uint8_t* key, size_t len); private: @@ -98,6 +102,7 @@ class MeshCoreRadioAdapter final : public ::chat::IMeshAdapter, bool transmitFrame(const uint8_t* data, size_t size); bool sendAdvert(bool broadcast); bool handleNodeInfoAppData(const ::chat::MeshIncomingData& incoming); + ::chat::meshcore::PayloadProfile payloadProfile() const; ::chat::runtime::RuntimeContext buildRuntimeContext() const; ::chat::runtime::ProtocolRuntimeBundle protocolRuntimeBundle( const ::chat::runtime::IProtocolRuntimeContextProvider& context_provider); diff --git a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp index ea200c55..32f2baff 100644 --- a/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp +++ b/platform/nrf52/arduino_common/src/chat/infra/meshcore/meshcore_radio_adapter.cpp @@ -201,6 +201,7 @@ bool MeshCoreRadioAdapter::sendAppData(::chat::ChannelId channel, uint32_t portn uint8_t frame[255] = {}; size_t frame_len = 0; + const ::chat::meshcore::PayloadProfile profile = payloadProfile(); if (dest != 0) { @@ -215,7 +216,8 @@ bool MeshCoreRadioAdapter::sendAppData(::chat::ChannelId channel, uint32_t portn std::memcpy(&plain[plain_len], payload, body_len); plain_len += body_len; - if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + if (!::chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeFlood, kPayloadTypeDirectData, nullptr, 0, @@ -271,7 +273,8 @@ bool MeshCoreRadioAdapter::sendAppData(::chat::ChannelId channel, uint32_t portn std::memcpy(&plain[plain_len], payload, body_len); plain_len += body_len; - if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + if (!::chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeFlood, kPayloadTypeGrpData, nullptr, 0, @@ -330,6 +333,13 @@ bool MeshCoreRadioAdapter::requestNodeInfo(::chat::NodeId dest, bool want_respon return context; } +::chat::meshcore::PayloadProfile MeshCoreRadioAdapter::payloadProfile() const +{ + return config_.meshcore_send_profile == ::chat::MeshCorePayloadSendProfile::V1Only + ? ::chat::meshcore::PayloadProfile::V1 + : ::chat::meshcore::PayloadProfile::V2; +} + ::chat::runtime::ProtocolRuntimeBundle MeshCoreRadioAdapter::protocolRuntimeBundle( const ::chat::runtime::IProtocolRuntimeContextProvider& context_provider) { @@ -1159,8 +1169,16 @@ bool MeshCoreRadioAdapter::sendPeerRequestPayload(const uint8_t* pubkey, size_t uint8_t datagram[kMeshcoreMaxPayloadSize] = {}; size_t datagram_len = 0; - if (!::chat::meshcore::buildPeerDatagramPayload(pubkey[0], - public_key_[0], + const ::chat::meshcore::PayloadProfile profile = payloadProfile(); + uint8_t peer_hash[::chat::meshcore::kMeshCoreV2HashBytes] = {}; + uint8_t self_hash[::chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!::chat::meshcore::copyPublicHash(profile, pubkey, len, + peer_hash, sizeof(peer_hash)) || + !::chat::meshcore::copyPublicHash(profile, public_key_, sizeof(public_key_), + self_hash, sizeof(self_hash)) || + !::chat::meshcore::buildPeerDatagramPayload(profile, + peer_hash, + self_hash, key16, key32, plain, @@ -1175,7 +1193,8 @@ bool MeshCoreRadioAdapter::sendPeerRequestPayload(const uint8_t* pubkey, size_t uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; const uint8_t route_type = force_flood ? kRouteTypeFlood : kRouteTypeFlood; - if (!::chat::meshcore::buildFrameNoTransport(route_type, + if (!::chat::meshcore::buildFrameNoTransport(profile, + route_type, kPayloadTypeReq, nullptr, 0, @@ -1234,13 +1253,15 @@ bool MeshCoreRadioAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t return false; } + const ::chat::meshcore::PayloadProfile profile = payloadProfile(); uint8_t cipher[kMeshcoreMaxPayloadSize] = {}; const size_t cipher_len = ::chat::meshcore::encryptThenMac(key16, key32, cipher, sizeof(cipher), payload, - payload_len); + payload_len, + ::chat::meshcore::payloadMacBytes(profile)); if (cipher_len == 0) { return false; @@ -1248,19 +1269,28 @@ bool MeshCoreRadioAdapter::sendAnonRequestPayload(const uint8_t* pubkey, size_t uint8_t datagram[kMeshcoreMaxPayloadSize] = {}; size_t datagram_len = 0; - datagram[datagram_len++] = pubkey[0]; - std::memcpy(datagram + datagram_len, public_key_, sizeof(public_key_)); - datagram_len += sizeof(public_key_); - if (datagram_len + cipher_len > sizeof(datagram)) + uint8_t peer_hash[::chat::meshcore::kMeshCoreV2HashBytes] = {}; + if (!::chat::meshcore::copyPublicHash(profile, pubkey, len, + peer_hash, sizeof(peer_hash))) { return false; } + const size_t hash_bytes = ::chat::meshcore::payloadHashBytes(profile); + if (hash_bytes + sizeof(public_key_) + cipher_len > sizeof(datagram)) + { + return false; + } + std::memcpy(datagram + datagram_len, peer_hash, hash_bytes); + datagram_len += hash_bytes; + std::memcpy(datagram + datagram_len, public_key_, sizeof(public_key_)); + datagram_len += sizeof(public_key_); std::memcpy(datagram + datagram_len, cipher, cipher_len); datagram_len += cipher_len; uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + if (!::chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeFlood, kPayloadTypeDirectData, nullptr, 0, @@ -1315,7 +1345,9 @@ bool MeshCoreRadioAdapter::sendTracePath(const uint8_t* path, size_t path_len, uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeDirect, + const ::chat::meshcore::PayloadProfile profile = payloadProfile(); + if (!::chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeTrace, nullptr, 0, @@ -1348,7 +1380,9 @@ bool MeshCoreRadioAdapter::sendControlData(const uint8_t* payload, size_t payloa uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeDirect, + const ::chat::meshcore::PayloadProfile profile = payloadProfile(); + if (!::chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeControl, nullptr, 0, @@ -1366,15 +1400,32 @@ bool MeshCoreRadioAdapter::sendControlData(const uint8_t* payload, size_t payloa bool MeshCoreRadioAdapter::sendRawData(const uint8_t* path, size_t path_len, const uint8_t* payload, size_t payload_len, uint32_t* out_est_timeout) +{ + return sendRawDataEx(::chat::meshcore::kMeshCorePayloadVer1, + path, path_len, payload, payload_len, out_est_timeout); +} + +bool MeshCoreRadioAdapter::sendRawDataEx(uint8_t raw_profile, const uint8_t* path, size_t path_len, + const uint8_t* payload, size_t payload_len, + uint32_t* out_est_timeout) { if (!payload || payload_len == 0 || path_len > 64 || (path_len > 0 && !path)) { return false; } + const ::chat::meshcore::PayloadProfile profile = + raw_profile == ::chat::meshcore::kMeshCorePayloadVer2 + ? ::chat::meshcore::PayloadProfile::V2 + : ::chat::meshcore::PayloadProfile::V1; + if (!::chat::meshcore::pathIsWellFormed(profile, path_len)) + { + return false; + } uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeDirect, + if (!::chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeDirect, kPayloadTypeRawCustom, path, path_len, @@ -1392,7 +1443,10 @@ bool MeshCoreRadioAdapter::sendRawData(const uint8_t* path, size_t path_len, } if (out_est_timeout) { - *out_est_timeout = estimateTimeoutMs(config_, frame_len, path_len, false); + *out_est_timeout = estimateTimeoutMs(config_, + frame_len, + ::chat::meshcore::pathHopCount(profile, path_len), + false); } return true; } @@ -1409,7 +1463,9 @@ bool MeshCoreRadioAdapter::sendAppAck(uint32_t signature) uint8_t frame[kMeshcoreMaxFrameSize] = {}; size_t frame_len = 0; - if (!::chat::meshcore::buildFrameNoTransport(kRouteTypeFlood, + const ::chat::meshcore::PayloadProfile profile = payloadProfile(); + if (!::chat::meshcore::buildFrameNoTransport(profile, + kRouteTypeFlood, kPayloadTypeAck, nullptr, 0,