Switch to BLAKE2b & Use the same salt for Argon2 and HKDF

This commit is contained in:
Matéo Duparc 2021-07-08 12:21:14 +02:00
parent 5ada55a93d
commit ed7a227080
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
6 changed files with 42 additions and 97 deletions

55
Cargo.lock generated
View File

@ -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",

View File

@ -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"

View File

@ -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.

View File

@ -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<u8>,
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<W: 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<R: Read>(reader: &mut R) -> io::Result<Option<Self>> {
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<S, E> ThenZeroize for Result<S, E> {
pub struct DobyCipher {
cipher: Box<dyn StreamCipher>,
hmac: Hmac<blake3::Hasher>,
hmac: Hmac<blake2::Blake2b>,
buffer: Vec<u8>,
}
@ -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(), &params.password_salt, &[], &mut master_key).zeroize(password)?;
let hkdf = Hkdf::<blake3::Hasher>::new(Some(&params.hkdf_salt), &master_key);
argon2.hash_password_into(Algorithm::Argon2id, password.as_bytes(), &params.salt, &[], &mut master_key).zeroize(password)?;
let hkdf = Hkdf::<blake2::Blake2b>::new(Some(&params.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);

View File

@ -85,8 +85,8 @@ pub fn encrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, params: &Encry
cipher.encrypt_chunk(&mut buff[..n], writer)?;
}
}
cipher.write_hmac(writer)?;
}
cipher.write_hmac(writer)?;
Ok(())
}

View File

@ -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("");