stem_rs/
util.rs

1//! Validation and helper functions for Tor-related data.
2//!
3//! This module provides utilities for validating Tor-specific identifiers and
4//! performing secure operations. These functions are used throughout the library
5//! to ensure data integrity and prevent protocol injection attacks.
6//!
7//! # Conceptual Role
8//!
9//! The validation functions in this module check format correctness for:
10//! - Relay fingerprints (40-character hex strings)
11//! - Relay nicknames (1-19 alphanumeric characters)
12//! - Circuit and stream identifiers
13//! - Hidden service addresses (v2 and v3)
14//! - IP addresses and ports
15//!
16//! # Security Considerations
17//!
18//! - [`secure_compare`] uses constant-time comparison to prevent timing attacks
19//! - Input validation prevents protocol injection in control commands
20//! - All validation functions are pure and have no side effects
21//!
22//! # Example
23//!
24//! ```rust
25//! use stem_rs::util::{is_valid_fingerprint, is_valid_nickname, is_valid_hidden_service_address};
26//!
27//! // Validate a relay fingerprint
28//! assert!(is_valid_fingerprint("9695DFC35FFEB861329B9F1AB04C46397020CE31"));
29//!
30//! // Validate a relay nickname
31//! assert!(is_valid_nickname("MyRelay"));
32//! assert!(!is_valid_nickname("invalid-name")); // hyphens not allowed
33//!
34//! // Validate hidden service addresses
35//! assert!(is_valid_hidden_service_address("facebookcorewwwi")); // v2
36//! ```
37//!
38//! # See Also
39//!
40//! - [`Controller`](crate::Controller) - Uses these validators for input checking
41//! - Python Stem equivalent: `stem.util.tor_tools`
42
43/// Validates a relay fingerprint string.
44///
45/// A valid fingerprint consists of exactly 40 hexadecimal characters
46/// (case-insensitive), representing a 160-bit SHA-1 hash of the relay's
47/// identity key.
48///
49/// # Arguments
50///
51/// * `s` - The string to validate
52///
53/// # Returns
54///
55/// `true` if the string is a valid fingerprint, `false` otherwise.
56///
57/// # Format
58///
59/// - Length: exactly 40 characters
60/// - Characters: hexadecimal digits (0-9, a-f, A-F)
61///
62/// # Example
63///
64/// ```rust
65/// use stem_rs::util::is_valid_fingerprint;
66///
67/// // Valid fingerprints (case-insensitive)
68/// assert!(is_valid_fingerprint("9695DFC35FFEB861329B9F1AB04C46397020CE31"));
69/// assert!(is_valid_fingerprint("9695dfc35ffeb861329b9f1ab04c46397020ce31"));
70///
71/// // Invalid fingerprints
72/// assert!(!is_valid_fingerprint("9695DFC35FFEB861329B9F1AB04C4639702")); // Too short
73/// assert!(!is_valid_fingerprint("ZZZZDFC35FFEB861329B9F1AB04C46397020CE31")); // Invalid chars
74/// ```
75///
76/// # This Compiles But Is Wrong
77///
78/// ```rust
79/// use stem_rs::util::is_valid_fingerprint;
80///
81/// // Don't include the "$" prefix - that's for fingerprint references
82/// let with_prefix = "$9695DFC35FFEB861329B9F1AB04C46397020CE31";
83/// assert!(!is_valid_fingerprint(with_prefix)); // Returns false!
84///
85/// // Use is_valid_fingerprint_with_prefix for prefixed fingerprints
86/// use stem_rs::util::is_valid_fingerprint_with_prefix;
87/// assert!(is_valid_fingerprint_with_prefix(with_prefix));
88/// ```
89pub fn is_valid_fingerprint(s: &str) -> bool {
90    s.len() == 40 && s.chars().all(|c| c.is_ascii_hexdigit())
91}
92
93/// Validates a relay fingerprint with optional `$` prefix.
94///
95/// This function accepts fingerprints with or without the `$` prefix
96/// commonly used in Tor control protocol responses.
97///
98/// # Arguments
99///
100/// * `s` - The string to validate
101///
102/// # Returns
103///
104/// `true` if the string is a valid fingerprint (with or without `$` prefix).
105///
106/// # Example
107///
108/// ```rust
109/// use stem_rs::util::is_valid_fingerprint_with_prefix;
110///
111/// // Both formats are valid
112/// assert!(is_valid_fingerprint_with_prefix("$9695DFC35FFEB861329B9F1AB04C46397020CE31"));
113/// assert!(is_valid_fingerprint_with_prefix("9695DFC35FFEB861329B9F1AB04C46397020CE31"));
114///
115/// // Invalid
116/// assert!(!is_valid_fingerprint_with_prefix("$ABCD")); // Too short
117/// ```
118pub fn is_valid_fingerprint_with_prefix(s: &str) -> bool {
119    if let Some(stripped) = s.strip_prefix('$') {
120        is_valid_fingerprint(stripped)
121    } else {
122        is_valid_fingerprint(s)
123    }
124}
125
126/// Validates a relay nickname.
127///
128/// A valid nickname consists of 1 to 19 alphanumeric ASCII characters.
129/// Nicknames are used to identify relays in a human-readable format.
130///
131/// # Arguments
132///
133/// * `s` - The string to validate
134///
135/// # Returns
136///
137/// `true` if the string is a valid nickname, `false` otherwise.
138///
139/// # Format
140///
141/// - Length: 1 to 19 characters
142/// - Characters: ASCII alphanumeric only (a-z, A-Z, 0-9)
143/// - No spaces, hyphens, underscores, or special characters
144///
145/// # Example
146///
147/// ```rust
148/// use stem_rs::util::is_valid_nickname;
149///
150/// // Valid nicknames
151/// assert!(is_valid_nickname("MyRelay"));
152/// assert!(is_valid_nickname("relay123"));
153/// assert!(is_valid_nickname("A")); // Single character is valid
154/// assert!(is_valid_nickname("1234567890123456789")); // 19 chars max
155///
156/// // Invalid nicknames
157/// assert!(!is_valid_nickname("")); // Empty
158/// assert!(!is_valid_nickname("12345678901234567890")); // 20 chars - too long
159/// assert!(!is_valid_nickname("my-relay")); // Hyphens not allowed
160/// assert!(!is_valid_nickname("my_relay")); // Underscores not allowed
161/// assert!(!is_valid_nickname("my relay")); // Spaces not allowed
162/// ```
163///
164/// # This Compiles But Is Wrong
165///
166/// ```rust
167/// use stem_rs::util::is_valid_nickname;
168///
169/// // Nicknames are NOT case-insensitive identifiers
170/// // "MyRelay" and "myrelay" are different nicknames
171/// let nick1 = "MyRelay";
172/// let nick2 = "myrelay";
173/// assert!(is_valid_nickname(nick1));
174/// assert!(is_valid_nickname(nick2));
175/// // But they refer to different relays!
176/// ```
177pub fn is_valid_nickname(s: &str) -> bool {
178    let len = s.len();
179    (1..=19).contains(&len) && s.chars().all(|c| c.is_ascii_alphanumeric())
180}
181
182/// Validates a circuit identifier.
183///
184/// Circuit IDs are numeric strings used to identify circuits in the
185/// Tor control protocol.
186///
187/// # Arguments
188///
189/// * `s` - The string to validate
190///
191/// # Returns
192///
193/// `true` if the string is a valid circuit ID, `false` otherwise.
194///
195/// # Format
196///
197/// - Non-empty string
198/// - Contains only ASCII digits (0-9)
199///
200/// # Example
201///
202/// ```rust
203/// use stem_rs::util::is_valid_circuit_id;
204///
205/// assert!(is_valid_circuit_id("1"));
206/// assert!(is_valid_circuit_id("123"));
207/// assert!(is_valid_circuit_id("999999"));
208///
209/// assert!(!is_valid_circuit_id("")); // Empty
210/// assert!(!is_valid_circuit_id("abc")); // Non-numeric
211/// assert!(!is_valid_circuit_id("12a")); // Mixed
212/// ```
213pub fn is_valid_circuit_id(s: &str) -> bool {
214    !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
215}
216
217/// Validates a stream identifier.
218///
219/// Stream IDs are numeric strings used to identify streams in the
220/// Tor control protocol. This function has the same validation rules
221/// as [`is_valid_circuit_id`].
222///
223/// # Arguments
224///
225/// * `s` - The string to validate
226///
227/// # Returns
228///
229/// `true` if the string is a valid stream ID, `false` otherwise.
230///
231/// # Example
232///
233/// ```rust
234/// use stem_rs::util::is_valid_stream_id;
235///
236/// assert!(is_valid_stream_id("1"));
237/// assert!(is_valid_stream_id("456"));
238///
239/// assert!(!is_valid_stream_id("")); // Empty
240/// assert!(!is_valid_stream_id("xyz")); // Non-numeric
241/// ```
242pub fn is_valid_stream_id(s: &str) -> bool {
243    !s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
244}
245
246/// Validates an IPv4 address string.
247///
248/// Checks if the string is a valid IPv4 address in dotted-decimal notation.
249/// Leading zeros in octets are not allowed (to avoid octal interpretation).
250///
251/// # Arguments
252///
253/// * `s` - The string to validate
254///
255/// # Returns
256///
257/// `true` if the string is a valid IPv4 address, `false` otherwise.
258///
259/// # Example
260///
261/// ```rust
262/// use stem_rs::util::is_valid_ipv4_address;
263///
264/// assert!(is_valid_ipv4_address("127.0.0.1"));
265/// assert!(is_valid_ipv4_address("192.168.1.1"));
266/// assert!(is_valid_ipv4_address("0.0.0.0"));
267/// assert!(is_valid_ipv4_address("255.255.255.255"));
268///
269/// assert!(!is_valid_ipv4_address("256.0.0.1")); // Octet > 255
270/// assert!(!is_valid_ipv4_address("01.02.03.04")); // Leading zeros
271/// assert!(!is_valid_ipv4_address("127.0.0")); // Missing octet
272/// ```
273pub fn is_valid_ipv4_address(s: &str) -> bool {
274    let parts: Vec<&str> = s.split('.').collect();
275    if parts.len() != 4 {
276        return false;
277    }
278    for part in &parts {
279        if part.is_empty() {
280            return false;
281        }
282        if part.len() > 1 && part.starts_with('0') {
283            return false;
284        }
285        if part.parse::<u8>().is_err() {
286            return false;
287        }
288    }
289    true
290}
291
292/// Validates an IPv6 address string.
293///
294/// Checks if the string is a valid IPv6 address. Supports compressed
295/// notation with `::` for consecutive zero groups.
296///
297/// # Arguments
298///
299/// * `s` - The string to validate
300///
301/// # Returns
302///
303/// `true` if the string is a valid IPv6 address, `false` otherwise.
304///
305/// # Example
306///
307/// ```rust
308/// use stem_rs::util::is_valid_ipv6_address;
309///
310/// assert!(is_valid_ipv6_address("2001:0db8:0000:0000:0000:ff00:0042:8329"));
311/// assert!(is_valid_ipv6_address("2001:db8::ff00:42:8329")); // Compressed
312/// assert!(is_valid_ipv6_address("::1")); // Loopback
313/// assert!(is_valid_ipv6_address("::")); // All zeros
314///
315/// assert!(!is_valid_ipv6_address("2001:db8::ff00::8329")); // Multiple ::
316/// ```
317pub fn is_valid_ipv6_address(s: &str) -> bool {
318    is_valid_ipv6_address_impl(s, false)
319}
320
321/// Validates an IPv6 address string, optionally with brackets.
322///
323/// Like [`is_valid_ipv6_address`], but also accepts addresses enclosed
324/// in square brackets (e.g., `[::1]`), which is common in URLs.
325///
326/// # Arguments
327///
328/// * `s` - The string to validate
329///
330/// # Returns
331///
332/// `true` if the string is a valid IPv6 address (with or without brackets).
333///
334/// # Example
335///
336/// ```rust
337/// use stem_rs::util::is_valid_ipv6_address_bracketed;
338///
339/// assert!(is_valid_ipv6_address_bracketed("[::1]"));
340/// assert!(is_valid_ipv6_address_bracketed("[2001:db8::1]"));
341/// assert!(is_valid_ipv6_address_bracketed("::1")); // Without brackets also valid
342/// ```
343pub fn is_valid_ipv6_address_bracketed(s: &str) -> bool {
344    is_valid_ipv6_address_impl(s, true)
345}
346
347/// Internal implementation for IPv6 address validation.
348fn is_valid_ipv6_address_impl(s: &str, allow_brackets: bool) -> bool {
349    let addr = if allow_brackets && s.starts_with('[') && s.ends_with(']') {
350        &s[1..s.len() - 1]
351    } else {
352        s
353    };
354
355    if addr.is_empty() {
356        return false;
357    }
358
359    let colon_count = addr.matches(':').count();
360    if colon_count > 7 {
361        return false;
362    }
363
364    let has_double_colon = addr.contains("::");
365    if !has_double_colon && colon_count != 7 {
366        return false;
367    }
368    if addr.matches("::").count() > 1 || addr.contains(":::") {
369        return false;
370    }
371
372    for group in addr.split(':') {
373        if group.len() > 4 {
374            return false;
375        }
376        if !group.is_empty() && !group.chars().all(|c| c.is_ascii_hexdigit()) {
377            return false;
378        }
379    }
380    true
381}
382
383/// Validates a port number string.
384///
385/// Checks if the string represents a valid TCP/UDP port number (1-65535).
386/// Port 0 is not considered valid.
387///
388/// # Arguments
389///
390/// * `s` - The string to validate
391///
392/// # Returns
393///
394/// `true` if the string is a valid port number, `false` otherwise.
395///
396/// # Example
397///
398/// ```rust
399/// use stem_rs::util::is_valid_port;
400///
401/// assert!(is_valid_port("80"));
402/// assert!(is_valid_port("443"));
403/// assert!(is_valid_port("9051")); // Tor control port
404/// assert!(is_valid_port("65535"));
405///
406/// assert!(!is_valid_port("0")); // Port 0 not valid
407/// assert!(!is_valid_port("65536")); // Too large
408/// assert!(!is_valid_port("abc")); // Non-numeric
409/// ```
410pub fn is_valid_port(s: &str) -> bool {
411    s.parse::<u16>().is_ok_and(|p| p > 0)
412}
413
414/// Validates a port number.
415///
416/// Checks if the port number is valid (1-65535). Port 0 is not considered valid.
417///
418/// # Arguments
419///
420/// * `port` - The port number to validate
421///
422/// # Returns
423///
424/// `true` if the port is valid (non-zero), `false` otherwise.
425///
426/// # Example
427///
428/// ```rust
429/// use stem_rs::util::is_valid_port_number;
430///
431/// assert!(is_valid_port_number(80));
432/// assert!(is_valid_port_number(9051));
433/// assert!(is_valid_port_number(65535));
434///
435/// assert!(!is_valid_port_number(0));
436/// ```
437pub fn is_valid_port_number(port: u16) -> bool {
438    port > 0
439}
440
441/// Checks if an IPv4 address is in a private range.
442///
443/// Returns `Some(true)` if the address is private (RFC 1918), `Some(false)`
444/// if public, or `None` if the address is invalid.
445///
446/// # Private Ranges
447///
448/// - `10.0.0.0/8` - Class A private
449/// - `172.16.0.0/12` - Class B private
450/// - `192.168.0.0/16` - Class C private
451/// - `127.0.0.0/8` - Loopback
452///
453/// # Arguments
454///
455/// * `s` - The IPv4 address string to check
456///
457/// # Returns
458///
459/// - `Some(true)` if the address is private
460/// - `Some(false)` if the address is public
461/// - `None` if the address is invalid
462///
463/// # Example
464///
465/// ```rust
466/// use stem_rs::util::is_private_address;
467///
468/// assert_eq!(is_private_address("10.0.0.1"), Some(true));
469/// assert_eq!(is_private_address("192.168.1.1"), Some(true));
470/// assert_eq!(is_private_address("127.0.0.1"), Some(true));
471/// assert_eq!(is_private_address("8.8.8.8"), Some(false));
472/// assert_eq!(is_private_address("invalid"), None);
473/// ```
474pub fn is_private_address(s: &str) -> Option<bool> {
475    if !is_valid_ipv4_address(s) {
476        return None;
477    }
478    let parts: Vec<u8> = s.split('.').filter_map(|p| p.parse().ok()).collect();
479    if parts.len() != 4 {
480        return None;
481    }
482    let is_private = parts[0] == 10
483        || parts[0] == 127
484        || (parts[0] == 192 && parts[1] == 168)
485        || (parts[0] == 172 && (16..=31).contains(&parts[1]));
486    Some(is_private)
487}
488
489/// Expands a compressed IPv6 address to full notation.
490///
491/// Converts an IPv6 address with `::` compression to its full 8-group
492/// representation with each group zero-padded to 4 digits.
493///
494/// # Arguments
495///
496/// * `s` - The IPv6 address string to expand
497///
498/// # Returns
499///
500/// - `Some(String)` with the expanded address if valid
501/// - `None` if the address is invalid
502///
503/// # Example
504///
505/// ```rust
506/// use stem_rs::util::expand_ipv6_address;
507///
508/// assert_eq!(
509///     expand_ipv6_address("::1"),
510///     Some("0000:0000:0000:0000:0000:0000:0000:0001".to_string())
511/// );
512/// assert_eq!(
513///     expand_ipv6_address("2001:db8::ff00:42:8329"),
514///     Some("2001:0db8:0000:0000:0000:ff00:0042:8329".to_string())
515/// );
516/// assert_eq!(expand_ipv6_address("invalid"), None);
517/// ```
518pub fn expand_ipv6_address(s: &str) -> Option<String> {
519    if !is_valid_ipv6_address(s) {
520        return None;
521    }
522
523    let mut groups: Vec<String> = Vec::with_capacity(8);
524
525    if s.contains("::") {
526        let parts: Vec<&str> = s.split("::").collect();
527        let left: Vec<&str> = if parts[0].is_empty() {
528            vec![]
529        } else {
530            parts[0].split(':').collect()
531        };
532        let right: Vec<&str> = if parts.len() > 1 && !parts[1].is_empty() {
533            parts[1].split(':').collect()
534        } else {
535            vec![]
536        };
537
538        for g in &left {
539            groups.push(format!("{:0>4}", g.to_lowercase()));
540        }
541        let zeros_needed = 8 - left.len() - right.len();
542        for _ in 0..zeros_needed {
543            groups.push("0000".to_string());
544        }
545        for g in &right {
546            groups.push(format!("{:0>4}", g.to_lowercase()));
547        }
548    } else {
549        for g in s.split(':') {
550            groups.push(format!("{:0>4}", g.to_lowercase()));
551        }
552    }
553
554    Some(groups.join(":"))
555}
556
557/// Validates a connection_id(s: &str.
558///
559/// Connection IDs have the same format as circuit IDs. This function
560/// is an alias for [`is_valid_circuit_id`].
561///
562/// # Arguments
563///
564/// * `s` - The string to validate
565///
566/// # Returns
567///
568/// `true` if the string is a valid connection ID, `false` otherwise.
569///
570/// # Example
571///
572/// ```rust
573/// use stem_rs::util::is_valid_connection_id;
574///
575/// assert!(is_valid_connection_id("1"));
576/// assert!(is_valid_connection_id("123"));
577/// assert!(!is_valid_connection_id(""));
578/// assert!(!is_valid_connection_id("abc"));
579/// ```
580pub fn is_valid_connection_id(s: &str) -> bool {
581    is_valid_circuit_id(s)
582}
583
584/// Validates a hidden service address (v2 or v3).
585///
586/// Checks if the string is a valid hidden service address, supporting both
587/// v2 (16 characters) and v3 (56 characters) formats. The `.onion` suffix
588/// is optional.
589///
590/// # Arguments
591///
592/// * `s` - The string to validate
593///
594/// # Returns
595///
596/// `true` if the string is a valid v2 or v3 hidden service address.
597///
598/// # Example
599///
600/// ```rust
601/// use stem_rs::util::is_valid_hidden_service_address;
602///
603/// // V2 addresses (16 base32 characters)
604/// assert!(is_valid_hidden_service_address("facebookcorewwwi"));
605/// assert!(is_valid_hidden_service_address("facebookcorewwwi.onion"));
606///
607/// // V3 addresses (56 base32 characters)
608/// let v3_addr = "a".repeat(56);
609/// assert!(is_valid_hidden_service_address(&v3_addr));
610///
611/// // Invalid
612/// assert!(!is_valid_hidden_service_address("invalid"));
613/// ```
614///
615/// # See Also
616///
617/// - [`is_valid_hidden_service_address_v2`] - V2 only validation
618/// - [`is_valid_hidden_service_address_v3`] - V3 only validation
619pub fn is_valid_hidden_service_address(s: &str) -> bool {
620    is_valid_hidden_service_address_v2(s) || is_valid_hidden_service_address_v3(s)
621}
622
623/// Validates a v2 hidden service address.
624///
625/// V2 hidden service addresses are 16 lowercase base32 characters
626/// (a-z, 2-7). The `.onion` suffix is optional.
627///
628/// # Deprecation Note
629///
630/// V2 hidden services are deprecated and no longer supported by Tor
631/// as of version 0.4.6. Use v3 addresses for new services.
632///
633/// # Arguments
634///
635/// * `s` - The string to validate
636///
637/// # Returns
638///
639/// `true` if the string is a valid v2 hidden service address.
640///
641/// # Example
642///
643/// ```rust
644/// use stem_rs::util::is_valid_hidden_service_address_v2;
645///
646/// assert!(is_valid_hidden_service_address_v2("facebookcorewwwi"));
647/// assert!(is_valid_hidden_service_address_v2("facebookcorewwwi.onion"));
648/// assert!(is_valid_hidden_service_address_v2("aaaaaaaaaaaaaaaa"));
649///
650/// // Invalid - uppercase not allowed
651/// assert!(!is_valid_hidden_service_address_v2("FACEBOOKCOREWWWI"));
652/// // Invalid - wrong length
653/// assert!(!is_valid_hidden_service_address_v2("abc"));
654/// ```
655pub fn is_valid_hidden_service_address_v2(s: &str) -> bool {
656    let addr = s.strip_suffix(".onion").unwrap_or(s);
657    addr.len() == 16 && addr.chars().all(is_base32_char)
658}
659
660/// Validates a v3 hidden service address.
661///
662/// V3 hidden service addresses are 56 lowercase base32 characters
663/// (a-z, 2-7). The `.onion` suffix is optional.
664///
665/// # Format
666///
667/// V3 addresses encode: `base32(PUBKEY | CHECKSUM | VERSION)`
668/// - PUBKEY: 32-byte Ed25519 public key
669/// - CHECKSUM: 2-byte truncated SHA3-256 hash
670/// - VERSION: 1-byte version (0x03)
671///
672/// Note: This function only validates the format (length and character set),
673/// not the cryptographic checksum.
674///
675/// # Arguments
676///
677/// * `s` - The string to validate
678///
679/// # Returns
680///
681/// `true` if the string is a valid v3 hidden service address format.
682///
683/// # Example
684///
685/// ```rust
686/// use stem_rs::util::is_valid_hidden_service_address_v3;
687///
688/// let v3_addr = "a".repeat(56);
689/// assert!(is_valid_hidden_service_address_v3(&v3_addr));
690/// assert!(is_valid_hidden_service_address_v3(&format!("{}.onion", v3_addr)));
691///
692/// // Invalid - wrong length
693/// assert!(!is_valid_hidden_service_address_v3(&"a".repeat(55)));
694/// // Invalid - uppercase not allowed
695/// assert!(!is_valid_hidden_service_address_v3(&"A".repeat(56)));
696/// ```
697pub fn is_valid_hidden_service_address_v3(s: &str) -> bool {
698    let addr = s.strip_suffix(".onion").unwrap_or(s);
699    addr.len() == 56 && addr.chars().all(is_base32_char)
700}
701
702/// Checks if a character is a valid base32 character.
703///
704/// Base32 characters are lowercase letters a-z and digits 2-7.
705fn is_base32_char(c: char) -> bool {
706    matches!(c, 'a'..='z' | '2'..='7')
707}
708
709/// Checks if a string contains exactly the specified number of hex digits.
710///
711/// This is a helper function for validating fixed-length hexadecimal strings
712/// like fingerprints and hashes.
713///
714/// # Arguments
715///
716/// * `s` - The string to check
717/// * `length` - The expected number of hex digits
718///
719/// # Returns
720///
721/// `true` if the string has exactly `length` hexadecimal characters.
722///
723/// # Example
724///
725/// ```rust
726/// use stem_rs::util::is_hex_digits;
727///
728/// assert!(is_hex_digits("abcd", 4));
729/// assert!(is_hex_digits("ABCD1234", 8));
730/// assert!(is_hex_digits("0123456789abcdef", 16));
731///
732/// assert!(!is_hex_digits("abcd", 5)); // Wrong length
733/// assert!(!is_hex_digits("ghij", 4)); // Invalid hex chars
734/// ```
735pub fn is_hex_digits(s: &str, length: usize) -> bool {
736    s.len() == length && s.chars().all(|c| c.is_ascii_hexdigit())
737}
738
739/// Compares two byte slices in constant time.
740///
741/// This function performs a timing-safe comparison of two byte slices,
742/// preventing timing attacks that could leak information about the
743/// contents of secret data.
744///
745/// # Security
746///
747/// This function is designed to take the same amount of time regardless
748/// of where the first difference occurs. This prevents attackers from
749/// using timing measurements to guess secret values byte-by-byte.
750///
751/// Use this function when comparing:
752/// - Authentication cookies
753/// - HMAC values
754/// - Password hashes
755/// - Any security-sensitive data
756///
757/// # Arguments
758///
759/// * `a` - First byte slice
760/// * `b` - Second byte slice
761///
762/// # Returns
763///
764/// `true` if the slices are equal, `false` otherwise.
765///
766/// # Implementation
767///
768/// The comparison XORs all bytes and accumulates differences, ensuring
769/// all bytes are always compared regardless of early mismatches.
770///
771/// # Example
772///
773/// ```rust
774/// use stem_rs::util::secure_compare;
775///
776/// let secret = b"my_secret_cookie";
777/// let attempt = b"my_secret_cookie";
778/// let wrong = b"wrong_cookie_val";
779///
780/// assert!(secure_compare(secret, attempt));
781/// assert!(!secure_compare(secret, wrong));
782///
783/// // Different lengths always return false
784/// assert!(!secure_compare(b"short", b"longer"));
785/// ```
786///
787/// # This Compiles But Is Wrong
788///
789/// ```rust
790/// // DON'T use regular equality for secrets - it's vulnerable to timing attacks
791/// let secret = b"authentication_cookie";
792/// let attempt = b"authentication_cookie";
793///
794/// // This is INSECURE - timing varies based on first differing byte
795/// // if secret == attempt { ... }
796///
797/// // Use secure_compare instead
798/// use stem_rs::util::secure_compare;
799/// if secure_compare(secret, attempt) {
800///     // Safe comparison
801/// }
802/// ```
803pub fn secure_compare(a: &[u8], b: &[u8]) -> bool {
804    if a.len() != b.len() {
805        return false;
806    }
807    let mut result: u8 = 0;
808    for (x, y) in a.iter().zip(b.iter()) {
809        result |= x ^ y;
810    }
811    result == 0
812}
813
814#[cfg(test)]
815mod tests {
816    use super::*;
817
818    #[test]
819    fn test_valid_fingerprint() {
820        assert!(is_valid_fingerprint(
821            "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"
822        ));
823        assert!(is_valid_fingerprint(
824            "abcd1234abcd1234abcd1234abcd1234abcd1234"
825        ));
826        assert!(is_valid_fingerprint(
827            "0123456789abcdef0123456789ABCDEF01234567"
828        ));
829    }
830
831    #[test]
832    fn test_invalid_fingerprint() {
833        assert!(!is_valid_fingerprint(""));
834        assert!(!is_valid_fingerprint("ABCD1234"));
835        assert!(!is_valid_fingerprint(
836            "GHIJ1234GHIJ1234GHIJ1234GHIJ1234GHIJ1234"
837        ));
838        assert!(!is_valid_fingerprint(
839            "ABCD1234ABCD1234ABCD1234ABCD1234ABCD12345"
840        ));
841    }
842
843    #[test]
844    fn test_fingerprint_with_prefix() {
845        assert!(is_valid_fingerprint_with_prefix(
846            "$ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"
847        ));
848        assert!(is_valid_fingerprint_with_prefix(
849            "ABCD1234ABCD1234ABCD1234ABCD1234ABCD1234"
850        ));
851        assert!(!is_valid_fingerprint_with_prefix("$ABCD"));
852    }
853
854    #[test]
855    fn test_valid_nickname() {
856        assert!(is_valid_nickname("MyRelay"));
857        assert!(is_valid_nickname("relay123"));
858        assert!(is_valid_nickname("A"));
859        assert!(is_valid_nickname("1234567890123456789"));
860    }
861
862    #[test]
863    fn test_invalid_nickname() {
864        assert!(!is_valid_nickname(""));
865        assert!(!is_valid_nickname("12345678901234567890"));
866        assert!(!is_valid_nickname("my-relay"));
867        assert!(!is_valid_nickname("my_relay"));
868        assert!(!is_valid_nickname("my relay"));
869    }
870
871    #[test]
872    fn test_valid_circuit_id() {
873        assert!(is_valid_circuit_id("1"));
874        assert!(is_valid_circuit_id("123"));
875        assert!(is_valid_circuit_id("999999"));
876    }
877
878    #[test]
879    fn test_invalid_circuit_id() {
880        assert!(!is_valid_circuit_id(""));
881        assert!(!is_valid_circuit_id("abc"));
882        assert!(!is_valid_circuit_id("12a"));
883    }
884
885    #[test]
886    fn test_valid_stream_id() {
887        assert!(is_valid_stream_id("1"));
888        assert!(is_valid_stream_id("456"));
889    }
890
891    #[test]
892    fn test_invalid_stream_id() {
893        assert!(!is_valid_stream_id(""));
894        assert!(!is_valid_stream_id("xyz"));
895    }
896
897    #[test]
898    fn test_valid_ipv4_address() {
899        assert!(is_valid_ipv4_address("127.0.0.1"));
900        assert!(is_valid_ipv4_address("192.168.1.1"));
901        assert!(is_valid_ipv4_address("0.0.0.0"));
902        assert!(is_valid_ipv4_address("255.255.255.255"));
903    }
904
905    #[test]
906    fn test_invalid_ipv4_address() {
907        assert!(!is_valid_ipv4_address(""));
908        assert!(!is_valid_ipv4_address("127.0.0"));
909        assert!(!is_valid_ipv4_address("127.0.0.1.1"));
910        assert!(!is_valid_ipv4_address("256.0.0.1"));
911        assert!(!is_valid_ipv4_address("abc.def.ghi.jkl"));
912        assert!(!is_valid_ipv4_address("localhost"));
913    }
914
915    #[test]
916    fn test_valid_port() {
917        assert!(is_valid_port("1"));
918        assert!(is_valid_port("80"));
919        assert!(is_valid_port("443"));
920        assert!(is_valid_port("9051"));
921        assert!(is_valid_port("65535"));
922    }
923
924    #[test]
925    fn test_invalid_port() {
926        assert!(!is_valid_port(""));
927        assert!(!is_valid_port("0"));
928        assert!(!is_valid_port("65536"));
929        assert!(!is_valid_port("-1"));
930        assert!(!is_valid_port("abc"));
931    }
932
933    #[test]
934    fn test_valid_hidden_service_v2() {
935        assert!(is_valid_hidden_service_address_v2("abcdefghijklmnop"));
936        assert!(is_valid_hidden_service_address_v2("abcdefghijklmnop.onion"));
937        assert!(is_valid_hidden_service_address_v2("2222222222222222"));
938    }
939
940    #[test]
941    fn test_invalid_hidden_service_v2() {
942        assert!(!is_valid_hidden_service_address_v2(""));
943        assert!(!is_valid_hidden_service_address_v2("abc"));
944        assert!(!is_valid_hidden_service_address_v2("ABCDEFGHIJKLMNOP"));
945        assert!(!is_valid_hidden_service_address_v2("abcdefghijklmno1"));
946    }
947
948    #[test]
949    fn test_valid_hidden_service_v3() {
950        let v3_addr = "a".repeat(56);
951        assert!(is_valid_hidden_service_address_v3(&v3_addr));
952        assert!(is_valid_hidden_service_address_v3(&format!(
953            "{}.onion",
954            v3_addr
955        )));
956    }
957
958    #[test]
959    fn test_invalid_hidden_service_v3() {
960        assert!(!is_valid_hidden_service_address_v3(""));
961        assert!(!is_valid_hidden_service_address_v3(&"a".repeat(55)));
962        assert!(!is_valid_hidden_service_address_v3(&"A".repeat(56)));
963    }
964
965    #[test]
966    fn test_hidden_service_address_combined() {
967        assert!(is_valid_hidden_service_address("abcdefghijklmnop"));
968        assert!(is_valid_hidden_service_address(&"a".repeat(56)));
969        assert!(!is_valid_hidden_service_address("invalid"));
970    }
971
972    #[test]
973    fn test_is_hex_digits() {
974        assert!(is_hex_digits("abcd", 4));
975        assert!(is_hex_digits("ABCD1234", 8));
976        assert!(!is_hex_digits("abcd", 5));
977        assert!(!is_hex_digits("ghij", 4));
978    }
979
980    #[test]
981    fn test_secure_compare_equal() {
982        assert!(secure_compare(b"hello", b"hello"));
983        assert!(secure_compare(b"", b""));
984        assert!(secure_compare(&[0, 1, 2, 3], &[0, 1, 2, 3]));
985    }
986
987    #[test]
988    fn test_secure_compare_not_equal() {
989        assert!(!secure_compare(b"hello", b"world"));
990        assert!(!secure_compare(b"hello", b"hell"));
991        assert!(!secure_compare(&[0, 1, 2, 3], &[0, 1, 2, 4]));
992    }
993
994    #[test]
995    fn test_secure_compare_different_lengths() {
996        assert!(!secure_compare(b"short", b"longer"));
997        assert!(!secure_compare(b"", b"x"));
998    }
999
1000    #[test]
1001    fn test_valid_hidden_service_real_addresses() {
1002        assert!(is_valid_hidden_service_address_v2("facebookcorewwwi"));
1003        assert!(is_valid_hidden_service_address_v2("aaaaaaaaaaaaaaaa"));
1004    }
1005
1006    #[test]
1007    fn test_invalid_hidden_service_v2_with_invalid_chars() {
1008        assert!(!is_valid_hidden_service_address_v2("facebookc0rewwwi"));
1009        assert!(!is_valid_hidden_service_address_v2("facebookcorew wi"));
1010    }
1011
1012    #[test]
1013    fn test_valid_fingerprint_with_dollar_prefix() {
1014        assert!(is_valid_fingerprint_with_prefix(
1015            "$A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EB"
1016        ));
1017        assert!(is_valid_fingerprint_with_prefix(
1018            "$a7569a83b5706ab1b1a9cb52eff7d2d32e4553eb"
1019        ));
1020    }
1021
1022    #[test]
1023    fn test_invalid_fingerprint_various() {
1024        assert!(!is_valid_fingerprint(
1025            "A7569A83B5706AB1B1A9CB52EFF7D2D32E4553E"
1026        ));
1027        assert!(!is_valid_fingerprint(
1028            "A7569A83B5706AB1B1A9CB52EFF7D2D32E4553E33"
1029        ));
1030        assert!(!is_valid_fingerprint(
1031            "A7569A83B5706AB1B1A9CB52EFF7D2D32E4553EG"
1032        ));
1033    }
1034
1035    #[test]
1036    fn test_valid_nickname_various() {
1037        assert!(is_valid_nickname("caerSidi"));
1038        assert!(is_valid_nickname("a"));
1039        assert!(is_valid_nickname("abcABC123"));
1040    }
1041
1042    #[test]
1043    fn test_invalid_nickname_various() {
1044        assert!(!is_valid_nickname("toolongggggggggggggg"));
1045        assert!(!is_valid_nickname("bad_character"));
1046        assert!(!is_valid_nickname("bad-character"));
1047        assert!(!is_valid_nickname("bad character"));
1048    }
1049
1050    #[test]
1051    fn test_valid_circuit_id_various() {
1052        assert!(is_valid_circuit_id("0"));
1053        assert!(is_valid_circuit_id("2"));
1054        assert!(is_valid_circuit_id("123456789"));
1055    }
1056
1057    #[test]
1058    fn test_is_hex_digits_various() {
1059        assert!(is_hex_digits("12345", 5));
1060        assert!(is_hex_digits("AbCdE", 5));
1061        assert!(is_hex_digits("abcdef", 6));
1062        assert!(is_hex_digits("ABCDEF", 6));
1063        assert!(!is_hex_digits("X", 1));
1064        assert!(!is_hex_digits("1234", 5));
1065        assert!(!is_hex_digits("ABCDEF", 5));
1066    }
1067
1068    #[test]
1069    fn test_valid_hidden_service_v3_real_addresses() {
1070        let valid_base32_56 = "a".repeat(56);
1071        assert!(is_valid_hidden_service_address_v3(&valid_base32_56));
1072
1073        let too_short = "a".repeat(55);
1074        assert!(!is_valid_hidden_service_address_v3(&too_short));
1075
1076        let too_long = "a".repeat(57);
1077        assert!(!is_valid_hidden_service_address_v3(&too_long));
1078    }
1079
1080    #[test]
1081    fn test_hidden_service_with_onion_suffix() {
1082        assert!(is_valid_hidden_service_address_v2("facebookcorewwwi.onion"));
1083        let v3_addr = format!("{}.onion", "a".repeat(56));
1084        assert!(is_valid_hidden_service_address_v3(&v3_addr));
1085    }
1086
1087    #[test]
1088    fn test_secure_compare_timing_safety() {
1089        let a = b"secret_cookie_value_1234567890ab";
1090        let b = b"secret_cookie_value_1234567890ab";
1091        let c = b"secret_cookie_value_1234567890ac";
1092        let d = b"different_length";
1093
1094        assert!(secure_compare(a, b));
1095        assert!(!secure_compare(a, c));
1096        assert!(!secure_compare(a, d));
1097
1098        assert!(secure_compare(&[], &[]));
1099
1100        let base = b"0123456789abcdef";
1101        let diff_start = b"X123456789abcdef";
1102        let diff_middle = b"01234567X9abcdef";
1103        let diff_end = b"0123456789abcdeX";
1104
1105        assert!(!secure_compare(base, diff_start));
1106        assert!(!secure_compare(base, diff_middle));
1107        assert!(!secure_compare(base, diff_end));
1108    }
1109
1110    #[test]
1111    fn test_valid_ipv6_address() {
1112        assert!(is_valid_ipv6_address(
1113            "2001:0db8:0000:0000:0000:ff00:0042:8329"
1114        ));
1115        assert!(is_valid_ipv6_address("2001:db8::ff00:42:8329"));
1116        assert!(is_valid_ipv6_address("::1"));
1117        assert!(is_valid_ipv6_address("::"));
1118        assert!(is_valid_ipv6_address("fe80::1"));
1119        assert!(!is_valid_ipv6_address("::ffff:192.0.2.1")); // ipv4-mapped not supported
1120    }
1121
1122    #[test]
1123    fn test_invalid_ipv6_address() {
1124        assert!(!is_valid_ipv6_address(""));
1125        assert!(!is_valid_ipv6_address("2001:db8::ff00::8329")); // multiple ::
1126        assert!(!is_valid_ipv6_address("2001:db8:ff00:42:8329")); // too few groups
1127        assert!(!is_valid_ipv6_address("2001:db8:::ff00:42:8329")); // :::
1128        assert!(!is_valid_ipv6_address(
1129            "2001:db8:0000:0000:0000:0000:0000:0000:0000"
1130        )); // too many
1131        assert!(!is_valid_ipv6_address("gggg:db8::1")); // invalid hex
1132    }
1133
1134    #[test]
1135    fn test_ipv6_address_bracketed() {
1136        assert!(is_valid_ipv6_address_bracketed("[::1]"));
1137        assert!(is_valid_ipv6_address_bracketed("[2001:db8::1]"));
1138        assert!(is_valid_ipv6_address_bracketed("::1"));
1139        assert!(!is_valid_ipv6_address_bracketed("[invalid]"));
1140    }
1141
1142    #[test]
1143    fn test_is_private_address() {
1144        assert_eq!(is_private_address("10.0.0.1"), Some(true));
1145        assert_eq!(is_private_address("10.255.255.255"), Some(true));
1146        assert_eq!(is_private_address("192.168.1.1"), Some(true));
1147        assert_eq!(is_private_address("172.16.0.1"), Some(true));
1148        assert_eq!(is_private_address("172.31.255.255"), Some(true));
1149        assert_eq!(is_private_address("127.0.0.1"), Some(true));
1150        assert_eq!(is_private_address("8.8.8.8"), Some(false));
1151        assert_eq!(is_private_address("172.15.0.1"), Some(false));
1152        assert_eq!(is_private_address("172.32.0.1"), Some(false));
1153        assert_eq!(is_private_address("invalid"), None);
1154    }
1155
1156    #[test]
1157    fn test_expand_ipv6_address() {
1158        assert_eq!(
1159            expand_ipv6_address("2001:db8::ff00:42:8329"),
1160            Some("2001:0db8:0000:0000:0000:ff00:0042:8329".to_string())
1161        );
1162        assert_eq!(
1163            expand_ipv6_address("::"),
1164            Some("0000:0000:0000:0000:0000:0000:0000:0000".to_string())
1165        );
1166        assert_eq!(
1167            expand_ipv6_address("::1"),
1168            Some("0000:0000:0000:0000:0000:0000:0000:0001".to_string())
1169        );
1170        assert_eq!(
1171            expand_ipv6_address("fe80::"),
1172            Some("fe80:0000:0000:0000:0000:0000:0000:0000".to_string())
1173        );
1174        assert_eq!(expand_ipv6_address("invalid"), None);
1175    }
1176
1177    #[test]
1178    fn test_is_valid_connection_id() {
1179        assert!(is_valid_connection_id("1"));
1180        assert!(is_valid_connection_id("123"));
1181        assert!(!is_valid_connection_id(""));
1182        assert!(!is_valid_connection_id("abc"));
1183    }
1184
1185    #[test]
1186    fn test_is_valid_port_number() {
1187        assert!(is_valid_port_number(1));
1188        assert!(is_valid_port_number(80));
1189        assert!(is_valid_port_number(65535));
1190        assert!(!is_valid_port_number(0));
1191    }
1192
1193    #[test]
1194    fn test_ipv4_leading_zeros() {
1195        assert!(!is_valid_ipv4_address("01.02.03.04"));
1196        assert!(!is_valid_ipv4_address("1.2.3.001"));
1197        assert!(is_valid_ipv4_address("1.2.3.0"));
1198    }
1199}
1200
1201#[cfg(test)]
1202mod proptests {
1203    use super::*;
1204    use proptest::char::range as char_range;
1205    use proptest::prelude::*;
1206
1207    fn hex_char() -> impl Strategy<Value = char> {
1208        prop_oneof![
1209            char_range('a', 'f'),
1210            char_range('A', 'F'),
1211            char_range('0', '9'),
1212        ]
1213    }
1214
1215    fn valid_fingerprint_strategy() -> impl Strategy<Value = String> {
1216        proptest::collection::vec(hex_char(), 40).prop_map(|chars| chars.into_iter().collect())
1217    }
1218
1219    fn alphanumeric_char() -> impl Strategy<Value = char> {
1220        prop_oneof![
1221            char_range('a', 'z'),
1222            char_range('A', 'Z'),
1223            char_range('0', '9'),
1224        ]
1225    }
1226
1227    fn valid_nickname_strategy() -> impl Strategy<Value = String> {
1228        proptest::collection::vec(alphanumeric_char(), 1..=19)
1229            .prop_map(|chars| chars.into_iter().collect())
1230    }
1231
1232    fn base32_char() -> impl Strategy<Value = char> {
1233        prop_oneof![char_range('a', 'z'), char_range('2', '7'),]
1234    }
1235
1236    fn valid_v2_address_strategy() -> impl Strategy<Value = String> {
1237        proptest::collection::vec(base32_char(), 16).prop_map(|chars| chars.into_iter().collect())
1238    }
1239
1240    fn valid_v3_address_strategy() -> impl Strategy<Value = String> {
1241        proptest::collection::vec(base32_char(), 56).prop_map(|chars| chars.into_iter().collect())
1242    }
1243
1244    proptest! {
1245        #![proptest_config(ProptestConfig::with_cases(100))]
1246
1247        #[test]
1248        fn prop_valid_fingerprint_accepted(fp in valid_fingerprint_strategy()) {
1249            prop_assert!(is_valid_fingerprint(&fp), "valid fingerprint rejected: {}", fp);
1250        }
1251
1252        #[test]
1253        fn prop_invalid_fingerprint_wrong_length(
1254            chars in proptest::collection::vec(hex_char(), 0..40usize)
1255        ) {
1256            let s: String = chars.into_iter().collect();
1257            prop_assert!(!is_valid_fingerprint(&s), "short fingerprint accepted: {}", s);
1258        }
1259
1260        #[test]
1261        fn prop_invalid_fingerprint_too_long(
1262            chars in proptest::collection::vec(hex_char(), 41..60usize)
1263        ) {
1264            let s: String = chars.into_iter().collect();
1265            prop_assert!(!is_valid_fingerprint(&s), "long fingerprint accepted: {}", s);
1266        }
1267
1268        #[test]
1269        fn prop_valid_nickname_accepted(nick in valid_nickname_strategy()) {
1270            prop_assert!(is_valid_nickname(&nick), "valid nickname rejected: {}", nick);
1271        }
1272
1273        #[test]
1274        fn prop_invalid_nickname_too_long(
1275            chars in proptest::collection::vec(alphanumeric_char(), 20..30usize)
1276        ) {
1277            let s: String = chars.into_iter().collect();
1278            prop_assert!(!is_valid_nickname(&s), "long nickname accepted: {}", s);
1279        }
1280
1281        #[test]
1282        fn prop_valid_v2_address_accepted(addr in valid_v2_address_strategy()) {
1283            prop_assert!(
1284                is_valid_hidden_service_address_v2(&addr),
1285                "valid v2 address rejected: {}", addr
1286            );
1287            prop_assert!(
1288                is_valid_hidden_service_address(&addr),
1289                "valid v2 address rejected by combined check: {}", addr
1290            );
1291        }
1292
1293        #[test]
1294        fn prop_valid_v3_address_accepted(addr in valid_v3_address_strategy()) {
1295            prop_assert!(
1296                is_valid_hidden_service_address_v3(&addr),
1297                "valid v3 address rejected: {}", addr
1298            );
1299            prop_assert!(
1300                is_valid_hidden_service_address(&addr),
1301                "valid v3 address rejected by combined check: {}", addr
1302            );
1303        }
1304
1305        #[test]
1306        fn prop_invalid_v2_address_wrong_length(
1307            chars in proptest::collection::vec(base32_char(), 0..16usize)
1308        ) {
1309            let s: String = chars.into_iter().collect();
1310            prop_assert!(
1311                !is_valid_hidden_service_address_v2(&s),
1312                "short v2 address accepted: {}", s
1313            );
1314        }
1315
1316        #[test]
1317        fn prop_invalid_v3_address_wrong_length(
1318            chars in proptest::collection::vec(base32_char(), 0..56usize)
1319        ) {
1320            let s: String = chars.into_iter().collect();
1321            if s.len() != 16 {
1322                prop_assert!(
1323                    !is_valid_hidden_service_address(&s),
1324                    "wrong length address accepted: {}", s
1325                );
1326            }
1327        }
1328    }
1329
1330    #[test]
1331    fn prop_invalid_nickname_empty() {
1332        assert!(!is_valid_nickname(""));
1333    }
1334}