stem_rs/descriptor/
hidden.rs

1//! Hidden service descriptor parsing for Tor onion services.
2//!
3//! This module provides parsing for hidden service descriptors (v2 and v3)
4//! which describe onion services accessible through the Tor network. Unlike
5//! other descriptor types, these describe a hidden service rather than a relay.
6//! They're created by the service itself and can only be fetched via relays
7//! with the HSDir flag.
8//!
9//! # Overview
10//!
11//! Hidden services (also known as onion services) allow servers to receive
12//! incoming connections through the Tor network without revealing their IP
13//! address. Each hidden service publishes descriptors that contain the
14//! information clients need to connect.
15//!
16//! # Descriptor Versions
17//!
18//! ## Version 2 (Deprecated)
19//!
20//! Version 2 hidden service descriptors use RSA cryptography and have `.onion`
21//! addresses that are 16 characters long. These are being phased out in favor
22//! of v3 descriptors.
23//!
24//! Key components:
25//! - `descriptor_id`: Base32 hash identifying this descriptor
26//! - `permanent_key`: RSA public key of the hidden service
27//! - `introduction_points`: List of relays that can introduce clients
28//!
29//! ## Version 3 (Current)
30//!
31//! Version 3 hidden service descriptors use Ed25519/Curve25519 cryptography
32//! and have `.onion` addresses that are 56 characters long. They provide
33//! improved security through multiple encryption layers.
34//!
35//! Key components:
36//! - `signing_cert`: Ed25519 certificate for the descriptor
37//! - `superencrypted`: Outer encryption layer containing client authorization
38//! - Introduction points are in the inner encrypted layer
39//!
40//! # Encryption Layers (V3)
41//!
42//! V3 descriptors have two encryption layers:
43//!
44//! 1. **Outer Layer** ([`OuterLayer`]): Contains client authorization data
45//!    and the encrypted inner layer. Decrypted using the blinded public key
46//!    and subcredential.
47//!
48//! 2. **Inner Layer** ([`InnerLayer`]): Contains the actual introduction
49//!    points and service configuration. Requires the descriptor cookie for
50//!    client-authorized services.
51//!
52//! # Security Considerations
53//!
54//! - V2 descriptors are deprecated and should not be used for new services
55//! - V3 descriptor decryption requires cryptographic keys not stored in the
56//!   descriptor itself
57//! - Introduction point information is sensitive and encrypted
58//! - The `.onion` address encodes a checksum to prevent typos
59//!
60//! # Example
61//!
62//! ```rust,no_run
63//! use stem_rs::descriptor::hidden::{HiddenServiceDescriptorV2, HiddenServiceDescriptorV3};
64//! use stem_rs::descriptor::Descriptor;
65//!
66//! // Parse a v2 descriptor
67//! let v2_content = "rendezvous-service-descriptor ...";
68//! // let desc_v2 = HiddenServiceDescriptorV2::parse(v2_content)?;
69//!
70//! // Parse a v3 descriptor
71//! let v3_content = "hs-descriptor 3\n...";
72//! // let desc_v3 = HiddenServiceDescriptorV3::parse(v3_content)?;
73//!
74//! // Convert between v3 address and identity key
75//! let key = [0u8; 32];
76//! let address = HiddenServiceDescriptorV3::address_from_identity_key(&key);
77//! assert!(address.ends_with(".onion"));
78//! ```
79//!
80//! # See Also
81//!
82//! - [`crate::descriptor`]: Base descriptor traits and utilities
83//! - [`crate::descriptor::certificate`]: Ed25519 certificates used in v3 descriptors
84//!
85//! # See also
86//!
87//! - [Tor Rendezvous Specification v2](https://gitweb.torproject.org/torspec.git/tree/rend-spec-v2.txt) (deprecated)
88//! - [Tor Rendezvous Specification v3](https://gitweb.torproject.org/torspec.git/tree/rend-spec-v3.txt)
89
90use 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/// Introduction point for a version 2 hidden service.
101///
102/// An introduction point is a Tor relay that acts as an intermediary between
103/// clients and the hidden service. Clients connect to introduction points to
104/// establish a rendezvous with the service.
105///
106/// # Fields
107///
108/// - `identifier`: Base32-encoded hash of the introduction point's identity key
109/// - `address`: IPv4 address where the introduction point is reachable
110/// - `port`: Port number for the introduction point
111/// - `onion_key`: RSA public key for encrypting the introduction
112/// - `service_key`: RSA public key for the hidden service at this point
113/// - `intro_authentication`: Optional authentication data as (type, data) pairs
114///
115/// # Security
116///
117/// Introduction points do not know the hidden service's actual location.
118/// They only relay encrypted introduction requests.
119///
120/// # Example
121///
122/// ```rust,ignore
123/// // Introduction points are typically parsed from a descriptor
124/// let desc = HiddenServiceDescriptorV2::parse(content)?;
125/// let intro_points = desc.introduction_points()?;
126///
127/// for point in intro_points {
128///     println!("Introduction point: {} at {}:{}",
129///              point.identifier, point.address, point.port);
130/// }
131/// ```
132#[derive(Debug, Clone, PartialEq)]
133pub struct IntroductionPointV2 {
134    /// Base32-encoded hash of the introduction point's identity key.
135    pub identifier: String,
136    /// IPv4 address of the introduction point relay.
137    pub address: String,
138    /// Port number where the introduction point is listening.
139    pub port: u16,
140    /// RSA public key for encrypting introduction requests (PEM format).
141    pub onion_key: Option<String>,
142    /// RSA public key for the hidden service at this introduction point (PEM format).
143    pub service_key: Option<String>,
144    /// Authentication data as (auth_type, auth_data) pairs for establishing connections.
145    pub intro_authentication: Vec<(String, String)>,
146}
147
148impl IntroductionPointV2 {
149    /// Parses an introduction point from its descriptor content.
150    ///
151    /// # Arguments
152    ///
153    /// * `content` - The raw text content of a single introduction point block
154    ///
155    /// # Returns
156    ///
157    /// A parsed `IntroductionPointV2` on success.
158    ///
159    /// # Errors
160    ///
161    /// Returns [`Error::Parse`] if:
162    /// - The port number is not a valid u16
163    /// - Required fields are missing or malformed
164    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/// Version 2 hidden service descriptor.
219///
220/// A v2 hidden service descriptor contains all the information needed for
221/// clients to connect to a hidden service using the v2 protocol. This includes
222/// the service's public key, publication time, supported protocol versions,
223/// and encrypted introduction points.
224///
225/// # Deprecation Notice
226///
227/// Version 2 hidden services are deprecated and being phased out by the Tor
228/// Project. New services should use version 3 descriptors
229/// ([`HiddenServiceDescriptorV3`]) which provide stronger cryptography.
230///
231/// # Structure
232///
233/// The descriptor contains:
234/// - `descriptor_id`: Unique identifier (base32 hash of service key and time)
235/// - `permanent_key`: RSA-1024 public key of the hidden service
236/// - `secret_id_part`: Hash component for descriptor ID validation
237/// - `published`: When this descriptor was created
238/// - `protocol_versions`: Supported rendezvous protocol versions (typically 2,3)
239/// - `introduction_points_*`: Encrypted or encoded introduction point data
240/// - `signature`: RSA signature over the descriptor
241///
242/// # Introduction Points
243///
244/// Introduction points may be encrypted if the service uses client
245/// authorization. Use [`introduction_points()`](Self::introduction_points)
246/// to decode them when unencrypted.
247///
248/// # Example
249///
250/// ```rust,ignore
251/// use stem_rs::descriptor::hidden::HiddenServiceDescriptorV2;
252/// use stem_rs::descriptor::Descriptor;
253///
254/// let content = std::fs::read_to_string("descriptor.txt")?;
255/// let desc = HiddenServiceDescriptorV2::parse(&content)?;
256///
257/// println!("Descriptor ID: {}", desc.descriptor_id);
258/// println!("Published: {}", desc.published);
259/// println!("Protocol versions: {:?}", desc.protocol_versions);
260///
261/// // Get introduction points (if not encrypted)
262/// if let Ok(points) = desc.introduction_points() {
263///     for point in points {
264///         println!("Intro point: {} at {}:{}",
265///                  point.identifier, point.address, point.port);
266///     }
267/// }
268/// ```
269///
270/// # Security
271///
272/// - The `permanent_key` is the long-term identity of the service
273/// - The `signature` should be verified against `permanent_key`
274/// - Introduction points may be encrypted for client authorization
275#[derive(Debug, Clone, PartialEq)]
276pub struct HiddenServiceDescriptorV2 {
277    /// Unique identifier for this descriptor (base32-encoded hash).
278    pub descriptor_id: String,
279    /// Hidden service descriptor version (always 2 for this type).
280    pub version: u32,
281    /// RSA-1024 public key of the hidden service (PEM format).
282    pub permanent_key: Option<String>,
283    /// Hash of time period, cookie, and replica for descriptor ID validation.
284    pub secret_id_part: String,
285    /// UTC timestamp when this descriptor was published.
286    pub published: DateTime<Utc>,
287    /// List of supported rendezvous protocol versions (typically [2, 3]).
288    pub protocol_versions: Vec<u32>,
289    /// Raw base64-encoded introduction points blob (MESSAGE block).
290    pub introduction_points_encoded: Option<String>,
291    /// Decoded introduction points content (may be encrypted).
292    pub introduction_points_content: Option<Vec<u8>>,
293    /// RSA signature over the descriptor content (PEM format).
294    pub signature: String,
295    /// Raw bytes of the original descriptor content.
296    raw_content: Vec<u8>,
297    /// Lines from the descriptor that were not recognized.
298    unrecognized_lines: Vec<String>,
299}
300
301impl HiddenServiceDescriptorV2 {
302    /// Decodes and parses the introduction points from this descriptor.
303    ///
304    /// Introduction points are act as intermediaries between
305    /// clients and the hidden service. This method decodes the base64-encoded
306    /// introduction points blob and parses each introduction point.
307    ///
308    /// # Returns
309    ///
310    /// A vector of [`IntroductionPointV2`] on success, or an empty vector
311    /// if no introduction points are present.
312    ///
313    /// # Errors
314    ///
315    /// Returns [`Error::Parse`] if:
316    /// - The introduction points content is not valid UTF-8
317    /// - The content is encrypted (starts with something other than
318    ///   "introduction-point ")
319    /// - Individual introduction points fail to parse
320    ///
321    /// # Example
322    ///
323    /// ```rust,ignore
324    /// let desc = HiddenServiceDescriptorV2::parse(content)?;
325    /// leo_points = desc.introduction_points()?;
326    ///
327    /// for point in intro_points {
328    ///     println!("Relay: {} at {}:{}",
329    ///              point.identifier, point.address, point.port);
330    /// }
331    /// ```
332    ///
333    /// # Security
334    ///
335    /// If the hidden service uses client authorization, the introduction
336    /// points will be encrypted and this method will return an error.
337    /// Decryption requires the client's authorization cookie.
338    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(&current_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(&current_block)?);
370        }
371
372        Ok(points)
373    }
374
375    /// Parses a comma-separated list of protocol versions.
376    ///
377    /// # Arguments
378    ///
379    /// * `value` - Comma-separated version numbers (e.g., "2,3")
380    ///
381    /// # Returns
382    ///
383    /// A vector of version numbers.
384    ///
385    /// # Errors
386    ///
387    /// Returns [`Error::Parse`] if any version is not a valid u32.
388    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    /// Decodes base64-encoded introduction points content.
406    ///
407    /// Strips PEM headers and decodes the base64 content.
408    ///
409    /// # Arguments
410    ///
411    /// * `encoded` - The MESSAGE block content including headers
412    ///
413    /// # Returns
414    ///
415    /// The decoded bytes, or `None` if decoding fails.
416    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/// Link specifier for v3 introduction points.
600///
601/// Link specifiers describe how to connect to an introduction point relay.
602/// They can specify IPv4/IPv6 addresses, relay fingerprints, or Ed25519
603/// identity keys.
604///
605/// # Variants
606///
607/// - `IPv4`: IPv4 address and port
608/// - `IPv6`: IPv6 address and port  
609/// - `Fingerprint`: 40-character hex relay fingerprint
610/// - `Ed25519`: Base64-encoded Ed25519 public key
611/// - `Unknown`: Unrecognized link specifier type
612///
613/// # Wire Format
614///
615/// Link specifiers are encoded as:
616/// - 1 byte: type
617/// - 1 byte: length
618/// - N bytes: data (format depends on type)
619///
620/// # Example
621///
622/// ```rust
623/// use stem_rs::descriptor::hidden::LinkSpecifier;
624///
625/// let ipv4 = LinkSpecifier::IPv4 {
626///     address: "192.168.1.1".to_string(),
627///     port: 9001,
628/// };
629///
630/// let packed = ipv4.pack();
631/// assert_eq!(packed[0], 0); // Type 0 = IPv4
632/// assert_eq!(packed[1], 6); // Length = 6 bytes (4 addr + 2 port)
633/// ```
634#[derive(Debug, Clone, PartialEq)]
635pub enum LinkSpecifier {
636    /// IPv4 address and port (type 0).
637    IPv4 {
638        /// Dotted-decimal IPv4 address.
639        address: String,
640        /// TCP port number.
641        port: u16,
642    },
643    /// IPv6 address and port (type 1).
644    IPv6 {
645        /// Colon-separated IPv6 address.
646        address: String,
647        /// TCP port number.
648        port: u16,
649    },
650    /// Relay fingerprint (type 2).
651    ///
652    /// 40-character uppercase hex string representing the relay's
653    /// SHA-1 identity hash.
654    Fingerprint(String),
655    /// Ed25519 identity key (type 3).
656    ///
657    /// Base64-encoded 32-byte Ed25519 public key.
658    Ed25519(String),
659    /// Unknown or unrecognized link specifier type.
660    Unknown {
661        /// The type byte from the wire format.
662        link_type: u8,
663        /// Raw data bytes.
664        data: Vec<u8>,
665    },
666}
667
668impl LinkSpecifier {
669    /// Packs this link specifier into its wire format.
670    ///
671    /// The wire format is:
672    /// - 1 byte: type (0=IPv4, 1=IPv6, 2=fingerprint, 3=Ed25519)
673    /// - 1 byte: length of data
674    /// - N bytes: type-specific data
675    ///
676    /// # Returns
677    ///
678    /// A byte vector containing the packed link specifier.
679    ///
680    /// # Example
681    ///
682    /// ```rust
683    /// use stem_rs::descriptor::hidden::LinkSpecifier;
684    ///
685    /// let spec = LinkSpecifier::IPv4 {
686    ///     address: "1.2.3.4".to_string(),
687    ///     port: 9001,
688    /// };
689    /// let packed = spec.pack();
690    /// assert_eq!(packed.len(), 8); // 1 type + 1 len + 4 addr + 2 port
691    /// ```
692    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/// Introduction point for a version 3 hidden service.
748///
749/// V3 introduction points use modern cryptography (Ed25519/X25519) and
750/// support multiple ways to specify the relay's location via link specifiers.
751///
752/// # Fields
753///
754/// - `link_specifiers`: How to connect to this introduction point
755/// - `onion_key_raw`: Base64 ntor key for the introduction handshake
756/// - `auth_key_cert`: Ed25519 certificate cross-certifying the signing key
757/// - `enc_key_raw`: Base64 encryption key for introduction requests
758/// - `enc_key_cert`: Ed25519 certificate for the encryption key
759/// - `legacy_key_raw`: Optional RSA key for backward compatibility
760/// - `legacy_key_cert`: Optional certificate for the legacy key
761///
762/// # Cryptographic Keys
763///
764/// Each introduction point has several keys:
765///
766/// 1. **Onion Key**: X25519 key for the ntor handshake with the intro point
767/// 2. **Auth Key**: Ed25519 key that authenticates the introduction point
768/// 3. **Enc Key**: X25519 key for encrypting the introduction request
769/// 4. **Legacy Key**: Optional RSA key for older clients
770///
771/// # Example
772///
773/// ```rust,ignore
774/// let inner_layer = InnerLayer::parse(decrypted_content)?;
775///
776/// for intro_point in inner_layer.introduction_points {
777///     for spec in &intro_point.link_specifiers {
778///         match spec {
779///             LinkSpecifier::IPv4 { address, port } => {
780///                 println!("Connect to {}:{}", address, port);
781///             }
782///             _ => {}
783///         }
784///     }
785/// }
786/// ```
787#[derive(Debug, Clone, PartialEq)]
788pub struct IntroductionPointV3 {
789    /// Link specifiers describing how to connect to this introduction point.
790    pub link_specifiers: Vec<LinkSpecifier>,
791    /// Base64-encoded X25519 public key for ntor handshake.
792    pub onion_key_raw: Option<String>,
793    /// Ed25519 certificate cross-certifying the signing key with the auth key.
794    pub auth_key_cert: Option<String>,
795    /// Base64-encoded X25519 public key for encrypting introduction requests.
796    pub enc_key_raw: Option<String>,
797    /// Ed25519 certificate cross-certifying the signing key by the encryption key.
798    pub enc_key_cert: Option<String>,
799    /// Optional base64-encoded RSA public key for legacy clients.
800    pub legacy_key_raw: Option<String>,
801    /// Optional certificate for the legacy RSA key.
802    pub legacy_key_cert: Option<String>,
803}
804
805impl IntroductionPointV3 {
806    /// Encodes this introduction point to its descriptor format.
807    ///
808    /// Produces the text representation suitable for inclusion in a
809    /// hidden service descriptor's inner layer.
810    ///
811    /// # Returns
812    ///
813    /// A string containing the encoded introduction point.
814    ///
815    /// # Example
816    ///
817    /// ```rust
818    /// use stem_rs::descriptor::hidden::{IntroductionPointV3, LinkSpecifier};
819    ///
820    /// let intro = IntroductionPointV3 {
821    ///     link_specifiers: vec![LinkSpecifier::IPv4 {
822    ///         address: "1.2.3.4".to_string(),
823    ///         port: 9001,
824    ///     }],
825    ///     onion_key_raw: Some("AAAA...".to_string()),
826    ///     auth_key_cert: None,
827    ///     enc_key_raw: Some("BBBB...".to_string()),
828    ///     enc_key_cert: None,
829    ///     legacy_key_raw: None,
830    ///     legacy_key_cert: None,
831    /// };
832    ///
833    /// let encoded = intro.encode();
834    /// assert!(encoded.contains("introduction-point"));
835    /// ```
836    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    /// Parses an introduction point from its descriptor content.
877    ///
878    /// # Arguments
879    ///
880    /// * `content` - The raw text content of a single introduction point block
881    ///
882    /// # Returns
883    ///
884    /// A parsed `IntroductionPointV3` on success.
885    ///
886    /// # Errors
887    ///
888    /// Returns [`Error::Parse`] if the content is malformed.
889    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    /// Parses link specifiers from a base64-encoded string.
962    ///
963    /// The format is:
964    /// - 1 byte: count of link specifiers
965    /// - For each specifier:
966    ///   - 1 byte: type
967    ///   - 1 byte: length
968    ///   - N bytes: data
969    ///
970    /// # Arguments
971    ///
972    /// * `encoded` - Base64-encoded link specifiers
973    ///
974    /// # Returns
975    ///
976    /// A vector of parsed link specifiers, or `None` if decoding fails.
977    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/// Client authorized to access a v3 hidden service.
1047///
1048/// When a v3 hidden service uses client authorization, each authorized
1049/// client has an entry in the descriptor's outer layer containing
1050/// encrypted credentials.
1051///
1052/// # Fields
1053///
1054/// - `id`: Base64-encoded 8-byte client identifier
1055/// - `iv`: Base64-encoded 16-byte initialization vector
1056/// - `cookie`: Base64-encoded 16-byte encrypted authentication cookie
1057///
1058/// # Security
1059///
1060/// The cookie is encrypted with the client's private key. Only the
1061/// authorized client can decrypt it to access the inner layer.
1062#[derive(Debug, Clone, PartialEq)]
1063pub struct AuthorizedClient {
1064    /// Base64-encoded client identifier (8 bytes).
1065    pub id: String,
1066    /// Base64-encoded initialization vector (16 bytes).
1067    pub iv: String,
1068    /// Base64-encoded encrypted authentication cookie (16 bytes).
1069    pub cookie: String,
1070}
1071
1072/// Outer encryption layer of a v3 hidden service descriptor.
1073///
1074/// The outer layer is the first layer of encryption in a v3 descriptor.
1075/// It contains client authorization data and the encrypted inner layer.
1076///
1077/// # Structure
1078///
1079/// - `auth_type`: Type of client authorization (e.g., "x25519")
1080/// - `ephemeral_key`: Ephemeral X25519 public key for decryption
1081/// - `clients`: Map of client IDs to their authorization data
1082/// - `encrypted`: The encrypted inner layer (MESSAGE block)
1083///
1084/// # Decryption
1085///
1086/// To decrypt the outer layer, you need:
1087/// 1. The blinded public key derived from the service's identity key
1088/// 2. The subcredential derived from the identity key and time period
1089///
1090/// The decryption uses AES-256-CTR with keys derived via SHAKE-256.
1091///
1092/// # Client Authorization
1093///
1094/// If `auth_type` is set, only clients listed in `clients` can decrypt
1095/// the inner layer. Each client's cookie is encrypted with their public key.
1096///
1097/// # Example
1098///
1099/// ```rust,ignore
1100/// let outer = OuterLayer::parse(decrypted_superencrypted)?;
1101///
1102/// if let Some(auth_type) = &outer.auth_type {
1103///     println!("Authorization required: {}", auth_type);
1104///     println!("Authorized clients: {}", outer.clients.len());
1105/// }
1106/// ```
1107#[derive(Debug, Clone, PartialEq)]
1108pub struct OuterLayer {
1109    /// Type of client authorization (e.g., "x25519"), or None if public.
1110    pub auth_type: Option<String>,
1111    /// Ephemeral X25519 public key for descriptor encryption.
1112    pub ephemeral_key: Option<String>,
1113    /// Map of client IDs to their authorization credentials.
1114    pub clients: HashMap<String, AuthorizedClient>,
1115    /// Encrypted inner layer content (MESSAGE block).
1116    pub encrypted: Option<String>,
1117}
1118
1119impl OuterLayer {
1120    /// Parses the outer layer from decrypted content.
1121    ///
1122    /// # Arguments
1123    ///
1124    /// * `content` - Decrypted outer layer content
1125    ///
1126    /// # Returns
1127    ///
1128    /// A parsed `OuterLayer` on success.
1129    ///
1130    /// # Errors
1131    ///
1132    /// Returns [`Error::Parse`] if the content is malformed.
1133    ///
1134    /// # Example
1135    ///
1136    /// ```rust,ignore
1137    /// // After decrypting the superencrypted blob
1138    /// let outer = OuterLayer::parse(&decrypted_content)?;
1139    /// ```
1140    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/// Inner encryption layer of a v3 hidden service descriptor.
1192///
1193/// The inner layer contains the actual service configuration and
1194/// introduction points. It is encrypted within the outer layer.
1195///
1196/// # Structure
1197///
1198/// - `formats`: Supported CREATE2 cell formats (typically [2] for ntor)
1199/// - `intro_auth`: Required authentication methods for introduction
1200/// - `is_single_service`: Whether this is a single-onion service
1201/// - `introduction_points`: List of introduction point relays
1202///
1203/// # Decryption Requirements
1204///
1205/// To access the inner layer, you must first decrypt:
1206/// 1. The outer `superencrypted` blob using the blinded key and subcredential
1207/// 2. The inner `encrypted` blob using the descriptor cookie (if client auth)
1208///
1209/// # Single-Onion Services
1210///
1211/// If `is_single_service` is true, the service is running in single-onion
1212/// mode, which provides lower latency but reduced anonymity for the service.
1213///
1214/// # Example
1215///
1216/// ```rust,ignore
1217/// let inner = InnerLayer::parse(&decrypted_inner)?;
1218///
1219/// println!("CREATE2 formats: {:?}", inner.formats);
1220/// println!("Single-onion: {}", inner.is_single_service);
1221/// println!("Introduction points: {}", inner.introduction_points.len());
1222///
1223/// for intro in &inner.introduction_points {
1224///     println!("  - {:?}", intro.link_specifiers);
1225/// }
1226/// ```
1227#[derive(Debug, Clone, PartialEq)]
1228pub struct InnerLayer {
1229    /// Supported CREATE2 cell formats (typically [2] for ntor handshake).
1230    pub formats: Vec<u32>,
1231    /// Required authentication methods for introduction (e.g., ["ed25519"]).
1232    pub intro_auth: Vec<String>,
1233    /// Whether this is a single-onion (non-anonymous) service.
1234    pub is_single_service: bool,
1235    /// List of introduction points for connecting to the service.
1236    pub introduction_points: Vec<IntroductionPointV3>,
1237}
1238
1239impl InnerLayer {
1240    /// Parses the inner layer from decrypted content.
1241    ///
1242    /// # Arguments
1243    ///
1244    /// * `content` - Decrypted inner layer content
1245    ///
1246    /// # Returns
1247    ///
1248    /// A parsed `InnerLayer` on success.
1249    ///
1250    /// # Errors
1251    ///
1252    /// Returns [`Error::Parse`] if the content is malformed or
1253    /// introduction points fail to parse.
1254    ///
1255    /// # Example
1256    ///
1257    /// ```rust,ignore
1258    /// // After decrypting both layers
1259    /// let inner = InnerLayer::parse(&decrypted_content)?;
1260    ///
1261    /// for intro in inner.introduction_points {
1262    ///     // Connect to introduction points
1263    /// }
1264    /// ```
1265    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(&current_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(&current_block)?);
1314            }
1315        }
1316
1317        Ok(Self {
1318            formats,
1319            intro_auth,
1320            is_single_service,
1321            introduction_points,
1322        })
1323    }
1324}
1325
1326/// Version 3 hidden service descriptor.
1327///
1328/// A v3 hidden service descriptor uses modern Ed25519/X25519 cryptography
1329/// and provides stronger security than v2 descriptors. The `.onion` address
1330/// is 56 characters long (vs 16 for v2).
1331///
1332/// # Structure
1333///
1334/// The descriptor contains:
1335/// - `version`: Always 3 for this type
1336/// - `lifetime`: How long the descriptor is valid (in minutes)
1337/// - `signing_cert`: Ed25519 certificate for the descriptor signing key
1338/// - `revision_counter`: Monotonically increasing counter to prevent replay
1339/// - `superencrypted`: Encrypted outer layer (contains inner layer)
1340/// - `signature`: Ed25519 signature over the descriptor
1341///
1342/// # Encryption Layers
1343///
1344/// V3 descriptors have two encryption layers:
1345///
1346/// 1. **Superencrypted** (outer): Decrypted with blinded key + subcredential
1347/// 2. **Encrypted** (inner): May require client authorization cookie
1348///
1349/// The decryption process requires:
1350/// - The service's blinded public key (derived from identity key + time)
1351/// - The subcredential (derived from identity key)
1352/// - Optionally, the client's authorization cookie
1353///
1354/// # Address Format
1355///
1356/// V3 `.onion` addresses are 56 characters and encode:
1357/// - 32 bytes: Ed25519 public key
1358/// - 2 bytes: Checksum
1359/// - 1 byte: Version (0x03)
1360///
1361/// Use [`address_from_identity_key`](Self::address_from_identity_key) and
1362/// [`identity_key_from_address`](Self::identity_key_from_address) to convert.
1363///
1364/// # Example
1365///
1366/// ```rust,ignore
1367/// use stem_rs::descriptor::hidden::HiddenServiceDescriptorV3;
1368/// use stem_rs::descriptor::Descriptor;
1369///
1370/// let content = std::fs::read_to_string("v3_descriptor.txt")?;
1371/// let desc = HiddenServiceDescriptorV3::parse(&content)?;
1372///
1373/// println!("Version: {}", desc.version);
1374/// println!("Lifetime: {} minutes", desc.lifetime);
1375/// println!("Revision: {}", desc.revision_counter);
1376///
1377/// // Convert identity key to .onion address
1378/// let key = [0u8; 32]; // Your 32-byte Ed25519 public key
1379/// let address = HiddenServiceDescriptorV3::address_from_identity_key(&key);
1380/// println!("Address: {}", address);
1381/// ```
1382///
1383/// # Security
1384///
1385/// - The `revision_counter` prevents replay attacks
1386/// - The `signature` authenticates the descriptor
1387/// - Introduction points are encrypted and require decryption
1388/// - Client authorization adds an additional encryption layer
1389#[derive(Debug, Clone, PartialEq)]
1390pub struct HiddenServiceDescriptorV3 {
1391    /// Hidden service descriptor version (always 3 for this type).
1392    pub version: u32,
1393    /// Descriptor validity period in minutes (typically 180).
1394    pub lifetime: u32,
1395    /// Ed25519 certificate for the descriptor signing key (PEM format).
1396    pub signing_cert: Option<String>,
1397    /// Monotonically increasing counter to prevent replay attacks.
1398    pub revision_counter: u64,
1399    /// Encrypted outer layer containing client auth and inner layer.
1400    pub superencrypted: Option<String>,
1401    /// Ed25519 signature over the descriptor content.
1402    pub signature: String,
1403    /// Raw bytes of the original descriptor content.
1404    raw_content: Vec<u8>,
1405    /// Lines from the descriptor that were not recognized.
1406    unrecognized_lines: Vec<String>,
1407}
1408
1409impl HiddenServiceDescriptorV3 {
1410    /// Converts an Ed25519 identity key to a v3 `.onion` address.
1411    ///
1412    /// The address is computed as:
1413    /// 1. Compute checksum: SHA3-256(".onion checksum" || pubkey || version)[0:2]
1414    /// 2. Concatenate: pubkey || checksum || version
1415    /// 3. Base32-encode and append ".onion"
1416    ///
1417    /// # Arguments
1418    ///
1419    /// * `key` - 32-byte Ed25519 public key
1420    ///
1421    /// # Returns
1422    ///
1423    /// A 62-character string ending in ".onion" (56 chars + ".onion").
1424    ///
1425    /// # Example
1426    ///
1427    /// ```rust
1428    /// use stem_rs::descriptor::hidden::HiddenServiceDescriptorV3;
1429    ///
1430    /// let key = [0u8; 32];
1431    /// let address = HiddenServiceDescriptorV3::address_from_identity_key(&key);
1432    /// assert!(address.ends_with(".onion"));
1433    /// assert_eq!(address.len(), 62); // 56 + ".onion"
1434    /// ```
1435    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    /// Extracts the Ed25519 identity key from a v3 `.onion` address.
1454    ///
1455    /// Validates the address format, checksum, and version byte.
1456    ///
1457    /// # Arguments
1458    ///
1459    /// * `onion_address` - A v3 `.onion` address (with or without ".onion" suffix)
1460    ///
1461    /// # Returns
1462    ///
1463    /// The 32-byte Ed25519 public key on success.
1464    ///
1465    /// # Errors
1466    ///
1467    /// Returns [`Error::Parse`] if:
1468    /// - The address is not valid base32
1469    /// - The decoded length is not 35 bytes
1470    /// - The version byte is not 3
1471    /// - The checksum does not match
1472    ///
1473    /// # Example
1474    ///
1475    /// ```rust
1476    /// use stem_rs::descriptor::hidden::HiddenServiceDescriptorV3;
1477    ///
1478    /// let key = [0u8; 32];
1479    /// let address = HiddenServiceDescriptorV3::address_from_identity_key(&key);
1480    /// let recovered = HiddenServiceDescriptorV3::identity_key_from_address(&address).unwrap();
1481    /// assert_eq!(recovered, key.to_vec());
1482    /// ```
1483    ///
1484    /// # Security
1485    ///
1486    /// The checksum prevents typos in addresses from connecting to the wrong
1487    /// service. Always validate addresses before use.
1488    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
1666/// Extracts a PEM-formatted block from descriptor lines.
1667///
1668/// Reads lines starting from `start_idx` until finding a line that
1669/// starts with "-----END ".
1670///
1671/// # Arguments
1672///
1673/// * `lines` - Slice of descriptor lines
1674/// * `start_idx` - Index to start reading from
1675///
1676/// # Returns
1677///
1678/// A tuple of (block_content, end_index) where end_index is the line
1679/// containing the END marker.
1680fn 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
1695/// Extracts a MESSAGE block from descriptor lines.
1696///
1697/// Reads lines starting from `start_idx`, looking for content between
1698/// "-----BEGIN MESSAGE-----" and "-----END MESSAGE-----" markers.
1699///
1700/// # Arguments
1701///
1702/// * `lines` - Slice of descriptor lines
1703/// * `start_idx` - Index to start reading from
1704///
1705/// # Returns
1706///
1707/// A tuple of (block_content, end_index) where end_index is the line
1708/// containing the END marker.
1709fn 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
1735/// Decodes a base64-encoded string.
1736///
1737/// Handles standard base64 alphabet (A-Z, a-z, 0-9, +, /) and ignores
1738/// whitespace and padding characters.
1739///
1740/// # Arguments
1741///
1742/// * `input` - Base64-encoded string
1743///
1744/// # Returns
1745///
1746/// Decoded bytes, or `None` if the input contains invalid characters.
1747fn 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
1772/// Encodes bytes to a base64 string.
1773///
1774/// Uses standard base64 alphabet (A-Z, a-z, 0-9, +, /) without padding.
1775///
1776/// # Arguments
1777///
1778/// * `bytes` - Bytes to encode
1779///
1780/// # Returns
1781///
1782/// Base64-encoded string.
1783fn 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
1809/// Encodes bytes to a base32 string.
1810///
1811/// Uses RFC 4648 base32 alphabet (A-Z, 2-7) without padding.
1812/// This is the encoding used for `.onion` addresses.
1813///
1814/// # Arguments
1815///
1816/// * `bytes` - Bytes to encode
1817///
1818/// # Returns
1819///
1820/// Uppercase base32-encoded string.
1821fn 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
1845/// Decodes a base32-encoded string.
1846///
1847/// Uses RFC 4648 base32 alphabet (A-Z, 2-7), case-insensitive.
1848/// Ignores padding characters.
1849///
1850/// # Arguments
1851///
1852/// * `input` - Base32-encoded string
1853///
1854/// # Returns
1855///
1856/// Decoded bytes, or `None` if the input contains invalid characters.
1857fn 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}