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) * 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 * [HMAC](https://en.wikipedia.org/wiki/HMAC) ciphertext authentication
* Password brute-force resistance with [Argon2](https://en.wikipedia.org/wiki/Argon2) * 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 * Encryption from STDIN/STDOUT or from files
* Adjustable performance & secuity parameters * 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 ```rust
let hkdf = Hkdf::new( let hkdf = Hkdf::new(
@ -141,6 +141,7 @@ let hkdf = Hkdf::new(
master_key, //ikm master_key, //ikm
blake2b, //hash function 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 encryption_key: [u8; 32] = hkdf.expand(b"doby_encryption_key");
let authentication_key: [u8; 32] = hkdf.expand(b"doby_authentication_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_memory_cost);
hmac.update(argon2_parallelism); hmac.update(argon2_parallelism);
hmac.update(cipher); //1-byte representation of the symmetric cipher used to encrypt (either AES-CTR or XChaCha20) 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. 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 ```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 n = 1;
let mut chunk: [u8; block_size] = [0; block_size]; let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 { 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. Then, doby starts decryption.
```rust ```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 n = 1;
let mut chunk: [u8; block_size] = [0; block_size]; let mut chunk: [u8; block_size] = [0; block_size];
while n != 0 { while n != 0 {

View File

@ -55,24 +55,20 @@ impl Display for CipherAlgorithm {
pub struct EncryptionParams { pub struct EncryptionParams {
salt: [u8; SALT_LEN], salt: [u8; SALT_LEN],
pub argon2: ArgonParams, pub argon2: ArgonParams,
nonce: Vec<u8>,
pub cipher: CipherAlgorithm, pub cipher: CipherAlgorithm,
} }
impl EncryptionParams { impl EncryptionParams {
pub fn get_params_len(&self) -> usize { 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 { pub fn new(argon2_params: ArgonParams, cipher: CipherAlgorithm) -> EncryptionParams {
let mut salt = [0; SALT_LEN]; let mut salt = [0; SALT_LEN];
OsRng.fill(&mut salt); OsRng.fill(&mut salt);
let mut nonce = vec![0; cipher.get_nonce_size()];
OsRng.fill(&mut nonce[..]);
EncryptionParams { EncryptionParams {
salt, salt,
argon2: argon2_params, argon2: argon2_params,
nonce,
cipher, cipher,
} }
} }
@ -82,7 +78,6 @@ impl EncryptionParams {
writer.write_all(&self.argon2.m_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.argon2.parallelism.to_be_bytes())?;
writer.write_all(&(self.cipher as u8).to_be_bytes())?; writer.write_all(&(self.cipher as u8).to_be_bytes())?;
writer.write_all(&self.nonce)?;
Ok(()) Ok(())
} }
pub fn read<R: Read>(reader: &mut R) -> io::Result<Option<Self>> { pub fn read<R: Read>(reader: &mut R) -> io::Result<Option<Self>> {
@ -98,9 +93,6 @@ impl EncryptionParams {
reader.read_exact(&mut cipher_buff)?; reader.read_exact(&mut cipher_buff)?;
match CipherAlgorithm::try_from(cipher_buff[0]) { match CipherAlgorithm::try_from(cipher_buff[0]) {
Ok(cipher) => { Ok(cipher) => {
let mut nonce = vec![0; cipher.get_nonce_size()];
reader.read_exact(&mut nonce)?;
let argon2_params = ArgonParams { let argon2_params = ArgonParams {
t_cost: u32::from_be_bytes(t_cost), t_cost: u32::from_be_bytes(t_cost),
m_cost: u32::from_be_bytes(m_cost), m_cost: u32::from_be_bytes(m_cost),
@ -110,7 +102,6 @@ impl EncryptionParams {
Ok(Some(EncryptionParams { Ok(Some(EncryptionParams {
salt, salt,
argon2: argon2_params, argon2: argon2_params,
nonce,
cipher, cipher,
})) }))
} }
@ -144,6 +135,8 @@ impl DobyCipher {
let password = password.unwrap_or_ask(); let password = password.unwrap_or_ask();
argon2.hash_password_into(Algorithm::Argon2id, password.as_bytes(), &params.salt, &[], &mut master_key).zeroize(password)?; 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 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]; let mut encryption_key = [0; KEY_LEN];
hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap(); hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap();
let mut authentication_key = [0; KEY_LEN]; let mut authentication_key = [0; KEY_LEN];
@ -157,8 +150,8 @@ impl DobyCipher {
hmac.update(&encoded_params); hmac.update(&encoded_params);
let cipher: Box<dyn StreamCipher> = match params.cipher { let cipher: Box<dyn StreamCipher> = match params.cipher {
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::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, &params.nonce).unwrap()), CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &nonce).unwrap()),
}; };
encryption_key.zeroize(); encryption_key.zeroize();
@ -221,16 +214,15 @@ mod tests {
parallelism: 1, parallelism: 1,
}, CipherAlgorithm::XChaCha20); }, 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(); params.write(&mut buff).unwrap();
assert_eq!(buff[..64], params.salt); assert_eq!(buff[..64], params.salt);
assert_eq!(buff[64..68], vec![0, 0, 0, 0x01]); //t_cost 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[68..72], vec![0, 0, 0, 0x08]); //m_cost
assert_eq!(buff[72], 0x01); //parallelism assert_eq!(buff[72], 0x01); //parallelism
assert_eq!(buff[73], CipherAlgorithm::XChaCha20 as u8); 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(); let new_params = EncryptionParams::read(&mut buff.as_slice()).unwrap().unwrap();
assert_eq!(new_params, params); 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() { fn authentication() {
const BLOCK_SIZE: usize = 65536; const BLOCK_SIZE: usize = 65536;
const PLAINTEXT: &[u8; 13] = b"the plaintext"; const PLAINTEXT: &[u8; 13] = b"the plaintext";
const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+142;
const PASSWORD: &str = "the password"; const PASSWORD: &str = "the password";
let params = EncryptionParams::new(ArgonParams { let params = EncryptionParams::new(ArgonParams {
t_cost: 1, t_cost: 1,
@ -27,9 +28,9 @@ fn authentication() {
}, CipherAlgorithm::AesCtr); }, CipherAlgorithm::AesCtr);
let encrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap(); 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(); 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() { for i in 0..ciphertext.len() {
let mut compromised = ciphertext.clone(); 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)?; let buff_ciphertext_2 = fs::read(&tmp_ciphertext_2)?;
assert_ne!(buff_ciphertext_1, buff_ciphertext_2); assert_ne!(buff_ciphertext_1, buff_ciphertext_2);
assert_ne!(buff_ciphertext_2, PLAINTEXT); 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"); let tmp_decrypted_1 = tmp_path.join("decrypted_1");
doby_cmd().unwrap().arg(tmp_ciphertext_2).arg(&tmp_decrypted_1).assert().success().stdout("").stderr(""); 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)?; let ciphertext = fs::read(&tmp_ciphertext)?;
assert_eq!(ciphertext[4+SALT_LEN+4*2+1], cipher_algorithm as u8); 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(""); doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");