lib

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

commit 5213505cb380ebb788978ba1399a74b42489a7d8
parent 41d2a1e29f1a8447c424bc7558a5b775b337accc
Author: triesap <tyson@radroots.org>
Date:   Sun, 22 Mar 2026 01:27:53 +0000

simplex: expand proxy and service wire surface

Diffstat:
Mcrates/simplex-smp-proto/src/error.rs | 3+++
Mcrates/simplex-smp-proto/src/lib.rs | 25+++++++++++++++++--------
Mcrates/simplex-smp-proto/src/version.rs | 4++++
Mcrates/simplex-smp-proto/src/wire.rs | 862++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
4 files changed, 883 insertions(+), 11 deletions(-)

diff --git a/crates/simplex-smp-proto/src/error.rs b/crates/simplex-smp-proto/src/error.rs @@ -26,6 +26,8 @@ pub enum RadrootsSimplexSmpProtoError { InvalidLargeFieldLength(usize), + InvalidListLength(usize), + InvalidCorrelationIdLength(usize), InvalidNonceLength(usize), @@ -61,6 +63,7 @@ impl fmt::Display for RadrootsSimplexSmpProtoError { Self::InvalidLargeFieldLength(length) => { write!(f, "invalid SMP large field length {length}") } + Self::InvalidListLength(length) => write!(f, "invalid SMP list length {length}"), Self::InvalidCorrelationIdLength(length) => { write!(f, "invalid SMP correlation id length {length}") } diff --git a/crates/simplex-smp-proto/src/lib.rs b/crates/simplex-smp-proto/src/lib.rs @@ -15,30 +15,39 @@ pub mod prelude { RadrootsSimplexSmpQueueMode, RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpServerAddress, }; pub use crate::version::{ + RADROOTS_SIMPLEX_SMP_AUTH_COMMANDS_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_BLOCKED_ENTITY_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_DELETED_EVENT_TRANSPORT_VERSION, + RADROOTS_SIMPLEX_SMP_ENCRYPTED_BLOCK_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_INITIAL_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_INITIAL_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION, + RADROOTS_SIMPLEX_SMP_PROXY_SERVER_HANDSHAKE_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION, + RADROOTS_SIMPLEX_SMP_SENDING_PROXY_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_SERVER_HOSTNAMES_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION, RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION, RadrootsSimplexSmpVersionRange, }; pub use crate::wire::{ - RadrootsSimplexSmpBrokerMessage, RadrootsSimplexSmpBrokerTransmission, + RadrootsSimplexSmpBlockingInfo, RadrootsSimplexSmpBlockingReason, + RadrootsSimplexSmpBrokerError, RadrootsSimplexSmpBrokerMessage, + RadrootsSimplexSmpBrokerTransmission, RadrootsSimplexSmpCertChainPublicKey, RadrootsSimplexSmpCommand, RadrootsSimplexSmpCommandError, RadrootsSimplexSmpCommandTransmission, RadrootsSimplexSmpContactQueueRequest, - RadrootsSimplexSmpCorrelationId, RadrootsSimplexSmpError, RadrootsSimplexSmpMessageFlags, - RadrootsSimplexSmpMessagingQueueRequest, RadrootsSimplexSmpNewNotifierCredentials, - RadrootsSimplexSmpNewQueueRequest, RadrootsSimplexSmpNotifierIdsResponse, - RadrootsSimplexSmpQueueIdsResponse, RadrootsSimplexSmpQueueLinkData, - RadrootsSimplexSmpQueueRequestData, RadrootsSimplexSmpReceivedMessage, - RadrootsSimplexSmpSendCommand, RadrootsSimplexSmpServerNotifierCredentials, - RadrootsSimplexSmpSubscriptionMode, + RadrootsSimplexSmpCorrelationId, RadrootsSimplexSmpError, RadrootsSimplexSmpHandshakeError, + RadrootsSimplexSmpKeyList, RadrootsSimplexSmpMessageFlags, + RadrootsSimplexSmpMessagingQueueRequest, RadrootsSimplexSmpNetworkError, + RadrootsSimplexSmpNewNotifierCredentials, RadrootsSimplexSmpNewQueueRequest, + RadrootsSimplexSmpNotifierIdsResponse, RadrootsSimplexSmpProtocolServer, + RadrootsSimplexSmpProxyError, RadrootsSimplexSmpQueueIdsResponse, + RadrootsSimplexSmpQueueLinkData, RadrootsSimplexSmpQueueRequestData, + RadrootsSimplexSmpReceivedMessage, RadrootsSimplexSmpSendCommand, + RadrootsSimplexSmpServerNotifierCredentials, RadrootsSimplexSmpSubscriptionMode, + RadrootsSimplexSmpTransportError, }; } diff --git a/crates/simplex-smp-proto/src/version.rs b/crates/simplex-smp-proto/src/version.rs @@ -10,9 +10,13 @@ pub const RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION: u16 = 4; pub const RADROOTS_SIMPLEX_SMP_CURRENT_CLIENT_VERSION: u16 = RADROOTS_SIMPLEX_SMP_SHORT_LINKS_CLIENT_VERSION; pub const RADROOTS_SIMPLEX_SMP_INITIAL_TRANSPORT_VERSION: u16 = 6; +pub const RADROOTS_SIMPLEX_SMP_AUTH_COMMANDS_TRANSPORT_VERSION: u16 = 7; +pub const RADROOTS_SIMPLEX_SMP_SENDING_PROXY_TRANSPORT_VERSION: u16 = 8; pub const RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION: u16 = 9; pub const RADROOTS_SIMPLEX_SMP_DELETED_EVENT_TRANSPORT_VERSION: u16 = 10; +pub const RADROOTS_SIMPLEX_SMP_ENCRYPTED_BLOCK_TRANSPORT_VERSION: u16 = 11; pub const RADROOTS_SIMPLEX_SMP_BLOCKED_ENTITY_TRANSPORT_VERSION: u16 = 12; +pub const RADROOTS_SIMPLEX_SMP_PROXY_SERVER_HANDSHAKE_TRANSPORT_VERSION: u16 = 14; pub const RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION: u16 = 15; pub const RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION: u16 = 16; pub const RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION: u16 = 17; diff --git a/crates/simplex-smp-proto/src/wire.rs b/crates/simplex-smp-proto/src/wire.rs @@ -1,18 +1,24 @@ use crate::error::RadrootsSimplexSmpProtoError; use crate::uri::RadrootsSimplexSmpQueueMode; use crate::version::{ + RADROOTS_SIMPLEX_SMP_BLOCKED_ENTITY_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_CURRENT_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_INITIAL_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_NEW_NOTIFIER_CREDENTIALS_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION, RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION, - RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION, + RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION, RadrootsSimplexSmpVersionRange, }; +use alloc::boxed::Box; use alloc::string::{String, ToString}; use alloc::vec::Vec; const TAG_NEW: &[u8] = b"NEW"; const TAG_SUB: &[u8] = b"SUB"; +const TAG_SUBS: &[u8] = b"SUBS"; const TAG_KEY: &[u8] = b"KEY"; +const TAG_RKEY: &[u8] = b"RKEY"; +const TAG_LSET: &[u8] = b"LSET"; +const TAG_LDEL: &[u8] = b"LDEL"; const TAG_NKEY: &[u8] = b"NKEY"; const TAG_NDEL: &[u8] = b"NDEL"; const TAG_GET: &[u8] = b"GET"; @@ -23,19 +29,33 @@ const TAG_QUE: &[u8] = b"QUE"; const TAG_SKEY: &[u8] = b"SKEY"; const TAG_SEND: &[u8] = b"SEND"; const TAG_PING: &[u8] = b"PING"; +const TAG_LKEY: &[u8] = b"LKEY"; +const TAG_LGET: &[u8] = b"LGET"; const TAG_NSUB: &[u8] = b"NSUB"; +const TAG_NSUBS: &[u8] = b"NSUBS"; +const TAG_PRXY: &[u8] = b"PRXY"; +const TAG_PFWD: &[u8] = b"PFWD"; +const TAG_RFWD: &[u8] = b"RFWD"; const TAG_IDS: &[u8] = b"IDS"; +const TAG_LNK: &[u8] = b"LNK"; +const TAG_SOK: &[u8] = b"SOK"; +const TAG_SOKS: &[u8] = b"SOKS"; const TAG_NID: &[u8] = b"NID"; const TAG_MSG: &[u8] = b"MSG"; const TAG_NMSG: &[u8] = b"NMSG"; +const TAG_PKEY: &[u8] = b"PKEY"; +const TAG_RRES: &[u8] = b"RRES"; +const TAG_PRES: &[u8] = b"PRES"; const TAG_END: &[u8] = b"END"; +const TAG_ENDS: &[u8] = b"ENDS"; const TAG_DELD: &[u8] = b"DELD"; const TAG_INFO: &[u8] = b"INFO"; const TAG_OK: &[u8] = b"OK"; const TAG_ERR: &[u8] = b"ERR"; const TAG_PONG: &[u8] = b"PONG"; +const COMMAND_ERR_UNKNOWN: &[u8] = b"UNKNOWN"; const COMMAND_ERR_SYNTAX: &[u8] = b"SYNTAX"; const COMMAND_ERR_PROHIBITED: &[u8] = b"PROHIBITED"; const COMMAND_ERR_NO_AUTH: &[u8] = b"NO_AUTH"; @@ -69,6 +89,85 @@ pub struct RadrootsSimplexSmpContactQueueRequest { } #[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsSimplexSmpKeyList { + pub first: Vec<u8>, + pub rest: Vec<Vec<u8>>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsSimplexSmpProtocolServer { + pub hosts: Vec<String>, + pub port: String, + pub key_hash: Vec<u8>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsSimplexSmpCertChainPublicKey { + pub certificate_chain: Vec<Vec<u8>>, + pub signed_public_key: Vec<u8>, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsSimplexSmpBlockingReason { + Spam, + Content, + Other(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RadrootsSimplexSmpBlockingInfo { + pub reason: RadrootsSimplexSmpBlockingReason, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsSimplexSmpHandshakeError { + Parse, + Identity, + BadAuth, + BadService, + Other(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsSimplexSmpTransportError { + Block, + Version, + LargeMsg, + Session, + NoAuth, + Handshake(RadrootsSimplexSmpHandshakeError), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsSimplexSmpNetworkError { + Connect(String), + Tls(String), + UnknownCa, + Failed, + Timeout, + Subscribe(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsSimplexSmpBrokerError { + Response(String), + Unexpected(String), + Network(RadrootsSimplexSmpNetworkError), + Host, + NoService, + Transport(RadrootsSimplexSmpTransportError), + Timeout, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RadrootsSimplexSmpProxyError { + Protocol(Box<RadrootsSimplexSmpError>), + Broker(RadrootsSimplexSmpBrokerError), + BasicAuth, + NoSession, +} + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsSimplexSmpQueueRequestData { Messaging(Option<RadrootsSimplexSmpMessagingQueueRequest>), Contact(Option<RadrootsSimplexSmpContactQueueRequest>), @@ -152,7 +251,14 @@ pub struct RadrootsSimplexSmpSendCommand { pub enum RadrootsSimplexSmpCommand { New(RadrootsSimplexSmpNewQueueRequest), Sub, + Subs, Key(Vec<u8>), + RKey(RadrootsSimplexSmpKeyList), + LSet { + link_id: Vec<u8>, + link_data: RadrootsSimplexSmpQueueLinkData, + }, + LDel, NKey { notifier_auth_public_key: Vec<u8>, recipient_notification_dh_public_key: Vec<u8>, @@ -166,7 +272,20 @@ pub enum RadrootsSimplexSmpCommand { SKey(Vec<u8>), Send(RadrootsSimplexSmpSendCommand), Ping, + LKey(Vec<u8>), + LGet, NSub, + NSubs, + Prxy { + server: RadrootsSimplexSmpProtocolServer, + basic_auth: Option<String>, + }, + PFwd { + relay_version: u16, + public_key: Vec<u8>, + encrypted_transmission: Vec<u8>, + }, + RFwd(Vec<u8>), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -203,6 +322,7 @@ pub struct RadrootsSimplexSmpReceivedMessage { #[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsSimplexSmpCommandError { + Unknown, Syntax, Prohibited, NoAuth, @@ -216,11 +336,18 @@ pub enum RadrootsSimplexSmpError { Block, Session, Command(RadrootsSimplexSmpCommandError), + Proxy(RadrootsSimplexSmpProxyError), Auth, + Blocked(RadrootsSimplexSmpBlockingInfo), + Service, + Crypto, Quota, + Store(String), NoMsg, LargeMsg, + Expired, Internal, + Duplicate, Other(Vec<u8>), } @@ -253,13 +380,27 @@ impl RadrootsSimplexSmpCorrelationId { #[derive(Debug, Clone, PartialEq, Eq)] pub enum RadrootsSimplexSmpBrokerMessage { Ids(RadrootsSimplexSmpQueueIdsResponse), + Lnk { + sender_id: Vec<u8>, + link_data: RadrootsSimplexSmpQueueLinkData, + }, + Sok(Option<Vec<u8>>), + Soks(i64), Nid(RadrootsSimplexSmpNotifierIdsResponse), Msg(RadrootsSimplexSmpReceivedMessage), NMsg { nonce: [u8; 24], encrypted_metadata: Vec<u8>, }, + PKey { + session_id: Vec<u8>, + version_range: RadrootsSimplexSmpVersionRange, + cert_chain_public_key: RadrootsSimplexSmpCertChainPublicKey, + }, + RRes(Vec<u8>), + PRes(Vec<u8>), End, + Ends(i64), Deld, Info(Vec<u8>), Ok, @@ -296,11 +437,24 @@ impl RadrootsSimplexSmpCommand { match self { Self::New(request) => encode_new_request(&mut buffer, request, transport_version)?, Self::Sub => buffer.extend_from_slice(TAG_SUB), + Self::Subs => buffer.extend_from_slice(TAG_SUBS), Self::Key(sender_auth_public_key) => { buffer.extend_from_slice(TAG_KEY); buffer.push(b' '); push_short_bytes(&mut buffer, sender_auth_public_key)?; } + Self::RKey(recipient_auth_public_keys) => { + buffer.extend_from_slice(TAG_RKEY); + buffer.push(b' '); + push_short_key_list(&mut buffer, recipient_auth_public_keys)?; + } + Self::LSet { link_id, link_data } => { + buffer.extend_from_slice(TAG_LSET); + buffer.push(b' '); + push_short_bytes(&mut buffer, link_id)?; + encode_queue_link_data(&mut buffer, link_data)?; + } + Self::LDel => buffer.extend_from_slice(TAG_LDEL), Self::NKey { notifier_auth_public_key, recipient_notification_dh_public_key, @@ -334,7 +488,36 @@ impl RadrootsSimplexSmpCommand { buffer.extend_from_slice(&send.message_body); } Self::Ping => buffer.extend_from_slice(TAG_PING), + Self::LKey(sender_auth_public_key) => { + buffer.extend_from_slice(TAG_LKEY); + buffer.push(b' '); + push_short_bytes(&mut buffer, sender_auth_public_key)?; + } + Self::LGet => buffer.extend_from_slice(TAG_LGET), Self::NSub => buffer.extend_from_slice(TAG_NSUB), + Self::NSubs => buffer.extend_from_slice(TAG_NSUBS), + Self::Prxy { server, basic_auth } => { + buffer.extend_from_slice(TAG_PRXY); + buffer.push(b' '); + encode_protocol_server(&mut buffer, server)?; + push_maybe_string(&mut buffer, basic_auth.as_deref())?; + } + Self::PFwd { + relay_version, + public_key, + encrypted_transmission, + } => { + buffer.extend_from_slice(TAG_PFWD); + buffer.push(b' '); + buffer.extend_from_slice(&relay_version.to_be_bytes()); + push_short_bytes(&mut buffer, public_key)?; + buffer.extend_from_slice(encrypted_transmission); + } + Self::RFwd(encrypted_forward_transmission) => { + buffer.extend_from_slice(TAG_RFWD); + buffer.push(b' '); + buffer.extend_from_slice(encrypted_forward_transmission); + } } Ok(buffer) } @@ -352,7 +535,14 @@ impl RadrootsSimplexSmpCommand { let command = match tag.as_slice() { TAG_NEW => Self::New(decode_new_request(&mut cursor, transport_version)?), TAG_SUB => Self::Sub, + TAG_SUBS => Self::Subs, TAG_KEY => Self::Key(cursor.read_short_bytes()?), + TAG_RKEY => Self::RKey(cursor.read_short_key_list()?), + TAG_LSET => Self::LSet { + link_id: cursor.read_short_bytes()?, + link_data: decode_queue_link_data(&mut cursor)?, + }, + TAG_LDEL => Self::LDel, TAG_NKEY => Self::NKey { notifier_auth_public_key: cursor.read_short_bytes()?, recipient_notification_dh_public_key: cursor.read_short_bytes()?, @@ -366,7 +556,20 @@ impl RadrootsSimplexSmpCommand { TAG_SKEY => Self::SKey(cursor.read_short_bytes()?), TAG_SEND => Self::Send(decode_send_payload(rest)?), TAG_PING => Self::Ping, + TAG_LKEY => Self::LKey(cursor.read_short_bytes()?), + TAG_LGET => Self::LGet, TAG_NSUB => Self::NSub, + TAG_NSUBS => Self::NSubs, + TAG_PRXY => Self::Prxy { + server: decode_protocol_server(&mut cursor)?, + basic_auth: cursor.read_maybe_string()?, + }, + TAG_PFWD => Self::PFwd { + relay_version: u16::from_be_bytes(cursor.read_array::<2>()?), + public_key: cursor.read_short_bytes()?, + encrypted_transmission: cursor.read_remaining().to_vec(), + }, + TAG_RFWD => Self::RFwd(cursor.read_remaining().to_vec()), _ => { return Err(RadrootsSimplexSmpProtoError::UnsupportedTag( String::from_utf8_lossy(&tag).into_owned(), @@ -392,6 +595,29 @@ impl RadrootsSimplexSmpBrokerMessage { let mut buffer = Vec::new(); match self { Self::Ids(response) => encode_ids_response(&mut buffer, response, transport_version)?, + Self::Lnk { + sender_id, + link_data, + } => { + buffer.extend_from_slice(TAG_LNK); + buffer.push(b' '); + push_short_bytes(&mut buffer, sender_id)?; + encode_queue_link_data(&mut buffer, link_data)?; + } + Self::Sok(service_id) => { + if transport_version >= RADROOTS_SIMPLEX_SMP_SERVICE_CERTS_TRANSPORT_VERSION { + buffer.extend_from_slice(TAG_SOK); + buffer.push(b' '); + push_maybe_short_bytes(&mut buffer, service_id.as_deref())?; + } else { + buffer.extend_from_slice(TAG_OK); + } + } + Self::Soks(queue_count) => { + buffer.extend_from_slice(TAG_SOKS); + buffer.push(b' '); + push_i64(&mut buffer, *queue_count); + } Self::Nid(response) => { buffer.extend_from_slice(TAG_NID); buffer.push(b' '); @@ -413,7 +639,34 @@ impl RadrootsSimplexSmpBrokerMessage { buffer.extend_from_slice(nonce); buffer.extend_from_slice(encrypted_metadata); } + Self::PKey { + session_id, + version_range, + cert_chain_public_key, + } => { + buffer.extend_from_slice(TAG_PKEY); + buffer.push(b' '); + push_short_bytes(&mut buffer, session_id)?; + buffer.extend_from_slice(&version_range.min.to_be_bytes()); + buffer.extend_from_slice(&version_range.max.to_be_bytes()); + encode_cert_chain_public_key(&mut buffer, cert_chain_public_key)?; + } + Self::RRes(encrypted_forward_response) => { + buffer.extend_from_slice(TAG_RRES); + buffer.push(b' '); + buffer.extend_from_slice(encrypted_forward_response); + } + Self::PRes(encrypted_response) => { + buffer.extend_from_slice(TAG_PRES); + buffer.push(b' '); + buffer.extend_from_slice(encrypted_response); + } Self::End => buffer.extend_from_slice(TAG_END), + Self::Ends(queue_count) => { + buffer.extend_from_slice(TAG_ENDS); + buffer.push(b' '); + push_i64(&mut buffer, *queue_count); + } Self::Deld => buffer.extend_from_slice(TAG_DELD), Self::Info(info) => { buffer.extend_from_slice(TAG_INFO); @@ -424,7 +677,13 @@ impl RadrootsSimplexSmpBrokerMessage { Self::Err(error) => { buffer.extend_from_slice(TAG_ERR); buffer.push(b' '); - buffer.extend_from_slice(&encode_error(error)); + if transport_version < RADROOTS_SIMPLEX_SMP_BLOCKED_ENTITY_TRANSPORT_VERSION + && matches!(error, RadrootsSimplexSmpError::Blocked(_)) + { + buffer.extend_from_slice(b"AUTH"); + } else { + buffer.extend_from_slice(&encode_error(error)); + } } Self::Pong => buffer.extend_from_slice(TAG_PONG), } @@ -443,6 +702,12 @@ impl RadrootsSimplexSmpBrokerMessage { let mut cursor = Cursor::new(rest); let message = match tag.as_slice() { TAG_IDS => Self::Ids(decode_ids_response(&mut cursor, transport_version)?), + TAG_LNK => Self::Lnk { + sender_id: cursor.read_short_bytes()?, + link_data: decode_queue_link_data(&mut cursor)?, + }, + TAG_SOK => Self::Sok(cursor.read_maybe(Cursor::read_short_bytes)?), + TAG_SOKS => Self::Soks(cursor.read_i64()?), TAG_NID => Self::Nid(RadrootsSimplexSmpNotifierIdsResponse { notifier_id: cursor.read_short_bytes()?, server_notification_dh_public_key: cursor.read_short_bytes()?, @@ -463,7 +728,20 @@ impl RadrootsSimplexSmpBrokerMessage { encrypted_metadata: cursor.read_remaining().to_vec(), } } + TAG_PKEY => { + let session_id = cursor.read_short_bytes()?; + let min = u16::from_be_bytes(cursor.read_array::<2>()?); + let max = u16::from_be_bytes(cursor.read_array::<2>()?); + Self::PKey { + session_id, + version_range: RadrootsSimplexSmpVersionRange::new(min, max)?, + cert_chain_public_key: decode_cert_chain_public_key(&mut cursor)?, + } + } + TAG_RRES => Self::RRes(cursor.read_remaining().to_vec()), + TAG_PRES => Self::PRes(cursor.read_remaining().to_vec()), TAG_END => Self::End, + TAG_ENDS => Self::Ends(cursor.read_i64()?), TAG_DELD => Self::Deld, TAG_INFO => Self::Info(cursor.read_remaining().to_vec()), TAG_OK => Self::Ok, @@ -477,7 +755,12 @@ impl RadrootsSimplexSmpBrokerMessage { }; if !matches!( message, - Self::Msg(_) | Self::NMsg { .. } | Self::Info(_) | Self::Err(_) + Self::Msg(_) + | Self::NMsg { .. } + | Self::RRes(_) + | Self::PRes(_) + | Self::Info(_) + | Self::Err(_) ) && !cursor.is_empty() { return Err(RadrootsSimplexSmpProtoError::TrailingBytes); @@ -819,6 +1102,42 @@ fn decode_contact_queue_request( }) } +fn encode_protocol_server( + buffer: &mut Vec<u8>, + server: &RadrootsSimplexSmpProtocolServer, +) -> Result<(), RadrootsSimplexSmpProtoError> { + push_short_string_list(buffer, &server.hosts)?; + push_short_string(buffer, &server.port)?; + push_short_bytes(buffer, &server.key_hash) +} + +fn decode_protocol_server( + cursor: &mut Cursor<'_>, +) -> Result<RadrootsSimplexSmpProtocolServer, RadrootsSimplexSmpProtoError> { + Ok(RadrootsSimplexSmpProtocolServer { + hosts: cursor.read_short_string_list()?, + port: cursor.read_short_string_lossy()?, + key_hash: cursor.read_short_bytes()?, + }) +} + +fn encode_cert_chain_public_key( + buffer: &mut Vec<u8>, + cert_chain_public_key: &RadrootsSimplexSmpCertChainPublicKey, +) -> Result<(), RadrootsSimplexSmpProtoError> { + push_large_bytes_list(buffer, &cert_chain_public_key.certificate_chain)?; + push_large_bytes(buffer, &cert_chain_public_key.signed_public_key) +} + +fn decode_cert_chain_public_key( + cursor: &mut Cursor<'_>, +) -> Result<RadrootsSimplexSmpCertChainPublicKey, RadrootsSimplexSmpProtoError> { + Ok(RadrootsSimplexSmpCertChainPublicKey { + certificate_chain: cursor.read_large_bytes_list()?, + signed_public_key: cursor.read_large_bytes()?, + }) +} + fn encode_queue_link_data( buffer: &mut Vec<u8>, link_data: &RadrootsSimplexSmpQueueLinkData, @@ -965,6 +1284,7 @@ fn encode_error(error: &RadrootsSimplexSmpError) -> Vec<u8> { RadrootsSimplexSmpError::Command(command_error) => { let mut bytes = b"CMD ".to_vec(); bytes.extend_from_slice(match command_error { + RadrootsSimplexSmpCommandError::Unknown => COMMAND_ERR_UNKNOWN, RadrootsSimplexSmpCommandError::Syntax => COMMAND_ERR_SYNTAX, RadrootsSimplexSmpCommandError::Prohibited => COMMAND_ERR_PROHIBITED, RadrootsSimplexSmpCommandError::NoAuth => COMMAND_ERR_NO_AUTH, @@ -974,11 +1294,30 @@ fn encode_error(error: &RadrootsSimplexSmpError) -> Vec<u8> { }); bytes } + RadrootsSimplexSmpError::Proxy(proxy_error) => { + let mut bytes = b"PROXY ".to_vec(); + bytes.extend_from_slice(&encode_proxy_error(proxy_error)); + bytes + } RadrootsSimplexSmpError::Auth => b"AUTH".to_vec(), + RadrootsSimplexSmpError::Blocked(blocking_info) => { + let mut bytes = b"BLOCKED ".to_vec(); + bytes.extend_from_slice(&encode_blocking_info(blocking_info)); + bytes + } + RadrootsSimplexSmpError::Service => b"SERVICE".to_vec(), + RadrootsSimplexSmpError::Crypto => b"CRYPTO".to_vec(), RadrootsSimplexSmpError::Quota => b"QUOTA".to_vec(), + RadrootsSimplexSmpError::Store(store_error) => { + let mut bytes = b"STORE ".to_vec(); + bytes.extend_from_slice(store_error.as_bytes()); + bytes + } RadrootsSimplexSmpError::NoMsg => b"NO_MSG".to_vec(), RadrootsSimplexSmpError::LargeMsg => b"LARGE_MSG".to_vec(), + RadrootsSimplexSmpError::Expired => b"EXPIRED".to_vec(), RadrootsSimplexSmpError::Internal => b"INTERNAL".to_vec(), + RadrootsSimplexSmpError::Duplicate => b"DUPLICATE_".to_vec(), RadrootsSimplexSmpError::Other(raw) => raw.clone(), } } @@ -993,20 +1332,38 @@ fn decode_error(bytes: &[u8]) -> Result<RadrootsSimplexSmpError, RadrootsSimplex if bytes == b"AUTH" { return Ok(RadrootsSimplexSmpError::Auth); } + if bytes == b"SERVICE" { + return Ok(RadrootsSimplexSmpError::Service); + } + if bytes == b"CRYPTO" { + return Ok(RadrootsSimplexSmpError::Crypto); + } if bytes == b"QUOTA" { return Ok(RadrootsSimplexSmpError::Quota); } + if let Some(store_error) = bytes.strip_prefix(b"STORE ") { + return Ok(RadrootsSimplexSmpError::Store( + String::from_utf8_lossy(store_error).into_owned(), + )); + } if bytes == b"NO_MSG" { return Ok(RadrootsSimplexSmpError::NoMsg); } if bytes == b"LARGE_MSG" { return Ok(RadrootsSimplexSmpError::LargeMsg); } + if bytes == b"EXPIRED" { + return Ok(RadrootsSimplexSmpError::Expired); + } if bytes == b"INTERNAL" { return Ok(RadrootsSimplexSmpError::Internal); } + if bytes == b"DUPLICATE_" { + return Ok(RadrootsSimplexSmpError::Duplicate); + } if let Some(command) = bytes.strip_prefix(b"CMD ") { let command_error = match command { + COMMAND_ERR_UNKNOWN => RadrootsSimplexSmpCommandError::Unknown, COMMAND_ERR_SYNTAX => RadrootsSimplexSmpCommandError::Syntax, COMMAND_ERR_PROHIBITED => RadrootsSimplexSmpCommandError::Prohibited, COMMAND_ERR_NO_AUTH => RadrootsSimplexSmpCommandError::NoAuth, @@ -1018,9 +1375,227 @@ fn decode_error(bytes: &[u8]) -> Result<RadrootsSimplexSmpError, RadrootsSimplex }; return Ok(RadrootsSimplexSmpError::Command(command_error)); } + if let Some(proxy_error) = bytes.strip_prefix(b"PROXY ") { + if let Some(proxy_error) = decode_proxy_error(proxy_error) { + return Ok(RadrootsSimplexSmpError::Proxy(proxy_error)); + } + } + if let Some(blocking_info) = bytes.strip_prefix(b"BLOCKED ") { + if let Some(blocking_info) = decode_blocking_info(blocking_info) { + return Ok(RadrootsSimplexSmpError::Blocked(blocking_info)); + } + } Ok(RadrootsSimplexSmpError::Other(bytes.to_vec())) } +fn encode_proxy_error(error: &RadrootsSimplexSmpProxyError) -> Vec<u8> { + match error { + RadrootsSimplexSmpProxyError::Protocol(error) => { + let mut bytes = b"PROTOCOL ".to_vec(); + bytes.extend_from_slice(&encode_error(error)); + bytes + } + RadrootsSimplexSmpProxyError::Broker(error) => { + let mut bytes = b"BROKER ".to_vec(); + bytes.extend_from_slice(&encode_broker_error(error)); + bytes + } + RadrootsSimplexSmpProxyError::BasicAuth => b"BASIC_AUTH".to_vec(), + RadrootsSimplexSmpProxyError::NoSession => b"NO_SESSION".to_vec(), + } +} + +fn decode_proxy_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpProxyError> { + let (tag, rest) = parse_tag(bytes).ok()?; + match tag.as_slice() { + b"PROTOCOL" => Some(RadrootsSimplexSmpProxyError::Protocol(Box::new( + decode_error(rest).ok()?, + ))), + b"BROKER" => Some(RadrootsSimplexSmpProxyError::Broker(decode_broker_error( + rest, + )?)), + b"BASIC_AUTH" if rest.is_empty() => Some(RadrootsSimplexSmpProxyError::BasicAuth), + b"NO_SESSION" if rest.is_empty() => Some(RadrootsSimplexSmpProxyError::NoSession), + _ => None, + } +} + +fn encode_broker_error(error: &RadrootsSimplexSmpBrokerError) -> Vec<u8> { + match error { + RadrootsSimplexSmpBrokerError::Response(response_error) => { + let mut bytes = b"RESPONSE ".to_vec(); + push_short_string(&mut bytes, response_error) + .expect("response_error length is bounded by SMP short-string encoding"); + bytes + } + RadrootsSimplexSmpBrokerError::Unexpected(response_error) => { + let mut bytes = b"UNEXPECTED ".to_vec(); + push_short_string(&mut bytes, response_error) + .expect("response_error length is bounded by SMP short-string encoding"); + bytes + } + RadrootsSimplexSmpBrokerError::Network(_) => b"NETWORK".to_vec(), + RadrootsSimplexSmpBrokerError::Host => b"HOST".to_vec(), + RadrootsSimplexSmpBrokerError::NoService => b"NO_SERVICE".to_vec(), + RadrootsSimplexSmpBrokerError::Transport(error) => { + let mut bytes = b"TRANSPORT ".to_vec(); + bytes.extend_from_slice(&encode_transport_error(error)); + bytes + } + RadrootsSimplexSmpBrokerError::Timeout => b"TIMEOUT".to_vec(), + } +} + +fn decode_broker_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpBrokerError> { + let (tag, rest) = parse_tag(bytes).ok()?; + match tag.as_slice() { + b"RESPONSE" => Some(RadrootsSimplexSmpBrokerError::Response( + decode_short_string_lossy(rest).ok()?, + )), + b"UNEXPECTED" => Some(RadrootsSimplexSmpBrokerError::Unexpected( + decode_short_string_lossy(rest).ok()?, + )), + b"TRANSPORT" => Some(RadrootsSimplexSmpBrokerError::Transport( + decode_transport_error(rest)?, + )), + b"NETWORK" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::Network( + RadrootsSimplexSmpNetworkError::Failed, + )), + b"NETWORK" => Some(RadrootsSimplexSmpBrokerError::Network( + decode_network_error(rest)?, + )), + b"TIMEOUT" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::Timeout), + b"HOST" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::Host), + b"NO_SERVICE" if rest.is_empty() => Some(RadrootsSimplexSmpBrokerError::NoService), + _ => None, + } +} + +fn encode_transport_error(error: &RadrootsSimplexSmpTransportError) -> Vec<u8> { + match error { + RadrootsSimplexSmpTransportError::Block => b"BLOCK".to_vec(), + RadrootsSimplexSmpTransportError::Version => b"VERSION".to_vec(), + RadrootsSimplexSmpTransportError::LargeMsg => b"LARGE_MSG".to_vec(), + RadrootsSimplexSmpTransportError::Session => b"SESSION".to_vec(), + RadrootsSimplexSmpTransportError::NoAuth => b"NO_AUTH".to_vec(), + RadrootsSimplexSmpTransportError::Handshake(error) => { + let mut bytes = b"HANDSHAKE ".to_vec(); + bytes.extend_from_slice(&encode_handshake_error(error)); + bytes + } + } +} + +fn decode_transport_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpTransportError> { + let (tag, rest) = parse_tag(bytes).ok()?; + match tag.as_slice() { + b"BLOCK" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::Block), + b"VERSION" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::Version), + b"LARGE_MSG" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::LargeMsg), + b"SESSION" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::Session), + b"NO_AUTH" if rest.is_empty() => Some(RadrootsSimplexSmpTransportError::NoAuth), + b"HANDSHAKE" => Some(RadrootsSimplexSmpTransportError::Handshake( + decode_handshake_error(rest), + )), + _ => None, + } +} + +#[cfg(test)] +fn encode_network_error(error: &RadrootsSimplexSmpNetworkError) -> Vec<u8> { + match error { + RadrootsSimplexSmpNetworkError::Connect(connect_error) => { + let mut bytes = b"CONNECT ".to_vec(); + push_short_string(&mut bytes, connect_error) + .expect("connect_error length is bounded by SMP short-string encoding"); + bytes + } + RadrootsSimplexSmpNetworkError::Tls(tls_error) => { + let mut bytes = b"TLS ".to_vec(); + push_short_string(&mut bytes, tls_error) + .expect("tls_error length is bounded by SMP short-string encoding"); + bytes + } + RadrootsSimplexSmpNetworkError::UnknownCa => b"UNKNOWNCA".to_vec(), + RadrootsSimplexSmpNetworkError::Failed => b"FAILED".to_vec(), + RadrootsSimplexSmpNetworkError::Timeout => b"TIMEOUT".to_vec(), + RadrootsSimplexSmpNetworkError::Subscribe(subscribe_error) => { + let mut bytes = b"SUBSCRIBE ".to_vec(); + push_short_string(&mut bytes, subscribe_error) + .expect("subscribe_error length is bounded by SMP short-string encoding"); + bytes + } + } +} + +fn decode_network_error(bytes: &[u8]) -> Option<RadrootsSimplexSmpNetworkError> { + let (tag, rest) = parse_tag(bytes).ok()?; + match tag.as_slice() { + b"CONNECT" => Some(RadrootsSimplexSmpNetworkError::Connect( + decode_short_string_lossy(rest).ok()?, + )), + b"TLS" => Some(RadrootsSimplexSmpNetworkError::Tls( + decode_short_string_lossy(rest).ok()?, + )), + b"UNKNOWNCA" if rest.is_empty() => Some(RadrootsSimplexSmpNetworkError::UnknownCa), + b"FAILED" if rest.is_empty() => Some(RadrootsSimplexSmpNetworkError::Failed), + b"TIMEOUT" if rest.is_empty() => Some(RadrootsSimplexSmpNetworkError::Timeout), + b"SUBSCRIBE" => Some(RadrootsSimplexSmpNetworkError::Subscribe( + decode_short_string_lossy(rest).ok()?, + )), + _ => None, + } +} + +fn encode_handshake_error(error: &RadrootsSimplexSmpHandshakeError) -> Vec<u8> { + match error { + RadrootsSimplexSmpHandshakeError::Parse => b"PARSE".to_vec(), + RadrootsSimplexSmpHandshakeError::Identity => b"IDENTITY".to_vec(), + RadrootsSimplexSmpHandshakeError::BadAuth => b"BAD_AUTH".to_vec(), + RadrootsSimplexSmpHandshakeError::BadService => b"BAD_SERVICE".to_vec(), + RadrootsSimplexSmpHandshakeError::Other(raw) => raw.as_bytes().to_vec(), + } +} + +fn decode_handshake_error(bytes: &[u8]) -> RadrootsSimplexSmpHandshakeError { + match bytes { + b"PARSE" => RadrootsSimplexSmpHandshakeError::Parse, + b"IDENTITY" => RadrootsSimplexSmpHandshakeError::Identity, + b"BAD_AUTH" => RadrootsSimplexSmpHandshakeError::BadAuth, + b"BAD_SERVICE" => RadrootsSimplexSmpHandshakeError::BadService, + raw => RadrootsSimplexSmpHandshakeError::Other(String::from_utf8_lossy(raw).into_owned()), + } +} + +fn encode_blocking_info(info: &RadrootsSimplexSmpBlockingInfo) -> Vec<u8> { + let mut bytes = b"reason=".to_vec(); + bytes.extend_from_slice(match &info.reason { + RadrootsSimplexSmpBlockingReason::Spam => b"spam", + RadrootsSimplexSmpBlockingReason::Content => b"content", + RadrootsSimplexSmpBlockingReason::Other(reason) => reason.as_bytes(), + }); + bytes +} + +fn decode_blocking_info(bytes: &[u8]) -> Option<RadrootsSimplexSmpBlockingInfo> { + let reason = bytes.strip_prefix(b"reason=")?; + let reason = match reason { + b"spam" => RadrootsSimplexSmpBlockingReason::Spam, + b"content" => RadrootsSimplexSmpBlockingReason::Content, + raw => RadrootsSimplexSmpBlockingReason::Other(String::from_utf8_lossy(raw).into_owned()), + }; + Some(RadrootsSimplexSmpBlockingInfo { reason }) +} + +fn decode_short_string_lossy(bytes: &[u8]) -> Result<String, RadrootsSimplexSmpProtoError> { + let mut cursor = Cursor::new(bytes); + let value = cursor.read_short_string_lossy()?; + if !cursor.is_empty() { + return Err(RadrootsSimplexSmpProtoError::TrailingBytes); + } + Ok(value) +} + fn parse_tag(bytes: &[u8]) -> Result<(Vec<u8>, &[u8]), RadrootsSimplexSmpProtoError> { if bytes.is_empty() { return Err(RadrootsSimplexSmpProtoError::UnexpectedEof); @@ -1063,6 +1638,10 @@ fn decode_bool(value: u8) -> Result<bool, RadrootsSimplexSmpProtoError> { } } +fn push_i64(buffer: &mut Vec<u8>, value: i64) { + buffer.extend_from_slice(&value.to_be_bytes()); +} + fn push_short_bytes( buffer: &mut Vec<u8>, bytes: &[u8], @@ -1074,6 +1653,13 @@ fn push_short_bytes( Ok(()) } +fn push_short_string( + buffer: &mut Vec<u8>, + value: &str, +) -> Result<(), RadrootsSimplexSmpProtoError> { + push_short_bytes(buffer, value.as_bytes()) +} + fn push_large_bytes( buffer: &mut Vec<u8>, bytes: &[u8], @@ -1085,6 +1671,53 @@ fn push_large_bytes( Ok(()) } +fn push_short_key_list( + buffer: &mut Vec<u8>, + keys: &RadrootsSimplexSmpKeyList, +) -> Result<(), RadrootsSimplexSmpProtoError> { + let len = 1 + keys.rest.len(); + let len = + u8::try_from(len).map_err(|_| RadrootsSimplexSmpProtoError::InvalidListLength(len))?; + buffer.push(len); + push_short_bytes(buffer, &keys.first)?; + for key in &keys.rest { + push_short_bytes(buffer, key)?; + } + Ok(()) +} + +fn push_short_string_list( + buffer: &mut Vec<u8>, + values: &[String], +) -> Result<(), RadrootsSimplexSmpProtoError> { + if values.is_empty() { + return Err(RadrootsSimplexSmpProtoError::InvalidListLength(0)); + } + let len = u8::try_from(values.len()) + .map_err(|_| RadrootsSimplexSmpProtoError::InvalidListLength(values.len()))?; + buffer.push(len); + for value in values { + push_short_string(buffer, value)?; + } + Ok(()) +} + +fn push_large_bytes_list( + buffer: &mut Vec<u8>, + values: &[Vec<u8>], +) -> Result<(), RadrootsSimplexSmpProtoError> { + if values.is_empty() { + return Err(RadrootsSimplexSmpProtoError::InvalidListLength(0)); + } + let len = u8::try_from(values.len()) + .map_err(|_| RadrootsSimplexSmpProtoError::InvalidListLength(values.len()))?; + buffer.push(len); + for value in values { + push_large_bytes(buffer, value)?; + } + Ok(()) +} + fn push_maybe<T, F>( buffer: &mut Vec<u8>, value: Option<T>, @@ -1203,11 +1836,58 @@ impl<'a> Cursor<'a> { Ok(self.read_exact(len)?.to_vec()) } + fn read_short_string_lossy(&mut self) -> Result<String, RadrootsSimplexSmpProtoError> { + Ok(String::from_utf8_lossy(&self.read_short_bytes()?).into_owned()) + } + + fn read_short_key_list( + &mut self, + ) -> Result<RadrootsSimplexSmpKeyList, RadrootsSimplexSmpProtoError> { + let len = usize::from(self.read_byte()?); + if len == 0 { + return Err(RadrootsSimplexSmpProtoError::InvalidListLength(0)); + } + let first = self.read_short_bytes()?; + let mut rest = Vec::with_capacity(len.saturating_sub(1)); + for _ in 1..len { + rest.push(self.read_short_bytes()?); + } + Ok(RadrootsSimplexSmpKeyList { first, rest }) + } + + fn read_short_string_list(&mut self) -> Result<Vec<String>, RadrootsSimplexSmpProtoError> { + let len = usize::from(self.read_byte()?); + if len == 0 { + return Err(RadrootsSimplexSmpProtoError::InvalidListLength(0)); + } + let mut values = Vec::with_capacity(len); + for _ in 0..len { + values.push(self.read_short_string_lossy()?); + } + Ok(values) + } + fn read_large_bytes(&mut self) -> Result<Vec<u8>, RadrootsSimplexSmpProtoError> { let len = usize::from(u16::from_be_bytes(self.read_array::<2>()?)); Ok(self.read_exact(len)?.to_vec()) } + fn read_large_bytes_list(&mut self) -> Result<Vec<Vec<u8>>, RadrootsSimplexSmpProtoError> { + let len = usize::from(self.read_byte()?); + if len == 0 { + return Err(RadrootsSimplexSmpProtoError::InvalidListLength(0)); + } + let mut values = Vec::with_capacity(len); + for _ in 0..len { + values.push(self.read_large_bytes()?); + } + Ok(values) + } + + fn read_i64(&mut self) -> Result<i64, RadrootsSimplexSmpProtoError> { + Ok(i64::from_be_bytes(self.read_array::<8>()?)) + } + fn read_maybe_string(&mut self) -> Result<Option<String>, RadrootsSimplexSmpProtoError> { self.read_maybe(|cursor| { let value = cursor.read_short_bytes()?; @@ -1512,4 +2192,180 @@ mod tests { b"NEW \x03\x01\x02\x03\x02\x04\x05A\x0bserver-passS".to_vec() ); } + + #[test] + fn round_trips_proxy_and_short_link_commands() { + let prxy = RadrootsSimplexSmpCommandTransmission { + authorization: Vec::new(), + correlation_id: Some(correlation_id(1)), + entity_id: Vec::new(), + command: RadrootsSimplexSmpCommand::Prxy { + server: RadrootsSimplexSmpProtocolServer { + hosts: vec![ + "smp4.simplex.im".to_string(), + "simplexabc.onion".to_string(), + ], + port: "5223".to_string(), + key_hash: vec![0xaa, 0xbb, 0xcc], + }, + basic_auth: Some("relay-pass".to_string()), + }, + }; + + let rkey = RadrootsSimplexSmpCommandTransmission { + authorization: vec![0x42], + correlation_id: Some(correlation_id(2)), + entity_id: vec![0x11], + command: RadrootsSimplexSmpCommand::RKey(RadrootsSimplexSmpKeyList { + first: vec![0x01, 0x02], + rest: vec![vec![0x03, 0x04], vec![0x05, 0x06]], + }), + }; + + let pkey_encoded = prxy.encode().unwrap(); + let pkey_decoded = RadrootsSimplexSmpCommandTransmission::decode(&pkey_encoded).unwrap(); + assert_eq!(pkey_decoded, prxy); + + let rkey_encoded = rkey.encode().unwrap(); + let rkey_decoded = RadrootsSimplexSmpCommandTransmission::decode(&rkey_encoded).unwrap(); + assert_eq!(rkey_decoded, rkey); + } + + #[test] + fn round_trips_proxy_forward_commands() { + let pfwd = RadrootsSimplexSmpCommandTransmission { + authorization: Vec::new(), + correlation_id: Some(correlation_id(3)), + entity_id: vec![0x90, 0x91], + command: RadrootsSimplexSmpCommand::PFwd { + relay_version: 16, + public_key: vec![0x10, 0x11, 0x12], + encrypted_transmission: vec![0xde, 0xad, 0xbe, 0xef], + }, + }; + let rfwd = RadrootsSimplexSmpCommandTransmission { + authorization: Vec::new(), + correlation_id: Some(correlation_id(4)), + entity_id: Vec::new(), + command: RadrootsSimplexSmpCommand::RFwd(vec![0xca, 0xfe, 0xba, 0xbe]), + }; + + let pfwd_encoded = pfwd.encode().unwrap(); + let pfwd_decoded = RadrootsSimplexSmpCommandTransmission::decode(&pfwd_encoded).unwrap(); + assert_eq!(pfwd_decoded, pfwd); + + let rfwd_encoded = rfwd.encode().unwrap(); + let rfwd_decoded = RadrootsSimplexSmpCommandTransmission::decode(&rfwd_encoded).unwrap(); + assert_eq!(rfwd_decoded, rfwd); + } + + #[test] + fn round_trips_service_and_proxy_broker_messages() { + let service = RadrootsSimplexSmpBrokerTransmission { + authorization: Vec::new(), + correlation_id: Some(correlation_id(5)), + entity_id: vec![0x44], + message: RadrootsSimplexSmpBrokerMessage::Sok(Some(vec![0x20, 0x21])), + }; + let proxy = RadrootsSimplexSmpBrokerTransmission { + authorization: Vec::new(), + correlation_id: None, + entity_id: Vec::new(), + message: RadrootsSimplexSmpBrokerMessage::PKey { + session_id: vec![0x31, 0x32], + version_range: RadrootsSimplexSmpVersionRange::new(8, 16).unwrap(), + cert_chain_public_key: RadrootsSimplexSmpCertChainPublicKey { + certificate_chain: vec![vec![0x41, 0x42], vec![0x43, 0x44, 0x45]], + signed_public_key: vec![0x51, 0x52, 0x53], + }, + }, + }; + + let service_encoded = service.encode().unwrap(); + let service_decoded = + RadrootsSimplexSmpBrokerTransmission::decode(&service_encoded).unwrap(); + assert_eq!(service_decoded, service); + + let proxy_encoded = proxy.encode().unwrap(); + let proxy_decoded = RadrootsSimplexSmpBrokerTransmission::decode(&proxy_encoded).unwrap(); + assert_eq!(proxy_decoded, proxy); + } + + #[test] + fn round_trips_proxy_and_blocked_errors() { + let proxy_error = RadrootsSimplexSmpBrokerTransmission { + authorization: Vec::new(), + correlation_id: Some(correlation_id(6)), + entity_id: vec![0x77], + message: RadrootsSimplexSmpBrokerMessage::Err(RadrootsSimplexSmpError::Proxy( + RadrootsSimplexSmpProxyError::Broker(RadrootsSimplexSmpBrokerError::Transport( + RadrootsSimplexSmpTransportError::Handshake( + RadrootsSimplexSmpHandshakeError::Identity, + ), + )), + )), + }; + let blocked_error = RadrootsSimplexSmpBrokerTransmission { + authorization: Vec::new(), + correlation_id: Some(correlation_id(7)), + entity_id: vec![0x88], + message: RadrootsSimplexSmpBrokerMessage::Err(RadrootsSimplexSmpError::Blocked( + RadrootsSimplexSmpBlockingInfo { + reason: RadrootsSimplexSmpBlockingReason::Spam, + }, + )), + }; + + let proxy_encoded = proxy_error.encode().unwrap(); + let proxy_decoded = RadrootsSimplexSmpBrokerTransmission::decode(&proxy_encoded).unwrap(); + assert_eq!(proxy_decoded, proxy_error); + + let blocked_encoded = blocked_error.encode().unwrap(); + let blocked_decoded = + RadrootsSimplexSmpBrokerTransmission::decode(&blocked_encoded).unwrap(); + assert_eq!(blocked_decoded, blocked_error); + } + + #[test] + fn service_ok_downgrades_to_ok_before_service_certs() { + let encoded = RadrootsSimplexSmpBrokerMessage::Sok(Some(vec![0x10])) + .encode_for_version(RADROOTS_SIMPLEX_SMP_SHORT_LINKS_TRANSPORT_VERSION) + .unwrap(); + + assert_eq!(encoded, b"OK".to_vec()); + } + + #[test] + fn blocked_error_downgrades_to_auth_before_blocked_entity_version() { + let encoded = RadrootsSimplexSmpBrokerMessage::Err(RadrootsSimplexSmpError::Blocked( + RadrootsSimplexSmpBlockingInfo { + reason: RadrootsSimplexSmpBlockingReason::Content, + }, + )) + .encode_for_version(RADROOTS_SIMPLEX_SMP_SENDER_AUTH_KEY_TRANSPORT_VERSION) + .unwrap(); + + assert_eq!(encoded, b"ERR AUTH".to_vec()); + } + + #[test] + fn decodes_optional_network_detail_and_preserves_encode_behavior() { + let detailed = decode_broker_error(b"NETWORK CONNECT \x03dns").unwrap(); + assert_eq!( + detailed, + RadrootsSimplexSmpBrokerError::Network(RadrootsSimplexSmpNetworkError::Connect( + "dns".to_string(), + )) + ); + + let encoded = + encode_network_error(&RadrootsSimplexSmpNetworkError::Connect("dns".to_string())); + assert_eq!(encoded, b"CONNECT \x03dns".to_vec()); + assert_eq!( + encode_broker_error(&RadrootsSimplexSmpBrokerError::Network( + RadrootsSimplexSmpNetworkError::Connect("dns".to_string()), + )), + b"NETWORK".to_vec() + ); + } }