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}