diff --git a/Cargo.lock b/Cargo.lock index 29492cf..eeeaafd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -220,10 +220,10 @@ dependencies = [ "clap", "cpufeatures 0.2.1", "hkdf", - "hmac", "num_enum", "rand", "rpassword", + "subtle", "tempfile", "zeroize", ] diff --git a/Cargo.toml b/Cargo.toml index ba324cc..583824c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ num_enum = "0.5" cpufeatures = "0.2" aes = { version = "0.7", features = ["ctr"] } chacha20 = "0.8" -hmac = "0.11" +subtle = "2.4" blake2 = "0.9" hkdf = "0.11" argon2 = "0.3" diff --git a/README.md b/README.md index 7dca224..420ffcc 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ doby started as a fork of [aef](https://github.com/wyhaya/aef) by [wyhaya](https * Fast: written in [rust](https://www.rust-lang.org), encrypts with [AES-256-CTR](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Counter_(CTR)) or [XChaCha20](https://en.wikipedia.org/wiki/Salsa20#XChaCha) * [HMAC](https://en.wikipedia.org/wiki/HMAC) ciphertext authentication * Password brute-force resistance with [Argon2](https://en.wikipedia.org/wiki/Argon2) -* Increase the plaintext size of only 142 bytes +* Increase the plaintext size of only 113 bytes * Encryption from STDIN/STDOUT or from files * Adjustable performance & secuity parameters @@ -152,9 +152,9 @@ NOTE: To reduce the size of the header, the `nonce` is derived from the `master_ Next, doby initializes a [BLAKE2b](https://en.wikipedia.org/wiki/BLAKE_(hash_function)#BLAKE2) HMAC with `authentication_key` and add all public encryption parameters to it. ```rust -let hmac = Hmac::new( +let hmac = Blake2b::new_keyed( authentication_key, - blake2b, //hash function + 32, //digest size ); hmac.update(random_salt); //integers are encoded in big-endian @@ -217,7 +217,7 @@ So here is what an encrypted file layout looks like: HMAC - 64 bytes + 32 bytes @@ -254,7 +254,7 @@ while n != 0 { Once the whole ciphertext is decrypted, doby computes and verifies the HMAC. ```rust -hmac.digest() == last_64_bytes_read // the default blake2b output size is 64 bytes +hmac.digest() == last_32_bytes_read ``` If the verification success, the file is successfully decrypted and authenticated. diff --git a/src/crypto.rs b/src/crypto.rs index 9229f3c..24b191d 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -1,8 +1,9 @@ use std::{convert::TryFrom, fmt::{self, Display, Formatter}, io::{self, Read, Write}}; +use blake2::{Blake2b, VarBlake2b, digest::{Update, VariableOutput}}; use num_enum::TryFromPrimitive; use chacha20::XChaCha20; use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}}; -use hmac::{Hmac, Mac, NewMac}; +use subtle::ConstantTimeEq; use rand::{Rng, rngs::OsRng}; use argon2::{Argon2, Version, Algorithm}; use hkdf::Hkdf; @@ -11,7 +12,7 @@ use zeroize::Zeroize; pub const SALT_LEN: usize = 64; const AES_NONCE_LEN: usize = 16; const XCHACHA20_NONCE_LEN: usize = 24; -pub const HASH_LEN: usize = 64; +pub const HMAC_LEN: usize = 32; const KEY_LEN: usize = 32; #[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)] @@ -99,7 +100,7 @@ impl EncryptionParams { pub struct DobyCipher { cipher: Box, - hmac: Hmac, + hasher: VarBlake2b, buffer: Vec, } @@ -108,7 +109,7 @@ impl DobyCipher { let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params.argon2.clone()); let mut master_key = [0; KEY_LEN]; argon2.hash_password_into(password, ¶ms.salt, &mut master_key).unwrap(); - let hkdf = Hkdf::::new(Some(¶ms.salt), &master_key); + let hkdf = Hkdf::::new(Some(¶ms.salt), &master_key); master_key.zeroize(); let mut nonce = vec![0; params.cipher.get_nonce_size()]; hkdf.expand(b"doby_nonce", &mut nonce).unwrap(); @@ -119,9 +120,9 @@ impl DobyCipher { let mut encoded_params = Vec::with_capacity(EncryptionParams::LEN); params.write(&mut encoded_params).unwrap(); - let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap(); + let mut hasher = VarBlake2b::new_keyed(&authentication_key, HMAC_LEN); authentication_key.zeroize(); - hmac.update(&encoded_params); + hasher.update(&encoded_params); let cipher: Box = match params.cipher { CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &nonce).unwrap()), @@ -131,20 +132,19 @@ impl DobyCipher { Self { cipher, - hmac, + hasher, buffer: Vec::new(), } } pub fn encrypt_chunk(&mut self, buff: &mut [u8], writer: &mut W) -> io::Result<()> { self.cipher.apply_keystream(buff); - self.hmac.update(buff); + self.hasher.update(&buff); writer.write_all(buff) } - pub fn write_hmac(self, writer: &mut W) -> io::Result { - let tag = self.hmac.finalize().into_bytes(); - writer.write(&tag) + pub fn write_hmac(self, writer: &mut W) -> io::Result<()> { + writer.write_all(&self.hasher.finalize_boxed()) } //buff size must be > to HASH_LEN @@ -153,27 +153,27 @@ impl DobyCipher { buff[..buffer_len].clone_from_slice(&self.buffer); let read = reader.read(&mut buff[buffer_len..])?; - let n = if buffer_len + read >= HASH_LEN { + let n = if buffer_len + read >= HMAC_LEN { self.buffer.clear(); - buffer_len + read - HASH_LEN + buffer_len + read - HMAC_LEN } else { 0 }; self.buffer.extend_from_slice(&buff[n..buffer_len+read]); - self.hmac.update(&buff[..n]); + self.hasher.update(&buff[..n]); self.cipher.apply_keystream(&mut buff[..n]); Ok(n) } pub fn verify_hmac(self) -> bool { - self.hmac.verify(&self.buffer).is_ok() + self.hasher.finalize_boxed().ct_eq(&self.buffer).into() } } #[cfg(test)] mod tests { - use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HASH_LEN}; + use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HMAC_LEN}; #[test] fn encryption_params() { let params = EncryptionParams::new( @@ -204,17 +204,17 @@ mod tests { let password = "I like spaghetti"; let plaintext = b"but I love so much to listen to HARDCORE music on big subwoofer"; let mut buff: [u8; 63] = *plaintext; - let mut vec = Vec::with_capacity(buff.len()+HASH_LEN); + let mut vec = Vec::with_capacity(buff.len()+HMAC_LEN); let mut enc_cipher = DobyCipher::new(password.as_bytes(), ¶ms); enc_cipher.encrypt_chunk(&mut buff, &mut vec).unwrap(); assert_ne!(buff, *plaintext); assert_eq!(buff, vec.as_slice()); - assert_eq!(enc_cipher.write_hmac(&mut vec).unwrap(), HASH_LEN); - assert_eq!(vec.len(), buff.len()+HASH_LEN); + assert!(enc_cipher.write_hmac(&mut vec).is_ok()); + assert_eq!(vec.len(), buff.len()+HMAC_LEN); let mut dec_cipher = DobyCipher::new(password.as_bytes(), ¶ms); - let mut decrypted = vec![0; buff.len()+HASH_LEN]; + let mut decrypted = vec![0; buff.len()+HMAC_LEN]; let mut n = dec_cipher.decrypt_chunk(&mut vec.as_slice(), &mut decrypted[..]).unwrap(); assert_eq!(n, buff.len()); n = dec_cipher.decrypt_chunk(&mut &vec[n..], &mut decrypted[n..]).unwrap(); diff --git a/tests/authentication.rs b/tests/authentication.rs index e578c3a..b51ded7 100644 --- a/tests/authentication.rs +++ b/tests/authentication.rs @@ -18,7 +18,7 @@ fn different_elements(v1: &Vec, v2: &Vec) -> usize { fn authentication() { const BLOCK_SIZE: usize = 65536; const PLAINTEXT: &[u8; 13] = b"the plaintext"; - const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+145; + const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+113; const PASSWORD: &str = "the password"; let params = EncryptionParams::new( argon2::Params::new(8, 1, 1, None).unwrap(), diff --git a/tests/cli.rs b/tests/cli.rs index e57f565..ce2b37b 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,7 +1,7 @@ use std::{convert::TryInto, fs::{self, File, create_dir}, io::{self, Read, Write}, path::PathBuf}; use assert_cmd::{Command, cargo::{CargoError, cargo_bin}}; use tempfile::TempDir; -use doby::crypto::{CipherAlgorithm, SALT_LEN, HASH_LEN}; +use doby::crypto::{CipherAlgorithm, SALT_LEN, HMAC_LEN}; const PLAINTEXT: &[u8] = b"the plaintext"; const PASSWORD: &str = "the password"; @@ -85,7 +85,7 @@ fn force_encrypt() -> io::Result<()> { let buff_ciphertext_2 = fs::read(&tmp_ciphertext_2)?; assert_ne!(buff_ciphertext_1, buff_ciphertext_2); assert_ne!(buff_ciphertext_2, PLAINTEXT); - assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+142); + assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+113); let tmp_decrypted_1 = tmp_path.join("decrypted_1"); doby_cmd().unwrap().arg(tmp_ciphertext_2).arg(&tmp_decrypted_1).assert().success().stdout("").stderr(""); @@ -108,7 +108,7 @@ fn test_cipher(cipher_str: &str, cipher_algorithm: CipherAlgorithm) -> io::Resul let ciphertext = fs::read(&tmp_ciphertext)?; assert_eq!(ciphertext[4+SALT_LEN+4*3], cipher_algorithm as u8); - assert_eq!(ciphertext.len(), PLAINTEXT.len()+17+SALT_LEN+HASH_LEN); + assert_eq!(ciphertext.len(), PLAINTEXT.len()+17+SALT_LEN+HMAC_LEN); doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");