Skip to main content

native_ossl/
util.rs

1//! General-purpose utilities.
2
3use native_ossl_sys as sys;
4
5// ── Hex encoding ─────────────────────────────────────────────────────────────
6
7/// Encode bytes as a lowercase hexadecimal string.
8#[must_use]
9pub fn hex_encode<T: AsRef<[u8]>>(b: T) -> String {
10    b.as_ref().iter().fold(String::new(), |mut s, x| {
11        use std::fmt::Write;
12        write!(s, "{x:02x}").unwrap();
13        s
14    })
15}
16
17/// Decode a lowercase hexadecimal string into bytes.
18///
19/// # Panics
20///
21/// Panics if `s` has odd length or contains non-hex characters.
22#[must_use]
23pub fn hex_decode(s: &str) -> Vec<u8> {
24    (0..s.len())
25        .step_by(2)
26        .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
27        .collect()
28}
29
30// ── Constant-time comparison ──────────────────────────────────────────────────
31
32/// Compare two byte slices in constant time, returning `true` iff they are equal.
33///
34/// The comparison time is proportional to the slice length and does not depend
35/// on the data values, preventing timing side-channel attacks.
36///
37/// Slices of different lengths return `false` immediately (without constant-time
38/// behaviour), since lengths are generally not considered secret.  If you need
39/// to conceal the length, pad both inputs to the same size before calling.
40///
41/// Backed by `CRYPTO_memcmp` from OpenSSL.
42#[must_use]
43pub fn ct_eq(a: &[u8], b: &[u8]) -> bool {
44    if a.len() != b.len() {
45        return false;
46    }
47    if a.is_empty() {
48        return true;
49    }
50    // SAFETY: both slices are valid for `a.len()` bytes (guaranteed by Rust)
51    // and are not mutated; CRYPTO_memcmp reads them in constant time.
52    let rc = unsafe {
53        sys::CRYPTO_memcmp(
54            a.as_ptr().cast::<std::ffi::c_void>(),
55            b.as_ptr().cast::<std::ffi::c_void>(),
56            a.len(),
57        )
58    };
59    rc == 0
60}
61
62// ── SecretBuf ─────────────────────────────────────────────────────────────────
63
64/// A heap buffer that is securely zeroed via `OPENSSL_cleanse` on drop.
65///
66/// Use to hold key material, passwords, and other sensitive byte sequences.
67/// The zeroing is performed by OpenSSL's `OPENSSL_cleanse`, which is the
68/// FIPS-approved memory-clearing function and is not eliminated by the
69/// compiler's dead-store optimiser.
70///
71/// # Example
72///
73/// ```ignore
74/// use native_ossl::util::SecretBuf;
75///
76/// let mut key = SecretBuf::with_len(32);
77/// native_ossl::rand::Rand::fill(key.as_mut_slice()).unwrap();
78/// // key bytes are securely erased when `key` is dropped.
79/// ```
80pub struct SecretBuf {
81    data: Vec<u8>,
82}
83
84impl SecretBuf {
85    /// Wrap an existing allocation. Takes ownership; the buffer will be
86    /// securely zeroed when the `SecretBuf` is dropped.
87    #[must_use]
88    pub fn new(data: Vec<u8>) -> Self {
89        SecretBuf { data }
90    }
91
92    /// Allocate a zero-initialised buffer of `len` bytes.
93    #[must_use]
94    pub fn with_len(len: usize) -> Self {
95        SecretBuf {
96            data: vec![0u8; len],
97        }
98    }
99
100    /// Copy `data` into a new secure buffer.
101    #[must_use]
102    pub fn from_slice(data: &[u8]) -> Self {
103        SecretBuf {
104            data: data.to_vec(),
105        }
106    }
107
108    /// Number of bytes in the buffer.
109    #[must_use]
110    pub fn len(&self) -> usize {
111        self.data.len()
112    }
113
114    /// `true` if the buffer holds no bytes.
115    #[must_use]
116    pub fn is_empty(&self) -> bool {
117        self.data.is_empty()
118    }
119
120    /// Expose the buffer as a mutable byte slice.
121    ///
122    /// Useful for writing derived key material directly into the buffer.
123    pub fn as_mut_slice(&mut self) -> &mut [u8] {
124        &mut self.data
125    }
126}
127
128impl AsRef<[u8]> for SecretBuf {
129    fn as_ref(&self) -> &[u8] {
130        &self.data
131    }
132}
133
134impl Drop for SecretBuf {
135    fn drop(&mut self) {
136        if !self.data.is_empty() {
137            unsafe {
138                sys::OPENSSL_cleanse(
139                    self.data.as_mut_ptr().cast::<std::ffi::c_void>(),
140                    self.data.len(),
141                );
142            }
143        }
144    }
145}
146
147// SAFETY: the buffer is owned; no aliasing across threads.
148unsafe impl Send for SecretBuf {}
149unsafe impl Sync for SecretBuf {}
150
151#[cfg(test)]
152mod tests {
153    use super::{ct_eq, SecretBuf};
154
155    #[test]
156    fn ct_eq_equal_slices() {
157        assert!(ct_eq(b"hello", b"hello"));
158    }
159
160    #[test]
161    fn ct_eq_different_values() {
162        assert!(!ct_eq(b"hello", b"world"));
163    }
164
165    #[test]
166    fn ct_eq_different_lengths() {
167        assert!(!ct_eq(b"hi", b"hii"));
168    }
169
170    #[test]
171    fn ct_eq_empty_slices() {
172        assert!(ct_eq(b"", b""));
173    }
174
175    #[test]
176    fn ct_eq_one_byte_differ() {
177        assert!(!ct_eq(&[0u8; 32], &{
178            let mut v = [0u8; 32];
179            v[31] = 1;
180            v
181        }));
182    }
183
184    #[test]
185    fn with_len_creates_correct_size() {
186        let buf = SecretBuf::with_len(32);
187        assert_eq!(buf.len(), 32);
188        assert!(!buf.is_empty());
189    }
190
191    #[test]
192    fn from_slice_copies_data() {
193        let src = b"secret key material";
194        let buf = SecretBuf::from_slice(src);
195        assert_eq!(buf.as_ref(), src);
196    }
197
198    #[test]
199    fn new_wraps_ownership() {
200        let v = vec![1u8, 2, 3];
201        let buf = SecretBuf::new(v);
202        assert_eq!(buf.as_ref(), &[1, 2, 3]);
203    }
204
205    #[test]
206    fn empty_buf_is_empty() {
207        let buf = SecretBuf::new(vec![]);
208        assert!(buf.is_empty());
209        // Drop must not call cleanse on a zero-length buffer (no panic).
210    }
211
212    #[test]
213    fn as_mut_slice_writes_through() {
214        let mut buf = SecretBuf::with_len(4);
215        buf.as_mut_slice().copy_from_slice(&[10, 20, 30, 40]);
216        assert_eq!(buf.as_ref(), &[10, 20, 30, 40]);
217    }
218}