Derive nonce with HKDF
This commit is contained in:
parent
9e176d898f
commit
9ac3bd7a53
14
README.md
14
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 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 {
|
||||
|
@ -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(), ¶ms.salt, &[], &mut master_key).zeroize(password)?;
|
||||
let hkdf = Hkdf::<blake2::Blake2b>::new(Some(¶ms.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, ¶ms.nonce).unwrap()),
|
||||
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, ¶ms.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);
|
||||
|
@ -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(), ¶ms).unwrap();
|
||||
let mut ciphertext = Vec::with_capacity(PLAINTEXT.len()+158);
|
||||
let mut ciphertext = Vec::with_capacity(CIPHERTEXT_SIZE);
|
||||
encrypt(&mut &PLAINTEXT[..], &mut ciphertext, ¶ms, 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();
|
||||
|
@ -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("");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user