encrust_core/
hashstrings.rs

1//! Functions to search for strings or bytes at run-time without having to include the strings
2//! or byte patterns themselves in the binary.
3//! Macros are used to make it possible to ensure that the plain text is not present in the
4//! executable, see the documentation for [`encrust`] for examples of macro usage.
5
6use rapidhash::rapidhash_seeded;
7use zeroize::Zeroize;
8
9/// Used to specify whether a [`Hashstring`] should ignore case when comparing strings.
10#[cfg_attr(docsrs, doc(cfg(feature = "hashstrings")))]
11pub enum Sensitivity {
12    /// Ignore case when comparing strings.
13    CaseInsensitive,
14    /// Do *NOT* ignore case when comparing strings.
15    CaseSensitive,
16}
17
18/// The hash of a string.
19/// Can be used to search for strings without storing the string itself in memory.
20///
21/// # Example
22/// ```
23/// use encrust_core::{Hashstring, Sensitivity};
24///
25/// let hashstring = Hashstring::new("A string", 0xabcdef, Sensitivity::CaseSensitive);
26/// assert!(hashstring == "A string");
27/// assert!(hashstring != "a string");
28///
29/// let case_insensitive_hashstring =
30///     Hashstring::new("A string", 0xfedcba, Sensitivity::CaseInsensitive);
31/// assert!(case_insensitive_hashstring == "A string");
32/// assert!(case_insensitive_hashstring == "a string");
33/// ```
34#[cfg_attr(docsrs, doc(cfg(feature = "hashstrings")))]
35pub struct Hashstring {
36    value: u64,
37    seed: u64,
38    sensitivity: Sensitivity,
39}
40
41impl Hashstring {
42    /// Create a new [`Hashstring`] using the provided string and random seed.
43    ///
44    /// Note that if `Sensitivity::CaseInsensitive` is used, a new `String` is allocated with the
45    /// provided `s` converted to lowercase. The newly allocated string is overwritten using
46    /// `Zeroize` after calculating the hash.
47    ///
48    /// This function does not zeroize the original string. To avoid ever having the string in
49    /// memory, it is recommended to use the `hashstring!` macro.
50    pub fn new(s: &str, seed: u64, sensitivity: Sensitivity) -> Self {
51        let value = match sensitivity {
52            Sensitivity::CaseInsensitive => {
53                let mut lowercase_string = s.to_lowercase();
54                let hash = rapidhash_seeded(lowercase_string.as_bytes(), seed);
55                Zeroize::zeroize(&mut lowercase_string);
56
57                hash
58            }
59            Sensitivity::CaseSensitive => rapidhash_seeded(s.as_bytes(), seed),
60        };
61
62        Self {
63            value,
64            seed,
65            sensitivity,
66        }
67    }
68
69    /// Used by the macros to get the hash value to create `Hashstring` from raw data.
70    /// Should not be used outside of the provided macros.
71    #[doc(hidden)]
72    #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
73    #[cfg(feature = "macros")]
74    pub fn get_raw_value(&self) -> u64 {
75        self.value
76    }
77
78    /// Used by the macros to create `Hashstring` from raw data.
79    /// Should not be used outside of the provided macros.
80    #[doc(hidden)]
81    #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
82    #[cfg(feature = "macros")]
83    pub fn new_from_raw_value(value: u64, seed: u64, sensitivity: Sensitivity) -> Self {
84        Self {
85            value,
86            seed,
87            sensitivity,
88        }
89    }
90}
91
92impl PartialEq<&str> for Hashstring {
93    fn eq(&self, other: &&str) -> bool {
94        let other_value = match self.sensitivity {
95            Sensitivity::CaseInsensitive => {
96                rapidhash_seeded(other.to_lowercase().as_bytes(), self.seed)
97            }
98            Sensitivity::CaseSensitive => rapidhash_seeded(other.as_bytes(), self.seed),
99        };
100
101        self.value == other_value
102    }
103}
104
105/// The hash of a slice of u8's.
106/// Can be used to search for data without storing the data itself in memory.
107///
108/// # Example
109/// ```
110/// use encrust_core::Hashbytes;
111///
112/// let hashbytes = Hashbytes::new(&[1, 2, 3], 0xc0ffee);
113/// assert!(hashbytes == &[1, 2, 3]);
114/// assert!(hashbytes != &[4, 5, 6]);
115/// ```
116#[cfg_attr(docsrs, doc(cfg(feature = "hashstrings")))]
117pub struct Hashbytes {
118    value: u64,
119    seed: u64,
120}
121
122impl Hashbytes {
123    /// Create a new [`Hashbytes`] using the provided `u8` slice and random seed.
124    ///
125    /// This function does not zeroize the original data. To avoid ever having the data in memory,
126    /// it is recommended to use the `hashbytes` macro.
127    pub fn new(bytes: &[u8], seed: u64) -> Self {
128        let value = rapidhash_seeded(bytes, seed);
129
130        Self { value, seed }
131    }
132
133    /// Used by the macros to get the hash value to create `Hashbytes` from raw data.
134    /// Should not be used outside of the provided macros.
135    #[doc(hidden)]
136    #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
137    #[cfg(feature = "macros")]
138    pub fn get_raw_value(&self) -> u64 {
139        self.value
140    }
141
142    /// Used by the macros to create `Hashbytes` from raw data.
143    /// Should not be used outside of the provided macros.
144    #[doc(hidden)]
145    #[cfg_attr(docsrs, doc(cfg(feature = "macros")))]
146    #[cfg(feature = "macros")]
147    pub fn new_from_raw_value(value: u64, seed: u64) -> Self {
148        Self { value, seed }
149    }
150}
151
152impl PartialEq<&[u8]> for Hashbytes {
153    fn eq(&self, other: &&[u8]) -> bool {
154        let other_value = rapidhash_seeded(other, self.seed);
155
156        self.value == other_value
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use rand::RngCore;
163
164    use super::*;
165
166    const A_STRING: &str = "A string😶";
167    const A_LOWERCASE_STRING: &str = "a string😶";
168    const A_STRING_BYTES: &[u8] = A_STRING.as_bytes();
169    const A_LOWERCASE_STRING_BYTES: &[u8] = A_LOWERCASE_STRING.as_bytes();
170
171    #[test]
172    fn test_hashstrings() {
173        let case_sensitive_hashstring =
174            Hashstring::new(A_STRING, rand::rng().next_u64(), Sensitivity::CaseSensitive);
175        let case_insensitive_hashstring = Hashstring::new(
176            A_STRING,
177            rand::rng().next_u64(),
178            Sensitivity::CaseInsensitive,
179        );
180
181        assert!(case_sensitive_hashstring.eq(&A_STRING));
182        assert!(case_sensitive_hashstring.ne(&A_LOWERCASE_STRING));
183        assert!(case_insensitive_hashstring.eq(&A_STRING));
184        assert!(case_insensitive_hashstring.eq(&A_LOWERCASE_STRING));
185    }
186
187    #[test]
188    fn test_hashbytes() {
189        let hashbytes = Hashbytes::new(A_STRING_BYTES, rand::rng().next_u64());
190
191        assert!(hashbytes.eq(&A_STRING_BYTES));
192        assert!(hashbytes.ne(&A_LOWERCASE_STRING_BYTES));
193    }
194}