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:
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");
+ }
+}