1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
//! This defines [`CryptorKey`] type, which is an encryption key that can be
//! used to securely encrypt/decrypt data.

use chacha20poly1305::{ChaCha20Poly1305, KeyInit};

use rand::{CryptoRng, RngCore};
use std::path::Path;
use utilities::crypto::error::CryptoError;
use zeroize::{Zeroize, ZeroizeOnDrop};

use crate::infrastructure::sensitive_info::SensitiveInfoConfig;

/// The [`CryptorKey`] type is a default-length symmetric encryption key
/// for an AEAD scheme. It can be used to securely encrypt data.
#[derive(Clone, Eq, Zeroize, ZeroizeOnDrop)]
pub struct CryptorKey {
    pub(super) key_material: Box<chacha20poly1305::Key>,
    config: SensitiveInfoConfig,
}

/// An implementation of the `std::fmt::Display` trait for [`CryptorKey`]
impl std::fmt::Display for CryptorKey {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        if cfg!(debug_assertions) && !self.config.is_redacted() {
            // this is a Debug build *AND* redacted flag is FALSE
            write!(
                f,
                "\n---Cryptor key begin---\n\n\
                \
                \tKey: 0x{key:02x?}\n\
                \tKey length: {key_len}\n\
                \
                \n---Cryptor key end---\n",
                key = self.key_material,
                key_len = self.key_material.len(),
            )
        } else {
            // this is a Release build *OR* redacted flag is TRUE
            write!(
                f,
                "\n---Cryptor key begin---\n\n\
                \t{redacted}\n\
                \n---Cryptor key end---\n",
                redacted = self.config.clone().redacted_label(),
            )
        }
    }
}

/// An implementation of the `std::fmt::Debug` trait for [`CryptorKey`]
impl std::fmt::Debug for CryptorKey {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if cfg!(debug_assertions) && !self.config.is_redacted() {
            // this is a Debug build *AND* redacted flag is FALSE
            f.debug_struct("CryptorKey")
                .field("key_material", &self.key_material)
                .finish()
        } else {
            // this is a Release build *OR* redacted flag is TRUE
            write!(f, "CryptorKey {}", self.config.clone().redacted_label())
        }
    }
}

/// Implement the `PartialEq` trait for the [`CryptorKey`] type.
impl PartialEq for CryptorKey {
    /// Determines if two [`CryptorKey`] instances are equal.
    ///
    /// This function compares the `key_material` fields of two [`CryptorKey`]
    /// instances. It does not compare the `config` fields because the
    /// `config` field does not affect the functional equivalence of two
    /// [`CryptorKey`] instances.
    ///
    /// # Arguments
    ///
    /// * `other` - The other [`CryptorKey`] instance to compare with.
    ///
    /// # Returns
    ///
    /// Returns `true` if the `data` and `context` fields are equal between the
    /// two [`CryptorKey`] instances, Otherwise returns `false`.
    fn eq(&self, other: &Self) -> bool {
        // Don't compare the config fields
        self.key_material == other.key_material
    }
}

/// Implementation for [`CryptorKey`]
impl CryptorKey {
    /// Length of the encryption key in `bytes`
    const CRYPTOR_KEY_LENGTH: usize = 32;

    /// Constructs a new instance of the [`CryptorKey`] type.
    ///
    /// # Arguments
    ///
    /// * `rng` - A mutable reference to an object implementing both the
    ///   `CryptoRng` and `RngCore` traits. This is used to generate the
    ///   cryptographic key for the ChaCha20Poly1305 algorithm.
    ///
    /// # Returns
    ///
    /// Returns a new instance of the [`CryptorKey`] type with a fresh
    /// cryptographic key generated via the provided random number generator
    /// (`rng`) and a new [`SensitiveInfoConfig`] object.
    ///
    /// A [`CryptorKey`] is generated uniformly at random. It is a 32-byte pseudorandom key for use in the [ChaCha20Poly1305 scheme](https://www.rfc-editor.org/rfc/rfc8439) for
    /// authenticated encryption with associated data (AEAD).
    pub fn new(rng: &mut (impl CryptoRng + RngCore)) -> Self {
        Self {
            key_material: Box::new(ChaCha20Poly1305::generate_key(rng)),
            config: SensitiveInfoConfig::new(true),
        }
    }

