1use std::collections::HashMap;
91use std::fmt;
92use std::str::FromStr;
93
94use chrono::{DateTime, NaiveDateTime, Utc};
95
96use crate::Error;
97
98use super::{compute_digest, Descriptor, DigestEncoding, DigestHash};
99
100#[derive(Debug, Clone, PartialEq)]
133pub struct IntroductionPointV2 {
134 pub identifier: String,
136 pub address: String,
138 pub port: u16,
140 pub onion_key: Option<String>,
142 pub service_key: Option<String>,
144 pub intro_authentication: Vec<(String, String)>,
146}
147
148impl IntroductionPointV2 {
149 fn parse(content: &str) -> Result<Self, Error> {
165 let mut identifier = String::new();
166 let mut address = String::new();
167 let mut port: u16 = 0;
168 let mut onion_key: Option<String> = None;
169 let mut service_key: Option<String> = None;
170 let intro_authentication: Vec<(String, String)> = Vec::new();
171
172 let lines: Vec<&str> = content.lines().collect();
173 let mut idx = 0;
174
175 while idx < lines.len() {
176 let line = lines[idx];
177 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
178 (&line[..space_pos], line[space_pos + 1..].trim())
179 } else {
180 (line, "")
181 };
182
183 match keyword {
184 "introduction-point" => identifier = value.to_string(),
185 "ip-address" => address = value.to_string(),
186 "onion-port" => {
187 port = value.parse().map_err(|_| Error::Parse {
188 location: "introduction-point".to_string(),
189 reason: format!("invalid port: {}", value),
190 })?;
191 }
192 "onion-key" => {
193 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
194 onion_key = Some(block);
195 idx = end_idx;
196 }
197 "service-key" => {
198 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
199 service_key = Some(block);
200 idx = end_idx;
201 }
202 _ => {}
203 }
204 idx += 1;
205 }
206
207 Ok(Self {
208 identifier,
209 address,
210 port,
211 onion_key,
212 service_key,
213 intro_authentication,
214 })
215 }
216}
217
218#[derive(Debug, Clone, PartialEq)]
276pub struct HiddenServiceDescriptorV2 {
277 pub descriptor_id: String,
279 pub version: u32,
281 pub permanent_key: Option<String>,
283 pub secret_id_part: String,
285 pub published: DateTime<Utc>,
287 pub protocol_versions: Vec<u32>,
289 pub introduction_points_encoded: Option<String>,
291 pub introduction_points_content: Option<Vec<u8>>,
293 pub signature: String,
295 raw_content: Vec<u8>,
297 unrecognized_lines: Vec<String>,
299}
300
301impl HiddenServiceDescriptorV2 {
302 pub fn introduction_points(&self) -> Result<Vec<IntroductionPointV2>, Error> {
339 let content = match &self.introduction_points_content {
340 Some(c) if !c.is_empty() => c,
341 _ => return Ok(Vec::new()),
342 };
343
344 let content_str = std::str::from_utf8(content).map_err(|_| Error::Parse {
345 location: "introduction-points".to_string(),
346 reason: "invalid UTF-8 in introduction points".to_string(),
347 })?;
348
349 if !content_str.starts_with("introduction-point ") {
350 return Err(Error::Parse {
351 location: "introduction-points".to_string(),
352 reason: "content is encrypted or malformed".to_string(),
353 });
354 }
355
356 let mut points = Vec::new();
357 let mut current_block = String::new();
358
359 for line in content_str.lines() {
360 if line.starts_with("introduction-point ") && !current_block.is_empty() {
361 points.push(IntroductionPointV2::parse(¤t_block)?);
362 current_block.clear();
363 }
364 current_block.push_str(line);
365 current_block.push('\n');
366 }
367
368 if !current_block.is_empty() {
369 points.push(IntroductionPointV2::parse(¤t_block)?);
370 }
371
372 Ok(points)
373 }
374
375 fn parse_protocol_versions(value: &str) -> Result<Vec<u32>, Error> {
389 if value.is_empty() {
390 return Ok(Vec::new());
391 }
392
393 value
394 .split(',')
395 .map(|v| {
396 let v = v.trim();
397 v.parse::<u32>().map_err(|_| Error::Parse {
398 location: "protocol-versions".to_string(),
399 reason: format!("invalid version: {}", v),
400 })
401 })
402 .collect()
403 }
404
405 fn decode_introduction_points(encoded: &str) -> Option<Vec<u8>> {
417 let content = encoded
418 .lines()
419 .filter(|l| !l.starts_with("-----"))
420 .collect::<Vec<_>>()
421 .join("");
422
423 if content.is_empty() {
424 return Some(Vec::new());
425 }
426
427 base64_decode(&content)
428 }
429}
430
431impl Descriptor for HiddenServiceDescriptorV2 {
432 fn parse(content: &str) -> Result<Self, Error> {
433 let raw_content = content.as_bytes().to_vec();
434 let lines: Vec<&str> = content.lines().collect();
435
436 let mut descriptor_id = String::new();
437 let mut version: u32 = 0;
438 let mut permanent_key: Option<String> = None;
439 let mut secret_id_part = String::new();
440 let mut published: Option<DateTime<Utc>> = None;
441 let mut protocol_versions: Vec<u32> = Vec::new();
442 let mut introduction_points_encoded: Option<String> = None;
443 let mut introduction_points_content: Option<Vec<u8>> = None;
444 let mut signature = String::new();
445 let mut unrecognized_lines: Vec<String> = Vec::new();
446
447 let mut idx = 0;
448 while idx < lines.len() {
449 let line = lines[idx];
450
451 if line.starts_with("@type ") {
452 idx += 1;
453 continue;
454 }
455
456 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
457 (&line[..space_pos], line[space_pos + 1..].trim())
458 } else {
459 (line, "")
460 };
461
462 match keyword {
463 "rendezvous-service-descriptor" => {
464 descriptor_id = value.to_string();
465 }
466 "version" => {
467 version = value.parse().map_err(|_| Error::Parse {
468 location: "version".to_string(),
469 reason: format!("invalid version: {}", value),
470 })?;
471 }
472 "permanent-key" => {
473 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
474 permanent_key = Some(block);
475 idx = end_idx;
476 }
477 "secret-id-part" => {
478 secret_id_part = value.to_string();
479 }
480 "publication-time" => {
481 let datetime = NaiveDateTime::parse_from_str(value, "%Y-%m-%d %H:%M:%S")
482 .map_err(|e| Error::Parse {
483 location: "publication-time".to_string(),
484 reason: format!("invalid datetime: {} - {}", value, e),
485 })?;
486 published = Some(datetime.and_utc());
487 }
488 "protocol-versions" => {
489 protocol_versions = Self::parse_protocol_versions(value)?;
490 }
491 "introduction-points" => {
492 let (block, end_idx) = extract_message_block(&lines, idx + 1);
493 introduction_points_encoded = Some(block.clone());
494 introduction_points_content = Self::decode_introduction_points(&block);
495 idx = end_idx;
496 }
497 "signature" => {
498 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
499 signature = block;
500 idx = end_idx;
501 }
502 _ => {
503 if !line.is_empty() && !line.starts_with("-----") {
504 unrecognized_lines.push(line.to_string());
505 }
506 }
507 }
508 idx += 1;
509 }
510
511 let published = published.ok_or_else(|| Error::Parse {
512 location: "publication-time".to_string(),
513 reason: "missing publication-time".to_string(),
514 })?;
515
516 Ok(Self {
517 descriptor_id,
518 version,
519 permanent_key,
520 secret_id_part,
521 published,
522 protocol_versions,
523 introduction_points_encoded,
524 introduction_points_content,
525 signature,
526 raw_content,
527 unrecognized_lines,
528 })
529 }
530
531 fn to_descriptor_string(&self) -> String {
532 let mut result = String::new();
533
534 result.push_str(&format!(
535 "rendezvous-service-descriptor {}\n",
536 self.descriptor_id
537 ));
538 result.push_str(&format!("version {}\n", self.version));
539
540 if let Some(ref key) = self.permanent_key {
541 result.push_str("permanent-key\n");
542 result.push_str(key);
543 result.push('\n');
544 }
545
546 result.push_str(&format!("secret-id-part {}\n", self.secret_id_part));
547 result.push_str(&format!(
548 "publication-time {}\n",
549 self.published.format("%Y-%m-%d %H:%M:%S")
550 ));
551
552 let versions: Vec<String> = self
553 .protocol_versions
554 .iter()
555 .map(|v| v.to_string())
556 .collect();
557 result.push_str(&format!("protocol-versions {}\n", versions.join(",")));
558
559 if let Some(ref encoded) = self.introduction_points_encoded {
560 result.push_str("introduction-points\n");
561 result.push_str(encoded);
562 result.push('\n');
563 }
564
565 result.push_str("signature\n");
566 result.push_str(&self.signature);
567 result.push('\n');
568
569 result
570 }
571
572 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
573 Ok(compute_digest(&self.raw_content, hash, encoding))
574 }
575
576 fn raw_content(&self) -> &[u8] {
577 &self.raw_content
578 }
579
580 fn unrecognized_lines(&self) -> &[String] {
581 &self.unrecognized_lines
582 }
583}
584
585impl FromStr for HiddenServiceDescriptorV2 {
586 type Err = Error;
587
588 fn from_str(s: &str) -> Result<Self, Self::Err> {
589 Self::parse(s)
590 }
591}
592
593impl fmt::Display for HiddenServiceDescriptorV2 {
594 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
595 write!(f, "{}", self.to_descriptor_string())
596 }
597}
598
599#[derive(Debug, Clone, PartialEq)]
635pub enum LinkSpecifier {
636 IPv4 {
638 address: String,
640 port: u16,
642 },
643 IPv6 {
645 address: String,
647 port: u16,
649 },
650 Fingerprint(String),
655 Ed25519(String),
659 Unknown {
661 link_type: u8,
663 data: Vec<u8>,
665 },
666}
667
668impl LinkSpecifier {
669 pub fn pack(&self) -> Vec<u8> {
693 match self {
694 LinkSpecifier::IPv4 { address, port } => {
695 let mut data = vec![0u8];
696 let parts: Vec<u8> = address.split('.').filter_map(|p| p.parse().ok()).collect();
697 let len = 6u8;
698 data.push(len);
699 data.extend_from_slice(&parts);
700 data.extend_from_slice(&port.to_be_bytes());
701 data
702 }
703 LinkSpecifier::IPv6 { address, port } => {
704 let mut data = vec![1u8];
705 let len = 18u8;
706 data.push(len);
707 let parts: Vec<u16> = address
708 .split(':')
709 .filter_map(|p| u16::from_str_radix(p, 16).ok())
710 .collect();
711 for part in parts {
712 data.extend_from_slice(&part.to_be_bytes());
713 }
714 data.extend_from_slice(&port.to_be_bytes());
715 data
716 }
717 LinkSpecifier::Fingerprint(fp) => {
718 let mut data = vec![2u8];
719 let len = 20u8;
720 data.push(len);
721 let bytes: Vec<u8> = (0..fp.len())
722 .step_by(2)
723 .filter_map(|i| u8::from_str_radix(&fp[i..i + 2], 16).ok())
724 .collect();
725 data.extend_from_slice(&bytes);
726 data
727 }
728 LinkSpecifier::Ed25519(key) => {
729 let mut data = vec![3u8];
730 let len = 32u8;
731 data.push(len);
732 if let Some(decoded) = base64_decode(key) {
733 data.extend_from_slice(&decoded);
734 }
735 data
736 }
737 LinkSpecifier::Unknown { link_type, data: d } => {
738 let mut data = vec![*link_type];
739 data.push(d.len() as u8);
740 data.extend_from_slice(d);
741 data
742 }
743 }
744 }
745}
746
747#[derive(Debug, Clone, PartialEq)]
788pub struct IntroductionPointV3 {
789 pub link_specifiers: Vec<LinkSpecifier>,
791 pub onion_key_raw: Option<String>,
793 pub auth_key_cert: Option<String>,
795 pub enc_key_raw: Option<String>,
797 pub enc_key_cert: Option<String>,
799 pub legacy_key_raw: Option<String>,
801 pub legacy_key_cert: Option<String>,
803}
804
805impl IntroductionPointV3 {
806 pub fn encode(&self) -> String {
837 let mut lines = Vec::new();
838
839 let mut link_data = vec![self.link_specifiers.len() as u8];
840 for spec in &self.link_specifiers {
841 link_data.extend(spec.pack());
842 }
843 lines.push(format!("introduction-point {}", base64_encode(&link_data)));
844
845 if let Some(ref key) = self.onion_key_raw {
846 lines.push(format!("onion-key ntor {}", key));
847 }
848
849 if let Some(ref cert) = self.auth_key_cert {
850 lines.push("auth-key".to_string());
851 lines.push(cert.clone());
852 }
853
854 if let Some(ref key) = self.enc_key_raw {
855 lines.push(format!("enc-key ntor {}", key));
856 }
857
858 if let Some(ref cert) = self.enc_key_cert {
859 lines.push("enc-key-cert".to_string());
860 lines.push(cert.clone());
861 }
862
863 if let Some(ref key) = self.legacy_key_raw {
864 lines.push("legacy-key".to_string());
865 lines.push(key.clone());
866 }
867
868 if let Some(ref cert) = self.legacy_key_cert {
869 lines.push("legacy-key-cert".to_string());
870 lines.push(cert.clone());
871 }
872
873 lines.join("\n")
874 }
875
876 fn parse(content: &str) -> Result<Self, Error> {
890 let mut link_specifiers = Vec::new();
891 let mut onion_key_raw: Option<String> = None;
892 let mut auth_key_cert: Option<String> = None;
893 let mut enc_key_raw: Option<String> = None;
894 let mut enc_key_cert: Option<String> = None;
895 let mut legacy_key_raw: Option<String> = None;
896 let mut legacy_key_cert: Option<String> = None;
897
898 let lines: Vec<&str> = content.lines().collect();
899 let mut idx = 0;
900
901 while idx < lines.len() {
902 let line = lines[idx];
903 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
904 (&line[..space_pos], line[space_pos + 1..].trim())
905 } else {
906 (line, "")
907 };
908
909 match keyword {
910 "introduction-point" => {
911 if let Some(specs) = Self::parse_link_specifiers(value) {
912 link_specifiers = specs;
913 }
914 }
915 "onion-key" => {
916 if let Some(stripped) = value.strip_prefix("ntor ") {
917 onion_key_raw = Some(stripped.to_string());
918 }
919 }
920 "auth-key" => {
921 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
922 auth_key_cert = Some(block);
923 idx = end_idx;
924 }
925 "enc-key" => {
926 if let Some(stripped) = value.strip_prefix("ntor ") {
927 enc_key_raw = Some(stripped.to_string());
928 }
929 }
930 "enc-key-cert" => {
931 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
932 enc_key_cert = Some(block);
933 idx = end_idx;
934 }
935 "legacy-key" => {
936 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
937 legacy_key_raw = Some(block);
938 idx = end_idx;
939 }
940 "legacy-key-cert" => {
941 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
942 legacy_key_cert = Some(block);
943 idx = end_idx;
944 }
945 _ => {}
946 }
947 idx += 1;
948 }
949
950 Ok(Self {
951 link_specifiers,
952 onion_key_raw,
953 auth_key_cert,
954 enc_key_raw,
955 enc_key_cert,
956 legacy_key_raw,
957 legacy_key_cert,
958 })
959 }
960
961 fn parse_link_specifiers(encoded: &str) -> Option<Vec<LinkSpecifier>> {
978 let decoded = base64_decode(encoded)?;
979 if decoded.is_empty() {
980 return Some(Vec::new());
981 }
982
983 let count = decoded[0] as usize;
984 let mut specifiers = Vec::new();
985 let mut offset = 1;
986
987 for _ in 0..count {
988 if offset + 2 > decoded.len() {
989 break;
990 }
991
992 let link_type = decoded[offset];
993 let length = decoded[offset + 1] as usize;
994 offset += 2;
995
996 if offset + length > decoded.len() {
997 break;
998 }
999
1000 let data = &decoded[offset..offset + length];
1001 offset += length;
1002
1003 let specifier = match link_type {
1004 0 if length == 6 => {
1005 let addr = format!("{}.{}.{}.{}", data[0], data[1], data[2], data[3]);
1006 let port = u16::from_be_bytes([data[4], data[5]]);
1007 LinkSpecifier::IPv4 {
1008 address: addr,
1009 port,
1010 }
1011 }
1012 1 if length == 18 => {
1013 let addr_parts: Vec<String> = (0..8)
1014 .map(|i| {
1015 format!("{:04x}", u16::from_be_bytes([data[i * 2], data[i * 2 + 1]]))
1016 })
1017 .collect();
1018 let addr = addr_parts.join(":");
1019 let port = u16::from_be_bytes([data[16], data[17]]);
1020 LinkSpecifier::IPv6 {
1021 address: addr,
1022 port,
1023 }
1024 }
1025 2 if length == 20 => {
1026 let fingerprint = data.iter().map(|b| format!("{:02X}", b)).collect();
1027 LinkSpecifier::Fingerprint(fingerprint)
1028 }
1029 3 if length == 32 => {
1030 let ed25519 = base64_encode(data);
1031 LinkSpecifier::Ed25519(ed25519)
1032 }
1033 _ => LinkSpecifier::Unknown {
1034 link_type,
1035 data: data.to_vec(),
1036 },
1037 };
1038
1039 specifiers.push(specifier);
1040 }
1041
1042 Some(specifiers)
1043 }
1044}
1045
1046#[derive(Debug, Clone, PartialEq)]
1063pub struct AuthorizedClient {
1064 pub id: String,
1066 pub iv: String,
1068 pub cookie: String,
1070}
1071
1072#[derive(Debug, Clone, PartialEq)]
1108pub struct OuterLayer {
1109 pub auth_type: Option<String>,
1111 pub ephemeral_key: Option<String>,
1113 pub clients: HashMap<String, AuthorizedClient>,
1115 pub encrypted: Option<String>,
1117}
1118
1119impl OuterLayer {
1120 pub fn parse(content: &str) -> Result<Self, Error> {
1141 let content = content.trim_end_matches('\0');
1142 let lines: Vec<&str> = content.lines().collect();
1143
1144 let mut auth_type: Option<String> = None;
1145 let mut ephemeral_key: Option<String> = None;
1146 let mut clients: HashMap<String, AuthorizedClient> = HashMap::new();
1147 let mut encrypted: Option<String> = None;
1148
1149 let mut idx = 0;
1150 while idx < lines.len() {
1151 let line = lines[idx];
1152 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
1153 (&line[..space_pos], line[space_pos + 1..].trim())
1154 } else {
1155 (line, "")
1156 };
1157
1158 match keyword {
1159 "desc-auth-type" => auth_type = Some(value.to_string()),
1160 "desc-auth-ephemeral-key" => ephemeral_key = Some(value.to_string()),
1161 "auth-client" => {
1162 let parts: Vec<&str> = value.split_whitespace().collect();
1163 if parts.len() >= 3 {
1164 let client = AuthorizedClient {
1165 id: parts[0].to_string(),
1166 iv: parts[1].to_string(),
1167 cookie: parts[2].to_string(),
1168 };
1169 clients.insert(parts[0].to_string(), client);
1170 }
1171 }
1172 "encrypted" => {
1173 let (block, end_idx) = extract_message_block(&lines, idx + 1);
1174 encrypted = Some(block);
1175 idx = end_idx;
1176 }
1177 _ => {}
1178 }
1179 idx += 1;
1180 }
1181
1182 Ok(Self {
1183 auth_type,
1184 ephemeral_key,
1185 clients,
1186 encrypted,
1187 })
1188 }
1189}
1190
1191#[derive(Debug, Clone, PartialEq)]
1228pub struct InnerLayer {
1229 pub formats: Vec<u32>,
1231 pub intro_auth: Vec<String>,
1233 pub is_single_service: bool,
1235 pub introduction_points: Vec<IntroductionPointV3>,
1237}
1238
1239impl InnerLayer {
1240 pub fn parse(content: &str) -> Result<Self, Error> {
1266 let mut formats: Vec<u32> = Vec::new();
1267 let mut intro_auth: Vec<String> = Vec::new();
1268 let mut is_single_service = false;
1269 let mut introduction_points: Vec<IntroductionPointV3> = Vec::new();
1270
1271 let intro_div = content.find("\nintroduction-point ");
1272 let (header_content, intro_content) = if let Some(div) = intro_div {
1273 (&content[..div], Some(&content[div + 1..]))
1274 } else {
1275 (content, None)
1276 };
1277
1278 for line in header_content.lines() {
1279 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
1280 (&line[..space_pos], line[space_pos + 1..].trim())
1281 } else {
1282 (line, "")
1283 };
1284
1285 match keyword {
1286 "create2-formats" => {
1287 formats = value
1288 .split_whitespace()
1289 .filter_map(|v| v.parse().ok())
1290 .collect();
1291 }
1292 "intro-auth-required" => {
1293 intro_auth = value.split_whitespace().map(|s| s.to_string()).collect();
1294 }
1295 "single-onion-service" => {
1296 is_single_service = true;
1297 }
1298 _ => {}
1299 }
1300 }
1301
1302 if let Some(intro_str) = intro_content {
1303 let mut current_block = String::new();
1304 for line in intro_str.lines() {
1305 if line.starts_with("introduction-point ") && !current_block.is_empty() {
1306 introduction_points.push(IntroductionPointV3::parse(¤t_block)?);
1307 current_block.clear();
1308 }
1309 current_block.push_str(line);
1310 current_block.push('\n');
1311 }
1312 if !current_block.is_empty() {
1313 introduction_points.push(IntroductionPointV3::parse(¤t_block)?);
1314 }
1315 }
1316
1317 Ok(Self {
1318 formats,
1319 intro_auth,
1320 is_single_service,
1321 introduction_points,
1322 })
1323 }
1324}
1325
1326#[derive(Debug, Clone, PartialEq)]
1390pub struct HiddenServiceDescriptorV3 {
1391 pub version: u32,
1393 pub lifetime: u32,
1395 pub signing_cert: Option<String>,
1397 pub revision_counter: u64,
1399 pub superencrypted: Option<String>,
1401 pub signature: String,
1403 raw_content: Vec<u8>,
1405 unrecognized_lines: Vec<String>,
1407}
1408
1409impl HiddenServiceDescriptorV3 {
1410 pub fn address_from_identity_key(key: &[u8]) -> String {
1436 use sha3::{Digest, Sha3_256};
1437
1438 let version = [3u8];
1439 let mut hasher = Sha3_256::new();
1440 hasher.update(b".onion checksum");
1441 hasher.update(key);
1442 hasher.update(version);
1443 let checksum = &hasher.finalize()[..2];
1444
1445 let mut address_bytes = Vec::with_capacity(35);
1446 address_bytes.extend_from_slice(key);
1447 address_bytes.extend_from_slice(checksum);
1448 address_bytes.push(3);
1449
1450 base32_encode(&address_bytes).to_lowercase() + ".onion"
1451 }
1452
1453 pub fn identity_key_from_address(onion_address: &str) -> Result<Vec<u8>, Error> {
1489 use sha3::{Digest, Sha3_256};
1490
1491 let address = onion_address.trim_end_matches(".onion").to_uppercase();
1492
1493 let decoded = base32_decode(&address).ok_or_else(|| Error::Parse {
1494 location: "onion_address".to_string(),
1495 reason: "invalid base32 encoding".to_string(),
1496 })?;
1497
1498 if decoded.len() != 35 {
1499 return Err(Error::Parse {
1500 location: "onion_address".to_string(),
1501 reason: format!("invalid address length: {}", decoded.len()),
1502 });
1503 }
1504
1505 let pubkey = &decoded[..32];
1506 let expected_checksum = &decoded[32..34];
1507 let version = decoded[34];
1508
1509 if version != 3 {
1510 return Err(Error::Parse {
1511 location: "onion_address".to_string(),
1512 reason: format!("unsupported version: {}", version),
1513 });
1514 }
1515
1516 let mut hasher = Sha3_256::new();
1517 hasher.update(b".onion checksum");
1518 hasher.update(pubkey);
1519 hasher.update([version]);
1520 let checksum = &hasher.finalize()[..2];
1521
1522 if checksum != expected_checksum {
1523 return Err(Error::Parse {
1524 location: "onion_address".to_string(),
1525 reason: "invalid checksum".to_string(),
1526 });
1527 }
1528
1529 Ok(pubkey.to_vec())
1530 }
1531}
1532
1533impl Descriptor for HiddenServiceDescriptorV3 {
1534 fn parse(content: &str) -> Result<Self, Error> {
1535 let raw_content = content.as_bytes().to_vec();
1536 let lines: Vec<&str> = content.lines().collect();
1537
1538 let mut version: u32 = 0;
1539 let mut lifetime: u32 = 0;
1540 let mut signing_cert: Option<String> = None;
1541 let mut revision_counter: u64 = 0;
1542 let mut superencrypted: Option<String> = None;
1543 let mut signature = String::new();
1544 let mut unrecognized_lines: Vec<String> = Vec::new();
1545
1546 let mut idx = 0;
1547 while idx < lines.len() {
1548 let line = lines[idx];
1549
1550 if line.starts_with("@type ") {
1551 idx += 1;
1552 continue;
1553 }
1554
1555 let (keyword, value) = if let Some(space_pos) = line.find(' ') {
1556 (&line[..space_pos], line[space_pos + 1..].trim())
1557 } else {
1558 (line, "")
1559 };
1560
1561 match keyword {
1562 "hs-descriptor" => {
1563 version = value.parse().map_err(|_| Error::Parse {
1564 location: "hs-descriptor".to_string(),
1565 reason: format!("invalid version: {}", value),
1566 })?;
1567 }
1568 "descriptor-lifetime" => {
1569 lifetime = value.parse().map_err(|_| Error::Parse {
1570 location: "descriptor-lifetime".to_string(),
1571 reason: format!("invalid lifetime: {}", value),
1572 })?;
1573 }
1574 "descriptor-signing-key-cert" => {
1575 let (block, end_idx) = extract_pem_block(&lines, idx + 1);
1576 signing_cert = Some(block);
1577 idx = end_idx;
1578 }
1579 "revision-counter" => {
1580 revision_counter = value.parse().map_err(|_| Error::Parse {
1581 location: "revision-counter".to_string(),
1582 reason: format!("invalid revision counter: {}", value),
1583 })?;
1584 }
1585 "superencrypted" => {
1586 let (block, end_idx) = extract_message_block(&lines, idx + 1);
1587 superencrypted = Some(block);
1588 idx = end_idx;
1589 }
1590 "signature" => {
1591 signature = value.to_string();
1592 }
1593 _ => {
1594 if !line.is_empty() && !line.starts_with("-----") {
1595 unrecognized_lines.push(line.to_string());
1596 }
1597 }
1598 }
1599 idx += 1;
1600 }
1601
1602 Ok(Self {
1603 version,
1604 lifetime,
1605 signing_cert,
1606 revision_counter,
1607 superencrypted,
1608 signature,
1609 raw_content,
1610 unrecognized_lines,
1611 })
1612 }
1613
1614 fn to_descriptor_string(&self) -> String {
1615 let mut result = String::new();
1616
1617 result.push_str(&format!("hs-descriptor {}\n", self.version));
1618 result.push_str(&format!("descriptor-lifetime {}\n", self.lifetime));
1619
1620 if let Some(ref cert) = self.signing_cert {
1621 result.push_str("descriptor-signing-key-cert\n");
1622 result.push_str(cert);
1623 result.push('\n');
1624 }
1625
1626 result.push_str(&format!("revision-counter {}\n", self.revision_counter));
1627
1628 if let Some(ref encrypted) = self.superencrypted {
1629 result.push_str("superencrypted\n");
1630 result.push_str(encrypted);
1631 result.push('\n');
1632 }
1633
1634 result.push_str(&format!("signature {}\n", self.signature));
1635
1636 result
1637 }
1638
1639 fn digest(&self, hash: DigestHash, encoding: DigestEncoding) -> Result<String, Error> {
1640 Ok(compute_digest(&self.raw_content, hash, encoding))
1641 }
1642
1643 fn raw_content(&self) -> &[u8] {
1644 &self.raw_content
1645 }
1646
1647 fn unrecognized_lines(&self) -> &[String] {
1648 &self.unrecognized_lines
1649 }
1650}
1651
1652impl FromStr for HiddenServiceDescriptorV3 {
1653 type Err = Error;
1654
1655 fn from_str(s: &str) -> Result<Self, Self::Err> {
1656 Self::parse(s)
1657 }
1658}
1659
1660impl fmt::Display for HiddenServiceDescriptorV3 {
1661 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1662 write!(f, "{}", self.to_descriptor_string())
1663 }
1664}
1665
1666fn extract_pem_block(lines: &[&str], start_idx: usize) -> (String, usize) {
1681 let mut block = String::new();
1682 let mut idx = start_idx;
1683 while idx < lines.len() {
1684 let line = lines[idx];
1685 block.push_str(line);
1686 block.push('\n');
1687 if line.starts_with("-----END ") {
1688 break;
1689 }
1690 idx += 1;
1691 }
1692 (block.trim_end().to_string(), idx)
1693}
1694
1695fn extract_message_block(lines: &[&str], start_idx: usize) -> (String, usize) {
1710 let mut block = String::new();
1711 let mut idx = start_idx;
1712 let mut in_block = false;
1713
1714 while idx < lines.len() {
1715 let line = lines[idx];
1716
1717 if line.starts_with("-----BEGIN MESSAGE-----") {
1718 in_block = true;
1719 }
1720
1721 if in_block {
1722 block.push_str(line);
1723 block.push('\n');
1724 }
1725
1726 if line.starts_with("-----END MESSAGE-----") {
1727 break;
1728 }
1729 idx += 1;
1730 }
1731
1732 (block.trim_end().to_string(), idx)
1733}
1734
1735fn base64_decode(input: &str) -> Option<Vec<u8>> {
1748 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1749
1750 let input = input.replace(['\n', '\r', ' '], "");
1751 let input = input.trim_end_matches('=');
1752
1753 let mut result = Vec::new();
1754 let mut buffer: u32 = 0;
1755 let mut bits: u32 = 0;
1756
1757 for c in input.chars() {
1758 let value = ALPHABET.iter().position(|&x| x == c as u8)? as u32;
1759 buffer = (buffer << 6) | value;
1760 bits += 6;
1761
1762 if bits >= 8 {
1763 bits -= 8;
1764 result.push((buffer >> bits) as u8);
1765 buffer &= (1 << bits) - 1;
1766 }
1767 }
1768
1769 Some(result)
1770}
1771
1772fn base64_encode(bytes: &[u8]) -> String {
1784 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1785 let mut result = String::new();
1786 let mut i = 0;
1787
1788 while i < bytes.len() {
1789 let b0 = bytes[i] as u32;
1790 let b1 = bytes.get(i + 1).map(|&b| b as u32).unwrap_or(0);
1791 let b2 = bytes.get(i + 2).map(|&b| b as u32).unwrap_or(0);
1792 let triple = (b0 << 16) | (b1 << 8) | b2;
1793
1794 result.push(ALPHABET[((triple >> 18) & 0x3F) as usize] as char);
1795 result.push(ALPHABET[((triple >> 12) & 0x3F) as usize] as char);
1796
1797 if i + 1 < bytes.len() {
1798 result.push(ALPHABET[((triple >> 6) & 0x3F) as usize] as char);
1799 }
1800 if i + 2 < bytes.len() {
1801 result.push(ALPHABET[(triple & 0x3F) as usize] as char);
1802 }
1803 i += 3;
1804 }
1805
1806 result
1807}
1808
1809fn base32_encode(bytes: &[u8]) -> String {
1822 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1823 let mut result = String::new();
1824 let mut buffer: u64 = 0;
1825 let mut bits: u32 = 0;
1826
1827 for &byte in bytes {
1828 buffer = (buffer << 8) | byte as u64;
1829 bits += 8;
1830
1831 while bits >= 5 {
1832 bits -= 5;
1833 result.push(ALPHABET[((buffer >> bits) & 0x1F) as usize] as char);
1834 }
1835 }
1836
1837 if bits > 0 {
1838 buffer <<= 5 - bits;
1839 result.push(ALPHABET[(buffer & 0x1F) as usize] as char);
1840 }
1841
1842 result
1843}
1844
1845fn base32_decode(input: &str) -> Option<Vec<u8>> {
1858 const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1859
1860 let input = input.trim_end_matches('=');
1861 let mut result = Vec::new();
1862 let mut buffer: u64 = 0;
1863 let mut bits: u32 = 0;
1864
1865 for c in input.chars() {
1866 let value = ALPHABET
1867 .iter()
1868 .position(|&x| x == c.to_ascii_uppercase() as u8)? as u64;
1869 buffer = (buffer << 5) | value;
1870 bits += 5;
1871
1872 if bits >= 8 {
1873 bits -= 8;
1874 result.push((buffer >> bits) as u8);
1875 buffer &= (1 << bits) - 1;
1876 }
1877 }
1878
1879 Some(result)
1880}
1881
1882#[cfg(test)]
1883mod tests {
1884 use super::*;
1885
1886 const DUCKDUCKGO_DESCRIPTOR: &str = r#"@type hidden-service-descriptor 1.0
1887rendezvous-service-descriptor y3olqqblqw2gbh6phimfuiroechjjafa
1888version 2
1889permanent-key
1890-----BEGIN RSA PUBLIC KEY-----
1891MIGJAoGBAJ/SzzgrXPxTlFrKVhXh3buCWv2QfcNgncUpDpKouLn3AtPH5Ocys0jE
1892aZSKdvaiQ62md2gOwj4x61cFNdi05tdQjS+2thHKEm/KsB9BGLSLBNJYY356bupg
1893I5gQozM65ENelfxYlysBjJ52xSDBd8C4f/p9umdzaaaCmzXG/nhzAgMBAAE=
1894-----END RSA PUBLIC KEY-----
1895secret-id-part e24kgecavwsznj7gpbktqsiwgvngsf4e
1896publication-time 2015-02-23 20:00:00
1897protocol-versions 2,3
1898introduction-points
1899-----BEGIN MESSAGE-----
1900aW50cm9kdWN0aW9uLXBvaW50IGl3a2k3N3h0YnZwNnF2ZWRmcndkem5jeHMzY2th
1901eWV1CmlwLWFkZHJlc3MgMTc4LjYyLjIyMi4xMjkKb25pb24tcG9ydCA0NDMKb25p
1902b24ta2V5Ci0tLS0tQkVHSU4gUlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFL
1903OTRCRVlJSFo0S2RFa2V5UGhiTENwUlc1RVNnKzJXUFFock00eXVLWUd1cTh3Rldn
1904dW1aWVI5CmsvV0EvL0ZZWE1CejBiQitja3Vacy9ZdTluSytITHpwR2FwVjBjbHN0
1905NEdVTWNCSW5VQ3pDY3BqSlRRc1FEZ20KMy9ZM2NxaDBXNTVnT0NGaG9tUTQvMVdP
1906WWc3WUNqazRYWUhKRTIwT2RHMkxsNXpvdEs2ZkFnTUJBQUU9Ci0tLS0tRU5EIFJT
1907QSBQVUJMSUMgS0VZLS0tLS0Kc2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVC
1908TElDIEtFWS0tLS0tCk1JR0pBb0dCQUpYbUpiOGxTeWRNTXFDZ0NnZmd2bEIyRTVy
1909cGQ1N2t6L0FxZzcvZDFIS2MzK2w1UW9Vdkh5dXkKWnNBbHlrYThFdTUzNGhsNDFv
1910cUVLcEFLWWNNbjFUTTB2cEpFR05WT2MrMDVCSW54STloOWYwTWcwMVBEMHRZdQpH
1911Y0xIWWdCemNyZkVtS3dNdE04V0VtY01KZDduMnVmZmFBdko4NDZXdWJiZVY3TVcx
1912WWVoQWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBLRVktLS0tLQppbnRyb2R1
1913Y3Rpb24tcG9pbnQgZW00Z2prNmVpaXVhbGhtbHlpaWZyemM3bGJ0cnNiaXAKaXAt
1914YWRkcmVzcyA0Ni40LjE3NC41Mgpvbmlvbi1wb3J0IDQ0Mwpvbmlvbi1rZXkKLS0t
1915LS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pBb0dCQUxCbWhkRjV3SHhI
1916cnBMU21qQVpvdHR4MjIwKzk5NUZkTU9PdFpOalJ3MURCU3ByVVpacXR4V2EKUDhU
1917S3BIS3p3R0pLQ1ZZSUlqN2xvaGJ2OVQ5dXJtbGZURTA1VVJHZW5ab2lmT0ZOejNZ
1918d01KVFhTY1FFQkoxMAo5aVdOTERUc2tMekRLQ0FiR2hibi9NS3dPZllHQmhOVGxq
1919ZHlUbU5ZNUVDUmJSempldjl2QWdNQkFBRT0KLS0tLS1FTkQgUlNBIFBVQkxJQyBL
1920RVktLS0tLQpzZXJ2aWNlLWtleQotLS0tLUJFR0lOIFJTQSBQVUJMSUMgS0VZLS0t
1921LS0KTUlHSkFvR0JBTXhNSG9BbXJiVU1zeGlJQ3AzaVRQWWdobjBZdWVLSHgyMTl3
1922dThPL1E1MVF5Y1ZWTHBYMjdkMQpoSlhrUEIzM1hRQlhzQlM3U3hzU3NTQ1EzR0V1
1923clFKN0d1QkxwWUlSL3Zxc2FrRS9sOHdjMkNKQzVXVWh5RkZrCisxVFdJVUk1dHhu
1924WEx5V0NSY0tEVXJqcWRvc0RhRG9zZ0hGZzIzTW54K3hYY2FRL2ZyQi9BZ01CQUFF
1925PQotLS0tLUVORCBSU0EgUFVCTElDIEtFWS0tLS0tCmludHJvZHVjdGlvbi1wb2lu
1926dCBqcWhmbDM2NHgzdXBlNmxxbnhpem9sZXdsZnJzdzJ6eQppcC1hZGRyZXNzIDYy
1927LjIxMC44Mi4xNjkKb25pb24tcG9ydCA0NDMKb25pb24ta2V5Ci0tLS0tQkVHSU4g
1928UlNBIFBVQkxJQyBLRVktLS0tLQpNSUdKQW9HQkFQVWtxeGdmWWR3MFBtL2c2TWJo
1929bVZzR0tsdWppZm1raGRmb0VldXpnbyt3bkVzR3Z3VWVienJ6CmZaSlJ0MGNhWEZo
1930bkNHZ1FEMklnbWFyVWFVdlAyNGZYby80bVl6TGNQZUk3Z1puZXVBUUpZdm05OFl2
1931OXZPSGwKTmFNL1d2RGtDc0ozR1ZOSjFIM3dMUFFSSTN2N0tiTnVjOXRDT1lsL3Iw
1932OU9oVmFXa3phakFnTUJBQUU9Ci0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K
1933c2VydmljZS1rZXkKLS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JR0pB
1934b0dCQUxieDhMZXFSb1Avcjl3OWhqd0Q0MVlVbTdQbzY5N3hSdHl0RjBNY3lMQ1M3
1935R1JpVVluamk3S1kKZmVwWGR2Ti9KbDVxUUtISUJiNjAya3VPVGwwcE44UStZZUZV
1936U0lJRGNtUEJMcEJEaEgzUHZyUU1jR1ZhaU9XSAo4dzBITVpDeGd3QWNDQzUxdzVW
1937d2l1bXhFSk5CVmNac094MG16TjFDbG95KzkwcTBsRlhMQWdNQkFBRT0KLS0tLS1F
1938TkQgUlNBIFBVQkxJQyBLRVktLS0tLQoK
1939-----END MESSAGE-----
1940signature
1941-----BEGIN SIGNATURE-----
1942VKMmsDIUUFOrpqvcQroIZjDZTKxqNs88a4M9Te8cR/ZvS7H2nffv6iQs0tom5X4D
19434Dy4iZiy+pwYxdHfaOxmdpgMCRvgPb34MExWr5YemH0QuGtnlp5Wxr8GYaAQVuZX
1944cZjQLW0juUYCbgIGdxVEBnlEt2rgBSM9+1oR7EAfV1U=
1945-----END SIGNATURE-----
1946"#;
1947
1948 const V3_DESCRIPTOR: &str = r#"hs-descriptor 3
1949descriptor-lifetime 180
1950descriptor-signing-key-cert
1951-----BEGIN ED25519 CERT-----
1952AQgABl5/AZLmgPpXVS59SEydKj7bRvvAduVOqQt3u4Tj5tVlfVKhAQAgBABUhpfe
1953/Wd3p/M74DphsGcIMee/npQ9BTzkzCyTyVmDbykek2EciWaOTCVZJVyiKPErngfW
1954BDwQZ8rhp05oCqhhY3oFHqG9KS7HGzv9g2v1/PrVJMbkfpwu1YK4b3zIZAk=
1955-----END ED25519 CERT-----
1956revision-counter 42
1957superencrypted
1958-----BEGIN MESSAGE-----
1959Jmu66WXn0+CDLXVM02n85rj84Fv4ynLcjFFWPoLNm6Op+S14CAm0H2qfMj8OO/jw
1960NJiNxY/L/8SeY5ZlvqPHzI8jBqKW7nT5CN7xLUEvzdFhG3AnWC48r8fp2E+TQ8gb
1961-----END MESSAGE-----
1962signature aglChCQF+lbzKgyxJJTpYGVShV/GMDRJ4+cRGCp+a2y/yX/tLSh7hzqI7rVZrUoGj74Xr1CLMYO3fXYCS+DPDQ
1963"#;
1964
1965 #[test]
1966 fn test_parse_v2_duckduckgo() {
1967 let desc = HiddenServiceDescriptorV2::parse(DUCKDUCKGO_DESCRIPTOR).unwrap();
1968
1969 assert_eq!(desc.descriptor_id, "y3olqqblqw2gbh6phimfuiroechjjafa");
1970 assert_eq!(desc.version, 2);
1971 assert_eq!(desc.secret_id_part, "e24kgecavwsznj7gpbktqsiwgvngsf4e");
1972 assert_eq!(desc.protocol_versions, vec![2, 3]);
1973 assert!(desc.permanent_key.is_some());
1974 assert!(desc.introduction_points_encoded.is_some());
1975
1976 let intro_points = desc.introduction_points().unwrap();
1977 assert_eq!(intro_points.len(), 3);
1978
1979 assert_eq!(
1980 intro_points[0].identifier,
1981 "iwki77xtbvp6qvedfrwdzncxs3ckayeu"
1982 );
1983 assert_eq!(intro_points[0].address, "178.62.222.129");
1984 assert_eq!(intro_points[0].port, 443);
1985 assert!(intro_points[0].onion_key.is_some());
1986 assert!(intro_points[0].service_key.is_some());
1987
1988 assert_eq!(
1989 intro_points[1].identifier,
1990 "em4gjk6eiiualhmlyiifrzc7lbtrsbip"
1991 );
1992 assert_eq!(intro_points[1].address, "46.4.174.52");
1993 assert_eq!(intro_points[1].port, 443);
1994
1995 assert_eq!(
1996 intro_points[2].identifier,
1997 "jqhfl364x3upe6lqnxizolewlfrsw2zy"
1998 );
1999 assert_eq!(intro_points[2].address, "62.210.82.169");
2000 assert_eq!(intro_points[2].port, 443);
2001 }
2002
2003 #[test]
2004 fn test_parse_v3_descriptor() {
2005 let desc = HiddenServiceDescriptorV3::parse(V3_DESCRIPTOR).unwrap();
2006
2007 assert_eq!(desc.version, 3);
2008 assert_eq!(desc.lifetime, 180);
2009 assert_eq!(desc.revision_counter, 42);
2010 assert!(desc.signing_cert.is_some());
2011 assert!(desc.superencrypted.is_some());
2012 assert!(!desc.signature.is_empty());
2013 }
2014
2015 #[test]
2016 fn test_v3_address_conversion() {
2017 let key = [0u8; 32];
2018 let address = HiddenServiceDescriptorV3::address_from_identity_key(&key);
2019 assert!(address.ends_with(".onion"));
2020 assert_eq!(address.len(), 62);
2021
2022 let recovered_key = HiddenServiceDescriptorV3::identity_key_from_address(&address).unwrap();
2023 assert_eq!(recovered_key, key.to_vec());
2024 }
2025
2026 #[test]
2027 fn test_v3_invalid_address() {
2028 let result = HiddenServiceDescriptorV3::identity_key_from_address("invalid.onion");
2029 assert!(result.is_err());
2030 }
2031
2032 #[test]
2033 fn test_base64_roundtrip() {
2034 let original = b"Hello, World!";
2035 let encoded = base64_encode(original);
2036 let decoded = base64_decode(&encoded).unwrap();
2037 assert_eq!(decoded, original);
2038 }
2039
2040 #[test]
2041 fn test_base32_roundtrip() {
2042 let original = b"Hello, World!";
2043 let encoded = base32_encode(original);
2044 let decoded = base32_decode(&encoded).unwrap();
2045 assert_eq!(decoded, original);
2046 }
2047
2048 #[test]
2049 fn test_outer_layer_parse() {
2050 let content = r#"desc-auth-type x25519
2051desc-auth-ephemeral-key AAAA
2052auth-client client1 iv1 cookie1
2053auth-client client2 iv2 cookie2
2054encrypted
2055-----BEGIN MESSAGE-----
2056dGVzdA==
2057-----END MESSAGE-----
2058"#;
2059 let layer = OuterLayer::parse(content).unwrap();
2060 assert_eq!(layer.auth_type, Some("x25519".to_string()));
2061 assert_eq!(layer.ephemeral_key, Some("AAAA".to_string()));
2062 assert_eq!(layer.clients.len(), 2);
2063 assert!(layer.encrypted.is_some());
2064 }
2065
2066 #[test]
2067 fn test_inner_layer_parse() {
2068 let content = "create2-formats 2\n";
2069 let layer = InnerLayer::parse(content).unwrap();
2070 assert_eq!(layer.formats, vec![2]);
2071 assert!(!layer.is_single_service);
2072 assert!(layer.introduction_points.is_empty());
2073 }
2074
2075 #[test]
2076 fn test_v2_to_string() {
2077 let desc = HiddenServiceDescriptorV2::parse(DUCKDUCKGO_DESCRIPTOR).unwrap();
2078 let output = desc.to_descriptor_string();
2079 assert!(output.contains("rendezvous-service-descriptor y3olqqblqw2gbh6phimfuiroechjjafa"));
2080 assert!(output.contains("version 2"));
2081 assert!(output.contains("protocol-versions 2,3"));
2082 }
2083
2084 #[test]
2085 fn test_v3_to_string() {
2086 let desc = HiddenServiceDescriptorV3::parse(V3_DESCRIPTOR).unwrap();
2087 let output = desc.to_descriptor_string();
2088 assert!(output.contains("hs-descriptor 3"));
2089 assert!(output.contains("descriptor-lifetime 180"));
2090 assert!(output.contains("revision-counter 42"));
2091 }
2092
2093 #[test]
2094 fn test_link_specifier_pack_ipv4() {
2095 let spec = LinkSpecifier::IPv4 {
2096 address: "1.2.3.4".to_string(),
2097 port: 9001,
2098 };
2099 let packed = spec.pack();
2100 assert_eq!(packed[0], 0);
2101 assert_eq!(packed[1], 6);
2102 assert_eq!(&packed[2..6], &[1, 2, 3, 4]);
2103 assert_eq!(u16::from_be_bytes([packed[6], packed[7]]), 9001);
2104 }
2105
2106 #[test]
2107 fn test_link_specifier_pack_fingerprint() {
2108 let spec =
2109 LinkSpecifier::Fingerprint("CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC".to_string());
2110 let packed = spec.pack();
2111 assert_eq!(packed[0], 2);
2112 assert_eq!(packed[1], 20);
2113 assert_eq!(packed.len(), 22);
2114 }
2115
2116 #[test]
2117 fn test_introduction_point_v3_encode() {
2118 let intro_point = IntroductionPointV3 {
2119 link_specifiers: vec![LinkSpecifier::IPv4 {
2120 address: "1.2.3.4".to_string(),
2121 port: 9001,
2122 }],
2123 onion_key_raw: Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string()),
2124 auth_key_cert: None,
2125 enc_key_raw: Some("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=".to_string()),
2126 enc_key_cert: None,
2127 legacy_key_raw: None,
2128 legacy_key_cert: None,
2129 };
2130
2131 let encoded = intro_point.encode();
2132 assert!(encoded.contains("introduction-point"));
2133 assert!(encoded.contains("onion-key ntor AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="));
2134 assert!(encoded.contains("enc-key ntor BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="));
2135 }
2136
2137 #[test]
2138 fn test_inner_layer_with_intro_points() {
2139 let content = r#"create2-formats 2
2140intro-auth-required ed25519
2141single-onion-service
2142introduction-point AQAGAQIDBCMp
2143onion-key ntor AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
2144enc-key ntor BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=
2145"#;
2146 let layer = InnerLayer::parse(content).unwrap();
2147 assert_eq!(layer.formats, vec![2]);
2148 assert_eq!(layer.intro_auth, vec!["ed25519"]);
2149 assert!(layer.is_single_service);
2150 assert_eq!(layer.introduction_points.len(), 1);
2151
2152 let intro = &layer.introduction_points[0];
2153 assert_eq!(
2154 intro.onion_key_raw,
2155 Some("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_string())
2156 );
2157 assert_eq!(
2158 intro.enc_key_raw,
2159 Some("BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=".to_string())
2160 );
2161 }
2162
2163 #[test]
2164 fn test_v3_known_address() {
2165 let hs_address = "sltib6sxkuxh2scmtuvd5w2g7pahnzkovefxpo4e4ptnkzl5kkq5h2ad.onion";
2166 let hs_pubkey: [u8; 32] = [
2167 0x92, 0xe6, 0x80, 0xfa, 0x57, 0x55, 0x2e, 0x7d, 0x48, 0x4c, 0x9d, 0x2a, 0x3e, 0xdb,
2168 0x46, 0xfb, 0xc0, 0x76, 0xe5, 0x4e, 0xa9, 0x0b, 0x77, 0xbb, 0x84, 0xe3, 0xe6, 0xd5,
2169 0x65, 0x7d, 0x52, 0xa1,
2170 ];
2171
2172 let address = HiddenServiceDescriptorV3::address_from_identity_key(&hs_pubkey);
2173 assert_eq!(address, hs_address);
2174
2175 let recovered_key =
2176 HiddenServiceDescriptorV3::identity_key_from_address(hs_address).unwrap();
2177 assert_eq!(recovered_key, hs_pubkey.to_vec());
2178 }
2179
2180 use proptest::prelude::*;
2181
2182 fn valid_descriptor_id() -> impl Strategy<Value = String> {
2183 "[a-z2-7]{32}".prop_map(|s| s.to_string())
2184 }
2185
2186 fn valid_secret_id_part() -> impl Strategy<Value = String> {
2187 "[a-z2-7]{32}".prop_map(|s| s.to_string())
2188 }
2189
2190 fn valid_timestamp() -> impl Strategy<Value = DateTime<Utc>> {
2191 (
2192 2015u32..2025,
2193 1u32..13,
2194 1u32..29,
2195 0u32..24,
2196 0u32..60,
2197 0u32..60,
2198 )
2199 .prop_map(|(year, month, day, hour, min, sec)| {
2200 let naive = chrono::NaiveDate::from_ymd_opt(year as i32, month, day)
2201 .unwrap()
2202 .and_hms_opt(hour, min, sec)
2203 .unwrap();
2204 naive.and_utc()
2205 })
2206 }
2207
2208 fn simple_v2_descriptor() -> impl Strategy<Value = HiddenServiceDescriptorV2> {
2209 (
2210 valid_descriptor_id(),
2211 valid_secret_id_part(),
2212 valid_timestamp(),
2213 proptest::collection::vec(2u32..4, 1..3),
2214 )
2215 .prop_map(|(descriptor_id, secret_id_part, published, protocol_versions)| {
2216 HiddenServiceDescriptorV2 {
2217 descriptor_id,
2218 version: 2,
2219 permanent_key: Some("-----BEGIN RSA PUBLIC KEY-----\nMIGJAoGBAJ/SzzgrXPxTlFrKVhXh3buCWv2QfcNgncUpDpKouLn3AtPH5Ocys0jE\naZSKdvaiQ62md2gOwj4x61cFNdi05tdQjS+2thHKEm/KsB9BGLSLBNJYY356bupg\nI5gQozM65ENelfxYlysBjJ52xSDBd8C4f/p9umdzaaaCmzXG/nhzAgMBAAE=\n-----END RSA PUBLIC KEY-----".to_string()),
2220 secret_id_part,
2221 published,
2222 protocol_versions,
2223 introduction_points_encoded: None,
2224 introduction_points_content: None,
2225 signature: "-----BEGIN SIGNATURE-----\ntest\n-----END SIGNATURE-----".to_string(),
2226 raw_content: Vec::new(),
2227 unrecognized_lines: Vec::new(),
2228 }
2229 })
2230 }
2231
2232 fn simple_v3_descriptor() -> impl Strategy<Value = HiddenServiceDescriptorV3> {
2233 (60u32..180, 1u64..1000).prop_map(|(lifetime, revision_counter)| {
2234 HiddenServiceDescriptorV3 {
2235 version: 3,
2236 lifetime,
2237 signing_cert: Some(
2238 "-----BEGIN ED25519 CERT-----\ntest\n-----END ED25519 CERT-----".to_string(),
2239 ),
2240 revision_counter,
2241 superencrypted: Some(
2242 "-----BEGIN MESSAGE-----\ndGVzdA==\n-----END MESSAGE-----".to_string(),
2243 ),
2244 signature: "testsignature".to_string(),
2245 raw_content: Vec::new(),
2246 unrecognized_lines: Vec::new(),
2247 }
2248 })
2249 }
2250
2251 proptest! {
2252 #![proptest_config(ProptestConfig::with_cases(100))]
2253
2254 #[test]
2255 fn prop_hidden_service_v2_roundtrip(desc in simple_v2_descriptor()) {
2256 let serialized = desc.to_descriptor_string();
2257 let parsed = HiddenServiceDescriptorV2::parse(&serialized);
2258
2259 prop_assert!(parsed.is_ok(), "Failed to parse serialized v2 descriptor: {:?}", parsed.err());
2260
2261 let parsed = parsed.unwrap();
2262
2263 prop_assert_eq!(&desc.descriptor_id, &parsed.descriptor_id, "descriptor_id mismatch");
2264 prop_assert_eq!(desc.version, parsed.version, "version mismatch");
2265 prop_assert_eq!(&desc.secret_id_part, &parsed.secret_id_part, "secret_id_part mismatch");
2266 prop_assert_eq!(&desc.protocol_versions, &parsed.protocol_versions, "protocol_versions mismatch");
2267 }
2268
2269 #[test]
2270 fn prop_hidden_service_v3_roundtrip(desc in simple_v3_descriptor()) {
2271 let serialized = desc.to_descriptor_string();
2272 let parsed = HiddenServiceDescriptorV3::parse(&serialized);
2273
2274 prop_assert!(parsed.is_ok(), "Failed to parse serialized v3 descriptor: {:?}", parsed.err());
2275
2276 let parsed = parsed.unwrap();
2277
2278 prop_assert_eq!(desc.version, parsed.version, "version mismatch");
2279 prop_assert_eq!(desc.lifetime, parsed.lifetime, "lifetime mismatch");
2280 prop_assert_eq!(desc.revision_counter, parsed.revision_counter, "revision_counter mismatch");
2281 }
2282
2283 #[test]
2284 fn prop_v3_address_roundtrip(key in proptest::collection::vec(any::<u8>(), 32..=32)) {
2285 let key_array: [u8; 32] = key.clone().try_into().unwrap();
2286 let address = HiddenServiceDescriptorV3::address_from_identity_key(&key_array);
2287 let recovered = HiddenServiceDescriptorV3::identity_key_from_address(&address);
2288
2289 prop_assert!(recovered.is_ok(), "Failed to recover key from address");
2290 prop_assert_eq!(recovered.unwrap(), key, "Key round-trip failed");
2291 }
2292 }
2293}