Derive nonce with HKDF

This commit is contained in:
Matéo Duparc 2021-08-30 16:06:55 +02:00
parent 9e176d898f
commit 9ac3bd7a53
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
4 changed files with 19 additions and 26 deletions

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 158 bytes
* Increase the plaintext size of only 142 bytes
* Encryption from STDIN/STDOUT or from files
* Adjustable performance & secuity parameters
@ -133,7 +133,7 @@ let master_key: [u8; 32] = argon2id(
);
```
Then, doby uses [HKDF](https://en.wikipedia.org/wiki/HKDF) with the previous 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 `nonce`, the `encryption_key` and the `authentication_key`.
```rust
let hkdf = Hkdf::new(
@ -141,6 +141,7 @@ let hkdf = Hkdf::new(
master_key, //ikm
blake2b, //hash function
);
let nonce: [u8; 16] = hkdf.expand(b"doby_nonce"); //(16 bytes for AES-CTR, 24 for XChaCha20)
let encryption_key: [u8; 32] = hkdf.expand(b"doby_encryption_key");
let authentication_key: [u8; 32] = hkdf.expand(b"doby_authentication_key");
```
@ -157,15 +158,14 @@ hmac.update(argon2_time_cost);
hmac.update(argon2_memory_cost);
hmac.update(argon2_parallelism);
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)
```
All this parameters are also written in plain text in the header of the doby output.
Now, doby initializes a symmetric cipher with `encryption_key` and `random_nonce` (either AES-CTR or XChaCha20, based on the `--cipher` option) and starts the actual encryption. It reads chunks from the plaintext (according to the `--block-size` parameter), encrypts them with the cipher and updates the HMAC with the ciphertext.
Now, doby initializes a symmetric cipher with `encryption_key` and `nonce` (either AES-CTR or XChaCha20, based on the `--cipher` option) and starts the actual encryption. It reads chunks from the plaintext (according to the `--block-size` parameter), encrypts them with the cipher and updates the HMAC with the ciphertext.
```rust
let cipher = Aes256Ctr::new(encryption_key, random_nonce); //example with AES-CTR
let cipher = Aes256Ctr::new(encryption_key, nonce); //example with AES-CTR
let mut n = 1;
let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 {
@ -196,12 +196,12 @@ let master_key: [u8; 32] = argon2id(
);
```
`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.
`nonce`, `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.
```rust
let cipher = XChaCha20::new(encryption_key, nonce_read_from_input); //example with XChaCha20
let cipher = XChaCha20::new(encryption_key, nonce); //example with XChaCha20
let mut n = 1;
let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 {

View File

@ -55,24 +55,20 @@ impl Display for CipherAlgorithm {
pub struct EncryptionParams {
salt: [u8; SALT_LEN],
pub argon2: ArgonParams,
nonce: Vec<u8>,
pub cipher: CipherAlgorithm,
}
impl EncryptionParams {
pub fn get_params_len(&self) -> usize {
SALT_LEN + 4*2 + 2 + self.cipher.get_nonce_size()
SALT_LEN + 4*2 + 2
}
pub fn new(argon2_params: ArgonParams, cipher: CipherAlgorithm) -> EncryptionParams {
let mut salt = [0; SALT_LEN];
OsRng.fill(&mut salt);
let mut nonce = vec![0; cipher.get_nonce_size()];
OsRng.fill(&mut nonce[..]);
EncryptionParams {
salt,
argon2: argon2_params,
nonce,
cipher,
}
}
@ -82,7 +78,6 @@ impl EncryptionParams {
writer.write_all(&self.argon2.m_cost.to_be_bytes())?;
writer.write_all(&self.argon2.parallelism.to_be_bytes())?;
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>> {
@ -98,9 +93,6 @@ impl EncryptionParams {
reader.read_exact(&mut cipher_buff)?;
match CipherAlgorithm::try_from(cipher_buff[0]) {
Ok(cipher) => {
let mut nonce = vec![0; cipher.get_nonce_size()];
reader.read_exact(&mut nonce)?;
let argon2_params = ArgonParams {
t_cost: u32::from_be_bytes(t_cost),
m_cost: u32::from_be_bytes(m_cost),
@ -110,7 +102,6 @@ impl EncryptionParams {
Ok(Some(EncryptionParams {
salt,
argon2: argon2_params,
nonce,
cipher,
}))
}
@ -144,6 +135,8 @@ impl DobyCipher {
let password = password.unwrap_or_ask();
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 nonce = vec![0; params.cipher.get_nonce_size()];
hkdf.expand(b"doby_nonce", &mut nonce).unwrap();
let mut encryption_key = [0; KEY_LEN];
hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap();
let mut authentication_key = [0; KEY_LEN];
@ -157,8 +150,8 @@ impl DobyCipher {
hmac.update(&encoded_params);
let cipher: Box<dyn StreamCipher> = match params.cipher {
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &params.nonce).unwrap()),
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &params.nonce).unwrap()),
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &nonce).unwrap()),
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &nonce).unwrap()),
};
encryption_key.zeroize();
@ -221,16 +214,15 @@ mod tests {
parallelism: 1,
}, CipherAlgorithm::XChaCha20);
assert_eq!(params.get_params_len(), 98);
assert_eq!(params.get_params_len(), 74);
let mut buff = Vec::with_capacity(98);
let mut buff = Vec::with_capacity(74);
params.write(&mut buff).unwrap();
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], 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

@ -19,6 +19,7 @@ fn different_elements<T: Eq>(v1: &Vec<T>, v2: &Vec<T>) -> usize {
fn authentication() {
const BLOCK_SIZE: usize = 65536;
const PLAINTEXT: &[u8; 13] = b"the plaintext";
const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+142;
const PASSWORD: &str = "the password";
let params = EncryptionParams::new(ArgonParams {
t_cost: 1,
@ -27,9 +28,9 @@ fn authentication() {
}, CipherAlgorithm::AesCtr);
let encrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap();
let mut ciphertext = Vec::with_capacity(PLAINTEXT.len()+158);
let mut ciphertext = Vec::with_capacity(CIPHERTEXT_SIZE);
encrypt(&mut &PLAINTEXT[..], &mut ciphertext, &params, encrypter, BLOCK_SIZE, None).unwrap();
assert_eq!(ciphertext.len(), PLAINTEXT.len()+158);
assert_eq!(ciphertext.len(), CIPHERTEXT_SIZE);
for i in 0..ciphertext.len() {
let mut compromised = ciphertext.clone();

View File

@ -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()+158);
assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+142);
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*2+1], cipher_algorithm as u8);
assert_eq!(ciphertext.len(), PLAINTEXT.len()+14+SALT_LEN+HASH_LEN+cipher_algorithm.get_nonce_size());
assert_eq!(ciphertext.len(), PLAINTEXT.len()+14+SALT_LEN+HASH_LEN);
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");