lib

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

short_link.rs (39290B)


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