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)
|
* 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 {
|
||||||
|
|
|
@ -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(), ¶ms.salt, &[], &mut master_key).zeroize(password)?;
|
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 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];
|
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, ¶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, ¶ms.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);
|
||||||
|
|
|
@ -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(), ¶ms).unwrap();
|
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();
|
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() {
|
for i in 0..ciphertext.len() {
|
||||||
let mut compromised = ciphertext.clone();
|
let mut compromised = ciphertext.clone();
|
||||||
|
|
|
@ -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("");
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue