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(¶ms.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(¶ms.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 }