From ed7a2270807740c2b39d773ae05bec12145c0f95 Mon Sep 17 00:00:00 2001 From: Hardcore Sushi Date: Thu, 8 Jul 2021 12:21:14 +0200 Subject: [PATCH] Switch to BLAKE2b & Use the same salt for Argon2 and HKDF --- Cargo.lock | 55 +++++---------------------------------------------- Cargo.toml | 2 +- README.md | 23 +++++++++++---------- src/crypto.rs | 49 +++++++++++++++++++-------------------------- src/lib.rs | 2 +- tests/cli.rs | 8 ++++---- 6 files changed, 42 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 70d22b6..0649d84 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "495ee669413bfbe9e8cace80f4d3d78e6d8c8d99579f97fb93bde351b185f2d4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cipher", "cpufeatures", "ctr", @@ -34,18 +34,6 @@ dependencies = [ "password-hash", ] -[[package]] -name = "arrayref" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" - -[[package]] -name = "arrayvec" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" - [[package]] name = "assert_cmd" version = "1.0.6" @@ -94,21 +82,6 @@ dependencies = [ "opaque-debug", ] -[[package]] -name = "blake3" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b64485778c4f16a6a5a9d335e80d449ac6c70cdd6a06d2af18a6f6f775a125b3" -dependencies = [ - "arrayref", - "arrayvec", - "cc", - "cfg-if 0.1.10", - "constant_time_eq", - "crypto-mac 0.8.0", - "digest", -] - [[package]] name = "bstr" version = "0.2.16" @@ -120,18 +93,6 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "cc" -version = "1.0.68" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" - -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -144,7 +105,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "cipher", "cpufeatures", ] @@ -173,12 +134,6 @@ dependencies = [ "vec_map", ] -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - [[package]] name = "cpufeatures" version = "0.1.5" @@ -250,7 +205,7 @@ dependencies = [ "aes", "argon2", "assert_cmd", - "blake3", + "blake2", "chacha20", "clap", "cpufeatures", @@ -285,7 +240,7 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "wasi", ] @@ -544,7 +499,7 @@ version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "rand", "redox_syscall", diff --git a/Cargo.toml b/Cargo.toml index 4c2e978..4fad60e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ cpufeatures = "0.1" aes = { version = "0.7", features = ["ctr"] } chacha20 = "0.7" hmac = "0.11" -blake3 = "0.3" +blake2 = "0.9" hkdf = "0.11" argon2 = "0.2" rpassword = "5.0" diff --git a/README.md b/README.md index 752e70c..0a423e1 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 190 bytes +* Increase the plaintext size of only 158 bytes * Encryption from STDIN/STDOUT or from files * Adjustable performance & secuity parameters @@ -113,37 +113,36 @@ doby first derives your password with Argon2 (version 19) in Argon2id mode with ```rust let master_key: [u8; 32] = argon2id( password, - random_password_salt, + random_salt, argon2_time_cost, argon2_memory_cost, argon2_parallelism, ); ``` -Then, doby uses [HKDF](https://en.wikipedia.org/wiki/HKDF) with a new random salt to compute the `encryption_key` and the `authentication_key`. +Then, doby uses [HKDF](https://en.wikipedia.org/wiki/HKDF) with the previous random salt to compute the `encryption_key` and the `authentication_key`. ```rust let hkdf = Hkdf::new( - random_hkdf_salt, + random_salt, master_key, //ikm - blake3, //hash function + blake2b, //hash function ); let encryption_key: [u8; 32] = hkdf.expand(b"doby_encryption_key"); let authentication_key: [u8; 32] = hkdf.expand(b"doby_authentication_key"); ``` -Next, doby initializes a [BLAKE3](https://en.wikipedia.org/wiki/BLAKE_%28hash_function%29#BLAKE3) HMAC with `authentication_key` and add all public encryption parameters to it. +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( authentication_key, - blake3, //hash function + blake2b, //hash function ); -hmac.update(random_password_salt); +hmac.update(random_salt); hmac.update(argon2_time_cost); hmac.update(argon2_memory_cost); hmac.update(argon2_parallelism); -hmac.update(random_hkdf_salt); hmac.update(cipher); //1-byte representation of the symmetric cipher used to encrypt (either AES-CTR or XChaCha20) hmac.update(random_nonce); //random nonce used for encryption (16 bytes for AES-CTR, 24 for XChaCha20) ``` @@ -177,14 +176,14 @@ doby reads the public encryption values from the input header to get all paramet ```rust let master_key: [u8; 32] = argon2id( password, - password_salt_read_from_input, + salt_read_from_input, argon2_time_cost_read_from_input, argon2_memory_cost_read_from_input, argon2_parallelism_read_from_input, ); ``` -`encryption_key` and `authentication_key` are computed from `master_key` and the HKDF salt in the same way as during encryption. The HMAC is also initialized and updated with the values read from the header. +`encryption_key` and `authentication_key` are computed from `master_key` in the same way as during encryption. The HMAC is also initialized and updated with the values read from the header. Then, doby starts decryption. @@ -203,7 +202,7 @@ while n != 0 { Once the whole ciphertext is decrypted, doby computes and verifies the HMAC. ```rust -hmac.digest() == last_32_bytes_read +hmac.digest() == last_64_bytes_read // the default blake2b output size is 64 bytes ``` If the verification success, the file is successfully decrypted and authenticated. diff --git a/src/crypto.rs b/src/crypto.rs index d769ca2..fba4c35 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -13,11 +13,11 @@ use hkdf::Hkdf; use zeroize::Zeroize; use crate::Password; -const SALT_LEN: usize = 64; +pub const SALT_LEN: usize = 64; const AES_NONCE_LEN: usize = 16; const XCHACHA20_NONCE_LEN: usize = 24; -const HASH_LEN: usize = 32; -const KEY_LEN: usize = HASH_LEN; +pub const HASH_LEN: usize = 64; +const KEY_LEN: usize = 32; #[derive(Debug, PartialEq, Eq)] pub struct ArgonParams { @@ -53,54 +53,47 @@ impl Display for CipherAlgorithm { #[derive(Debug, PartialEq, Eq)] pub struct EncryptionParams { - password_salt: [u8; SALT_LEN], + salt: [u8; SALT_LEN], pub argon2: ArgonParams, - hkdf_salt: [u8; SALT_LEN], nonce: Vec, pub cipher: CipherAlgorithm, } impl EncryptionParams { fn get_params_len(&self) -> usize { - SALT_LEN*2 + 4*2 + 2 + self.cipher.get_nonce_size() + SALT_LEN + 4*2 + 2 + self.cipher.get_nonce_size() } pub fn new(argon2_params: ArgonParams, cipher: CipherAlgorithm) -> EncryptionParams { - let mut password_salt = [0; SALT_LEN]; - OsRng.fill(&mut password_salt); - let mut hkdf_salt = [0; SALT_LEN]; - OsRng.fill(&mut hkdf_salt); + let mut salt = [0; SALT_LEN]; + OsRng.fill(&mut salt); let mut nonce = vec![0; cipher.get_nonce_size()]; OsRng.fill(&mut nonce[..]); EncryptionParams { - password_salt, + salt, argon2: argon2_params, - hkdf_salt, nonce, cipher, } } pub fn write(&self, writer: &mut W) -> io::Result<()> { - writer.write_all(&self.password_salt)?; + writer.write_all(&self.salt)?; writer.write_all(&self.argon2.t_cost.to_be_bytes())?; writer.write_all(&self.argon2.m_cost.to_be_bytes())?; writer.write_all(&self.argon2.parallelism.to_be_bytes())?; - writer.write_all(&self.hkdf_salt)?; writer.write_all(&(self.cipher as u8).to_be_bytes())?; writer.write_all(&self.nonce)?; Ok(()) } pub fn read(reader: &mut R) -> io::Result> { - let mut password_salt = [0; SALT_LEN]; - reader.read_exact(&mut password_salt)?; + let mut salt = [0; SALT_LEN]; + reader.read_exact(&mut salt)?; let mut t_cost = [0; 4]; reader.read_exact(&mut t_cost)?; let mut m_cost = [0; 4]; reader.read_exact(&mut m_cost)?; let mut parallelism = [0; 1]; reader.read_exact(&mut parallelism)?; - let mut hkdf_salt = [0; SALT_LEN]; - reader.read_exact(&mut hkdf_salt)?; let mut cipher_buff = [0; 1]; reader.read_exact(&mut cipher_buff)?; match CipherAlgorithm::try_from(cipher_buff[0]) { @@ -115,9 +108,8 @@ impl EncryptionParams { }; Ok(Some(EncryptionParams { - password_salt, + salt, argon2: argon2_params, - hkdf_salt, nonce, cipher, })) @@ -140,7 +132,7 @@ impl ThenZeroize for Result { pub struct DobyCipher { cipher: Box, - hmac: Hmac, + hmac: Hmac, buffer: Vec, } @@ -150,8 +142,8 @@ impl DobyCipher { Ok(argon2) => { let mut master_key = [0; KEY_LEN]; let password = password.unwrap_or_ask(); - argon2.hash_password_into(Algorithm::Argon2id, password.as_bytes(), ¶ms.password_salt, &[], &mut master_key).zeroize(password)?; - let hkdf = Hkdf::::new(Some(¶ms.hkdf_salt), &master_key); + argon2.hash_password_into(Algorithm::Argon2id, password.as_bytes(), ¶ms.salt, &[], &mut master_key).zeroize(password)?; + let hkdf = Hkdf::::new(Some(¶ms.salt), &master_key); let mut encryption_key = [0; KEY_LEN]; hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap(); let mut authentication_key = [0; KEY_LEN]; @@ -229,17 +221,16 @@ mod tests { parallelism: 1, }, CipherAlgorithm::XChaCha20); - assert_eq!(params.get_params_len(), 162); + assert_eq!(params.get_params_len(), 98); - let mut buff = Vec::with_capacity(162); + let mut buff = Vec::with_capacity(98); params.write(&mut buff).unwrap(); - assert_eq!(buff[..64], params.password_salt); + assert_eq!(buff[..64], params.salt); assert_eq!(buff[64..68], vec![0, 0, 0, 0x01]); //t_cost assert_eq!(buff[68..72], vec![0, 0, 0, 0x08]); //m_cost assert_eq!(buff[72], 0x01); //parallelism - assert_eq!(buff[73..137], params.hkdf_salt); - assert_eq!(buff[137], CipherAlgorithm::XChaCha20 as u8); - assert_eq!(buff[138..], params.nonce); + assert_eq!(buff[73], CipherAlgorithm::XChaCha20 as u8); + assert_eq!(buff[74..], params.nonce); let new_params = EncryptionParams::read(&mut buff.as_slice()).unwrap().unwrap(); assert_eq!(new_params, params); diff --git a/src/lib.rs b/src/lib.rs index 64a4f61..25a0c1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,8 +85,8 @@ pub fn encrypt(reader: &mut R, writer: &mut W, params: &Encry cipher.encrypt_chunk(&mut buff[..n], writer)?; } } - cipher.write_hmac(writer)?; } + cipher.write_hmac(writer)?; Ok(()) } diff --git a/tests/cli.rs b/tests/cli.rs index a721c42..e35e6db 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,7 +1,7 @@ use std::{io::{self, Read, Write}, fs::{self, File, create_dir}, path::PathBuf}; use assert_cmd::{Command, cargo::{CargoError, cargo_bin}}; use tempfile::TempDir; -use doby::crypto::CipherAlgorithm; +use doby::crypto::{CipherAlgorithm, SALT_LEN, HASH_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()+190); + assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+158); let tmp_decrypted_1 = tmp_path.join("decrypted_1"); doby_cmd().unwrap().arg(tmp_ciphertext_2).arg(&tmp_decrypted_1).assert().success().stdout("").stderr(""); @@ -107,8 +107,8 @@ fn test_cipher(cipher_str: &str, cipher_algorithm: CipherAlgorithm) -> io::Resul doby_cmd().unwrap().arg("-c").arg(cipher_str).arg(tmp_plaintext).arg(&tmp_ciphertext).assert().success().stdout("").stderr(""); let ciphertext = fs::read(&tmp_ciphertext)?; - assert_eq!(ciphertext[4+64*2+4*2+1], cipher_algorithm as u8); - assert_eq!(ciphertext.len(), PLAINTEXT.len()+174+cipher_algorithm.get_nonce_size()); + assert_eq!(ciphertext[4+SALT_LEN+4*2+1], cipher_algorithm as u8); + assert_eq!(ciphertext.len(), PLAINTEXT.len()+14+SALT_LEN+HASH_LEN+cipher_algorithm.get_nonce_size()); doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");