lib

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

short_link.rs (39334B)


      1 use crate::error::{RadrootsSimplexAgentProtoError, RadrootsSimplexAgentUnsupportedLinkKind};
      2 use crate::model::RadrootsSimplexAgentConnectionLink;
      3 use alloc::format;
      4 use alloc::string::{String, ToString};
      5 use alloc::vec::Vec;
      6 use base64::Engine as _;
      7 use base64::engine::general_purpose::{URL_SAFE, URL_SAFE_NO_PAD};
      8 use core::fmt;
      9 use core::str::FromStr;
     10 use radroots_simplex_smp_crypto::prelude::{
     11     RadrootsSimplexOfficialX3dhParams, decode_ed25519_public_key_x509,
     12     decode_official_x448_public_key_der, decode_x25519_public_key_x509,
     13     encode_ed25519_public_key_x509, encode_official_x448_public_key_der,
     14     encode_x25519_public_key_x509,
     15 };
     16 use radroots_simplex_smp_proto::prelude::{
     17     RadrootsSimplexSmpQueueMode, RadrootsSimplexSmpQueueUri, RadrootsSimplexSmpServerAddress,
     18     RadrootsSimplexSmpVersionRange,
     19 };
     20 
     21 pub const RADROOTS_SIMPLEX_AGENT_SHORT_LINK_ID_LENGTH: usize = 24;
     22 pub const RADROOTS_SIMPLEX_AGENT_SHORT_LINK_KEY_LENGTH: usize = 32;
     23 pub const RADROOTS_SIMPLEX_AGENT_SHORT_LINK_SERVER_KEY_HASH_LENGTH: usize = 32;
     24 const SIMPLEX_AGENT_SHORT_LINK_MIN_VERSION: u16 = 2;
     25 const SIMPLEX_AGENT_SHORT_LINK_CURRENT_VERSION: u16 = 7;
     26 const SIMPLEX_CONNECTION_MODE_INVITATION: u8 = b'I';
     27 const SIMPLEX_QUEUE_MODE_MESSAGING: u8 = b'M';
     28 const SIMPLEX_QUEUE_MODE_CONTACT: u8 = b'C';
     29 const SIMPLEX_MAYBE_NOTHING: u8 = b'0';
     30 const SIMPLEX_MAYBE_JUST: u8 = b'1';
     31 const SIMPLEX_RATCHET_KEM_PROPOSED: u8 = b'P';
     32 const SIMPLEX_RATCHET_KEM_ACCEPTED: u8 = b'A';
     33 const SIMPLEX_USER_LINK_DATA_LARGE_TAG: u8 = u8::MAX;
     34 
     35 type ShortLinkResult<T> = Result<T, RadrootsSimplexAgentProtoError>;
     36 type OptionalKemParams = (Option<Vec<u8>>, Option<Vec<u8>>);
     37 
     38 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
     39 pub enum RadrootsSimplexAgentShortLinkScheme {
     40     Simplex,
     41     Https,
     42 }
     43 
     44 #[derive(Debug, Clone, PartialEq, Eq)]
     45 pub struct RadrootsSimplexAgentShortInvitationLink {
     46     pub scheme: RadrootsSimplexAgentShortLinkScheme,
     47     pub hosts: Vec<String>,
     48     pub port: Option<u16>,
     49     pub server_key_hash: Option<Vec<u8>>,
     50     pub link_id: Vec<u8>,
     51     pub link_key: Vec<u8>,
     52 }
     53 
     54 #[derive(Debug, Clone, PartialEq, Eq)]
     55 pub struct RadrootsSimplexAgentShortInvitationFixedData {
     56     pub agent_version_range: RadrootsSimplexSmpVersionRange,
     57     pub root_public_signature_key: Vec<u8>,
     58     pub invitation: RadrootsSimplexAgentConnectionLink,
     59     pub link_entity_id: Option<Vec<u8>>,
     60 }
     61 
     62 #[derive(Debug, Clone, PartialEq, Eq)]
     63 pub struct RadrootsSimplexAgentShortInvitationUserData {
     64     pub agent_version_range: RadrootsSimplexSmpVersionRange,
     65     pub user_data: Vec<u8>,
     66 }
     67 
     68 impl RadrootsSimplexAgentShortInvitationLink {
     69     pub fn render(&self) -> Result<String, RadrootsSimplexAgentProtoError> {
     70         validate_field_length(
     71             "link_id",
     72             &self.link_id,
     73             RADROOTS_SIMPLEX_AGENT_SHORT_LINK_ID_LENGTH,
     74         )?;
     75         validate_field_length(
     76             "link_key",
     77             &self.link_key,
     78             RADROOTS_SIMPLEX_AGENT_SHORT_LINK_KEY_LENGTH,
     79         )?;
     80         let link_id = URL_SAFE_NO_PAD.encode(&self.link_id);
     81         let link_key = URL_SAFE_NO_PAD.encode(&self.link_key);
     82         let mut output = match self.scheme {
     83             RadrootsSimplexAgentShortLinkScheme::Simplex => {
     84                 format!("simplex:/i#{link_id}/{link_key}")
     85             }
     86             RadrootsSimplexAgentShortLinkScheme::Https => {
     87                 let host =
     88                     self.hosts
     89                         .first()
     90                         .ok_or(RadrootsSimplexAgentProtoError::InvalidLink(
     91                             "https short invitation link requires a primary host".to_string(),
     92                         ))?;
     93                 validate_host(host)?;
     94                 format!("https://{host}/i#{link_id}/{link_key}")
     95             }
     96         };
     97 
     98         let mut query = Vec::<String>::new();
     99         let query_hosts = match self.scheme {
    100             RadrootsSimplexAgentShortLinkScheme::Simplex => self.hosts.as_slice(),
    101             RadrootsSimplexAgentShortLinkScheme::Https => self.hosts.get(1..).unwrap_or(&[]),
    102         };
    103         if !query_hosts.is_empty() {
    104             for host in query_hosts {
    105                 validate_host(host)?;
    106             }
    107             query.push(format!("h={}", query_hosts.join(",")));
    108         }
    109         if let Some(port) = self.port {
    110             query.push(format!("p={port}"));
    111         }
    112         if let Some(server_key_hash) = self.server_key_hash.as_ref() {
    113             validate_field_length(
    114                 "server_key_hash",
    115                 server_key_hash,
    116                 RADROOTS_SIMPLEX_AGENT_SHORT_LINK_SERVER_KEY_HASH_LENGTH,
    117             )?;
    118             query.push(format!("c={}", URL_SAFE_NO_PAD.encode(server_key_hash)));
    119         }
    120         if !query.is_empty() {
    121             output.push('?');
    122             output.push_str(&query.join("&"));
    123         }
    124         Ok(output)
    125     }
    126 }
    127 
    128 pub fn encode_short_invitation_fixed_data(
    129     root_public_signature_key: &[u8],
    130     invitation: &RadrootsSimplexAgentConnectionLink,
    131 ) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    132     let agent_version_range = official_agent_version_range()?;
    133     let encoded_root_public_key = encode_ed25519_public_key_x509(root_public_signature_key)
    134         .map_err(|error| RadrootsSimplexAgentProtoError::InvalidLink(error.to_string()))?;
    135     let mut buffer = Vec::new();
    136     push_version_range(&mut buffer, agent_version_range);
    137     push_short_bytes(&mut buffer, &encoded_root_public_key)?;
    138     encode_official_invitation_connection_request(&mut buffer, agent_version_range, invitation)?;
    139     Ok(buffer)
    140 }
    141 
    142 pub fn decode_short_invitation_fixed_data(
    143     bytes: &[u8],
    144 ) -> Result<RadrootsSimplexAgentShortInvitationFixedData, RadrootsSimplexAgentProtoError> {
    145     let mut cursor = ShortLinkDataCursor::new(bytes);
    146     let agent_version_range = cursor.read_version_range()?;
    147     let root_public_signature_key = decode_ed25519_public_key_x509(&cursor.read_short_bytes()?)
    148         .map_err(|error| RadrootsSimplexAgentProtoError::InvalidLink(error.to_string()))?;
    149     let mut invitation = decode_official_invitation_connection_request(&mut cursor)?;
    150     let link_entity_id = if cursor.remaining().is_empty() {
    151         None
    152     } else {
    153         Some(cursor.read_short_bytes()?)
    154     };
    155     if let Some(link_entity_id) = link_entity_id.as_ref() {
    156         invitation.connection_id = link_entity_id.clone();
    157     }
    158     Ok(RadrootsSimplexAgentShortInvitationFixedData {
    159         agent_version_range,
    160         root_public_signature_key,
    161         invitation,
    162         link_entity_id,
    163     })
    164 }
    165 
    166 pub fn encode_short_invitation_user_data(
    167     invitation: &RadrootsSimplexAgentConnectionLink,
    168 ) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    169     let agent_version_range = official_agent_version_range()?;
    170     let mut buffer = Vec::new();
    171     buffer.push(SIMPLEX_CONNECTION_MODE_INVITATION);
    172     push_version_range(&mut buffer, agent_version_range);
    173     push_user_link_data(&mut buffer, &invitation.connection_id)?;
    174     Ok(buffer)
    175 }
    176 
    177 pub fn decode_short_invitation_user_data(
    178     bytes: &[u8],
    179 ) -> Result<RadrootsSimplexAgentShortInvitationUserData, RadrootsSimplexAgentProtoError> {
    180     let mut cursor = ShortLinkDataCursor::new(bytes);
    181     cursor.expect_byte(SIMPLEX_CONNECTION_MODE_INVITATION)?;
    182     let agent_version_range = cursor.read_version_range()?;
    183     let user_data = cursor.read_user_link_data()?;
    184     Ok(RadrootsSimplexAgentShortInvitationUserData {
    185         agent_version_range,
    186         user_data,
    187     })
    188 }
    189 
    190 impl fmt::Display for RadrootsSimplexAgentShortInvitationLink {
    191     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
    192         self.render().map_err(|_| fmt::Error)?.fmt(f)
    193     }
    194 }
    195 
    196 impl FromStr for RadrootsSimplexAgentShortInvitationLink {
    197     type Err = RadrootsSimplexAgentProtoError;
    198 
    199     fn from_str(value: &str) -> Result<Self, Self::Err> {
    200         parse_short_invitation_link(value)
    201     }
    202 }
    203 
    204 pub fn parse_short_invitation_link(
    205     value: &str,
    206 ) -> Result<RadrootsSimplexAgentShortInvitationLink, RadrootsSimplexAgentProtoError> {
    207     let value = value.trim();
    208     if value.is_empty() {
    209         return Err(RadrootsSimplexAgentProtoError::InvalidLink(
    210             "empty short invitation link".to_string(),
    211         ));
    212     }
    213 
    214     if let Some(rest) = value.strip_prefix("simplex:/") {
    215         return parse_scheme_link(
    216             RadrootsSimplexAgentShortLinkScheme::Simplex,
    217             None,
    218             rest,
    219             value,
    220         );
    221     }
    222     if let Some(rest) = value.strip_prefix("https://") {
    223         let (authority, path) = rest
    224             .split_once('/')
    225             .ok_or_else(|| RadrootsSimplexAgentProtoError::InvalidLink(value.to_string()))?;
    226         if authority.is_empty() || authority.contains('@') {
    227             return Err(RadrootsSimplexAgentProtoError::InvalidLink(
    228                 value.to_string(),
    229             ));
    230         }
    231         validate_host(authority)?;
    232         return parse_scheme_link(
    233             RadrootsSimplexAgentShortLinkScheme::Https,
    234             Some(authority),
    235             path,
    236             value,
    237         );
    238     }
    239 
    240     Err(RadrootsSimplexAgentProtoError::InvalidLink(
    241         value.to_string(),
    242     ))
    243 }
    244 
    245 fn parse_scheme_link(
    246     scheme: RadrootsSimplexAgentShortLinkScheme,
    247     primary_host: Option<&str>,
    248     rest: &str,
    249     original: &str,
    250 ) -> Result<RadrootsSimplexAgentShortInvitationLink, RadrootsSimplexAgentProtoError> {
    251     let (raw_path, fragment_and_query) = rest
    252         .split_once('#')
    253         .ok_or_else(|| RadrootsSimplexAgentProtoError::InvalidLink(original.to_string()))?;
    254     let path = raw_path.strip_suffix('/').unwrap_or(raw_path);
    255     if path != "i" {
    256         return Err(RadrootsSimplexAgentProtoError::UnsupportedLink(
    257             unsupported_path_kind(path),
    258         ));
    259     }
    260 
    261     let (fragment, query) = fragment_and_query
    262         .split_once('?')
    263         .map_or((fragment_and_query, None), |(fragment, query)| {
    264             (fragment, Some(query))
    265         });
    266     let (link_id_raw, link_key_raw) = fragment
    267         .split_once('/')
    268         .ok_or_else(|| RadrootsSimplexAgentProtoError::InvalidLink(original.to_string()))?;
    269     if link_id_raw.is_empty() || link_key_raw.is_empty() || link_key_raw.contains('/') {
    270         return Err(RadrootsSimplexAgentProtoError::InvalidLink(
    271             original.to_string(),
    272         ));
    273     }
    274 
    275     let mut hosts = primary_host
    276         .map(|host| alloc::vec![host.to_string()])
    277         .unwrap_or_default();
    278     let mut port = None;
    279     let mut server_key_hash = None;
    280 
    281     if let Some(query) = query {
    282         for pair in query.split('&') {
    283             if pair.is_empty() {
    284                 continue;
    285             }
    286             let (key, raw_value) = pair.split_once('=').ok_or_else(|| {
    287                 RadrootsSimplexAgentProtoError::InvalidLinkParameter {
    288                     key: pair.to_string(),
    289                     reason: "parameter must use key=value form".to_string(),
    290                 }
    291             })?;
    292             match key {
    293                 "h" => {
    294                     if hosts.len() > primary_host.iter().count() {
    295                         return Err(duplicate_param("h"));
    296                     }
    297                     let parsed_hosts = parse_hosts(raw_value)?;
    298                     hosts.extend(parsed_hosts);
    299                 }
    300                 "p" => {
    301                     if port.replace(parse_port(raw_value)?).is_some() {
    302                         return Err(duplicate_param("p"));
    303                     }
    304                 }
    305                 "c" => {
    306                     if server_key_hash
    307                         .replace(decode_sized_base64url(
    308                             "server_key_hash",
    309                             raw_value,
    310                             RADROOTS_SIMPLEX_AGENT_SHORT_LINK_SERVER_KEY_HASH_LENGTH,
    311                         )?)
    312                         .is_some()
    313                     {
    314                         return Err(duplicate_param("c"));
    315                     }
    316                 }
    317                 _ => {
    318                     return Err(RadrootsSimplexAgentProtoError::InvalidLinkParameter {
    319                         key: key.to_string(),
    320                         reason: "unsupported short-link parameter".to_string(),
    321                     });
    322                 }
    323             }
    324         }
    325     }
    326 
    327     Ok(RadrootsSimplexAgentShortInvitationLink {
    328         scheme,
    329         hosts,
    330         port,
    331         server_key_hash,
    332         link_id: decode_sized_base64url(
    333             "link_id",
    334             link_id_raw,
    335             RADROOTS_SIMPLEX_AGENT_SHORT_LINK_ID_LENGTH,
    336         )?,
    337         link_key: decode_sized_base64url(
    338             "link_key",
    339             link_key_raw,
    340             RADROOTS_SIMPLEX_AGENT_SHORT_LINK_KEY_LENGTH,
    341         )?,
    342     })
    343 }
    344 
    345 fn unsupported_path_kind(path: &str) -> RadrootsSimplexAgentUnsupportedLinkKind {
    346     match path {
    347         "contact" => RadrootsSimplexAgentUnsupportedLinkKind::FullContactLink,
    348         "a" | "address" => RadrootsSimplexAgentUnsupportedLinkKind::ContactAddress,
    349         "g" | "group" => RadrootsSimplexAgentUnsupportedLinkKind::Group,
    350         "c" | "channel" => RadrootsSimplexAgentUnsupportedLinkKind::Channel,
    351         "r" | "relay" => RadrootsSimplexAgentUnsupportedLinkKind::Relay,
    352         "f" | "file" => RadrootsSimplexAgentUnsupportedLinkKind::File,
    353         "x" | "xrcp" => RadrootsSimplexAgentUnsupportedLinkKind::Xrcp,
    354         "b" | "bot" => RadrootsSimplexAgentUnsupportedLinkKind::Bot,
    355         _ => RadrootsSimplexAgentUnsupportedLinkKind::Unknown(path.to_string()),
    356     }
    357 }
    358 
    359 fn decode_base64url(
    360     field: &'static str,
    361     value: &str,
    362 ) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    363     URL_SAFE_NO_PAD
    364         .decode(value.as_bytes())
    365         .or_else(|_| URL_SAFE.decode(value.as_bytes()))
    366         .map_err(|_| RadrootsSimplexAgentProtoError::InvalidBase64Url {
    367             field,
    368             value: value.to_string(),
    369         })
    370 }
    371 
    372 fn decode_sized_base64url(
    373     field: &'static str,
    374     value: &str,
    375     expected: usize,
    376 ) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    377     let bytes = decode_base64url(field, value)?;
    378     validate_field_length(field, &bytes, expected)?;
    379     Ok(bytes)
    380 }
    381 
    382 fn validate_field_length(
    383     field: &'static str,
    384     bytes: &[u8],
    385     expected: usize,
    386 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    387     if bytes.len() != expected {
    388         return Err(RadrootsSimplexAgentProtoError::InvalidLinkFieldLength {
    389             field,
    390             expected,
    391             actual: bytes.len(),
    392         });
    393     }
    394     Ok(())
    395 }
    396 
    397 fn parse_hosts(value: &str) -> Result<Vec<String>, RadrootsSimplexAgentProtoError> {
    398     if value.is_empty() {
    399         return Err(RadrootsSimplexAgentProtoError::InvalidLinkParameter {
    400             key: "h".to_string(),
    401             reason: "host list cannot be empty".to_string(),
    402         });
    403     }
    404     let hosts = value
    405         .split(',')
    406         .map(|host| host.trim().to_string())
    407         .collect::<Vec<_>>();
    408     for host in &hosts {
    409         validate_host(host)?;
    410     }
    411     Ok(hosts)
    412 }
    413 
    414 fn validate_host(host: &str) -> Result<(), RadrootsSimplexAgentProtoError> {
    415     if host.is_empty()
    416         || host
    417             .chars()
    418             .any(|ch| ch.is_ascii_whitespace() || matches!(ch, '/' | '?' | '#' | '&' | '=' | ','))
    419     {
    420         return Err(RadrootsSimplexAgentProtoError::InvalidLinkParameter {
    421             key: "h".to_string(),
    422             reason: "host contains an invalid short-link character".to_string(),
    423         });
    424     }
    425     Ok(())
    426 }
    427 
    428 fn parse_port(value: &str) -> Result<u16, RadrootsSimplexAgentProtoError> {
    429     value
    430         .parse::<u16>()
    431         .map_err(|_| RadrootsSimplexAgentProtoError::InvalidPort(value.to_string()))
    432 }
    433 
    434 fn duplicate_param(key: &str) -> RadrootsSimplexAgentProtoError {
    435     RadrootsSimplexAgentProtoError::InvalidLinkParameter {
    436         key: key.to_string(),
    437         reason: "duplicate short-link parameter".to_string(),
    438     }
    439 }
    440 
    441 fn official_agent_version_range() -> ShortLinkResult<RadrootsSimplexSmpVersionRange> {
    442     Ok(RadrootsSimplexSmpVersionRange::new(
    443         SIMPLEX_AGENT_SHORT_LINK_MIN_VERSION,
    444         SIMPLEX_AGENT_SHORT_LINK_CURRENT_VERSION,
    445     )?)
    446 }
    447 
    448 fn encode_official_invitation_connection_request(
    449     buffer: &mut Vec<u8>,
    450     agent_version_range: RadrootsSimplexSmpVersionRange,
    451     invitation: &RadrootsSimplexAgentConnectionLink,
    452 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    453     buffer.push(SIMPLEX_CONNECTION_MODE_INVITATION);
    454     push_version_range(buffer, agent_version_range);
    455     push_queue_list(buffer, core::slice::from_ref(&invitation.invitation_queue))?;
    456     push_maybe_large_bytes(buffer, None)?;
    457     encode_official_x3dh_params(buffer, &invitation.e2e_ratchet_params)
    458 }
    459 
    460 fn decode_official_invitation_connection_request(
    461     cursor: &mut ShortLinkDataCursor<'_>,
    462 ) -> Result<RadrootsSimplexAgentConnectionLink, RadrootsSimplexAgentProtoError> {
    463     cursor.expect_byte(SIMPLEX_CONNECTION_MODE_INVITATION)?;
    464     let _agent_version_range = cursor.read_version_range()?;
    465     let invitation_queues = cursor.read_queue_list()?;
    466     let _client_data = cursor.read_maybe_large_bytes()?;
    467     let e2e_ratchet_params = cursor.read_x3dh_params()?;
    468     let invitation_queue = invitation_queues.into_iter().next().ok_or_else(|| {
    469         RadrootsSimplexAgentProtoError::InvalidLink(
    470             "short invitation connection request has no SMP queues".to_string(),
    471         )
    472     })?;
    473     Ok(RadrootsSimplexAgentConnectionLink {
    474         invitation_queue,
    475         connection_id: Vec::new(),
    476         e2e_ratchet_params,
    477         contact_address: false,
    478     })
    479 }
    480 
    481 fn push_version_range(buffer: &mut Vec<u8>, version_range: RadrootsSimplexSmpVersionRange) {
    482     buffer.extend_from_slice(&version_range.min.to_be_bytes());
    483     buffer.extend_from_slice(&version_range.max.to_be_bytes());
    484 }
    485 
    486 fn push_queue_list(
    487     buffer: &mut Vec<u8>,
    488     queues: &[RadrootsSimplexSmpQueueUri],
    489 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    490     if queues.is_empty() || queues.len() > u8::MAX as usize {
    491         return Err(RadrootsSimplexAgentProtoError::InvalidShortFieldLength(
    492             queues.len(),
    493         ));
    494     }
    495     buffer.push(queues.len() as u8);
    496     for queue in queues {
    497         encode_official_queue_uri(buffer, queue)?;
    498     }
    499     Ok(())
    500 }
    501 
    502 fn encode_official_queue_uri(
    503     buffer: &mut Vec<u8>,
    504     queue: &RadrootsSimplexSmpQueueUri,
    505 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    506     push_version_range(buffer, queue.version_range);
    507     encode_official_server_address(buffer, &queue.server)?;
    508     push_short_bytes(buffer, &decode_base64url("sender_id", &queue.sender_id)?)?;
    509     let queue_public_key =
    510         decode_base64url("recipient_dh_public_key", &queue.recipient_dh_public_key)?;
    511     let queue_public_key = encode_x25519_public_key_x509(
    512         &decode_x25519_public_key_x509(&queue_public_key)
    513             .map_err(|error| RadrootsSimplexAgentProtoError::InvalidLink(error.to_string()))?,
    514     )
    515     .map_err(|error| RadrootsSimplexAgentProtoError::InvalidLink(error.to_string()))?;
    516     push_short_bytes(buffer, &queue_public_key)?;
    517     if queue.version_range.min >= 4 {
    518         if let Some(queue_mode) = queue.queue_mode {
    519             buffer.push(match queue_mode {
    520                 RadrootsSimplexSmpQueueMode::Messaging => SIMPLEX_QUEUE_MODE_MESSAGING,
    521                 RadrootsSimplexSmpQueueMode::Contact => SIMPLEX_QUEUE_MODE_CONTACT,
    522             });
    523         }
    524     } else if queue.sender_can_secure() {
    525         buffer.push(b'T');
    526     }
    527     Ok(())
    528 }
    529 
    530 fn encode_official_server_address(
    531     buffer: &mut Vec<u8>,
    532     server: &RadrootsSimplexSmpServerAddress,
    533 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    534     push_string_list(buffer, &server.hosts)?;
    535     let port = server
    536         .port
    537         .map_or_else(String::new, |port| port.to_string());
    538     push_string(buffer, &port)?;
    539     push_short_bytes(
    540         buffer,
    541         &decode_base64url("server_identity", &server.server_identity)?,
    542     )
    543 }
    544 
    545 fn encode_official_x3dh_params(
    546     buffer: &mut Vec<u8>,
    547     params: &RadrootsSimplexOfficialX3dhParams,
    548 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    549     push_version_range(buffer, params.version_range);
    550     push_short_bytes(
    551         buffer,
    552         &encode_official_x448_public_key_der(&params.key_1).map_err(|error| {
    553             RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string())
    554         })?,
    555     )?;
    556     push_short_bytes(
    557         buffer,
    558         &encode_official_x448_public_key_der(&params.key_2).map_err(|error| {
    559             RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string())
    560         })?,
    561     )?;
    562     buffer.push(SIMPLEX_MAYBE_NOTHING);
    563     Ok(())
    564 }
    565 
    566 fn push_string_list(
    567     buffer: &mut Vec<u8>,
    568     values: &[String],
    569 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    570     if values.is_empty() || values.len() > u8::MAX as usize {
    571         return Err(RadrootsSimplexAgentProtoError::InvalidShortFieldLength(
    572             values.len(),
    573         ));
    574     }
    575     buffer.push(values.len() as u8);
    576     for value in values {
    577         push_string(buffer, value)?;
    578     }
    579     Ok(())
    580 }
    581 
    582 fn push_string(buffer: &mut Vec<u8>, value: &str) -> Result<(), RadrootsSimplexAgentProtoError> {
    583     push_short_bytes(buffer, value.as_bytes())
    584 }
    585 
    586 fn push_short_bytes(
    587     buffer: &mut Vec<u8>,
    588     value: &[u8],
    589 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    590     if value.len() > u8::MAX as usize {
    591         return Err(RadrootsSimplexAgentProtoError::InvalidShortFieldLength(
    592             value.len(),
    593         ));
    594     }
    595     buffer.push(value.len() as u8);
    596     buffer.extend_from_slice(value);
    597     Ok(())
    598 }
    599 
    600 fn push_user_link_data(
    601     buffer: &mut Vec<u8>,
    602     value: &[u8],
    603 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    604     if value.len() < SIMPLEX_USER_LINK_DATA_LARGE_TAG as usize {
    605         push_short_bytes(buffer, value)
    606     } else {
    607         buffer.push(SIMPLEX_USER_LINK_DATA_LARGE_TAG);
    608         push_large_bytes(buffer, value)
    609     }
    610 }
    611 
    612 fn push_large_bytes(
    613     buffer: &mut Vec<u8>,
    614     value: &[u8],
    615 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    616     if value.len() > u16::MAX as usize {
    617         return Err(RadrootsSimplexAgentProtoError::InvalidLargeFieldLength(
    618             value.len(),
    619         ));
    620     }
    621     buffer.extend_from_slice(&(value.len() as u16).to_be_bytes());
    622     buffer.extend_from_slice(value);
    623     Ok(())
    624 }
    625 
    626 fn push_maybe_large_bytes(
    627     buffer: &mut Vec<u8>,
    628     value: Option<&[u8]>,
    629 ) -> Result<(), RadrootsSimplexAgentProtoError> {
    630     match value {
    631         Some(value) => {
    632             buffer.push(SIMPLEX_MAYBE_JUST);
    633             push_large_bytes(buffer, value)
    634         }
    635         None => {
    636             buffer.push(SIMPLEX_MAYBE_NOTHING);
    637             Ok(())
    638         }
    639     }
    640 }
    641 
    642 struct ShortLinkDataCursor<'a> {
    643     bytes: &'a [u8],
    644     offset: usize,
    645 }
    646 
    647 impl<'a> ShortLinkDataCursor<'a> {
    648     const fn new(bytes: &'a [u8]) -> Self {
    649         Self { bytes, offset: 0 }
    650     }
    651 
    652     fn expect_byte(&mut self, expected: u8) -> Result<(), RadrootsSimplexAgentProtoError> {
    653         let actual = self.read_byte()?;
    654         if actual != expected {
    655             return Err(RadrootsSimplexAgentProtoError::InvalidTag(
    656                 String::from_utf8_lossy(&[actual]).into_owned(),
    657             ));
    658         }
    659         Ok(())
    660     }
    661 
    662     fn read_version_range(
    663         &mut self,
    664     ) -> Result<RadrootsSimplexSmpVersionRange, RadrootsSimplexAgentProtoError> {
    665         if self.remaining().len() < 4 {
    666             return Err(RadrootsSimplexAgentProtoError::UnexpectedEof);
    667         }
    668         let min = u16::from_be_bytes([self.bytes[self.offset], self.bytes[self.offset + 1]]);
    669         let max = u16::from_be_bytes([self.bytes[self.offset + 2], self.bytes[self.offset + 3]]);
    670         self.offset += 4;
    671         Ok(RadrootsSimplexSmpVersionRange::new(min, max)?)
    672     }
    673 
    674     fn read_queue_list(
    675         &mut self,
    676     ) -> Result<Vec<RadrootsSimplexSmpQueueUri>, RadrootsSimplexAgentProtoError> {
    677         let len = self.read_byte()? as usize;
    678         if len == 0 {
    679             return Err(RadrootsSimplexAgentProtoError::InvalidShortFieldLength(0));
    680         }
    681         let mut queues = Vec::with_capacity(len);
    682         for _ in 0..len {
    683             queues.push(self.read_queue_uri()?);
    684         }
    685         Ok(queues)
    686     }
    687 
    688     fn read_queue_uri(
    689         &mut self,
    690     ) -> Result<RadrootsSimplexSmpQueueUri, RadrootsSimplexAgentProtoError> {
    691         let version_range = self.read_version_range()?;
    692         let server = self.read_server_address()?;
    693         let sender_id = URL_SAFE.encode(self.read_short_bytes()?);
    694         let recipient_dh_public_key = self.read_short_bytes()?;
    695         let recipient_dh_public_key = encode_x25519_public_key_x509(
    696             &decode_x25519_public_key_x509(&recipient_dh_public_key)
    697                 .map_err(|error| RadrootsSimplexAgentProtoError::InvalidLink(error.to_string()))?,
    698         )
    699         .map_err(|error| RadrootsSimplexAgentProtoError::InvalidLink(error.to_string()))?;
    700         let recipient_dh_public_key = URL_SAFE.encode(recipient_dh_public_key);
    701         let queue_mode = match self.peek_byte() {
    702             Some(SIMPLEX_QUEUE_MODE_MESSAGING) => {
    703                 self.read_byte()?;
    704                 Some(RadrootsSimplexSmpQueueMode::Messaging)
    705             }
    706             Some(SIMPLEX_QUEUE_MODE_CONTACT) => {
    707                 self.read_byte()?;
    708                 Some(RadrootsSimplexSmpQueueMode::Contact)
    709             }
    710             Some(b'T') if version_range.min < 4 => {
    711                 self.read_byte()?;
    712                 Some(RadrootsSimplexSmpQueueMode::Messaging)
    713             }
    714             Some(b'F') if version_range.min < 4 => {
    715                 self.read_byte()?;
    716                 Some(RadrootsSimplexSmpQueueMode::Contact)
    717             }
    718             _ => None,
    719         };
    720         Ok(RadrootsSimplexSmpQueueUri {
    721             server,
    722             sender_id,
    723             version_range,
    724             recipient_dh_public_key,
    725             queue_mode,
    726         })
    727     }
    728 
    729     fn read_server_address(
    730         &mut self,
    731     ) -> Result<RadrootsSimplexSmpServerAddress, RadrootsSimplexAgentProtoError> {
    732         let hosts = self.read_string_list()?;
    733         let port = match self.read_string()?.as_str() {
    734             "" => None,
    735             value => Some(
    736                 value
    737                     .parse::<u16>()
    738                     .map_err(|_| RadrootsSimplexAgentProtoError::InvalidPort(value.to_string()))?,
    739             ),
    740         };
    741         let server_identity = URL_SAFE.encode(self.read_short_bytes()?);
    742         Ok(RadrootsSimplexSmpServerAddress {
    743             server_identity,
    744             hosts,
    745             port,
    746         })
    747     }
    748 
    749     fn read_string_list(&mut self) -> Result<Vec<String>, RadrootsSimplexAgentProtoError> {
    750         let len = self.read_byte()? as usize;
    751         if len == 0 {
    752             return Err(RadrootsSimplexAgentProtoError::InvalidShortFieldLength(0));
    753         }
    754         let mut values = Vec::with_capacity(len);
    755         for _ in 0..len {
    756             values.push(self.read_string()?);
    757         }
    758         Ok(values)
    759     }
    760 
    761     fn read_string(&mut self) -> Result<String, RadrootsSimplexAgentProtoError> {
    762         String::from_utf8(self.read_short_bytes()?)
    763             .map_err(|error| RadrootsSimplexAgentProtoError::InvalidUtf8(error.to_string()))
    764     }
    765 
    766     fn read_x3dh_params(
    767         &mut self,
    768     ) -> Result<RadrootsSimplexOfficialX3dhParams, RadrootsSimplexAgentProtoError> {
    769         let version_range = self.read_version_range()?;
    770         let key_1 =
    771             decode_official_x448_public_key_der(&self.read_short_bytes()?).map_err(|error| {
    772                 RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string())
    773             })?;
    774         let key_2 =
    775             decode_official_x448_public_key_der(&self.read_short_bytes()?).map_err(|error| {
    776                 RadrootsSimplexAgentProtoError::InvalidE2eParameters(error.to_string())
    777             })?;
    778         let (pq_public_key, pq_ciphertext) = self.read_optional_kem_params()?;
    779         Ok(RadrootsSimplexOfficialX3dhParams {
    780             version_range,
    781             key_1,
    782             key_2,
    783             pq_public_key,
    784             pq_ciphertext,
    785         })
    786     }
    787 
    788     fn read_optional_kem_params(
    789         &mut self,
    790     ) -> Result<OptionalKemParams, RadrootsSimplexAgentProtoError> {
    791         match self.read_byte()? {
    792             SIMPLEX_MAYBE_NOTHING => Ok((None, None)),
    793             SIMPLEX_MAYBE_JUST => match self.read_byte()? {
    794                 SIMPLEX_RATCHET_KEM_PROPOSED => Ok((Some(self.read_large_bytes()?), None)),
    795                 SIMPLEX_RATCHET_KEM_ACCEPTED => {
    796                     let ciphertext = self.read_large_bytes()?;
    797                     let public_key = self.read_large_bytes()?;
    798                     Ok((Some(public_key), Some(ciphertext)))
    799                 }
    800                 tag => Err(RadrootsSimplexAgentProtoError::InvalidTag(
    801                     String::from_utf8_lossy(&[tag]).into_owned(),
    802                 )),
    803             },
    804             tag => Err(RadrootsSimplexAgentProtoError::InvalidTag(
    805                 String::from_utf8_lossy(&[tag]).into_owned(),
    806             )),
    807         }
    808     }
    809 
    810     fn read_maybe_large_bytes(
    811         &mut self,
    812     ) -> Result<Option<Vec<u8>>, RadrootsSimplexAgentProtoError> {
    813         match self.read_byte()? {
    814             SIMPLEX_MAYBE_NOTHING => Ok(None),
    815             SIMPLEX_MAYBE_JUST => Ok(Some(self.read_large_bytes()?)),
    816             tag => Err(RadrootsSimplexAgentProtoError::InvalidTag(
    817                 String::from_utf8_lossy(&[tag]).into_owned(),
    818             )),
    819         }
    820     }
    821 
    822     fn read_user_link_data(&mut self) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    823         let len = self.read_byte()?;
    824         if len == SIMPLEX_USER_LINK_DATA_LARGE_TAG {
    825             self.read_large_bytes()
    826         } else {
    827             self.read_exact(len as usize)
    828         }
    829     }
    830 
    831     fn read_short_bytes(&mut self) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    832         let len = self.read_byte()? as usize;
    833         self.read_exact(len)
    834     }
    835 
    836     fn read_large_bytes(&mut self) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    837         if self.remaining().len() < 2 {
    838             return Err(RadrootsSimplexAgentProtoError::UnexpectedEof);
    839         }
    840         let len =
    841             u16::from_be_bytes([self.bytes[self.offset], self.bytes[self.offset + 1]]) as usize;
    842         self.offset += 2;
    843         self.read_exact(len)
    844     }
    845 
    846     fn read_byte(&mut self) -> Result<u8, RadrootsSimplexAgentProtoError> {
    847         if self.offset >= self.bytes.len() {
    848             return Err(RadrootsSimplexAgentProtoError::UnexpectedEof);
    849         }
    850         let value = self.bytes[self.offset];
    851         self.offset += 1;
    852         Ok(value)
    853     }
    854 
    855     fn peek_byte(&self) -> Option<u8> {
    856         self.bytes.get(self.offset).copied()
    857     }
    858 
    859     fn read_exact(&mut self, len: usize) -> Result<Vec<u8>, RadrootsSimplexAgentProtoError> {
    860         if self.remaining().len() < len {
    861             return Err(RadrootsSimplexAgentProtoError::UnexpectedEof);
    862         }
    863         let value = self.remaining()[..len].to_vec();
    864         self.offset += len;
    865         Ok(value)
    866     }
    867 
    868     fn remaining(&self) -> &'a [u8] {
    869         &self.bytes[self.offset..]
    870     }
    871 }
    872 
    873 #[cfg(test)]
    874 mod tests {
    875     use super::*;
    876 
    877     fn sample_link() -> RadrootsSimplexAgentShortInvitationLink {
    878         RadrootsSimplexAgentShortInvitationLink {
    879             scheme: RadrootsSimplexAgentShortLinkScheme::Simplex,
    880             hosts: alloc::vec!["relay-a.example".to_string(), "relay-b.example".to_string()],
    881             port: Some(5223),
    882             server_key_hash: Some((0_u8..32).collect()),
    883             link_id: (32_u8..56).collect(),
    884             link_key: (64_u8..96).collect(),
    885         }
    886     }
    887 
    888     fn sample_connection_link() -> RadrootsSimplexAgentConnectionLink {
    889         let queue_key =
    890             radroots_simplex_smp_crypto::prelude::RadrootsSimplexSmpX25519Keypair::from_seed(
    891                 b"rr-synth-short-link-queue-dh",
    892             );
    893         let server_id = URL_SAFE.encode([7_u8; 32]);
    894         let sender_id = URL_SAFE.encode([9_u8; RADROOTS_SIMPLEX_AGENT_SHORT_LINK_ID_LENGTH]);
    895         let queue_dh = URL_SAFE.encode(
    896             radroots_simplex_smp_crypto::prelude::encode_x25519_public_key_x509(
    897                 &queue_key.public_key,
    898             )
    899             .expect("queue key"),
    900         );
    901         let key_1 = radroots_simplex_smp_crypto::prelude::official_x448_keypair_from_seed(
    902             b"rr-synth-short-link-x3dh-1",
    903         );
    904         let key_2 = radroots_simplex_smp_crypto::prelude::official_x448_keypair_from_seed(
    905             b"rr-synth-short-link-x3dh-2",
    906         );
    907         RadrootsSimplexAgentConnectionLink {
    908             invitation_queue:
    909                 radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpQueueUri::parse(&format!(
    910                     "smp://{server_id}@relay.example/{sender_id}#/?v=4&dh={queue_dh}&q=m"
    911                 ))
    912                 .expect("queue"),
    913             connection_id: b"conn-synth-short-link".to_vec(),
    914             e2e_ratchet_params:
    915                 radroots_simplex_smp_crypto::prelude::RadrootsSimplexOfficialX3dhParams {
    916                     version_range:
    917                         radroots_simplex_smp_proto::prelude::RadrootsSimplexSmpVersionRange::new(
    918                             1, 2,
    919                         )
    920                         .expect("version range"),
    921                     key_1: key_1.public_key,
    922                     key_2: key_2.public_key,
    923                     pq_public_key: None,
    924                     pq_ciphertext: None,
    925                 },
    926             contact_address: false,
    927         }
    928     }
    929 
    930     #[test]
    931     fn renders_and_parses_simplex_invitation_short_link() {
    932         let link = sample_link();
    933         let rendered = link.render().expect("rendered link");
    934 
    935         assert!(rendered.starts_with("simplex:/i#"));
    936         assert!(rendered.contains("?h=relay-a.example,relay-b.example&p=5223&c="));
    937         let fragment = rendered
    938             .split_once('#')
    939             .expect("fragment")
    940             .1
    941             .split_once('?')
    942             .expect("query")
    943             .0;
    944         assert!(!fragment.contains('='));
    945         assert_eq!(
    946             parse_short_invitation_link(&rendered).expect("parsed"),
    947             link
    948         );
    949     }
    950 
    951     #[test]
    952     fn renders_and_parses_https_invitation_short_link() {
    953         let mut link = sample_link();
    954         link.scheme = RadrootsSimplexAgentShortLinkScheme::Https;
    955         link.hosts = alloc::vec!["relay-a.example".to_string(), "relay-b.example".to_string()];
    956 
    957         let rendered = link.render().expect("rendered link");
    958 
    959         assert!(rendered.starts_with("https://relay-a.example/i#"));
    960         assert!(rendered.contains("?h=relay-b.example&p=5223&c="));
    961         assert_eq!(
    962             parse_short_invitation_link(&rendered).expect("parsed"),
    963             link
    964         );
    965     }
    966 
    967     #[test]
    968     fn rejects_full_contact_links() {
    969         let error = parse_short_invitation_link("simplex:/contact#/?v=1&smp=ignored&e2e=ignored")
    970             .expect_err("full links fail");
    971 
    972         assert!(matches!(
    973             error,
    974             RadrootsSimplexAgentProtoError::UnsupportedLink(
    975                 RadrootsSimplexAgentUnsupportedLinkKind::FullContactLink
    976             )
    977         ));
    978     }
    979 
    980     #[test]
    981     fn rejects_unsupported_short_link_kinds() {
    982         let link = sample_link().render().expect("rendered link");
    983         let (_, fragment) = link.split_once('#').expect("fragment");
    984         let contact = format!("simplex:/a#{fragment}");
    985         let group = format!("simplex:/g#{fragment}");
    986         let channel = format!("simplex:/c#{fragment}");
    987 
    988         assert!(matches!(
    989             parse_short_invitation_link(&contact),
    990             Err(RadrootsSimplexAgentProtoError::UnsupportedLink(
    991                 RadrootsSimplexAgentUnsupportedLinkKind::ContactAddress
    992             ))
    993         ));
    994         assert!(matches!(
    995             parse_short_invitation_link(&group),
    996             Err(RadrootsSimplexAgentProtoError::UnsupportedLink(
    997                 RadrootsSimplexAgentUnsupportedLinkKind::Group
    998             ))
    999         ));
   1000         assert!(matches!(
   1001             parse_short_invitation_link(&channel),
   1002             Err(RadrootsSimplexAgentProtoError::UnsupportedLink(
   1003                 RadrootsSimplexAgentUnsupportedLinkKind::Channel
   1004             ))
   1005         ));
   1006     }
   1007 
   1008     #[test]
   1009     fn rejects_invalid_base64url_parts() {
   1010         let error =
   1011             parse_short_invitation_link("simplex:/i#***/AAAA").expect_err("invalid link id fails");
   1012 
   1013         assert!(matches!(
   1014             error,
   1015             RadrootsSimplexAgentProtoError::InvalidBase64Url {
   1016                 field: "link_id",
   1017                 ..
   1018             }
   1019         ));
   1020     }
   1021 
   1022     #[test]
   1023     fn rejects_wrong_sized_decodable_parts() {
   1024         let link_id = URL_SAFE_NO_PAD.encode([1_u8; RADROOTS_SIMPLEX_AGENT_SHORT_LINK_ID_LENGTH]);
   1025         let link_key = URL_SAFE_NO_PAD.encode([2_u8; 4]);
   1026         let error = parse_short_invitation_link(&format!("simplex:/i#{link_id}/{link_key}"))
   1027             .expect_err("short link key fails");
   1028 
   1029         assert!(matches!(
   1030             error,
   1031             RadrootsSimplexAgentProtoError::InvalidLinkFieldLength {
   1032                 field: "link_key",
   1033                 expected: RADROOTS_SIMPLEX_AGENT_SHORT_LINK_KEY_LENGTH,
   1034                 actual: 4,
   1035             }
   1036         ));
   1037     }
   1038 
   1039     #[test]
   1040     fn rejects_unknown_query_parameters() {
   1041         let link = sample_link().render().expect("rendered link");
   1042         let error = parse_short_invitation_link(&format!("{link}&z=1"))
   1043             .expect_err("unknown parameter fails");
   1044 
   1045         assert!(matches!(
   1046             error,
   1047             RadrootsSimplexAgentProtoError::InvalidLinkParameter { key, .. } if key == "z"
   1048         ));
   1049     }
   1050 
   1051     #[test]
   1052     fn encodes_and_decodes_short_invitation_fixed_data() {
   1053         let invitation = sample_connection_link();
   1054         let root_public_key = vec![42_u8; 32];
   1055         let encoded =
   1056             encode_short_invitation_fixed_data(&root_public_key, &invitation).expect("encoded");
   1057         let decoded = decode_short_invitation_fixed_data(&encoded).expect("decoded");
   1058         let encoded_user_data = encode_short_invitation_user_data(&invitation).expect("user data");
   1059         let decoded_user_data =
   1060             decode_short_invitation_user_data(&encoded_user_data).expect("decoded user data");
   1061 
   1062         assert_ne!(&encoded[..6], b"RRSIF1");
   1063         assert_eq!(decoded.agent_version_range.min, 2);
   1064         assert_eq!(decoded.agent_version_range.max, 7);
   1065         assert_eq!(decoded.root_public_signature_key, root_public_key);
   1066         assert_eq!(decoded.link_entity_id, None);
   1067         assert!(decoded.invitation.connection_id.is_empty());
   1068         assert_eq!(
   1069             decoded.invitation.invitation_queue,
   1070             invitation.invitation_queue
   1071         );
   1072         assert_eq!(
   1073             decoded.invitation.e2e_ratchet_params,
   1074             invitation.e2e_ratchet_params
   1075         );
   1076         assert_eq!(decoded_user_data.agent_version_range.min, 2);
   1077         assert_eq!(decoded_user_data.agent_version_range.max, 7);
   1078         assert_eq!(
   1079             decoded_user_data.user_data,
   1080             b"conn-synth-short-link".to_vec()
   1081         );
   1082     }
   1083 
   1084     #[test]
   1085     fn rejects_legacy_radroots_short_invitation_fixed_data() {
   1086         let mut legacy = b"RRSIF1".to_vec();
   1087         legacy.push(32);
   1088         legacy.extend_from_slice(&[42_u8; 32]);
   1089         legacy.extend_from_slice(&0_u16.to_be_bytes());
   1090 
   1091         assert!(decode_short_invitation_fixed_data(&legacy).is_err());
   1092     }
   1093 }