Skip to main content

update_kit/utils/
security.rs

1use crate::errors::UpdateKitError;
2
3/// Validates that the given URL uses HTTPS.
4///
5/// Returns `InsecureUrl` error if the URL does not start with `https://`.
6pub fn require_https(url: &str) -> Result<(), UpdateKitError> {
7    if !url.starts_with("https://") {
8        return Err(UpdateKitError::InsecureUrl(format!(
9            "Only HTTPS URLs are allowed. Got: {url}"
10        )));
11    }
12    Ok(())
13}
14
15/// Constant-time comparison of two hex strings (case-insensitive).
16///
17/// Returns `true` if both strings are equal (ignoring case), using XOR
18/// accumulation to avoid timing side-channels.
19pub fn timing_safe_equal(a: &str, b: &str) -> bool {
20    let a_lower = a.to_ascii_lowercase();
21    let b_lower = b.to_ascii_lowercase();
22
23    if a_lower.len() != b_lower.len() {
24        return false;
25    }
26
27    let result = a_lower
28        .as_bytes()
29        .iter()
30        .zip(b_lower.as_bytes().iter())
31        .fold(0u8, |acc, (x, y)| acc | (x ^ y));
32
33    result == 0
34}
35
36#[cfg(test)]
37mod tests {
38    use super::*;
39
40    #[test]
41    fn require_https_accepts_https() {
42        assert!(require_https("https://example.com").is_ok());
43        assert!(require_https("https://example.com/path?q=1").is_ok());
44    }
45
46    #[test]
47    fn require_https_rejects_http() {
48        let result = require_https("http://example.com");
49        assert!(result.is_err());
50        let err = result.unwrap_err();
51        assert!(
52            matches!(err, UpdateKitError::InsecureUrl(_)),
53            "Expected InsecureUrl, got: {err:?}"
54        );
55    }
56
57    #[test]
58    fn require_https_rejects_other_schemes() {
59        assert!(require_https("ftp://example.com").is_err());
60        assert!(require_https("file:///etc/passwd").is_err());
61        assert!(require_https("").is_err());
62    }
63
64    #[test]
65    fn timing_safe_equal_matches_same() {
66        assert!(timing_safe_equal("abc123", "abc123"));
67    }
68
69    #[test]
70    fn timing_safe_equal_matches_case_insensitively() {
71        assert!(timing_safe_equal("ABC123", "abc123"));
72        assert!(timing_safe_equal("AbCdEf", "abcdef"));
73    }
74
75    #[test]
76    fn timing_safe_equal_rejects_different_strings() {
77        assert!(!timing_safe_equal("abc123", "abc124"));
78        assert!(!timing_safe_equal("abc123", "xyz789"));
79    }
80
81    #[test]
82    fn timing_safe_equal_rejects_different_lengths() {
83        assert!(!timing_safe_equal("abc", "abcd"));
84        assert!(!timing_safe_equal("", "a"));
85    }
86
87    #[test]
88    fn timing_safe_equal_empty_strings() {
89        assert!(timing_safe_equal("", ""));
90    }
91}