feat(mesh): wire ARCHY identity broadcast for trust over both radios (#50)
The ARCHY:2 identity broadcast (DID + ed25519 + x25519) was unwired dead code on both send and receive. Wiring it lets a radio peer prove its archipelago identity, so the assistant's trusted-only gate (and encrypted DMs) work over meshcore AND Meshtastic — the latter otherwise only exposes synthetic node keys. - session.rs: broadcast ARCHY:2 as channel text at startup + each advert tick - frames.rs: parse inbound ARCHY:2 on the channel path, dedupe-keyed by archipelago pubkey (federation_peer_contact_id) so it MERGES with the federation-seeded peer instead of duplicating; self-echo guarded - threads our_x25519_secret into handle_channel_payload (was reserved) Reuses the existing handle_identity_received verifier (ed/x25519 consistency check + shared-secret derivation). Compiles clean. Needs a live 2-radio test before trusting trusted-only over radio. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
87d0d53205
commit
ef601c6d26
@ -3,8 +3,8 @@
|
||||
use super::super::message_types::TypedEnvelope;
|
||||
use super::super::protocol;
|
||||
use super::decode::{
|
||||
is_mc_chunk_frame, resolve_peer, store_plain_message, try_base64_typed, try_chunk_reassemble,
|
||||
try_decrypt_base64, try_decrypt_ratchet_base64,
|
||||
handle_identity_received, is_mc_chunk_frame, resolve_peer, store_plain_message,
|
||||
try_base64_typed, try_chunk_reassemble, try_decrypt_base64, try_decrypt_ratchet_base64,
|
||||
};
|
||||
use super::dispatch::handle_typed_message;
|
||||
use super::MeshState;
|
||||
@ -18,7 +18,6 @@ pub(super) async fn handle_frame(
|
||||
state: &Arc<MeshState>,
|
||||
our_x25519_secret: &[u8; 32],
|
||||
) -> bool {
|
||||
let _ = our_x25519_secret; // reserved for future per-frame decryption
|
||||
match frame.code {
|
||||
protocol::PUSH_NEW_CONTACT | protocol::PUSH_CONTACT_ADVERT => {
|
||||
info!(
|
||||
@ -109,7 +108,8 @@ pub(super) async fn handle_frame(
|
||||
match protocol::parse_channel_msg_v3_raw(&frame.data) {
|
||||
Ok((channel_idx, payload)) => {
|
||||
if !payload.is_empty() {
|
||||
handle_channel_payload(state, channel_idx, &payload).await;
|
||||
handle_channel_payload(state, channel_idx, &payload, our_x25519_secret)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("Failed to parse v3 channel message: {}", e),
|
||||
@ -121,7 +121,8 @@ pub(super) async fn handle_frame(
|
||||
match protocol::parse_channel_msg_v1_raw(&frame.data) {
|
||||
Ok((channel_idx, payload)) => {
|
||||
if !payload.is_empty() {
|
||||
handle_channel_payload(state, channel_idx, &payload).await;
|
||||
handle_channel_payload(state, channel_idx, &payload, our_x25519_secret)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
Err(e) => warn!("Failed to parse channel message: {}", e),
|
||||
@ -146,7 +147,12 @@ pub(super) async fn handle_frame(
|
||||
/// local mesh peer pubkeys (or we can't tell), the inner payload is
|
||||
/// dispatched through the direct-message path so it lands in the right
|
||||
/// chat. Otherwise it's handled as a normal channel text/typed message.
|
||||
async fn handle_channel_payload(state: &Arc<MeshState>, channel_idx: u8, payload: &[u8]) {
|
||||
async fn handle_channel_payload(
|
||||
state: &Arc<MeshState>,
|
||||
channel_idx: u8,
|
||||
payload: &[u8],
|
||||
our_x25519_secret: &[u8; 32],
|
||||
) {
|
||||
// DM-via-channel wrapper (text form): the channel text carries an
|
||||
// ASCII "@DM:<base64>" token somewhere in the body. We locate the
|
||||
// marker anywhere in the payload (the firmware auto-prepends the
|
||||
@ -326,6 +332,36 @@ async fn handle_channel_payload(state: &Arc<MeshState>, channel_idx: u8, payload
|
||||
return;
|
||||
}
|
||||
|
||||
// Archipelago identity broadcast (`ARCHY:`): upsert the sender's real
|
||||
// archipelago identity (DID + ed25519 + x25519) so trust-gating and
|
||||
// encrypted DMs work over BOTH meshcore and Meshtastic — the latter
|
||||
// otherwise only exposes synthetic node keys. Keyed by the archipelago
|
||||
// pubkey (federation_peer_contact_id) so it MERGES with the federation-
|
||||
// seeded peer instead of creating a duplicate chat thread. Not stored as
|
||||
// a chat message.
|
||||
if let Ok(text) = std::str::from_utf8(payload) {
|
||||
if let Some((did, ed_hex, x_hex)) =
|
||||
super::super::protocol::parse_identity_broadcast(text)
|
||||
{
|
||||
// Ignore our own identity echoed back by the radio/channel.
|
||||
if ed_hex.eq_ignore_ascii_case(&state.our_ed_pubkey_hex) {
|
||||
return;
|
||||
}
|
||||
let contact_id = super::super::federation_peer_contact_id(&ed_hex);
|
||||
handle_identity_received(
|
||||
contact_id,
|
||||
0,
|
||||
&did,
|
||||
&ed_hex,
|
||||
&x_hex,
|
||||
state,
|
||||
our_x25519_secret,
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Regular channel broadcast (not DM-wrapped)
|
||||
let chan_contact_id = u32::MAX - (channel_idx as u32);
|
||||
let chan_name = format!("Channel {}", channel_idx);
|
||||
|
||||
@ -363,9 +363,9 @@ pub(super) async fn run_mesh_session(
|
||||
state: &Arc<MeshState>,
|
||||
preferred_path: Option<&str>,
|
||||
our_did: &str,
|
||||
_our_ed_pubkey_hex: &str,
|
||||
our_ed_pubkey_hex: &str,
|
||||
our_x25519_secret: &[u8; 32],
|
||||
_our_x25519_pubkey_hex: &str,
|
||||
our_x25519_pubkey_hex: &str,
|
||||
server_name: Option<&str>,
|
||||
shutdown: &mut tokio::sync::watch::Receiver<bool>,
|
||||
cmd_rx: &mut mpsc::Receiver<MeshCommand>,
|
||||
@ -424,6 +424,19 @@ pub(super) async fn run_mesh_session(
|
||||
warn!("Failed to send initial advert: {}", e);
|
||||
}
|
||||
|
||||
// Archipelago identity advert (`ARCHY:2:{ed}:{x25519}`): broadcast as channel
|
||||
// text so peers can bind our radio presence to our DID + keys. The firmware
|
||||
// advert alone carries the meshcore key (and nothing on Meshtastic), so this
|
||||
// is what makes trust-gating + encrypted DMs work across BOTH transports.
|
||||
let identity_advert = super::super::protocol::encode_identity_broadcast(
|
||||
our_did,
|
||||
our_ed_pubkey_hex,
|
||||
our_x25519_pubkey_hex,
|
||||
);
|
||||
if let Err(e) = device.send_channel_text(0, identity_advert.as_bytes()).await {
|
||||
warn!("Failed to broadcast archipelago identity: {}", e);
|
||||
}
|
||||
|
||||
// Fetch existing contacts from the device
|
||||
refresh_contacts(&mut device, state).await;
|
||||
|
||||
@ -491,6 +504,11 @@ pub(super) async fn run_mesh_session(
|
||||
} else {
|
||||
consecutive_write_failures = 0;
|
||||
}
|
||||
// Re-broadcast archipelago identity so peers that joined since
|
||||
// startup (or missed it) can bind our DID/keys.
|
||||
if let Err(e) = device.send_channel_text(0, identity_advert.as_bytes()).await {
|
||||
warn!("Failed to re-broadcast archipelago identity: {}", e);
|
||||
}
|
||||
refresh_contacts(&mut device, state).await;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user