lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit bf002de77c06790d55ded6a81cff197cd157ad31
parent 9fdd88480b6aa683ddf9ec4213861c8c72ca176e
Author: triesap <tyson@radroots.org>
Date:   Sat, 28 Mar 2026 15:35:44 +0000

simplex: implement live smp queue and message flow

Diffstat:
MCargo.lock | 26++++++++++++++++++++++++++
MCargo.toml | 2++
Mcrates/simplex-agent-runtime/Cargo.toml | 1+
Mcrates/simplex-agent-runtime/src/runtime.rs | 894++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Mcrates/simplex-agent-store/src/store.rs | 96++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mcrates/simplex-smp-crypto/Cargo.toml | 3+++
Mcrates/simplex-smp-crypto/src/error.rs | 11+++++++++++
Mcrates/simplex-smp-crypto/src/lib.rs | 6++++++
Acrates/simplex-smp-crypto/src/message.rs | 196+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 1051 insertions(+), 184 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2601,6 +2601,7 @@ dependencies = [ name = "radroots-simplex-agent-runtime" version = "0.1.0-alpha.1" dependencies = [ + "base64 0.22.1", "radroots-simplex-agent-proto", "radroots-simplex-agent-store", "radroots-simplex-smp-crypto", @@ -2654,6 +2655,8 @@ dependencies = [ "getrandom 0.2.17", "radroots-simplex-smp-proto", "sha2", + "x25519-dalek", + "xsalsa20poly1305", ] [[package]] @@ -4589,6 +4592,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" [[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", +] + +[[package]] name = "x509-parser" version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -4635,6 +4648,19 @@ dependencies = [ ] [[package]] +name = "xsalsa20poly1305" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02a6dad357567f81cd78ee75f7c61f1b30bb2fe4390be8fb7c69e2ac8dffb6c7" +dependencies = [ + "aead", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] name = "xtask" version = "0.1.0-alpha.1" dependencies = [ diff --git a/Cargo.toml b/Cargo.toml @@ -120,6 +120,8 @@ reqwest = { version = "0.12", default-features = false } rustls = { version = "0.23", default-features = false, features = ["ring", "std"] } rust_decimal = { version = "1", default-features = false } rust_decimal_macros = { version = "1" } +xsalsa20poly1305 = { version = "0.9", default-features = false } +x25519-dalek = { version = "2", default-features = false, features = ["static_secrets"] } sled = { version = "0.34" } tempfile = { version = "3" } thiserror = { version = "1" } diff --git a/crates/simplex-agent-runtime/Cargo.toml b/crates/simplex-agent-runtime/Cargo.toml @@ -25,6 +25,7 @@ std = [ ] [dependencies] +base64 = { version = "0.22", default-features = false, features = ["alloc"] } radroots-simplex-agent-proto = { workspace = true, default-features = false } radroots-simplex-agent-store = { workspace = true, default-features = false } radroots-simplex-smp-crypto = { workspace = true, default-features = false } diff --git a/crates/simplex-agent-runtime/src/runtime.rs b/crates/simplex-agent-runtime/src/runtime.rs @@ -4,13 +4,16 @@ use alloc::collections::VecDeque; use alloc::format; use alloc::string::String; use alloc::vec::Vec; +use base64::Engine as _; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; use radroots_simplex_agent_proto::prelude::{ RadrootsSimplexAgentConnectionLink, RadrootsSimplexAgentConnectionMode, RadrootsSimplexAgentConnectionStatus, RadrootsSimplexAgentDecryptedMessage, RadrootsSimplexAgentEncryptedPayload, RadrootsSimplexAgentEnvelope, RadrootsSimplexAgentMessage, RadrootsSimplexAgentMessageFrame, RadrootsSimplexAgentMessageHeader, RadrootsSimplexAgentMessageReceipt, - RadrootsSimplexAgentQueueDescriptor, encode_decrypted_message, encode_envelope, + RadrootsSimplexAgentQueueAddress, RadrootsSimplexAgentQueueDescriptor, + decode_decrypted_message, decode_envelope, encode_decrypted_message, encode_envelope, }; use radroots_simplex_agent_store::prelude::{ RadrootsSimplexAgentOutboundMessage, RadrootsSimplexAgentPendingCommand, @@ -18,12 +21,15 @@ use radroots_simplex_agent_store::prelude::{ RadrootsSimplexAgentStore, }; use radroots_simplex_smp_crypto::prelude::{ - RadrootsSimplexSmpCommandAuthorization, RadrootsSimplexSmpRatchetState, + RADROOTS_SIMPLEX_SMP_NONCE_LENGTH, RadrootsSimplexSmpCommandAuthorization, + RadrootsSimplexSmpRatchetState, RadrootsSimplexSmpX25519Keypair, decrypt_padded, + derive_shared_secret, encrypt_padded, random_nonce, }; use radroots_simplex_smp_proto::prelude::{ - RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, RadrootsSimplexSmpBrokerMessage, - RadrootsSimplexSmpCommand, RadrootsSimplexSmpCorrelationId, RadrootsSimplexSmpMessageFlags, - RadrootsSimplexSmpNewQueueRequest, RadrootsSimplexSmpQueueMode, + RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, + RadrootsSimplexSmpBrokerMessage, RadrootsSimplexSmpCommand, RadrootsSimplexSmpCorrelationId, + RadrootsSimplexSmpMessageFlags, RadrootsSimplexSmpNewQueueRequest, + RadrootsSimplexSmpQueueIdsResponse, RadrootsSimplexSmpQueueMode, RadrootsSimplexSmpQueueRequestData, RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpSendCommand, RadrootsSimplexSmpSubscriptionMode, }; @@ -35,6 +41,23 @@ use sha2::{Digest, Sha256}; #[cfg(feature = "std")] use std::path::{Path, PathBuf}; +const SIMPLEX_E2E_CONFIRMATION_LENGTH: usize = 15_904; +const SIMPLEX_E2E_MESSAGE_LENGTH: usize = 16_000; + +#[derive(Debug, Clone)] +struct SimplexClientMessageEnvelope { + sender_public_key: Option<Vec<u8>>, + nonce: [u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH], + ciphertext: Vec<u8>, +} + +#[derive(Debug, Clone)] +struct SimplexReceivedBody { + timestamp: u64, + flags: RadrootsSimplexSmpMessageFlags, + sent_body: Vec<u8>, +} + pub struct RadrootsSimplexAgentRuntimeBuilder { store: Option<RadrootsSimplexAgentStore>, queue_capacity: usize, @@ -120,16 +143,22 @@ pub struct RadrootsSimplexAgentRuntime { impl RadrootsSimplexAgentRuntime { pub fn create_connection( &mut self, - invitation_queue: RadrootsSimplexSmpQueueUri, - e2e_public_key: Vec<u8>, + mut invitation_queue: RadrootsSimplexSmpQueueUri, + e2e_seed: Vec<u8>, contact_address: bool, now: u64, ) -> Result<String, RadrootsSimplexAgentRuntimeError> { + let e2e_keypair = RadrootsSimplexSmpX25519Keypair::from_seed(&e2e_seed); + invitation_queue.recipient_dh_public_key = encode_queue_public_key(&e2e_keypair.public_key); + invitation_queue.sender_id = placeholder_sender_id( + invitation_queue.server.server_identity.as_bytes(), + &now.to_be_bytes(), + ); let local_dh_public_key = derive_material( b"connection-create-local-dh", &[ invitation_queue.to_string().as_bytes(), - &e2e_public_key, + &e2e_keypair.public_key, &now.to_be_bytes(), ], ); @@ -145,18 +174,20 @@ impl RadrootsSimplexAgentRuntime { } else { RadrootsSimplexAgentConnectionMode::Direct }, - RadrootsSimplexAgentConnectionStatus::InvitationReady, + RadrootsSimplexAgentConnectionStatus::CreatePending, None, ratchet_state, ); let invitation = RadrootsSimplexAgentConnectionLink { invitation_queue: invitation_queue.clone(), connection_id: connection.id.as_bytes().to_vec(), - e2e_public_key, + e2e_public_key: e2e_keypair.public_key.clone(), contact_address, }; - self.store.connection_mut(&connection.id)?.invitation = Some(invitation.clone()); + self.store.connection_mut(&connection.id)?.invitation = Some(invitation); let receive_auth_state = self.store.generate_queue_auth_state()?; + let delivery_keypair = RadrootsSimplexSmpX25519Keypair::generate() + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; let descriptor = RadrootsSimplexAgentQueueDescriptor { queue_uri: invitation_queue, replaced_queue: None, @@ -170,25 +201,26 @@ impl RadrootsSimplexAgentRuntime { true, receive_auth_state, )?; + { + let connection = self.store.connection_mut(&connection.id)?; + connection.local_e2e_public_key = Some(e2e_keypair.public_key); + connection.local_e2e_private_key = Some(e2e_keypair.private_key); + let queue = connection + .queues + .iter_mut() + .find(|queue| queue.descriptor.queue_address() == descriptor.queue_address()) + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX receive queue missing after create_connection".into(), + ) + })?; + queue.delivery_private_key = Some(delivery_keypair.private_key); + } self.store.enqueue_command( &connection.id, - RadrootsSimplexAgentPendingCommandKind::CreateQueue { - descriptor: descriptor.clone(), - }, - now, - )?; - self.store.enqueue_command( - &connection.id, - RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { - queue: descriptor.queue_address(), - }, + RadrootsSimplexAgentPendingCommandKind::CreateQueue { descriptor }, now, )?; - self.events - .push_back(RadrootsSimplexAgentRuntimeEvent::InvitationReady { - connection_id: connection.id.clone(), - invitation, - }); self.flush_store()?; Ok(connection.id) } @@ -196,9 +228,18 @@ impl RadrootsSimplexAgentRuntime { pub fn join_connection( &mut self, invitation: RadrootsSimplexAgentConnectionLink, - reply_queue: RadrootsSimplexSmpQueueUri, + mut reply_queue: RadrootsSimplexSmpQueueUri, now: u64, ) -> Result<String, RadrootsSimplexAgentRuntimeError> { + let local_e2e_keypair = RadrootsSimplexSmpX25519Keypair::generate() + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + let shared_secret = + derive_shared_secret(&local_e2e_keypair.private_key, &invitation.e2e_public_key) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + reply_queue.recipient_dh_public_key = + encode_queue_public_key(&local_e2e_keypair.public_key); + reply_queue.sender_id = + placeholder_sender_id(invitation.connection_id.as_slice(), &now.to_be_bytes()); let local_dh_public_key = derive_material( b"connection-join-local-dh", &[ @@ -231,6 +272,8 @@ impl RadrootsSimplexAgentRuntime { sender_key: Some(send_auth_state.public_key.clone()), }; let receive_auth_state = self.store.generate_queue_auth_state()?; + let delivery_keypair = RadrootsSimplexSmpX25519Keypair::generate() + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; let receive_descriptor = RadrootsSimplexAgentQueueDescriptor { queue_uri: reply_queue, replaced_queue: None, @@ -251,6 +294,24 @@ impl RadrootsSimplexAgentRuntime { true, receive_auth_state, )?; + { + let connection = self.store.connection_mut(&connection.id)?; + connection.local_e2e_public_key = Some(local_e2e_keypair.public_key.clone()); + connection.local_e2e_private_key = Some(local_e2e_keypair.private_key); + connection.shared_secret = Some(shared_secret); + let queue = connection + .queues + .iter_mut() + .find(|queue| { + queue.descriptor.queue_address() == receive_descriptor.queue_address() + }) + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX reply receive queue missing after join_connection".into(), + ) + })?; + queue.delivery_private_key = Some(delivery_keypair.private_key); + } self.store.enqueue_command( &connection.id, RadrootsSimplexAgentPendingCommandKind::SecureQueue { @@ -266,31 +327,6 @@ impl RadrootsSimplexAgentRuntime { }, now, )?; - self.store.enqueue_command( - &connection.id, - RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { - queue: receive_descriptor.queue_address(), - }, - now, - )?; - let confirmation_payload = - self.next_encrypted_payload(&connection.id, invitation.connection_id)?; - self.store.enqueue_command( - &connection.id, - RadrootsSimplexAgentPendingCommandKind::SendEnvelope { - queue: send_descriptor.queue_address(), - envelope: RadrootsSimplexAgentEnvelope::Confirmation { - reply_queue: true, - encrypted: confirmation_payload, - }, - delivery: None, - }, - now, - )?; - self.events - .push_back(RadrootsSimplexAgentRuntimeEvent::ConfirmationRequired { - connection_id: connection.id.clone(), - }); self.flush_store()?; Ok(connection.id) } @@ -304,7 +340,13 @@ impl RadrootsSimplexAgentRuntime { self.store .set_status(connection_id, RadrootsSimplexAgentConnectionStatus::Allowed)?; let send_queue = self.store.primary_send_queue(connection_id)?; - let encrypted = self.next_encrypted_payload(connection_id, local_info)?; + let encrypted = self.next_encrypted_payload( + connection_id, + encode_decrypted_message(&RadrootsSimplexAgentDecryptedMessage::ConnectionInfo( + local_info, + ))?, + None, + )?; self.store.enqueue_command( connection_id, RadrootsSimplexAgentPendingCommandKind::SendEnvelope { @@ -382,7 +424,7 @@ impl RadrootsSimplexAgentRuntime { let prepared = self .store .prepare_outbound_message(connection_id, message_hash.clone())?; - let encrypted = self.next_encrypted_payload(connection_id, ciphertext)?; + let encrypted = self.next_encrypted_payload(connection_id, ciphertext, None)?; self.store.enqueue_command( connection_id, RadrootsSimplexAgentPendingCommandKind::SendEnvelope { @@ -412,11 +454,31 @@ impl RadrootsSimplexAgentRuntime { receipt_info: Vec<u8>, now: u64, ) -> Result<(), RadrootsSimplexAgentRuntimeError> { - let send_queue = self.store.primary_send_queue(connection_id)?; + let receive_queue = self + .store + .connection(connection_id)? + .last_received_queue + .clone() + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` has no received queue to acknowledge" + )) + })?; + let broker_message_id = self + .store + .connection(connection_id)? + .last_received_broker_message_id + .clone() + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` has no broker message id to acknowledge" + )) + })?; self.store.enqueue_command( connection_id, RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { - queue: send_queue.descriptor.queue_address(), + queue: receive_queue, + broker_message_id, receipt: RadrootsSimplexAgentMessageReceipt { message_id, message_hash, @@ -521,11 +583,7 @@ impl RadrootsSimplexAgentRuntime { }); } RadrootsSimplexAgentDecryptedMessage::Message(frame) => { - self.store.record_inbound_message( - connection_id, - frame.header.message_id, - transport_hash, - )?; + let _ = transport_hash; match frame.message { RadrootsSimplexAgentMessage::Hello => { self.store.set_status( @@ -609,8 +667,16 @@ impl RadrootsSimplexAgentRuntime { now: u64, limit: usize, ) -> Result<(), RadrootsSimplexAgentRuntimeError> { - for command in self.store.take_ready_commands(now, limit) { - self.dispatch_ready_command(transport, &command, now)?; + let mut remaining = limit; + while remaining > 0 { + let ready = self.store.take_ready_commands(now, remaining); + if ready.is_empty() { + break; + } + remaining = remaining.saturating_sub(ready.len()); + for command in ready { + self.dispatch_ready_command(transport, &command, now)?; + } } self.flush_store()?; Ok(()) @@ -693,7 +759,7 @@ impl RadrootsSimplexAgentRuntime { &self, command: &RadrootsSimplexAgentPendingCommand, ) -> Result<RadrootsSimplexSmpTransportRequest, RadrootsSimplexAgentRuntimeError> { - let (queue_address, smp_command) = self.command_transport_parts(command)?; + let (queue_address, _entity_id, smp_command) = self.command_transport_parts(command)?; let queue = self .store .queue_record(&command.connection_id, &queue_address)?; @@ -705,18 +771,30 @@ impl RadrootsSimplexAgentRuntime { ) })?; let correlation_id = correlation_id_for_command(command.id); - Ok(RadrootsSimplexSmpTransportRequest { - server: queue.descriptor.queue_uri.server.clone(), - transport_version: RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, - correlation_id: Some(correlation_id), - entity_id: queue_address.sender_id, - command: smp_command, - authorization: RadrootsSimplexSmpCommandAuthorization::Ed25519( + let authorization = match &command.kind { + RadrootsSimplexAgentPendingCommandKind::SendEnvelope { .. } + if queue.role == RadrootsSimplexAgentQueueRole::Send + && matches!( + self.store.connection(&command.connection_id)?.status, + RadrootsSimplexAgentConnectionStatus::JoinPending + ) => + { + RadrootsSimplexSmpCommandAuthorization::None + } + _ => RadrootsSimplexSmpCommandAuthorization::Ed25519( radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpEd25519Keypair { public_key: auth.public_key, private_key: auth.private_key, }, ), + }; + Ok(RadrootsSimplexSmpTransportRequest { + server: queue.descriptor.queue_uri.server.clone(), + transport_version: RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, + correlation_id: Some(correlation_id), + entity_id: queue.entity_id, + command: smp_command, + authorization, }) } @@ -726,6 +804,7 @@ impl RadrootsSimplexAgentRuntime { ) -> Result< ( radroots_simplex_agent_proto::prelude::RadrootsSimplexAgentQueueAddress, + Vec<u8>, RadrootsSimplexSmpCommand, ), RadrootsSimplexAgentRuntimeError, @@ -735,15 +814,27 @@ impl RadrootsSimplexAgentRuntime { let auth_state = self .store .queue_auth_state(&command.connection_id, &descriptor.queue_address())?; + let delivery_private_key = self + .store + .queue_record(&command.connection_id, &descriptor.queue_address())? + .delivery_private_key + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX receive queue missing delivery private key".into(), + ) + })?; Ok(( descriptor.queue_address(), + Vec::new(), RadrootsSimplexSmpCommand::New(RadrootsSimplexSmpNewQueueRequest { recipient_auth_public_key: auth_state.public_key, - recipient_dh_public_key: descriptor - .queue_uri - .recipient_dh_public_key - .as_bytes() - .to_vec(), + recipient_dh_public_key: + RadrootsSimplexSmpX25519Keypair::public_key_from_private( + &delivery_private_key, + ) + .map_err(|error| { + RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()) + })?, basic_auth: None, subscription_mode: RadrootsSimplexSmpSubscriptionMode::Subscribe, queue_request_data: Some( @@ -766,43 +857,54 @@ impl RadrootsSimplexAgentRuntime { } RadrootsSimplexAgentPendingCommandKind::SecureQueue { queue, sender_key } => Ok(( queue.clone(), + queue.sender_id.clone(), RadrootsSimplexSmpCommand::SKey(sender_key.clone().unwrap_or_default()), )), RadrootsSimplexAgentPendingCommandKind::SendEnvelope { queue, envelope, .. } => Ok(( queue.clone(), + queue.sender_id.clone(), RadrootsSimplexSmpCommand::Send(RadrootsSimplexSmpSendCommand { flags: RadrootsSimplexSmpMessageFlags::notifications_enabled(), - message_body: encode_envelope(envelope)?, + message_body: self.encode_smp_message_body(&command.connection_id, envelope)?, }), )), - RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { queue } => { - Ok((queue.clone(), RadrootsSimplexSmpCommand::Sub)) - } - RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { queue, receipt } => Ok(( + RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { queue } => Ok(( + queue.clone(), + queue.sender_id.clone(), + RadrootsSimplexSmpCommand::Get, + )), + RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { + queue, + broker_message_id, + .. + } => Ok(( queue.clone(), - RadrootsSimplexSmpCommand::Ack(receipt.message_id.to_be_bytes().to_vec()), + queue.sender_id.clone(), + RadrootsSimplexSmpCommand::Ack(broker_message_id.clone()), )), - RadrootsSimplexAgentPendingCommandKind::RotateQueues { descriptors } => Ok(( - descriptors + RadrootsSimplexAgentPendingCommandKind::RotateQueues { descriptors } => { + let address = descriptors .first() .ok_or_else(|| { RadrootsSimplexAgentRuntimeError::Runtime( "queue rotation command requires at least one descriptor".into(), ) })? - .queue_address(), - RadrootsSimplexSmpCommand::Que, - )), - RadrootsSimplexAgentPendingCommandKind::TestQueues { queues } => Ok(( - queues.first().cloned().ok_or_else(|| { + .queue_address(); + let entity_id = address.sender_id.clone(); + Ok((address, entity_id, RadrootsSimplexSmpCommand::Que)) + } + RadrootsSimplexAgentPendingCommandKind::TestQueues { queues } => { + let address = queues.first().cloned().ok_or_else(|| { RadrootsSimplexAgentRuntimeError::Runtime( "queue test command requires at least one queue".into(), ) - })?, - RadrootsSimplexSmpCommand::Ping, - )), + })?; + let entity_id = address.sender_id.clone(); + Ok((address, entity_id, RadrootsSimplexSmpCommand::Ping)) + } } } @@ -821,6 +923,31 @@ impl RadrootsSimplexAgentRuntime { ), }, ), + RadrootsSimplexSmpBrokerMessage::Ids(ids) => { + self.process_queue_ids_response(command, ids)?; + self.record_command_outcome( + command.id, + RadrootsSimplexAgentCommandOutcome::Delivered, + ) + } + RadrootsSimplexSmpBrokerMessage::Msg(message) => { + let queue = queue_for_command(command).ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX command `{}` has no queue context for broker message", + command.id + )) + })?; + self.process_received_message_response( + &command.connection_id, + &queue, + message, + response.transport_hash, + )?; + self.record_command_outcome( + command.id, + RadrootsSimplexAgentCommandOutcome::Delivered, + ) + } _ => self .record_command_outcome(command.id, RadrootsSimplexAgentCommandOutcome::Delivered), } @@ -870,10 +997,281 @@ impl RadrootsSimplexAgentRuntime { Ok(()) } + fn encode_smp_message_body( + &self, + connection_id: &str, + envelope: &RadrootsSimplexAgentEnvelope, + ) -> Result<Vec<u8>, RadrootsSimplexAgentRuntimeError> { + let shared_secret = self + .store + .connection(connection_id)? + .shared_secret + .clone() + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` has no shared queue secret" + )) + })?; + let sender_public_key = match envelope { + RadrootsSimplexAgentEnvelope::Confirmation { encrypted, .. } => encrypted + .ratchet_header + .as_ref() + .map(|header| header.dh_public_key.clone()), + _ => None, + }; + let mut body = Vec::with_capacity(1 + 512); + body.push(b'_'); + body.extend_from_slice(&encode_envelope(envelope)?); + let nonce = random_nonce() + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + let padded_len = match envelope { + RadrootsSimplexAgentEnvelope::Confirmation { .. } => SIMPLEX_E2E_CONFIRMATION_LENGTH, + _ => SIMPLEX_E2E_MESSAGE_LENGTH, + }; + let ciphertext = encrypt_padded(&shared_secret, &nonce, &body, padded_len) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + encode_client_message_envelope(&SimplexClientMessageEnvelope { + sender_public_key, + nonce, + ciphertext, + }) + } + + fn process_queue_ids_response( + &mut self, + command: &RadrootsSimplexAgentPendingCommand, + ids: RadrootsSimplexSmpQueueIdsResponse, + ) -> Result<(), RadrootsSimplexAgentRuntimeError> { + let RadrootsSimplexAgentPendingCommandKind::CreateQueue { descriptor } = &command.kind + else { + return Err(RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX IDS response received for non-create command".into(), + )); + }; + + let old_address = descriptor.queue_address(); + let sender_id = URL_SAFE_NO_PAD.encode(&ids.sender_id); + let mut invitation_event = None; + let mut join_confirmation = None; + let subscribe_queue; + + { + let connection = self.store.connection_mut(&command.connection_id)?; + let queue = connection + .queues + .iter_mut() + .find(|queue| queue.descriptor.queue_address() == old_address) + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{}` missing receive queue for IDS", + command.connection_id + )) + })?; + let delivery_private_key = queue.delivery_private_key.clone().ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX receive queue missing delivery private key".into(), + ) + })?; + queue.delivery_shared_secret = Some( + derive_shared_secret(&delivery_private_key, &ids.server_dh_public_key).map_err( + |error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()), + )?, + ); + queue.entity_id = ids.recipient_id.clone(); + queue.descriptor.queue_uri.sender_id = sender_id; + if let Some(queue_mode) = ids.queue_mode { + queue.descriptor.queue_uri.queue_mode = Some(queue_mode); + } + let new_address = queue.descriptor.queue_address(); + subscribe_queue = new_address.clone(); + + if connection.status == RadrootsSimplexAgentConnectionStatus::CreatePending { + connection.status = RadrootsSimplexAgentConnectionStatus::InvitationReady; + if let Some(invitation) = connection.invitation.as_mut() { + invitation.invitation_queue = queue.descriptor.queue_uri.clone(); + invitation_event = Some(invitation.clone()); + } + } else if connection.status == RadrootsSimplexAgentConnectionStatus::JoinPending { + join_confirmation = Some(( + queue.descriptor.clone(), + connection.local_e2e_public_key.clone().ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{}` missing local E2E public key", + command.connection_id + )) + })?, + )); + } + } + + self.store.enqueue_command( + &command.connection_id, + RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { + queue: subscribe_queue, + }, + command.ready_at, + )?; + if let Some(invitation) = invitation_event { + self.events + .push_back(RadrootsSimplexAgentRuntimeEvent::InvitationReady { + connection_id: command.connection_id.clone(), + invitation, + }); + } + if let Some((reply_descriptor, sender_public_key)) = join_confirmation { + let send_queue = self.store.primary_send_queue(&command.connection_id)?; + let confirmation_payload = self.next_encrypted_payload( + &command.connection_id, + encode_decrypted_message( + &RadrootsSimplexAgentDecryptedMessage::ConnectionInfoReply { + reply_queues: vec![reply_descriptor], + info: Vec::new(), + }, + )?, + Some(sender_public_key), + )?; + self.store.enqueue_command( + &command.connection_id, + RadrootsSimplexAgentPendingCommandKind::SendEnvelope { + queue: send_queue.descriptor.queue_address(), + envelope: RadrootsSimplexAgentEnvelope::Confirmation { + reply_queue: true, + encrypted: confirmation_payload, + }, + delivery: None, + }, + command.ready_at, + )?; + self.events + .push_back(RadrootsSimplexAgentRuntimeEvent::ConfirmationRequired { + connection_id: command.connection_id.clone(), + }); + } + Ok(()) + } + + fn process_received_message_response( + &mut self, + connection_id: &str, + queue: &radroots_simplex_agent_proto::prelude::RadrootsSimplexAgentQueueAddress, + message: radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpReceivedMessage, + transport_hash: Vec<u8>, + ) -> Result<(), RadrootsSimplexAgentRuntimeError> { + let received = self.decode_received_message_body(connection_id, queue, &message)?; + if received.sent_body.is_empty() { + return Ok(()); + } + let (envelope, derived_secret) = + self.decode_agent_envelope_payload(connection_id, &received.sent_body)?; + if let Some(shared_secret) = derived_secret { + self.store.connection_mut(connection_id)?.shared_secret = Some(shared_secret); + } + let decrypted = extract_decrypted_message(&envelope)?; + { + let connection = self.store.connection_mut(connection_id)?; + connection.last_received_queue = Some(queue.clone()); + } + let _ = received.timestamp; + let _ = received.flags; + if let RadrootsSimplexAgentDecryptedMessage::Message(frame) = &decrypted { + self.store.record_inbound_message( + connection_id, + queue.clone(), + message.message_id.clone(), + frame.header.message_id, + transport_hash.clone(), + )?; + } + self.handle_inbound_decrypted_message(connection_id, decrypted, transport_hash) + } + + fn decode_received_message_body( + &mut self, + connection_id: &str, + queue: &radroots_simplex_agent_proto::prelude::RadrootsSimplexAgentQueueAddress, + message: &radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpReceivedMessage, + ) -> Result<SimplexReceivedBody, RadrootsSimplexAgentRuntimeError> { + let queue_record = self.store.queue_record(connection_id, queue)?; + let delivery_secret = queue_record.delivery_shared_secret.ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX receive queue on `{connection_id}` is missing delivery secret" + )) + })?; + let decrypted = decrypt_padded( + &delivery_secret, + &message.message_id, + &message.encrypted_body, + ) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + decode_received_body(&decrypted) + } + + fn decode_agent_envelope_payload( + &self, + connection_id: &str, + payload: &[u8], + ) -> Result<(RadrootsSimplexAgentEnvelope, Option<Vec<u8>>), RadrootsSimplexAgentRuntimeError> + { + let sent = decode_client_message_envelope(payload)?; + let derived_secret = match self.store.connection(connection_id)?.shared_secret.clone() { + Some(secret) => Some(secret), + None => { + let sender_public_key = sent.sender_public_key.as_deref().ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` received encrypted body without sender key" + )) + })?; + let private_key = self + .store + .connection(connection_id)? + .local_e2e_private_key + .as_deref() + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` missing local E2E private key" + )) + })?; + Some( + derive_shared_secret(private_key, sender_public_key).map_err(|error| { + RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()) + })?, + ) + } + }; + let shared_secret = derived_secret.clone().ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX connection `{connection_id}` has no shared secret" + )) + })?; + let decrypted = decrypt_padded(&shared_secret, &sent.nonce, &sent.ciphertext) + .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string()))?; + let (_, payload) = decrypted.split_first().ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX decrypted client body is empty".into(), + ) + })?; + let envelope = decode_envelope(payload)?; + let should_store_secret = self + .store + .connection(connection_id)? + .shared_secret + .is_none() + && sent.sender_public_key.is_some(); + Ok(( + envelope, + if should_store_secret { + derived_secret + } else { + None + }, + )) + } + fn next_encrypted_payload( &mut self, connection_id: &str, ciphertext: Vec<u8>, + sender_public_key: Option<Vec<u8>>, ) -> Result<RadrootsSimplexAgentEncryptedPayload, RadrootsSimplexAgentRuntimeError> { let ratchet_header = self .store @@ -886,6 +1284,22 @@ impl RadrootsSimplexAgentRuntime { .map_err(|error| RadrootsSimplexAgentRuntimeError::Runtime(error.to_string())) }) .transpose()?; + let ratchet_header = match (ratchet_header, sender_public_key) { + (Some(mut header), Some(public_key)) => { + header.dh_public_key = public_key; + Some(header) + } + (None, Some(public_key)) => Some( + radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpRatchetHeader { + previous_sending_chain_length: 0, + message_number: 0, + dh_public_key: public_key, + pq_public_key: None, + pq_ciphertext: None, + }, + ), + (header, None) => header, + }; Ok(RadrootsSimplexAgentEncryptedPayload { ratchet_header, ciphertext, @@ -920,12 +1334,212 @@ fn correlation_id_for_command(command_id: u64) -> RadrootsSimplexSmpCorrelationI RadrootsSimplexSmpCorrelationId::new(correlation) } +fn encode_queue_public_key(public_key: &[u8]) -> String { + URL_SAFE_NO_PAD.encode(public_key) +} + +fn placeholder_sender_id(seed_a: &[u8], seed_b: &[u8]) -> String { + let digest = derive_material(b"simplex-placeholder-sender-id", &[seed_a, seed_b]); + URL_SAFE_NO_PAD.encode(&digest[..18]) +} + +fn queue_for_command( + command: &RadrootsSimplexAgentPendingCommand, +) -> Option<RadrootsSimplexAgentQueueAddress> { + match &command.kind { + RadrootsSimplexAgentPendingCommandKind::CreateQueue { descriptor } => { + Some(descriptor.queue_address()) + } + RadrootsSimplexAgentPendingCommandKind::SecureQueue { queue, .. } + | RadrootsSimplexAgentPendingCommandKind::SendEnvelope { queue, .. } + | RadrootsSimplexAgentPendingCommandKind::SubscribeQueue { queue } + | RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { queue, .. } => { + Some(queue.clone()) + } + RadrootsSimplexAgentPendingCommandKind::RotateQueues { descriptors } => descriptors + .first() + .map(RadrootsSimplexAgentQueueDescriptor::queue_address), + RadrootsSimplexAgentPendingCommandKind::TestQueues { queues } => queues.first().cloned(), + } +} + +fn encode_client_message_envelope( + envelope: &SimplexClientMessageEnvelope, +) -> Result<Vec<u8>, RadrootsSimplexAgentRuntimeError> { + let mut buffer = Vec::with_capacity( + 2 + 1 + + envelope + .sender_public_key + .as_ref() + .map_or(0, |value| 1 + value.len()) + + 24 + + envelope.ciphertext.len(), + ); + buffer.extend_from_slice(&RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION.to_be_bytes()); + match envelope.sender_public_key.as_deref() { + Some(sender_public_key) => { + if sender_public_key.len() > u8::MAX as usize { + return Err(RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX sender public key exceeds short-field limit".into(), + )); + } + buffer.push(b'1'); + buffer.push(sender_public_key.len() as u8); + buffer.extend_from_slice(sender_public_key); + } + None => buffer.push(b'0'), + } + buffer.extend_from_slice(&envelope.nonce); + buffer.extend_from_slice(&envelope.ciphertext); + Ok(buffer) +} + +fn decode_client_message_envelope( + bytes: &[u8], +) -> Result<SimplexClientMessageEnvelope, RadrootsSimplexAgentRuntimeError> { + if bytes.len() < 2 + 1 + RADROOTS_SIMPLEX_SMP_NONCE_LENGTH { + return Err(RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX client message envelope is truncated".into(), + )); + } + let _version = u16::from_be_bytes([bytes[0], bytes[1]]); + let mut index = 2; + let sender_public_key = match bytes[index] { + b'0' => { + index += 1; + None + } + b'1' => { + index += 1; + let length = *bytes.get(index).ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX confirmation envelope is missing sender key length".into(), + ) + })? as usize; + index += 1; + let sender_public_key = bytes + .get(index..index + length) + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX confirmation envelope is missing sender key bytes".into(), + ) + })? + .to_vec(); + index += length; + Some(sender_public_key) + } + _ => { + return Err(RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX client message envelope has an unknown public header".into(), + )); + } + }; + let nonce_slice = bytes + .get(index..index + RADROOTS_SIMPLEX_SMP_NONCE_LENGTH) + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX client message envelope is missing nonce".into(), + ) + })?; + let mut nonce = [0_u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH]; + nonce.copy_from_slice(nonce_slice); + index += RADROOTS_SIMPLEX_SMP_NONCE_LENGTH; + let ciphertext = bytes + .get(index..) + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX client message envelope is missing ciphertext".into(), + ) + })? + .to_vec(); + Ok(SimplexClientMessageEnvelope { + sender_public_key, + nonce, + ciphertext, + }) +} + +fn decode_received_body( + bytes: &[u8], +) -> Result<SimplexReceivedBody, RadrootsSimplexAgentRuntimeError> { + if let Some(timestamp_bytes) = bytes.strip_prefix(b"QUOTA ") { + let timestamp: [u8; 8] = timestamp_bytes.try_into().map_err(|_| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX quota notification has an invalid timestamp".into(), + ) + })?; + return Ok(SimplexReceivedBody { + timestamp: u64::from_be_bytes(timestamp), + flags: RadrootsSimplexSmpMessageFlags::notifications_disabled(), + sent_body: Vec::new(), + }); + } + if bytes.len() < 10 { + return Err(RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX received body is truncated".into(), + )); + } + let timestamp = u64::from_be_bytes(bytes[..8].try_into().map_err(|_| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX received body is missing timestamp".into(), + ) + })?); + let flags_offset = bytes[8..] + .iter() + .position(|byte| *byte == b' ') + .ok_or_else(|| { + RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX received body is missing message flags separator".into(), + ) + })? + + 8; + let flags_bytes = &bytes[8..flags_offset]; + if flags_bytes.is_empty() { + return Err(RadrootsSimplexAgentRuntimeError::Runtime( + "SimpleX received body is missing message flags".into(), + )); + } + let flags = RadrootsSimplexSmpMessageFlags { + notification: match flags_bytes[0] { + 0 => false, + 1 => true, + other => { + return Err(RadrootsSimplexAgentRuntimeError::Runtime(format!( + "SimpleX received body has invalid notification flag `{other}`" + ))); + } + }, + reserved: flags_bytes[1..].to_vec(), + }; + Ok(SimplexReceivedBody { + timestamp, + flags, + sent_body: bytes[flags_offset + 1..].to_vec(), + }) +} + +fn extract_decrypted_message( + envelope: &RadrootsSimplexAgentEnvelope, +) -> Result<RadrootsSimplexAgentDecryptedMessage, RadrootsSimplexAgentRuntimeError> { + match envelope { + RadrootsSimplexAgentEnvelope::Confirmation { encrypted, .. } + | RadrootsSimplexAgentEnvelope::Message(encrypted) + | RadrootsSimplexAgentEnvelope::RatchetKey { encrypted, .. } => { + decode_decrypted_message(&encrypted.ciphertext).map_err(Into::into) + } + RadrootsSimplexAgentEnvelope::Invitation { + connection_info, .. + } => decode_decrypted_message(connection_info).map_err(Into::into), + } +} + #[cfg(test)] mod tests { use super::*; use alloc::collections::VecDeque; use radroots_simplex_smp_crypto::prelude::{ RadrootsSimplexSmpQueueAuthorizationMaterial, RadrootsSimplexSmpQueueAuthorizationScope, + RadrootsSimplexSmpX25519Keypair, }; use radroots_simplex_smp_proto::prelude::{ RadrootsSimplexSmpBrokerTransmission, RadrootsSimplexSmpQueueIdsResponse, @@ -946,6 +1560,22 @@ mod tests { .unwrap() } + fn ids_response( + recipient_id: &[u8], + sender_id: &[u8], + seed: &[u8], + ) -> RadrootsSimplexSmpBrokerMessage { + RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { + recipient_id: recipient_id.to_vec(), + sender_id: sender_id.to_vec(), + server_dh_public_key: RadrootsSimplexSmpX25519Keypair::from_seed(seed).public_key, + queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), + link_id: None, + service_id: None, + server_notification_credentials: None, + }) + } + #[derive(Default)] struct ScriptedTransport { responses: VecDeque<RadrootsSimplexSmpBrokerMessage>, @@ -1048,26 +1678,10 @@ mod tests { .unwrap(); let mut transport = ScriptedTransport::with_responses(vec![ - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient".to_vec(), - sender_id: b"sender".to_vec(), - server_dh_public_key: b"server-dh".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), + ids_response(b"recipient", b"sender", b"server-dh"), RadrootsSimplexSmpBrokerMessage::Ok, + ids_response(b"recipient-2", b"sender-2", b"server-dh-2"), RadrootsSimplexSmpBrokerMessage::Ok, - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient-2".to_vec(), - sender_id: b"sender-2".to_vec(), - server_dh_public_key: b"server-dh-2".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), RadrootsSimplexSmpBrokerMessage::Ok, RadrootsSimplexSmpBrokerMessage::Ok, ]); @@ -1106,26 +1720,10 @@ mod tests { .unwrap(); let mut setup_transport = ScriptedTransport::with_responses(vec![ - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient".to_vec(), - sender_id: b"sender".to_vec(), - server_dh_public_key: b"server-dh".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), + ids_response(b"recipient", b"sender", b"server-dh"), RadrootsSimplexSmpBrokerMessage::Ok, + ids_response(b"recipient-2", b"sender-2", b"server-dh-2"), RadrootsSimplexSmpBrokerMessage::Ok, - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient-2".to_vec(), - sender_id: b"sender-2".to_vec(), - server_dh_public_key: b"server-dh-2".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), RadrootsSimplexSmpBrokerMessage::Ok, RadrootsSimplexSmpBrokerMessage::Ok, ]); @@ -1184,26 +1782,10 @@ mod tests { .unwrap(); let mut setup_transport = ScriptedTransport::with_responses(vec![ - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient".to_vec(), - sender_id: b"sender".to_vec(), - server_dh_public_key: b"server-dh".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), + ids_response(b"recipient", b"sender", b"server-dh"), RadrootsSimplexSmpBrokerMessage::Ok, + ids_response(b"recipient-2", b"sender-2", b"server-dh-2"), RadrootsSimplexSmpBrokerMessage::Ok, - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient-2".to_vec(), - sender_id: b"sender-2".to_vec(), - server_dh_public_key: b"server-dh-2".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), RadrootsSimplexSmpBrokerMessage::Ok, RadrootsSimplexSmpBrokerMessage::Ok, ]); @@ -1286,26 +1868,10 @@ mod tests { .unwrap(); let mut setup_transport = ScriptedTransport::with_responses(vec![ - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient".to_vec(), - sender_id: b"sender".to_vec(), - server_dh_public_key: b"server-dh".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), + ids_response(b"recipient", b"sender", b"server-dh"), RadrootsSimplexSmpBrokerMessage::Ok, + ids_response(b"recipient-2", b"sender-2", b"server-dh-2"), RadrootsSimplexSmpBrokerMessage::Ok, - RadrootsSimplexSmpBrokerMessage::Ids(RadrootsSimplexSmpQueueIdsResponse { - recipient_id: b"recipient-2".to_vec(), - sender_id: b"sender-2".to_vec(), - server_dh_public_key: b"server-dh-2".to_vec(), - queue_mode: Some(RadrootsSimplexSmpQueueMode::Messaging), - link_id: None, - service_id: None, - server_notification_credentials: None, - }), RadrootsSimplexSmpBrokerMessage::Ok, RadrootsSimplexSmpBrokerMessage::Ok, ]); diff --git a/crates/simplex-agent-store/src/store.rs b/crates/simplex-agent-store/src/store.rs @@ -38,11 +38,14 @@ pub struct RadrootsSimplexAgentQueueAuthState { #[derive(Debug, Clone, PartialEq, Eq)] pub struct RadrootsSimplexAgentQueueRecord { pub descriptor: RadrootsSimplexAgentQueueDescriptor, + pub entity_id: Vec<u8>, pub role: RadrootsSimplexAgentQueueRole, pub subscribed: bool, pub primary: bool, pub tested: bool, pub auth_state: Option<RadrootsSimplexAgentQueueAuthState>, + pub delivery_private_key: Option<Vec<u8>>, + pub delivery_shared_secret: Option<Vec<u8>>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -94,6 +97,7 @@ pub enum RadrootsSimplexAgentPendingCommandKind { }, AckInboxMessage { queue: RadrootsSimplexAgentQueueAddress, + broker_message_id: Vec<u8>, receipt: RadrootsSimplexAgentMessageReceipt, }, RotateQueues { @@ -122,7 +126,12 @@ pub struct RadrootsSimplexAgentConnectionRecord { pub invitation: Option<RadrootsSimplexAgentConnectionLink>, pub queues: Vec<RadrootsSimplexAgentQueueRecord>, pub ratchet_state: Option<RadrootsSimplexSmpRatchetState>, + pub local_e2e_public_key: Option<Vec<u8>>, + pub local_e2e_private_key: Option<Vec<u8>>, + pub shared_secret: Option<Vec<u8>>, pub delivery_cursor: RadrootsSimplexAgentDeliveryCursor, + pub last_received_queue: Option<RadrootsSimplexAgentQueueAddress>, + pub last_received_broker_message_id: Option<Vec<u8>>, pub recent_messages: Vec<RadrootsSimplexAgentRecentMessageRecord>, pub staged_outbound_message: Option<RadrootsSimplexAgentOutboundMessage>, } @@ -145,7 +154,12 @@ struct RadrootsSimplexAgentConnectionSnapshot { invitation: Option<Vec<u8>>, queues: Vec<RadrootsSimplexAgentQueueRecordSnapshot>, ratchet_state: Option<RadrootsSimplexAgentRatchetStateSnapshot>, + local_e2e_public_key: Option<Vec<u8>>, + local_e2e_private_key: Option<Vec<u8>>, + shared_secret: Option<Vec<u8>>, delivery_cursor: RadrootsSimplexAgentDeliveryCursor, + last_received_queue: Option<RadrootsSimplexAgentQueueAddressSnapshot>, + last_received_broker_message_id: Option<Vec<u8>>, recent_messages: Vec<RadrootsSimplexAgentRecentMessageRecord>, staged_outbound_message: Option<RadrootsSimplexAgentOutboundMessage>, } @@ -154,11 +168,14 @@ struct RadrootsSimplexAgentConnectionSnapshot { #[derive(Debug, Clone, Serialize, Deserialize)] struct RadrootsSimplexAgentQueueRecordSnapshot { descriptor: RadrootsSimplexAgentQueueDescriptorSnapshot, + entity_id: Vec<u8>, role: String, subscribed: bool, primary: bool, tested: bool, auth_state: Option<RadrootsSimplexAgentQueueAuthState>, + delivery_private_key: Option<Vec<u8>>, + delivery_shared_secret: Option<Vec<u8>>, } #[cfg(feature = "std")] @@ -228,6 +245,7 @@ enum RadrootsSimplexAgentPendingCommandKindSnapshot { }, AckInboxMessage { queue: RadrootsSimplexAgentQueueAddressSnapshot, + broker_message_id: Vec<u8>, receipt: RadrootsSimplexAgentMessageReceiptSnapshot, }, RotateQueues { @@ -353,12 +371,17 @@ impl RadrootsSimplexAgentStore { invitation, queues: Vec::new(), ratchet_state, + local_e2e_public_key: None, + local_e2e_private_key: None, + shared_secret: None, delivery_cursor: RadrootsSimplexAgentDeliveryCursor { last_sent_message_id: None, last_received_message_id: None, last_sent_message_hash: None, last_received_message_hash: None, }, + last_received_queue: None, + last_received_broker_message_id: None, recent_messages: Vec::new(), staged_outbound_message: None, }; @@ -409,18 +432,22 @@ impl RadrootsSimplexAgentStore { .find(|queue| queue.descriptor.queue_address() == address) { queue.descriptor = descriptor; + queue.entity_id = address.sender_id.clone(); queue.role = role; queue.primary = primary; queue.auth_state = Some(auth_state); return Ok(()); } connection.queues.push(RadrootsSimplexAgentQueueRecord { + entity_id: address.sender_id.clone(), descriptor, role, subscribed: false, primary, tested: false, auth_state: Some(auth_state), + delivery_private_key: None, + delivery_shared_secret: None, }); Ok(()) } @@ -617,12 +644,16 @@ impl RadrootsSimplexAgentStore { pub fn record_inbound_message( &mut self, connection_id: &str, + queue_address: RadrootsSimplexAgentQueueAddress, + broker_message_id: Vec<u8>, message_id: RadrootsSimplexAgentMessageId, message_hash: Vec<u8>, ) -> Result<(), RadrootsSimplexAgentStoreError> { let connection = self.connection_mut(connection_id)?; connection.delivery_cursor.last_received_message_id = Some(message_id); connection.delivery_cursor.last_received_message_hash = Some(message_hash.clone()); + connection.last_received_queue = Some(queue_address); + connection.last_received_broker_message_id = Some(broker_message_id); connection .recent_messages .push(RadrootsSimplexAgentRecentMessageRecord { @@ -780,7 +811,12 @@ fn connection_to_snapshot( .map(queue_record_to_snapshot) .collect::<Result<Vec<_>, _>>()?, ratchet_state: record.ratchet_state.map(ratchet_state_to_snapshot), + local_e2e_public_key: record.local_e2e_public_key, + local_e2e_private_key: record.local_e2e_private_key, + shared_secret: record.shared_secret, delivery_cursor: record.delivery_cursor, + last_received_queue: record.last_received_queue.map(queue_address_to_snapshot), + last_received_broker_message_id: record.last_received_broker_message_id, recent_messages: record.recent_messages, staged_outbound_message: record.staged_outbound_message, }) @@ -814,7 +850,15 @@ fn connection_from_snapshot( .ratchet_state .map(ratchet_state_from_snapshot) .transpose()?, + local_e2e_public_key: snapshot.local_e2e_public_key, + local_e2e_private_key: snapshot.local_e2e_private_key, + shared_secret: snapshot.shared_secret, delivery_cursor: snapshot.delivery_cursor, + last_received_queue: snapshot + .last_received_queue + .map(queue_address_from_snapshot) + .transpose()?, + last_received_broker_message_id: snapshot.last_received_broker_message_id, recent_messages: snapshot.recent_messages, staged_outbound_message: snapshot.staged_outbound_message, }) @@ -826,11 +870,14 @@ fn queue_record_to_snapshot( ) -> Result<RadrootsSimplexAgentQueueRecordSnapshot, RadrootsSimplexAgentStoreError> { Ok(RadrootsSimplexAgentQueueRecordSnapshot { descriptor: queue_descriptor_to_snapshot(record.descriptor), + entity_id: record.entity_id, role: encode_queue_role(record.role).into(), subscribed: record.subscribed, primary: record.primary, tested: record.tested, auth_state: record.auth_state, + delivery_private_key: record.delivery_private_key, + delivery_shared_secret: record.delivery_shared_secret, }) } @@ -840,11 +887,14 @@ fn queue_record_from_snapshot( ) -> Result<RadrootsSimplexAgentQueueRecord, RadrootsSimplexAgentStoreError> { Ok(RadrootsSimplexAgentQueueRecord { descriptor: queue_descriptor_from_snapshot(snapshot.descriptor)?, + entity_id: snapshot.entity_id, role: decode_queue_role(&snapshot.role)?, subscribed: snapshot.subscribed, primary: snapshot.primary, tested: snapshot.tested, auth_state: snapshot.auth_state, + delivery_private_key: snapshot.delivery_private_key, + delivery_shared_secret: snapshot.delivery_shared_secret, }) } @@ -1037,16 +1087,19 @@ fn command_kind_to_snapshot( queue: queue_address_to_snapshot(queue), } } - RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { queue, receipt } => { - RadrootsSimplexAgentPendingCommandKindSnapshot::AckInboxMessage { - queue: queue_address_to_snapshot(queue), - receipt: RadrootsSimplexAgentMessageReceiptSnapshot { - message_id: receipt.message_id, - message_hash: receipt.message_hash, - receipt_info: receipt.receipt_info, - }, - } - } + RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { + queue, + broker_message_id, + receipt, + } => RadrootsSimplexAgentPendingCommandKindSnapshot::AckInboxMessage { + queue: queue_address_to_snapshot(queue), + broker_message_id, + receipt: RadrootsSimplexAgentMessageReceiptSnapshot { + message_id: receipt.message_id, + message_hash: receipt.message_hash, + receipt_info: receipt.receipt_info, + }, + }, RadrootsSimplexAgentPendingCommandKind::RotateQueues { descriptors } => { RadrootsSimplexAgentPendingCommandKindSnapshot::RotateQueues { descriptors: descriptors @@ -1097,16 +1150,19 @@ fn command_kind_from_snapshot( queue: queue_address_from_snapshot(queue)?, } } - RadrootsSimplexAgentPendingCommandKindSnapshot::AckInboxMessage { queue, receipt } => { - RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { - queue: queue_address_from_snapshot(queue)?, - receipt: RadrootsSimplexAgentMessageReceipt { - message_id: receipt.message_id, - message_hash: receipt.message_hash, - receipt_info: receipt.receipt_info, - }, - } - } + RadrootsSimplexAgentPendingCommandKindSnapshot::AckInboxMessage { + queue, + broker_message_id, + receipt, + } => RadrootsSimplexAgentPendingCommandKind::AckInboxMessage { + queue: queue_address_from_snapshot(queue)?, + broker_message_id, + receipt: RadrootsSimplexAgentMessageReceipt { + message_id: receipt.message_id, + message_hash: receipt.message_hash, + receipt_info: receipt.receipt_info, + }, + }, RadrootsSimplexAgentPendingCommandKindSnapshot::RotateQueues { descriptors } => { RadrootsSimplexAgentPendingCommandKind::RotateQueues { descriptors: descriptors diff --git a/crates/simplex-smp-crypto/Cargo.toml b/crates/simplex-smp-crypto/Cargo.toml @@ -20,6 +20,7 @@ std = [ "getrandom/std", "radroots-simplex-smp-proto/std", "sha2/std", + "xsalsa20poly1305/std", ] [dependencies] @@ -27,3 +28,5 @@ ed25519-dalek = { workspace = true, default-features = false, features = ["alloc getrandom = { workspace = true, default-features = false } radroots-simplex-smp-proto = { workspace = true, default-features = false } sha2 = { workspace = true, default-features = false } +xsalsa20poly1305 = { workspace = true, default-features = false } +x25519-dalek = { workspace = true, default-features = false, features = ["static_secrets"] } diff --git a/crates/simplex-smp-crypto/src/error.rs b/crates/simplex-smp-crypto/src/error.rs @@ -12,6 +12,8 @@ pub enum RadrootsSimplexSmpCryptoError { RatchetMessageRegression { received: u32, current: u32 }, InvalidSharedSecretLength(usize), InvalidCiphertextLength(usize), + InvalidNonceLength(usize), + InvalidMessageLength { actual: usize, padded: usize }, InvalidPublicKeyLength(usize), InvalidPrivateKeyLength(usize), InvalidSignatureLength(usize), @@ -54,6 +56,15 @@ impl fmt::Display for RadrootsSimplexSmpCryptoError { Self::InvalidCiphertextLength(length) => { write!(f, "invalid SMP ciphertext length {length}") } + Self::InvalidNonceLength(length) => { + write!(f, "invalid SMP nonce length {length}") + } + Self::InvalidMessageLength { actual, padded } => { + write!( + f, + "invalid SMP padded message length: actual {actual}, padded {padded}" + ) + } Self::InvalidPublicKeyLength(length) => { write!(f, "invalid SMP public key length {length}") } diff --git a/crates/simplex-smp-crypto/src/lib.rs b/crates/simplex-smp-crypto/src/lib.rs @@ -5,6 +5,7 @@ extern crate alloc; pub mod auth; pub mod error; +pub mod message; pub mod ratchet; pub mod prelude { @@ -14,6 +15,11 @@ pub mod prelude { verify_signature, }; pub use crate::error::RadrootsSimplexSmpCryptoError; + pub use crate::message::{ + RADROOTS_SIMPLEX_SMP_NONCE_LENGTH, RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH, + RadrootsSimplexSmpX25519Keypair, decrypt_no_pad, decrypt_padded, derive_shared_secret, + encrypt_no_pad, encrypt_padded, random_nonce, + }; pub use crate::ratchet::{ RadrootsSimplexSmpRatchetHeader, RadrootsSimplexSmpRatchetRole, RadrootsSimplexSmpRatchetState, diff --git a/crates/simplex-smp-crypto/src/message.rs b/crates/simplex-smp-crypto/src/message.rs @@ -0,0 +1,196 @@ +use crate::error::RadrootsSimplexSmpCryptoError; +use alloc::vec::Vec; +use getrandom::getrandom; +use sha2::{Digest, Sha256}; +use x25519_dalek::{PublicKey, StaticSecret}; +use xsalsa20poly1305::aead::{AeadInPlace, KeyInit}; +use xsalsa20poly1305::{Tag, XSalsa20Poly1305}; + +pub const RADROOTS_SIMPLEX_SMP_NONCE_LENGTH: usize = 24; +pub const RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH: usize = 32; +const RADROOTS_SIMPLEX_SMP_AUTH_TAG_LENGTH: usize = 16; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsSimplexSmpX25519Keypair { + pub public_key: Vec<u8>, + pub private_key: Vec<u8>, +} + +impl RadrootsSimplexSmpX25519Keypair { + pub fn generate() -> Result<Self, RadrootsSimplexSmpCryptoError> { + let mut secret = [0_u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH]; + getrandom(&mut secret).map_err(|_| RadrootsSimplexSmpCryptoError::EntropyUnavailable)?; + Ok(Self::from_secret_bytes(secret)) + } + + pub fn from_seed(seed: &[u8]) -> Self { + let digest = Sha256::digest(seed); + let mut secret = [0_u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH]; + secret.copy_from_slice(&digest[..RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH]); + Self::from_secret_bytes(secret) + } + + pub fn public_key_from_private( + private_key: &[u8], + ) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> { + let private: [u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH] = + private_key.try_into().map_err(|_| { + RadrootsSimplexSmpCryptoError::InvalidPrivateKeyLength(private_key.len()) + })?; + Ok(PublicKey::from(&StaticSecret::from(private)) + .as_bytes() + .to_vec()) + } + + fn from_secret_bytes(secret: [u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH]) -> Self { + let private = StaticSecret::from(secret); + let public = PublicKey::from(&private); + Self { + public_key: public.as_bytes().to_vec(), + private_key: private.to_bytes().to_vec(), + } + } +} + +pub fn derive_shared_secret( + private_key: &[u8], + public_key: &[u8], +) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> { + let private: [u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH] = private_key + .try_into() + .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPrivateKeyLength(private_key.len()))?; + let public: [u8; RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH] = public_key + .try_into() + .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidPublicKeyLength(public_key.len()))?; + let secret = StaticSecret::from(private).diffie_hellman(&PublicKey::from(public)); + Ok(secret.as_bytes().to_vec()) +} + +pub fn random_nonce() +-> Result<[u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH], RadrootsSimplexSmpCryptoError> { + let mut nonce = [0_u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH]; + getrandom(&mut nonce).map_err(|_| RadrootsSimplexSmpCryptoError::EntropyUnavailable)?; + Ok(nonce) +} + +pub fn encrypt_padded( + shared_secret: &[u8], + nonce: &[u8], + plaintext: &[u8], + padded_len: usize, +) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> { + if plaintext.len().saturating_add(2) > padded_len { + return Err(RadrootsSimplexSmpCryptoError::InvalidMessageLength { + actual: plaintext.len(), + padded: padded_len, + }); + } + let mut padded = Vec::with_capacity(padded_len); + padded.extend_from_slice(&(plaintext.len() as u16).to_be_bytes()); + padded.extend_from_slice(plaintext); + padded.resize(padded_len, 0); + encrypt_no_pad(shared_secret, nonce, &padded) +} + +pub fn decrypt_padded( + shared_secret: &[u8], + nonce: &[u8], + ciphertext: &[u8], +) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> { + let padded = decrypt_no_pad(shared_secret, nonce, ciphertext)?; + if padded.len() < 2 { + return Err(RadrootsSimplexSmpCryptoError::InvalidCiphertextLength( + padded.len(), + )); + } + let length = u16::from_be_bytes([padded[0], padded[1]]) as usize; + if length > padded.len().saturating_sub(2) { + return Err(RadrootsSimplexSmpCryptoError::InvalidCiphertextLength( + padded.len(), + )); + } + Ok(padded[2..2 + length].to_vec()) +} + +pub fn encrypt_no_pad( + shared_secret: &[u8], + nonce: &[u8], + plaintext: &[u8], +) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> { + let cipher = cipher(shared_secret)?; + let mut buffer = plaintext.to_vec(); + let tag = cipher + .encrypt_in_place_detached(&nonce_array(nonce)?.into(), b"", &mut buffer) + .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidCiphertextLength(plaintext.len()))?; + let mut encrypted = Vec::with_capacity(RADROOTS_SIMPLEX_SMP_AUTH_TAG_LENGTH + buffer.len()); + encrypted.extend_from_slice(&tag); + encrypted.extend_from_slice(&buffer); + Ok(encrypted) +} + +pub fn decrypt_no_pad( + shared_secret: &[u8], + nonce: &[u8], + ciphertext: &[u8], +) -> Result<Vec<u8>, RadrootsSimplexSmpCryptoError> { + if ciphertext.len() < RADROOTS_SIMPLEX_SMP_AUTH_TAG_LENGTH { + return Err(RadrootsSimplexSmpCryptoError::InvalidCiphertextLength( + ciphertext.len(), + )); + } + let cipher = cipher(shared_secret)?; + let (tag_bytes, encrypted) = ciphertext.split_at(RADROOTS_SIMPLEX_SMP_AUTH_TAG_LENGTH); + let tag = Tag::from_slice(tag_bytes); + let mut buffer = encrypted.to_vec(); + cipher + .decrypt_in_place_detached(&nonce_array(nonce)?.into(), b"", &mut buffer, tag) + .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidCiphertextLength(ciphertext.len()))?; + Ok(buffer) +} + +fn cipher(shared_secret: &[u8]) -> Result<XSalsa20Poly1305, RadrootsSimplexSmpCryptoError> { + if shared_secret.len() != RADROOTS_SIMPLEX_SMP_SHARED_SECRET_LENGTH { + return Err(RadrootsSimplexSmpCryptoError::InvalidSharedSecretLength( + shared_secret.len(), + )); + } + Ok( + XSalsa20Poly1305::new_from_slice(shared_secret).map_err(|_| { + RadrootsSimplexSmpCryptoError::InvalidSharedSecretLength(shared_secret.len()) + })?, + ) +} + +fn nonce_array( + nonce: &[u8], +) -> Result<[u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH], RadrootsSimplexSmpCryptoError> { + nonce + .try_into() + .map_err(|_| RadrootsSimplexSmpCryptoError::InvalidNonceLength(nonce.len())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn derives_repeatable_keypair_from_seed() { + let first = RadrootsSimplexSmpX25519Keypair::from_seed(b"seed"); + let second = RadrootsSimplexSmpX25519Keypair::from_seed(b"seed"); + assert_eq!(first, second); + } + + #[test] + fn encrypts_and_decrypts_padded_message() { + let alice = RadrootsSimplexSmpX25519Keypair::from_seed(b"alice"); + let bob = RadrootsSimplexSmpX25519Keypair::from_seed(b"bob"); + let alice_secret = derive_shared_secret(&alice.private_key, &bob.public_key).unwrap(); + let bob_secret = derive_shared_secret(&bob.private_key, &alice.public_key).unwrap(); + assert_eq!(alice_secret, bob_secret); + + let nonce = [5_u8; RADROOTS_SIMPLEX_SMP_NONCE_LENGTH]; + let ciphertext = encrypt_padded(&alice_secret, &nonce, b"hello", 32).unwrap(); + let plaintext = decrypt_padded(&bob_secret, &nonce, &ciphertext).unwrap(); + assert_eq!(plaintext, b"hello"); + } +}