1use std::{env, fs, process};
2
3use crypto::{Aes256Gcm, Xof, sha3::Shake256};
4use zeroize::{Zeroize, Zeroizing};
5
6const KEY_LENGTH: usize = 32;
7const NONCE_SEED_LENGTH: usize = 32;
8
9const KDF_INFO_CHACHA20_BLAKE3_KEY: &str = "crypt ChaCha20-BLAKE3 key";
10const KDF_INFO_CHACHA20_BLAKE3_NONCE: &str = "crypt ChaCha20-BLAKE3 nonce";
11const CHACHA20_BLAKE3_NONCE_LENGTH: usize = 24;
12
13const KDF_INFO_AES_KEY: &str = "crypt AES-256-GCM key";
14const KDF_INFO_AES_NONCE: &str = "crypt AES-256-GCM nonce";
15const AES_NONCE_LENGTH: usize = 12;
16
17const ARGON2_SALT_LENGTH: usize = 32;
18const ARGON2_ITERATIONS: u32 = 8;
19const ARGON2_MEMORY_KB: u32 = 1024 * 1024; const ARGON2_LANES: u32 = 4;
21const KDF_INFO_ARGON2_SALT: &str = "crypt Argon2 salt";
22
23fn main() {
24 let args: Vec<String> = env::args().collect();
25 if args.len() != 4 {
26 print_help_and_exit(1);
27 }
28
29 let action = &args[1];
30 let file_in = &args[2];
31 let file_out = &args[3];
32
33 let (confirm_password, encrypt_mode) = match action.as_str() {
34 "encrypt" => (true, true),
35 "decrypt" => (false, false),
36 _ => print_help_and_exit(1),
37 };
38
39 let mut password = match ask_for_password(confirm_password) {
40 Ok(p) => p,
41 Err(e) => {
42 eprintln!("Error: {e}");
43 process::exit(1);
44 }
45 };
46
47 let fn_ptr: fn(&[u8], &[u8]) -> Result<Vec<u8>, String> = if encrypt_mode { encrypt } else { decrypt };
48 let result = process_file(&password, file_in, file_out, fn_ptr);
49 password.zeroize();
50
51 if let Err(e) = result {
52 eprintln!("Error: {e}");
53 process::exit(1);
54 }
55}
56
57fn process_file(
58 password: &[u8],
59 file_in: &str,
60 file_out: &str,
61 f: fn(&[u8], &[u8]) -> Result<Vec<u8>, String>,
62) -> Result<(), String> {
63 if file_in == file_out {
64 return Err("input file can't be the same as output file".to_string());
65 }
66
67 let mut data_in = fs::read(file_in).map_err(|e| format!("error reading [{file_in}]: {e}"))?;
68
69 let mut data_out = f(password, &data_in)?;
70
71 let write_result = fs::write(file_out, &data_out).map_err(|e| format!("error writing to [{file_out}]: {e}"));
72
73 data_in.zeroize();
74 data_out.zeroize();
75
76 write_result
77}
78
79fn encrypt(password: &[u8], plaintext: &[u8]) -> Result<Vec<u8>, String> {
85 let nonce_seed: [u8; NONCE_SEED_LENGTH] = rand::random();
86
87 let chacha20_nonce = derive_key::<CHACHA20_BLAKE3_NONCE_LENGTH>(&nonce_seed, KDF_INFO_CHACHA20_BLAKE3_NONCE);
88 let aes_nonce = derive_key::<AES_NONCE_LENGTH>(&nonce_seed, KDF_INFO_AES_NONCE);
89 let argon2_salt = derive_key::<ARGON2_SALT_LENGTH>(&nonce_seed, KDF_INFO_ARGON2_SALT);
90
91 let root_key = argon2_derive_key(password, argon2_salt.as_slice())?;
92
93 let aes_key = derive_key::<KEY_LENGTH>(root_key.as_slice(), KDF_INFO_AES_KEY);
94 let chacha20_key = derive_key::<KEY_LENGTH>(root_key.as_slice(), KDF_INFO_CHACHA20_BLAKE3_KEY);
95
96 let aes = Aes256Gcm::new(&aes_key);
98 let mut aes_buf = plaintext.to_vec();
99 let tag = aes.encrypt_in_place(&mut aes_buf, &aes_nonce, &[]);
100 aes_buf.extend_from_slice(&tag);
101
102 let cipher = chacha20_blake3::ChaCha20Blake3::new(*chacha20_key);
104 let outer_ciphertext = cipher.encrypt(&chacha20_nonce, &aes_buf, &[]);
105
106 let mut result = Vec::with_capacity(NONCE_SEED_LENGTH + outer_ciphertext.len());
107 result.extend_from_slice(&nonce_seed);
108 result.extend_from_slice(&outer_ciphertext);
109
110 Ok(result)
111}
112
113fn decrypt(password: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>, String> {
114 if ciphertext.len() < (NONCE_SEED_LENGTH + chacha20_blake3::TAG_SIZE) {
115 return Err("ciphertext is too short".to_string());
116 }
117
118 let nonce_seed = &ciphertext[..NONCE_SEED_LENGTH];
119 let ciphertext = &ciphertext[NONCE_SEED_LENGTH..];
120
121 let chacha20_nonce = derive_key::<CHACHA20_BLAKE3_NONCE_LENGTH>(&nonce_seed, KDF_INFO_CHACHA20_BLAKE3_NONCE);
122 let aes_nonce = derive_key::<AES_NONCE_LENGTH>(&nonce_seed, KDF_INFO_AES_NONCE);
123 let argon2_salt = derive_key::<ARGON2_SALT_LENGTH>(&nonce_seed, KDF_INFO_ARGON2_SALT);
124
125 let root_key = argon2_derive_key(password, argon2_salt.as_slice())?;
126
127 let aes_key = derive_key::<KEY_LENGTH>(root_key.as_slice(), KDF_INFO_AES_KEY);
128 let chacha20_key = derive_key::<KEY_LENGTH>(root_key.as_slice(), KDF_INFO_CHACHA20_BLAKE3_KEY);
129
130 let cipher = chacha20_blake3::ChaCha20Blake3::new(*chacha20_key);
132 let aes_ciphertext = cipher
133 .decrypt(&chacha20_nonce, ciphertext, &[])
134 .map_err(|e| format!("error decrypting data with ChaCha20-BLAKE3: {e:?}"))?;
135
136 if aes_ciphertext.len() < Aes256Gcm::TAG_SIZE {
138 return Err("ciphertext is too short for AES-256-GCM tag".to_string());
139 }
140
141 let aes = Aes256Gcm::new(&aes_key);
142 let tag_pos = aes_ciphertext.len() - Aes256Gcm::TAG_SIZE;
143 let tag: [u8; 16] = aes_ciphertext[tag_pos..].try_into().unwrap();
144 let mut plaintext_buf = aes_ciphertext[..tag_pos].to_vec();
145 aes.decrypt_in_place(&mut plaintext_buf, &tag, &aes_nonce, &[])
146 .map_err(|_| "error decrypting data with AES-256-GCM: authentication failed".to_string())?;
147
148 Ok(plaintext_buf)
149}
150
151fn derive_key<const N: usize>(root_key: &[u8], info: &str) -> Zeroizing<[u8; N]> {
152 let mut out = Zeroizing::new([0u8; N]);
153
154 let mut shake = Shake256::new();
155 shake.absorb(root_key);
156 shake.absorb(&(root_key.len() as u64).to_le_bytes());
157 shake.absorb(info.as_bytes());
158 shake.absorb(&(info.len() as u64).to_le_bytes());
159 shake.absorb(&(N as u64).to_le_bytes());
160 shake.squeeze(out.as_mut_slice());
161
162 return out;
163}
164
165fn argon2_derive_key(password: &[u8], salt: &[u8]) -> Result<Zeroizing<[u8; KEY_LENGTH]>, String> {
166 let params = argon2::Params::new(ARGON2_MEMORY_KB, ARGON2_ITERATIONS, ARGON2_LANES, Some(KEY_LENGTH))
167 .map_err(|e| format!("error creating argon2 params: {e}"))?;
168 let argon2_instance = argon2::Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
169 let mut key = Zeroizing::new([0u8; KEY_LENGTH]);
170 argon2_instance
171 .hash_password_into(password, salt, key.as_mut_slice())
172 .map_err(|e| format!("error deriving key with argon2: {e}"))?;
173 Ok(key)
174}
175
176fn ask_for_password(confirm: bool) -> Result<Vec<u8>, String> {
177 eprint!("Password: ");
178 let password = term::read_password().map_err(|e| format!("error reading password: {e}"))?;
179 eprintln!();
180
181 if password.is_empty() {
182 return Err("password is empty".to_string());
183 }
184
185 if confirm {
186 eprint!("Confirm Password: ");
187 let mut confirmation =
188 term::read_password().map_err(|e| format!("error reading password confirmation: {e}"))?;
189 eprintln!();
190
191 let matches = password == confirmation;
192 confirmation.zeroize();
193
194 if !matches {
195 return Err("passwords don't match".to_string());
196 }
197 }
198
199 Ok(password)
200}
201
202fn print_help_and_exit(exit_code: i32) -> ! {
203 eprintln!("usage: crypt <encrypt|decrypt> <in> <out>");
204 process::exit(exit_code);
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 struct TestCase {
212 password: &'static str,
213 data: &'static str,
214 }
215
216 fn test_cases() -> Vec<TestCase> {
217 vec![
218 TestCase {
219 password: "",
220 data: "",
221 },
222 TestCase {
223 password: "password",
224 data: "",
225 },
226 TestCase {
227 password: "",
228 data: "data",
229 },
230 TestCase {
231 password: "password",
232 data: "data",
233 },
234 TestCase {
235 password: "password",
236 data: "77c7ce9a5d86bb386d443bb96390faa120633158699c8844c30b13ab0bf92760b7e4416aea397db91b4ac0e5dd56b8ef7e4b066162ab1fdc088319ce6defc87677c7ce9a5d86bb386d443bb96390faa120633158699c8844c30b13ab0bf92760b7e4416aea397db91b4ac0e5dd56b8ef7e4b066162ab1fdc088319ce6defc87677c7ce9a5d86bb386d443bb96390faa120633158699c8844c30b13ab0bf92760b7e4416aea397db91b4ac0e5dd56b8ef7e4b066162ab1fdc088319ce6defc876",
238 },
239 ]
240 }
241
242 #[test]
243 fn test_encrypt_decrypt() {
244 for (i, test) in test_cases().iter().enumerate() {
245 let password = test.password.as_bytes();
246 let data = test.data.as_bytes();
247
248 let ciphertext = encrypt(password, data).unwrap_or_else(|e| panic!("error encrypting data [{}]: {}", i, e));
249
250 assert!(
252 ciphertext != data && (data.is_empty() || &ciphertext[..data.len()] != data),
253 "ciphertext == data for {}",
254 i
255 );
256
257 let plaintext =
258 decrypt(password, &ciphertext).unwrap_or_else(|e| panic!("error decrypting data [{}]: {}", i, e));
259
260 let mut wrong_password = test.password.to_string();
262 wrong_password.push('1');
263 let ciphertext2 = ciphertext.clone();
264 let wrong_result = decrypt(wrong_password.as_bytes(), &ciphertext2);
265 assert!(
266 wrong_result.is_err(),
267 "expected error when using invalid password decrypting data for [{}]",
268 i
269 );
270
271 assert_eq!(
272 plaintext,
273 data,
274 "data ({}) != decrypted plaintext ({}) for {}",
275 test.data,
276 String::from_utf8_lossy(&plaintext),
277 i
278 );
279 }
280 }
281}