Reduce HMAC output to 32 bytes
This commit is contained in:
parent
1f50973381
commit
10153f6316
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -220,10 +220,10 @@ dependencies = [
|
||||
"clap",
|
||||
"cpufeatures 0.2.1",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"num_enum",
|
||||
"rand",
|
||||
"rpassword",
|
||||
"subtle",
|
||||
"tempfile",
|
||||
"zeroize",
|
||||
]
|
||||
|
@ -21,7 +21,7 @@ num_enum = "0.5"
|
||||
cpufeatures = "0.2"
|
||||
aes = { version = "0.7", features = ["ctr"] }
|
||||
chacha20 = "0.8"
|
||||
hmac = "0.11"
|
||||
subtle = "2.4"
|
||||
blake2 = "0.9"
|
||||
hkdf = "0.11"
|
||||
argon2 = "0.3"
|
||||
|
10
README.md
10
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 142 bytes
|
||||
* Increase the plaintext size of only 113 bytes
|
||||
* Encryption from STDIN/STDOUT or from files
|
||||
* Adjustable performance & secuity parameters
|
||||
|
||||
@ -152,9 +152,9 @@ NOTE: To reduce the size of the header, the `nonce` is derived from the `master_
|
||||
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(
|
||||
let hmac = Blake2b::new_keyed(
|
||||
authentication_key,
|
||||
blake2b, //hash function
|
||||
32, //digest size
|
||||
);
|
||||
hmac.update(random_salt);
|
||||
//integers are encoded in big-endian
|
||||
@ -217,7 +217,7 @@ So here is what an encrypted file layout looks like:
|
||||
</tr>
|
||||
<tr>
|
||||
<th align="left">HMAC</th>
|
||||
<td>64 bytes</td>
|
||||
<td>32 bytes</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@ -254,7 +254,7 @@ while n != 0 {
|
||||
Once the whole ciphertext is decrypted, doby computes and verifies the HMAC.
|
||||
|
||||
```rust
|
||||
hmac.digest() == last_64_bytes_read // the default blake2b output size is 64 bytes
|
||||
hmac.digest() == last_32_bytes_read
|
||||
```
|
||||
|
||||
If the verification success, the file is successfully decrypted and authenticated.
|
||||
|
@ -1,8 +1,9 @@
|
||||
use std::{convert::TryFrom, fmt::{self, Display, Formatter}, io::{self, Read, Write}};
|
||||
use blake2::{Blake2b, VarBlake2b, digest::{Update, VariableOutput}};
|
||||
use num_enum::TryFromPrimitive;
|
||||
use chacha20::XChaCha20;
|
||||
use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}};
|
||||
use hmac::{Hmac, Mac, NewMac};
|
||||
use subtle::ConstantTimeEq;
|
||||
use rand::{Rng, rngs::OsRng};
|
||||
use argon2::{Argon2, Version, Algorithm};
|
||||
use hkdf::Hkdf;
|
||||
@ -11,7 +12,7 @@ use zeroize::Zeroize;
|
||||
pub const SALT_LEN: usize = 64;
|
||||
const AES_NONCE_LEN: usize = 16;
|
||||
const XCHACHA20_NONCE_LEN: usize = 24;
|
||||
pub const HASH_LEN: usize = 64;
|
||||
pub const HMAC_LEN: usize = 32;
|
||||
const KEY_LEN: usize = 32;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)]
|
||||
@ -99,7 +100,7 @@ impl EncryptionParams {
|
||||
|
||||
pub struct DobyCipher {
|
||||
cipher: Box<dyn StreamCipher>,
|
||||
hmac: Hmac<blake2::Blake2b>,
|
||||
hasher: VarBlake2b,
|
||||
buffer: Vec<u8>,
|
||||
}
|
||||
|
||||
@ -108,7 +109,7 @@ impl DobyCipher {
|
||||
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, params.argon2.clone());
|
||||
let mut master_key = [0; KEY_LEN];
|
||||
argon2.hash_password_into(password, ¶ms.salt, &mut master_key).unwrap();
|
||||
let hkdf = Hkdf::<blake2::Blake2b>::new(Some(¶ms.salt), &master_key);
|
||||
let hkdf = Hkdf::<Blake2b>::new(Some(¶ms.salt), &master_key);
|
||||
master_key.zeroize();
|
||||
let mut nonce = vec![0; params.cipher.get_nonce_size()];
|
||||
hkdf.expand(b"doby_nonce", &mut nonce).unwrap();
|
||||
@ -119,9 +120,9 @@ impl DobyCipher {
|
||||
|
||||
let mut encoded_params = Vec::with_capacity(EncryptionParams::LEN);
|
||||
params.write(&mut encoded_params).unwrap();
|
||||
let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap();
|
||||
let mut hasher = VarBlake2b::new_keyed(&authentication_key, HMAC_LEN);
|
||||
authentication_key.zeroize();
|
||||
hmac.update(&encoded_params);
|
||||
hasher.update(&encoded_params);
|
||||
|
||||
let cipher: Box<dyn StreamCipher> = match params.cipher {
|
||||
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &nonce).unwrap()),
|
||||
@ -131,20 +132,19 @@ impl DobyCipher {
|
||||
|
||||
Self {
|
||||
cipher,
|
||||
hmac,
|
||||
hasher,
|
||||
buffer: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encrypt_chunk<W: Write>(&mut self, buff: &mut [u8], writer: &mut W) -> io::Result<()> {
|
||||
self.cipher.apply_keystream(buff);
|
||||
self.hmac.update(buff);
|
||||
self.hasher.update(&buff);
|
||||
writer.write_all(buff)
|
||||
}
|
||||
|
||||
pub fn write_hmac<W: Write>(self, writer: &mut W) -> io::Result<usize> {
|
||||
let tag = self.hmac.finalize().into_bytes();
|
||||
writer.write(&tag)
|
||||
pub fn write_hmac<W: Write>(self, writer: &mut W) -> io::Result<()> {
|
||||
writer.write_all(&self.hasher.finalize_boxed())
|
||||
}
|
||||
|
||||
//buff size must be > to HASH_LEN
|
||||
@ -153,27 +153,27 @@ impl DobyCipher {
|
||||
buff[..buffer_len].clone_from_slice(&self.buffer);
|
||||
let read = reader.read(&mut buff[buffer_len..])?;
|
||||
|
||||
let n = if buffer_len + read >= HASH_LEN {
|
||||
let n = if buffer_len + read >= HMAC_LEN {
|
||||
self.buffer.clear();
|
||||
buffer_len + read - HASH_LEN
|
||||
buffer_len + read - HMAC_LEN
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.buffer.extend_from_slice(&buff[n..buffer_len+read]);
|
||||
|
||||
self.hmac.update(&buff[..n]);
|
||||
self.hasher.update(&buff[..n]);
|
||||
self.cipher.apply_keystream(&mut buff[..n]);
|
||||
Ok(n)
|
||||
}
|
||||
|
||||
pub fn verify_hmac(self) -> bool {
|
||||
self.hmac.verify(&self.buffer).is_ok()
|
||||
self.hasher.finalize_boxed().ct_eq(&self.buffer).into()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HASH_LEN};
|
||||
use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HMAC_LEN};
|
||||
#[test]
|
||||
fn encryption_params() {
|
||||
let params = EncryptionParams::new(
|
||||
@ -204,17 +204,17 @@ mod tests {
|
||||
let password = "I like spaghetti";
|
||||
let plaintext = b"but I love so much to listen to HARDCORE music on big subwoofer";
|
||||
let mut buff: [u8; 63] = *plaintext;
|
||||
let mut vec = Vec::with_capacity(buff.len()+HASH_LEN);
|
||||
let mut vec = Vec::with_capacity(buff.len()+HMAC_LEN);
|
||||
|
||||
let mut enc_cipher = DobyCipher::new(password.as_bytes(), ¶ms);
|
||||
enc_cipher.encrypt_chunk(&mut buff, &mut vec).unwrap();
|
||||
assert_ne!(buff, *plaintext);
|
||||
assert_eq!(buff, vec.as_slice());
|
||||
assert_eq!(enc_cipher.write_hmac(&mut vec).unwrap(), HASH_LEN);
|
||||
assert_eq!(vec.len(), buff.len()+HASH_LEN);
|
||||
assert!(enc_cipher.write_hmac(&mut vec).is_ok());
|
||||
assert_eq!(vec.len(), buff.len()+HMAC_LEN);
|
||||
|
||||
let mut dec_cipher = DobyCipher::new(password.as_bytes(), ¶ms);
|
||||
let mut decrypted = vec![0; buff.len()+HASH_LEN];
|
||||
let mut decrypted = vec![0; buff.len()+HMAC_LEN];
|
||||
let mut n = dec_cipher.decrypt_chunk(&mut vec.as_slice(), &mut decrypted[..]).unwrap();
|
||||
assert_eq!(n, buff.len());
|
||||
n = dec_cipher.decrypt_chunk(&mut &vec[n..], &mut decrypted[n..]).unwrap();
|
||||
|
@ -18,7 +18,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()+145;
|
||||
const CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+113;
|
||||
const PASSWORD: &str = "the password";
|
||||
let params = EncryptionParams::new(
|
||||
argon2::Params::new(8, 1, 1, None).unwrap(),
|
||||
|
@ -1,7 +1,7 @@
|
||||
use std::{convert::TryInto, fs::{self, File, create_dir}, io::{self, Read, Write}, path::PathBuf};
|
||||
use assert_cmd::{Command, cargo::{CargoError, cargo_bin}};
|
||||
use tempfile::TempDir;
|
||||
use doby::crypto::{CipherAlgorithm, SALT_LEN, HASH_LEN};
|
||||
use doby::crypto::{CipherAlgorithm, SALT_LEN, HMAC_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()+142);
|
||||
assert!(buff_ciphertext_2.len() >= buff_ciphertext_1.len()+113);
|
||||
|
||||
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*3], cipher_algorithm as u8);
|
||||
assert_eq!(ciphertext.len(), PLAINTEXT.len()+17+SALT_LEN+HASH_LEN);
|
||||
assert_eq!(ciphertext.len(), PLAINTEXT.len()+17+SALT_LEN+HMAC_LEN);
|
||||
|
||||
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user