    /// Returns the [`CryptorKey`] stored in a file.
    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, CryptoError> {
        // read key from a file
        let mut key_bytes = std::fs::read(&path)
            .map_err(|e| CryptoError::FileIo(e, path.as_ref().to_path_buf()))?;

        let cryptor_key = Self::from_bytes(&key_bytes);
        key_bytes.zeroize();

        cryptor_key
    }

    /// Returns the [`CryptorKey`] key based on key material in the caller's
    /// byte array.
    pub fn from_bytes(cryptor_key: &[u8]) -> Result<Self, CryptoError> {
        if cryptor_key.len() != CryptorKey::CRYPTOR_KEY_LENGTH {
            return Err(CryptoError::InvalidEncryptionKey);
        }

        // fixed size array initialized to zeros
        let mut key_bytes = [0; CryptorKey::CRYPTOR_KEY_LENGTH];
        // copy caller's key material
        key_bytes.copy_from_slice(cryptor_key);

        Ok(Self {
            key_material: Box::new(key_bytes.into()),
            config: SensitiveInfoConfig::new(true),
        })
    }

    /// Converts [`CryptorKey`] to a byte array.
    ///
    /// # Warning
    ///
    /// This method gives direct access to the key material bytes.
    /// The caller should be careful to manually zeroize them after use to
    /// prevent unintended exposure of sensitive information. Consider using
    /// the `zeroize` crate to securely zero the data.
    ///
    /// # Example
    ///
    /// ```rust
    /// use zeroize::Zeroize;
    /// use lock_keeper::crypto::CryptorKey;///
    ///
    /// let mut rng = rand::thread_rng();
    /// let encryption_key = CryptorKey::new(&mut rng);
    /// let mut key_bytes = encryption_key.into_bytes();
    ///
    /// // Use the key bytes...
    ///
    /// // When done, zeroize the key bytes
    /// key_bytes.zeroize();
    /// ```
    pub fn into_bytes(self) -> [u8; CryptorKey::CRYPTOR_KEY_LENGTH] {
        (*self.key_material).into()
    }
}

/// Tests...
#[cfg(test)]
pub(super) mod test {

    use super::*;
    use crate::infrastructure::sensitive_info::sensitive_info_check;
    use anyhow::anyhow;
    use std::{fs, fs::File, io::Write};

    const CRYPTOR_KEY_FILENAME: &str = "temp_encryption_key.bin";

    /// Test the correctness of the sensitive information handling.
    #[test]
    fn cryptor_key_sensitive_info_handling_on_debug_and_display() {
        let mut rng = rand::thread_rng();
        let mut encryption_key = CryptorKey::new(&mut rng);

        // sensitive information should be redacted by default, let's test...
        sensitive_info_check(&encryption_key, &encryption_key.config).unwrap();

        // unredact sensitive information, and test
        encryption_key.config.unredact();
        sensitive_info_check(&encryption_key, &encryption_key.config).unwrap();
    }

    /// Test the conversion from bytes to struct
    #[test]
    fn cryptor_key_from_bytes() -> Result<(), CryptoError> {
        let mut rng = rand::thread_rng();

        let encryption_key = CryptorKey::new(&mut rng);

        // use the cloned key bytes to create a new key
        let encryption_key_bytes = encryption_key.key_material.clone();
        let mut encryption_key_from_bytes = CryptorKey::from_bytes(&encryption_key_bytes)?;
        // allow sensitive info to be shown
        encryption_key_from_bytes.config.unredact();

        println!(
            "\nEncryption key: 0x{key:02x?}\n\
            Key length: {key_len}\n\
            \
            \nConverted encryption key:\n{converted_key}\n",
            key = encryption_key_bytes,
            key_len = encryption_key_bytes.len(),
            converted_key = encryption_key_from_bytes,
        );

        // compare the original key and the newly created key from the original key
        // bytes
        assert_eq!(encryption_key, encryption_key_from_bytes,);

        Ok(())
    }

