encrust_core/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![cfg_attr(all(not(test), not(feature = "std")), no_std)]
3
4//! Crate implementing core functionality for `encrust`. See the main crate for documentation.
5
6#[cfg(feature = "hashstrings")]
7mod hashstrings;
8#[cfg(feature = "hashstrings")]
9pub use hashstrings::*;
10
11#[cfg(not(feature = "std"))]
12extern crate core;
13
14#[cfg(not(feature = "std"))]
15extern crate alloc;
16
17#[cfg(not(feature = "std"))]
18use alloc::{string::String, vec::Vec};
19#[cfg(not(feature = "std"))]
20use core::ops::{Deref, DerefMut};
21#[cfg(feature = "std")]
22use std::ops::{Deref, DerefMut};
23
24use rand::{RngCore, SeedableRng, rngs::SmallRng};
25use zeroize::Zeroize;
26
27/// Container struct for encrust, accepting [`Encrustable`] + `Zeroize` types for obfuscation and
28/// deobfuscation when needed.
29///
30/// Care should be taken if `T` has a non-trivial `Drop` implementation, as `T` is not dropped until
31/// `zeroize` has been called on it.
32pub struct Encrusted<T>
33where
34    T: Encrustable + Zeroize,
35{
36    data: T,
37    seed: u64,
38}
39
40impl<T> Encrusted<T>
41where
42    T: Encrustable + Zeroize,
43{
44    /// Accepts [`Encrustable`] + `Zeroize` data and obfuscates it using the provided seed.
45    pub fn new(mut data: T, seed: u64) -> Self {
46        let mut encrust_rng = SmallRng::seed_from_u64(seed);
47
48        // SAFETY:
49        // `Encrusted` takes ownership of the data and only exposes it after calling toggle_encrust
50        // again, ensuring that the underlying data is not accessed in a potential invalid state.
51        unsafe {
52            data.toggle_encrust(&mut encrust_rng);
53        }
54
55        Self { data, seed }
56    }
57
58    /// Creates an `Encrusted` object from pre-scrambeled data. This is used by macros to include
59    /// pre-scrambled objects in the source and should not be called manually.
60    ///
61    /// # Safety
62    /// Using this may cause data to be scrambled in unpredictable ways that could lead to safety
63    /// issues. This should not be used manually, but only through the provided macros.
64    #[doc(hidden)]
65    #[cfg(feature = "macros")]
66    pub const unsafe fn from_encrusted_data(data: T, seed: u64) -> Self {
67        Self { data, seed }
68    }
69
70    /// Changes the seed used to obfuscate the underlying data.
71    pub fn reseed(&mut self, new_seed: u64) {
72        {
73            let mut decruster = SmallRng::seed_from_u64(self.seed);
74
75            // SAFETY:
76            // In order to obfuscate with a new seed, the data needs to be deobfuscated first.
77            unsafe {
78                self.data.toggle_encrust(&mut decruster);
79            }
80        }
81
82        self.seed = new_seed;
83
84        let mut encrust_rng = SmallRng::seed_from_u64(self.seed);
85
86        // SAFETY:
87        // Obsucate the data again with a new seed.
88        unsafe {
89            self.data.toggle_encrust(&mut encrust_rng);
90        }
91    }
92
93    /// Deobfuscates the data contained in [`Encrusted`] and returns a [`Decrusted`] object that can
94    /// be used to access and modify the actual data.
95    pub fn decrust(&mut self) -> Decrusted<'_, T> {
96        Decrusted::new(self)
97    }
98}
99
100impl<T> Drop for Encrusted<T>
101where
102    T: Encrustable + Zeroize,
103{
104    /// [`Encrusted`]'s drop implementation calls zeroize on the underlying data including the seed
105    /// to prevent secrets from staying in memory when they are no longer needed.
106    ///
107    /// Note that the data is zeroized prior to being dropped, which may cause problems for the drop
108    /// implementation of the underlying data.
109    fn drop(&mut self) {
110        self.data.zeroize();
111        self.seed.zeroize();
112    }
113}
114
115/// Type used to access encrusted data. Use [`Encrusted::decrust`] to create `Decrusted` data.
116///
117/// When the `Decrusted` object is dropped, the underlying data is re-obfuscated.
118pub struct Decrusted<'decrusted, T>
119where
120    T: Encrustable + Zeroize,
121{
122    encrusted_data: &'decrusted mut Encrusted<T>,
123}
124
125impl<'decrusted, T> Decrusted<'decrusted, T>
126where
127    T: Encrustable + Zeroize,
128{
129    fn new(encrusted_data: &'decrusted mut Encrusted<T>) -> Self {
130        let mut decruster = SmallRng::seed_from_u64(encrusted_data.seed);
131
132        // SAFETY:
133        // This needs to happen to deobfuscate the data for use. Without this, invalid data can
134        // cause problems, such as strings with data that is not valid UTF-8.
135        unsafe {
136            encrusted_data.data.toggle_encrust(&mut decruster);
137        }
138
139        Self { encrusted_data }
140    }
141}
142
143impl<T> Drop for Decrusted<'_, T>
144where
145    T: Encrustable + Zeroize,
146{
147    fn drop(&mut self) {
148        let mut encrust_rng = SmallRng::seed_from_u64(self.encrusted_data.seed);
149
150        // SAFETY:
151        // This needs to happen to obfuscate the data when this object is dropped to ensure that
152        // data does not linger in memory unobfuscated when not needed. Data will not be accessible
153        // without deobfuscating the data, so this should not cause any issues.
154        unsafe {
155            self.encrusted_data.data.toggle_encrust(&mut encrust_rng);
156        }
157    }
158}
159
160impl<T> Deref for Decrusted<'_, T>
161where
162    T: Encrustable + Zeroize,
163{
164    type Target = T;
165
166    fn deref(&self) -> &Self::Target {
167        &self.encrusted_data.data
168    }
169}
170
171impl<T> DerefMut for Decrusted<'_, T>
172where
173    T: Encrustable + Zeroize,
174{
175    fn deref_mut(&mut self) -> &mut Self::Target {
176        &mut self.encrusted_data.data
177    }
178}
179
180/// Trait required to use data types with encrust. If it is avoidable, do not implement this
181/// manually, but use the derive macro to generate the implementation.
182pub trait Encrustable {
183    /// Called when obfuscating and deobfuscating data. Calling this function manually may lead to
184    /// safety issues and should not be done.
185    ///
186    /// # Safety
187    /// `toggle_encrust` directly modifies the underlying data in arbitrary ways, possibly making it
188    /// unsafe to use. This function should only ever be called by encrust to obfuscate objects or
189    /// deobfuscate them for reading.
190    unsafe fn toggle_encrust(&mut self, encrust_rng: &mut impl RngCore);
191}
192
193macro_rules! encrustable_number {
194    ( $( $t:ty ),* ) => {
195        $(
196            impl Encrustable for $t {
197                unsafe fn toggle_encrust(&mut self, encrust_rng: &mut impl ::rand::RngCore) {
198                    let mut bytes = self.to_le_bytes();
199
200                    // Using 8 bytes as most numbers that will be used with encrust are (most
201                    // likely) 64-bit or smaller.
202                    let mut key: [u8; 8] = [0; 8];
203                    for chunk in bytes.chunks_mut(8) {
204                        encrust_rng.fill_bytes(&mut key);
205                        for (byte, byte_key) in chunk.iter_mut().zip(key.iter()) {
206                            *byte ^= byte_key;
207                        }
208                    }
209
210                    *self = Self::from_le_bytes(bytes);
211                }
212            }
213        )*
214    };
215}
216
217encrustable_number!(
218    u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize
219);
220
221impl Encrustable for String {
222    unsafe fn toggle_encrust(&mut self, encrust_rng: &mut impl RngCore) {
223        // Safety: This modifies the underlying bytes directly, which is unsafe. However, the
224        // changes are reverted before granting access to the underlying memory again.
225        let bytes = unsafe { self.as_mut_vec() };
226
227        // Encrusting 16 bytes at a time as a micro-benchmark showed that it was most efficient on
228        // the tested x86-64 systems.
229        let mut key: [u8; 16] = [0; 16];
230        for chunk in bytes.chunks_mut(16) {
231            encrust_rng.fill_bytes(&mut key);
232            for (byte, byte_key) in chunk.iter_mut().zip(key.iter()) {
233                *byte ^= byte_key;
234            }
235        }
236    }
237}
238
239impl<T, const N: usize> Encrustable for [T; N]
240where
241    T: Encrustable,
242{
243    unsafe fn toggle_encrust(&mut self, encrust_rng: &mut impl RngCore) {
244        for element in self {
245            // Safety: This modifies the underlying bytes directly, which is unsafe. However, the
246            // changes are reverted before granting access to the underlying memory again.
247            unsafe {
248                element.toggle_encrust(encrust_rng);
249            }
250        }
251    }
252}
253
254impl<T> Encrustable for Vec<T>
255where
256    T: Encrustable,
257{
258    unsafe fn toggle_encrust(&mut self, encrust_rng: &mut impl RngCore) {
259        for element in self {
260            // Safety: This modifies the underlying bytes directly, which is unsafe. However, the
261            // changes are reverted before granting access to the underlying memory again.
262            unsafe {
263                element.toggle_encrust(encrust_rng);
264            }
265        }
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use super::*;
272
273    const TEST_STRING: &str = "The quick brown fox jumps over the lazy dog😊";
274
275    fn get_seed() -> u64 {
276        0x2357_bd11_1317_1d1f
277    }
278
279    macro_rules! test_ints {
280        ( $( $t:ty ),* ) => {
281            $(
282                {
283                    let mut encrusted = Encrusted::<$t>::new(0, get_seed());
284                    assert_ne!(encrusted.data, 0);
285
286                    {
287                        let decrusted = encrusted.decrust();
288                        assert_eq!(*decrusted, 0);
289                    }
290
291                    assert_ne!(encrusted.data, 0);
292                }
293
294                {
295                    let seed = get_seed();
296                    let mut encrust_rng = SmallRng::seed_from_u64(seed);
297                    let mut encrusted_data: $t = 0;
298
299                    // Safety: Testing from_encrusted_data requires pre-encrusted data, which is
300                    // an unsafe operation. The data will not be available without calling
301                    // `toggle_encrust` again.
302                    let mut encrusted = unsafe {
303                        encrusted_data.toggle_encrust(&mut encrust_rng);
304                        Encrusted::<$t>::from_encrusted_data(encrusted_data, seed)
305                    };
306
307                    assert_ne!(encrusted.data, 0);
308
309                    {
310                        let decrusted = encrusted.decrust();
311                        assert_eq!(*decrusted, 0);
312                    }
313
314                    assert_ne!(encrusted.data, 0);
315                }
316            )*
317        };
318    }
319
320    #[test]
321    fn test_ints() {
322        test_ints!(
323            u8, i8, u16, i16, u32, i32, u64, i64, u128, i128, usize, isize
324        );
325    }
326
327    #[test]
328    fn test_strings() {
329        let mut encrusted = Encrusted::new(TEST_STRING.to_string(), get_seed());
330        assert_ne!(encrusted.data.as_bytes(), TEST_STRING.as_bytes());
331
332        {
333            let decrusted = encrusted.decrust();
334            assert_eq!(*decrusted, TEST_STRING);
335        }
336
337        assert_ne!(encrusted.data.as_bytes(), TEST_STRING.as_bytes());
338    }
339
340    #[test]
341    fn test_strings_from_encrusted() {
342        let seed = get_seed();
343        let mut encrust_rng = SmallRng::seed_from_u64(seed);
344
345        let mut encrusted_string = TEST_STRING.to_string();
346
347        // Safety: Testing from_encrusted_data requires pre-encrusted data, which is an unsafe
348        // operation. The data will not be available without calling `toggle_encrust` again.
349        let mut encrusted = unsafe {
350            encrusted_string.toggle_encrust(&mut encrust_rng);
351            Encrusted::from_encrusted_data(encrusted_string, seed)
352        };
353
354        assert_ne!(encrusted.data.as_bytes(), TEST_STRING.as_bytes());
355
356        {
357            let decrusted = encrusted.decrust();
358            assert_eq!(*decrusted, TEST_STRING);
359        }
360
361        assert_ne!(encrusted.data.as_bytes(), TEST_STRING.as_bytes());
362    }
363
364    #[test]
365    fn test_arrays() {
366        let orig_array: [u8; 45] = [
367            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
368            24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
369        ];
370
371        let mut encrusted = Encrusted::new(orig_array, get_seed());
372        assert_ne!(encrusted.data, orig_array);
373
374        {
375            let decrusted = encrusted.decrust();
376            assert_eq!(*decrusted, orig_array);
377        }
378
379        assert_ne!(encrusted.data, orig_array);
380    }
381
382    #[test]
383    fn test_arrays_from_encrusted() {
384        let seed = get_seed();
385        let mut encrust_rng = SmallRng::seed_from_u64(seed);
386        let orig_array: [u8; 45] = [
387            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
388            24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44,
389        ];
390
391        let mut encrusted_array = orig_array;
392
393        // Safety: Testing from_encrusted_data requires pre-encrusted data, which is an unsafe
394        // operation. The data will not be available without calling `toggle_encrust` again.
395        let mut encrusted = unsafe {
396            encrusted_array.toggle_encrust(&mut encrust_rng);
397            Encrusted::from_encrusted_data(encrusted_array, seed)
398        };
399
400        assert_ne!(encrusted.data, orig_array);
401
402        {
403            let decrusted = encrusted.decrust();
404            assert_eq!(*decrusted, orig_array);
405        }
406
407        assert_ne!(encrusted.data, orig_array);
408    }
409
410    #[test]
411    fn test_vecs() {
412        let orig_vec = TEST_STRING.as_bytes().to_vec();
413
414        let mut encrusted = Encrusted::new(orig_vec.clone(), get_seed());
415        assert_ne!(encrusted.data, orig_vec);
416
417        {
418            let decrusted = encrusted.decrust();
419            assert_eq!(*decrusted, orig_vec);
420        }
421
422        assert_ne!(encrusted.data, orig_vec);
423    }
424
425    #[test]
426    fn test_vecs_from_encrusted() {
427        let seed = get_seed();
428        let mut encrust_rng = SmallRng::seed_from_u64(seed);
429        let orig_vec = TEST_STRING.as_bytes().to_vec();
430
431        let mut encrusted_vec = orig_vec.clone();
432
433        // Safety: Testing from_encrusted_data requires pre-encrusted data, which is an unsafe
434        // operation. The data will not be available without calling `toggle_encrust` again.
435        let mut encrusted = unsafe {
436            encrusted_vec.toggle_encrust(&mut encrust_rng);
437            Encrusted::from_encrusted_data(encrusted_vec, seed)
438        };
439
440        assert_ne!(encrusted.data, orig_vec);
441
442        {
443            let decrusted = encrusted.decrust();
444            assert_eq!(*decrusted, orig_vec);
445        }
446
447        assert_ne!(encrusted.data, orig_vec);
448    }
449
450    #[test]
451    fn test_reseed() {
452        let num = 828_627_825_u64;
453        let mut encrusted = Encrusted::new(num, get_seed());
454        let orig_seed = encrusted.seed;
455        let mut rng = rand::rng();
456
457        encrusted.reseed(rng.next_u64());
458
459        // May fail, but the seed is so large that a collision is highly unlikely if it is selected
460        // randomly.
461        assert_ne!(encrusted.seed, orig_seed);
462
463        {
464            let decrusted = encrusted.decrust();
465            assert_eq!(*decrusted, num);
466        }
467    }
468
469    /// Test to make sure that a previously encrusted object can be decrusted with the current
470    /// version of `encrust`.
471    #[test]
472    fn ensure_encrust_has_not_changed() {
473        // Safety: Comparing a `String` with invalid UTF-8 in a test should hopefully at worst crash
474        // the test.
475        let mut test_string = unsafe {
476            Encrusted::from_encrusted_data(
477                String::from_utf8_unchecked(
478                    [
479                        55u8, 10u8, 35u8, 94u8, 130u8, 81u8, 207u8, 225u8, 64u8, 17u8, 143u8, 78u8,
480                        95u8, 204u8, 50u8, 183u8, 54u8, 185u8, 59u8, 50u8, 163u8, 122u8, 131u8,
481                        136u8, 172u8, 79u8, 17u8, 12u8, 56u8, 64u8, 59u8, 173u8, 102u8, 54u8,
482                        184u8, 186u8, 1u8, 246u8, 193u8, 136u8, 220u8, 224u8, 117u8, 144u8, 131u8,
483                        65u8, 77u8,
484                    ]
485                    .to_vec(),
486                ),
487                #[allow(
488                    clippy::unreadable_literal,
489                    reason = "Arbitrary number chosen at random with no further meaning."
490                )]
491                5233902475398815152u64,
492            )
493        };
494
495        let decrusted_test_string = test_string.decrust();
496        assert_eq!(*decrusted_test_string, TEST_STRING);
497    }
498}