ssh_utils_lib/config/
crypto.rs

1use anyhow::{Context, Result};
2use argon2::Config;
3use openssl::symm::{Cipher, Crypter, Mode};
4use rand::{thread_rng, Rng};
5use sha2::{Digest, Sha256};
6
7/**
8    derive 16 bytes digest from password
9*/
10pub fn derive_sha256_digest(password: &str) -> [u8; 16] {
11    let mut hasher = Sha256::new();
12    hasher.update(password.as_bytes());
13    let result = hasher.finalize();
14    let mut salt = [0u8; 16];
15    salt.copy_from_slice(&result[..16]);
16    salt
17}
18
19/**
20    derive 32 bytes hash key from password by argon2
21*/
22pub fn derive_key_from_password(password: &str) -> Result<[u8; 32]> {
23    // Step 1: Derive a 16-byte SHA-256 digest from the password.
24    let salt = derive_sha256_digest(password);
25
26    let config = Config::owasp3();
27    let key = argon2::hash_raw(password.as_bytes(), &salt, &config)
28        .context("Failed to derive key using Argon2")?;
29    let mut key_arr = [0u8; 32];
30    key_arr.copy_from_slice(&key);
31    Ok(key_arr)
32}
33
34/**
35    generate random iv
36*/
37pub fn generate_iv() -> [u8; 16] {
38    let mut iv = [0u8; 16];
39    let mut rng = thread_rng();
40    rng.fill(&mut iv);
41    iv
42}
43
44pub fn aes_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
45    let cipher = Cipher::aes_256_ctr();
46    let mut crypter =
47        Crypter::new(cipher, Mode::Encrypt, key, Some(iv)).context("Failed to create Crypter")?;
48    let mut ciphertext = vec![0; data.len() + cipher.block_size()];
49    let mut count = crypter
50        .update(data, &mut ciphertext)
51        .context("Failed to encrypt data")?;
52    count += crypter
53        .finalize(&mut ciphertext[count..])
54        .context("Failed to finalize encryption")?;
55    ciphertext.truncate(count);
56    Ok(ciphertext)
57}
58
59pub fn aes_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result<Vec<u8>> {
60    // Step: Decrypt the data using AES-256-CTR
61    let cipher = Cipher::aes_256_ctr();
62    let mut crypter =
63        Crypter::new(cipher, Mode::Decrypt, key, Some(iv)).context("Failed to create Crypter")?;
64    let mut plaintext = vec![0; data.len() + cipher.block_size()];
65    let mut count = crypter
66        .update(data, &mut plaintext)
67        .context("Failed to decrypt data")?;
68    count += crypter
69        .finalize(&mut plaintext[count..])
70        .context("Failed to finalize decryption")?;
71    plaintext.truncate(count);
72    Ok(plaintext)
73}
74
75#[cfg(test)]
76mod tests {
77    use std::{io::Write, process::Command};
78
79    use tempfile::NamedTempFile;
80
81    use super::*;
82
83    #[test]
84    fn test_derive_key_from_password() {
85        let password = "super_secret_password";
86        let salt = b"super_secret_password";
87        // The expected key in hexadecimal format is obtained from the command:
88        // echo -n "super_secret_password" | ./argon2 super_secret_password -t 3 -k 12288 -p 1 -id -l 32 -r
89        // Since the salt in the command cannot be passed as a byte array, we have defined a custom salt
90        // Copy the logic of derive_key_from_password() for testing
91        let expected_key_hex = "66e5467d6adc707c5fe42c2516de285204f4ce590612e58eddaab21b763aaca2";
92        let expected_key = hex::decode(expected_key_hex).expect("Decoding failed");
93
94        let derived_key_result = derive_key_from_password(password);
95
96        let config = Config::owasp3();
97        let key = argon2::hash_raw(password.as_bytes(), salt, &config).unwrap();
98        
99        // Check if the result is Ok
100        assert!(derived_key_result.is_ok());
101        
102        let derived_key = derived_key_result.unwrap();
103        
104        // Check if the length is 32 bytes
105        assert_eq!(derived_key.len(), 32);
106
107        // Check if the key is non-zero
108        assert!(derived_key.iter().any(|&byte| byte != 0));
109
110        // Check if the generated key matches the expected value
111        assert_eq!(key, expected_key);
112    }
113
114    #[test]
115    fn test_derive_sha256_digest() {
116        let password = "super_secret_password";
117        
118        // Use Rust to execute the command line to get the expected digest value
119        let output = std::process::Command::new("sh")
120            .arg("-c")
121            .arg(format!(
122                "echo -n \"{}\" | sha256sum | awk '{{print $1}}' | cut -c 1-32",
123                password
124            ))
125            .output()
126            .expect("Failed to execute command");
127
128        let expected_digest_hex = String::from_utf8(output.stdout)
129            .expect("Failed to convert output to string")
130            .trim()
131            .to_string();
132        
133        let expected_digest = hex::decode(expected_digest_hex).expect("Decoding failed");
134
135        let derived_digest = derive_sha256_digest(password);
136        
137        // Check if the length is 16 bytes
138        assert_eq!(derived_digest.len(), 16);
139
140        // Check if the generated digest matches the expected value
141        assert_eq!(derived_digest, expected_digest.as_slice());
142    }
143
144    #[test]
145    fn test_aes_encrypt_decrypt() {
146        let key = b"01234567890123456789012345678901"; // A 32-byte key
147        let iv = b"0123456789012345"; // A 16-byte initialization vector
148        let data = b"Hello, AES encryption!";
149
150        // Write data to a temporary file
151        let mut data_file = NamedTempFile::new().expect("Failed to create temporary file");
152        data_file.write_all(data).expect("Failed to write data to file");
153        let data_file_path = data_file.path().to_str().unwrap();
154
155        // Use OpenSSL to generate the expected encrypted value
156        let expected_encrypted_output = Command::new("openssl")
157            .arg("enc")
158            .arg("-aes-256-ctr")
159            .arg("-in")
160            .arg(data_file_path)
161            .arg("-K")
162            .arg(hex::encode(key))
163            .arg("-iv")
164            .arg(hex::encode(iv))
165            .output()
166            .expect("Failed to execute openssl command");
167        let expected_encrypted_data = expected_encrypted_output.stdout;
168
169        // Test aes_encrypt
170        let encrypted_data = aes_encrypt(key, iv, data).expect("Failed to encrypt data");
171        assert!(!encrypted_data.is_empty(), "Encrypted data should not be empty");
172        assert_eq!(encrypted_data, expected_encrypted_data, "Encrypted data should match the expected value");
173
174        // Write encrypted_data to a temporary file
175        let mut encrypted_file = NamedTempFile::new().expect("Failed to create temporary file");
176        encrypted_file.write_all(&encrypted_data).expect("Failed to write encrypted data to file");
177        let encrypted_file_path = encrypted_file.path().to_str().unwrap();
178
179        // Use OpenSSL to generate the expected decrypted value
180        let expected_decrypted_output = Command::new("openssl")
181            .arg("enc")
182            .arg("-d")
183            .arg("-aes-256-ctr")
184            .arg("-in")
185            .arg(encrypted_file_path)
186            .arg("-K")
187            .arg(hex::encode(key))
188            .arg("-iv")
189            .arg(hex::encode(iv))
190            .output()
191            .expect("Failed to execute openssl command");
192        let expected_decrypted_data = expected_decrypted_output.stdout;
193
194        // Test aes_decrypt
195        let decrypted_data = aes_decrypt(key, iv, &encrypted_data).expect("Failed to decrypt data");
196        assert_eq!(decrypted_data, data, "Decrypted data should match original data");
197        assert_eq!(decrypted_data, expected_decrypted_data, "Decrypted data should match the expected value");
198    }
199}