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