    /// Test the conversion from bytes to struct when the length is wrong.
    #[test]
    fn key_from_bytes_wrong_length_fails() -> Result<(), CryptoError> {
        // test conversion with a key byte array that is TOO SHORT
        let number_of_key_bytes = CryptorKey::CRYPTOR_KEY_LENGTH - 1;
        let key_bytes = vec![0xab; number_of_key_bytes];

        let new_encryption_key_from_bytes = CryptorKey::from_bytes(&key_bytes);

        println!(
            "\nConvert encryption key using {number_of_key_bytes} bytes:\n{new_encryption_key_from_bytes:x?}\n\n",
            number_of_key_bytes=number_of_key_bytes,
            new_encryption_key_from_bytes=new_encryption_key_from_bytes,
        );

        // make sure we have an invalid key error
        assert_eq!(
            new_encryption_key_from_bytes.unwrap_err().to_string(),
            CryptoError::InvalidEncryptionKey.to_string()
        );

        // test conversion with a key byte array that is TOO LONG
        let number_of_key_bytes = CryptorKey::CRYPTOR_KEY_LENGTH + 1;
        let key_bytes = vec![0xab; number_of_key_bytes];

        let new_encryption_key_from_bytes = CryptorKey::from_bytes(&key_bytes);

        println!(
            "\nConvert encryption key using {number_of_key_bytes} bytes:\n{new_encryption_key_from_bytes:02x?}\n\n",
            number_of_key_bytes=number_of_key_bytes,
            new_encryption_key_from_bytes=new_encryption_key_from_bytes,
        );

        // make sure we have an invalid key error
        assert_eq!(
            new_encryption_key_from_bytes.unwrap_err().to_string(),
            CryptoError::InvalidEncryptionKey.to_string()
        );

        Ok(())
    }

    /// Test reading a key from a file.
    #[test]
    fn key_from_file() -> Result<(), CryptoError> {
        // Use a helper function to write/read the key to/from a file.
        // That way we can clean up the temp file in success and failure cases.
        let result = key_from_file_helper();

        // Always try to delete the file, even if key_from_file_helper()
        // encountered an error.
        match fs::remove_file(CRYPTOR_KEY_FILENAME) {
            Ok(()) => (),
            Err(e) => println!("Failed to remove a temp key file: {}", e),
        }

        // Now handle the result of key_from_file_helper()
        match result {
            Ok(()) => (),
            Err(e) => panic!("Test failed: {}", e),
        }

        Ok(())
    }

    /// Helper function, so we can handle read from file failures correctly.
    fn key_from_file_helper() -> Result<(), anyhow::Error> {
        const KEY_FILENAME: &str = "temp_encryption_key.bin";

        let mut rng = rand::thread_rng();

        let encryption_key = CryptorKey::new(&mut rng);

        // create a temp file to store the key
        let mut encryption_key_file =
            File::create(KEY_FILENAME).map_err(|_| anyhow!("Failed to create a temp key file."))?;

        // clone it for later, before we lose ownership
        let mut encryption_key_original = encryption_key.clone();

        encryption_key_file
            .write_all(&encryption_key.into_bytes())
            .expect("Failed to write to a temp key file.");

        // read the key from a file
        let mut encryption_key_from_file = CryptorKey::from_file(KEY_FILENAME)?;

        // allow sensitive info to be shown
        encryption_key_from_file.config.unredact();
        encryption_key_original.config.unredact();

        println!("\nEncryption key:\n{key}", key = encryption_key_original);
        println!(
            "\nEncryption key from a file:\n{key_from_file}",
            key_from_file = encryption_key_from_file
        );

        // make sure that key read from the file *is* the same as the original key
        if encryption_key_original != encryption_key_from_file {
            return Err(CryptoError::InvalidEncryptionKey.into());
        }

        Ok(())
    }
}