Browse Source

Reduce HMAC output to 32 bytes

master
Hardcore Sushi 7 months ago
parent
commit
10153f6316
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
  1. 2
      Cargo.lock
  2. 2
      Cargo.toml
  3. 10
      README.md
  4. 40
      src/crypto.rs
  5. 2
      tests/authentication.rs
  6. 6
      tests/cli.rs

2
Cargo.lock generated

@ -220,10 +220,10 @@ dependencies = [
"clap",
"cpufeatures 0.2.1",
"hkdf",
"hmac",
"num_enum",
"rand",
"rpassword",
"subtle",
"tempfile",
"zeroize",
]

2
Cargo.toml

@ -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

@ -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.

40
src/crypto.rs

@ -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, &params.salt, &mut master_key).unwrap();
let hkdf = Hkdf::<blake2::Blake2b>::new(Some(&params.salt), &master_key);
let hkdf = Hkdf::<Blake2b>::new(Some(&params.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(), &params);
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(), &params);
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();

2
tests/authentication.rs

@ -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(),

6
tests/cli.rs

@ -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…
Cancel
Save