Compare commits

...

3 Commits

Author SHA1 Message Date
Matéo Duparc 51f0a8b2fe
Add CLI test for Argon2 params 2021-07-04 16:28:26 +02:00
Matéo Duparc 6c8a4013cc
Ask password just when needed 2021-07-04 16:26:07 +02:00
Matéo Duparc 007d96dedd
Add integration test 2021-07-04 14:38:51 +02:00
8 changed files with 261 additions and 43 deletions

130
Cargo.lock generated
View File

@ -46,6 +46,20 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "assert_cmd"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa2dfc8228c6260bf620fc5a341afa8e27edcde388b19ffc5732320bfe657eb2"
dependencies = [
"bstr",
"doc-comment",
"predicates",
"predicates-core",
"predicates-tree",
"wait-timeout",
]
[[package]]
name = "atty"
version = "0.2.14"
@ -95,6 +109,17 @@ dependencies = [
"digest",
]
[[package]]
name = "bstr"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
]
[[package]]
name = "cc"
version = "1.0.68"
@ -203,6 +228,12 @@ dependencies = [
"syn",
]
[[package]]
name = "difference"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198"
[[package]]
name = "digest"
version = "0.9.0"
@ -218,6 +249,7 @@ version = "0.1.0"
dependencies = [
"aes",
"argon2",
"assert_cmd",
"blake3",
"chacha20",
"clap",
@ -227,9 +259,16 @@ dependencies = [
"num_enum",
"rand",
"rpassword",
"tempfile",
"zeroize",
]
[[package]]
name = "doc-comment"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
[[package]]
name = "generic-array"
version = "0.14.4"
@ -280,12 +319,24 @@ dependencies = [
"digest",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
[[package]]
name = "memchr"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "num_enum"
version = "0.5.1"
@ -331,6 +382,32 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "predicates"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49cfaf7fdaa3bfacc6fa3e7054e65148878354a5cfddcf661df4c851f8021df"
dependencies = [
"difference",
"predicates-core",
]
[[package]]
name = "predicates-core"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57e35a3326b75e49aa85f5dc6ec15b41108cf5aee58eabb1f274dd18b73c2451"
[[package]]
name = "predicates-tree"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15f553275e5721409451eb85e15fd9a860a6e5ab4496eb215987502b5f5391f2"
dependencies = [
"predicates-core",
"treeline",
]
[[package]]
name = "proc-macro-crate"
version = "0.1.5"
@ -398,6 +475,30 @@ dependencies = [
"rand_core",
]
[[package]]
name = "redox_syscall"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
dependencies = [
"bitflags",
]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
dependencies = [
"winapi",
]
[[package]]
name = "rpassword"
version = "5.0.1"
@ -437,6 +538,20 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "tempfile"
version = "3.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dac1c663cfc93810f88aed9b8941d48cabf856a1b111c29a40439018d870eb22"
dependencies = [
"cfg-if 1.0.0",
"libc",
"rand",
"redox_syscall",
"remove_dir_all",
"winapi",
]
[[package]]
name = "textwrap"
version = "0.11.0"
@ -455,6 +570,12 @@ dependencies = [
"serde",
]
[[package]]
name = "treeline"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f741b240f1a48843f9b8e0444fb55fb2a4ff67293b50a9179dfd5ea67f8d41"
[[package]]
name = "typenum"
version = "1.13.0"
@ -485,6 +606,15 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "wait-timeout"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
dependencies = [
"libc",
]
[[package]]
name = "wasi"
version = "0.10.2+wasi-snapshot-preview1"

View File

@ -26,4 +26,8 @@ blake3 = "0.3"
hkdf = "0.11"
argon2 = "0.2"
rpassword = "5.0"
zeroize = "1.3"
zeroize = "1.3"
[dev-dependencies]
assert_cmd = "1.0"
tempfile = "3.0"

View File

@ -10,7 +10,7 @@ use doby::{
};
const MAX_BLOCK_SIZE: usize = 1_073_741_824; //1GB
const PASSWORD: &[u8] = b"HARDCORE music is the best music of all time";
const PASSWORD: &str = "HARDCORE music is the best music of all time";
fn set_if_better(best_time: &mut Option<u128>, time: u128, best_block_size: &mut Option<usize>, block_size: usize) {
let mut better = true;
@ -49,7 +49,7 @@ fn main() -> io::Result<()> {
let mut reader = BufReader::with_capacity(block_size, &input);
let mut writer = BufWriter::with_capacity(block_size, &output);
let cipher = DobyCipher::new(PASSWORD, &params).unwrap();
let cipher = DobyCipher::new(PASSWORD.into(), &params).unwrap();
let t_encrypt = Instant::now();
encrypt(&mut reader, &mut writer, &params, cipher, block_size, None)?;
writer.flush()?;
@ -59,7 +59,7 @@ fn main() -> io::Result<()> {
reset(&mut reader)?;
reset(&mut writer)?;
let cipher = DobyCipher::new(PASSWORD, &params).unwrap();
let cipher = DobyCipher::new(PASSWORD.into(), &params).unwrap();
let t_decrypt = Instant::now();
decrypt(&mut reader, &mut writer, cipher, block_size)?;
writer.flush()?;

View File

@ -5,12 +5,12 @@ use std::{
io::{stdin, stdout, Read, Write},
};
use clap::{crate_name, crate_version, App, Arg, AppSettings};
use crate::crypto::{ArgonParams, CipherAlgorithm};
use crate::{Password, crypto::{ArgonParams, CipherAlgorithm}};
cpufeatures::new!(aes_ni, "aes");
pub struct CliArgs {
pub password: String,
pub password: Password,
pub force_encrypt: bool,
pub argon2_params: ArgonParams,
pub cipher: CipherAlgorithm,
@ -130,13 +130,8 @@ pub fn parse() -> Option<CliArgs> {
})
.unwrap_or_else(|| Some(Box::new(stdout())))?;
let password = match app.value_of("1_password") {
Some(s) => s.to_string(),
None => rpassword::read_password_from_tty(Some("Password: ")).unwrap(),
};
Some(CliArgs {
password,
password: app.value_of("1_password").into(),
force_encrypt: app.is_present("force-encrypt"),
argon2_params: params,
cipher,

View File

@ -11,6 +11,7 @@ use rand::{Rng, rngs::OsRng};
use argon2::{Argon2, Version, Algorithm};
use hkdf::Hkdf;
use zeroize::Zeroize;
use crate::Password;
const SALT_LEN: usize = 64;
const AES_NONCE_LEN: usize = 16;
@ -126,6 +127,17 @@ impl EncryptionParams {
}
}
trait ThenZeroize {
fn zeroize<T: Zeroize>(self, v: T) -> Self;
}
impl<S, E> ThenZeroize for Result<S, E> {
fn zeroize<T: Zeroize>(self, mut v: T) -> Self {
v.zeroize();
self
}
}
pub struct DobyCipher {
cipher: Box<dyn StreamCipher>,
hmac: Hmac<blake3::Hasher>,
@ -133,31 +145,42 @@ pub struct DobyCipher {
}
impl DobyCipher {
pub fn new(password: &[u8], params: &EncryptionParams) -> Result<Self, argon2::Error> {
let argon = Argon2::new(None, params.argon2.t_cost, params.argon2.m_cost, params.argon2.parallelism.into(), Version::V0x13)?;
let mut master_key = [0; KEY_LEN];
argon.hash_password_into(Algorithm::Argon2id, password, &params.password_salt, &[], &mut master_key)?;
pub fn new(mut password: Password, params: &EncryptionParams) -> Result<Self, argon2::Error> {
match Argon2::new(None, params.argon2.t_cost, params.argon2.m_cost, params.argon2.parallelism.into(), Version::V0x13) {
Ok(argon2) => {
let mut master_key = [0; KEY_LEN];
let password = password.unwrap_or_ask();
argon2.hash_password_into(Algorithm::Argon2id, password.as_bytes(), &params.password_salt, &[], &mut master_key).zeroize(password)?;
let hkdf = Hkdf::<blake3::Hasher>::new(Some(&params.hkdf_salt), &master_key);
let mut encryption_key = [0; KEY_LEN];
hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap();
let mut authentication_key = [0; KEY_LEN];
hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap();
master_key.zeroize();
let hkdf = Hkdf::<blake3::Hasher>::new(Some(&params.hkdf_salt), &master_key);
let mut encryption_key = [0; KEY_LEN];
hkdf.expand(b"doby_encryption_key", &mut encryption_key).unwrap();
let mut authentication_key = [0; KEY_LEN];
hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap();
master_key.zeroize();
let mut encoded_params = Vec::with_capacity(params.get_params_len());
params.write(&mut encoded_params).unwrap();
let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap();
authentication_key.zeroize();
hmac.update(&encoded_params);
let mut encoded_params = Vec::with_capacity(params.get_params_len());
params.write(&mut encoded_params).unwrap();
let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap();
hmac.update(&encoded_params);
let cipher: Box<dyn StreamCipher> = match params.cipher {
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &params.nonce).unwrap()),
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &params.nonce).unwrap()),
};
encryption_key.zeroize();
Ok(Self {
cipher: match params.cipher {
CipherAlgorithm::AesCtr => Box::new(Aes256Ctr::new_from_slices(&encryption_key, &params.nonce).unwrap()),
CipherAlgorithm::XChaCha20 => Box::new(XChaCha20::new_from_slices(&encryption_key, &params.nonce).unwrap()),
},
hmac,
buffer: Vec::new(),
})
Ok(Self {
cipher,
hmac,
buffer: Vec::new(),
})
}
Err(e) => {
password.zeroize();
Err(e)
}
}
}
pub fn encrypt_chunk<W: Write>(&mut self, buff: &mut [u8], writer: &mut W) -> io::Result<()> {
@ -229,19 +252,19 @@ mod tests {
m_cost: 8,
parallelism: 1,
}, CipherAlgorithm::AesCtr);
let password = b"I like spaghetti";
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 enc_cipher = DobyCipher::new(password, &params).unwrap();
let mut enc_cipher = DobyCipher::new(password.into(), &params).unwrap();
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);
let mut dec_cipher = DobyCipher::new(password, &params).unwrap();
let mut dec_cipher = DobyCipher::new(password.into(), &params).unwrap();
let mut decrypted = vec![0; buff.len()+HASH_LEN];
let mut n = dec_cipher.decrypt_chunk(&mut vec.as_slice(), &mut decrypted[..]).unwrap();
assert_eq!(n, buff.len());

View File

@ -3,9 +3,36 @@ pub mod crypto;
use std::io::{self, Read, Write};
use crypto::{DobyCipher, EncryptionParams};
use zeroize::Zeroize;
pub const MAGIC_BYTES: &[u8; 4] = b"DOBY";
pub struct Password(Option<String>);
impl Password {
fn unwrap_or_ask(self) -> String {
self.0.unwrap_or_else(|| rpassword::read_password_from_tty(Some("Password: ")).unwrap())
}
}
impl From<Option<&str>> for Password {
fn from(s: Option<&str>) -> Self {
Self(s.map(|s| String::from(s)))
}
}
impl From<&str> for Password {
fn from(s: &str) -> Self {
Some(s).into()
}
}
impl Zeroize for Password {
fn zeroize(&mut self) {
self.0.zeroize()
}
}
pub fn encrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, params: &EncryptionParams, mut cipher: DobyCipher, block_size: usize, already_read: Option<Vec<u8>>) -> io::Result<()> {
writer.write_all(MAGIC_BYTES)?;
params.write(writer)?;

View File

@ -1,5 +1,4 @@
use std::io::{BufWriter, BufReader, Read};
use zeroize::Zeroize;
use doby::{
cli,
crypto::{EncryptionParams, DobyCipher},
@ -9,10 +8,10 @@ use doby::{
};
fn main() {
if let Some(mut cli_args) = cli::parse() {
if let Some(cli_args) = cli::parse() {
let mut reader = BufReader::with_capacity(cli_args.block_size, cli_args.reader);
let mut writer = BufWriter::with_capacity(cli_args.block_size, cli_args.writer);
let mut magic_bytes = vec![0; MAGIC_BYTES.len()];
match reader.read(&mut magic_bytes) {
Ok(n) => {
@ -24,7 +23,7 @@ fn main() {
Ok(params) => {
match params {
Some(params) => {
match DobyCipher::new(cli_args.password.as_bytes(), &params) {
match DobyCipher::new(cli_args.password, &params) {
Ok(cipher) => {
match decrypt(&mut reader, &mut writer, cipher, cli_args.block_size) {
Ok(verified) => {
@ -45,7 +44,7 @@ fn main() {
}
} else { //otherwise, encrypt
let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher);
match DobyCipher::new(cli_args.password.as_bytes(), &params) {
match DobyCipher::new(cli_args.password, &params) {
Ok(cipher) => {
if let Err(e) = encrypt(
&mut reader,
@ -64,6 +63,5 @@ fn main() {
}
Err(e) => eprintln!("I/O error while reading magic bytes: {}", e),
}
cli_args.password.zeroize();
}
}

41
tests/cli.rs Normal file
View File

@ -0,0 +1,41 @@
use std::{io::{self, Read, Write}, fs::File, path::PathBuf};
use assert_cmd::{Command, cargo::CargoError};
use tempfile::{NamedTempFile, TempDir};
fn doby_cmd<>() -> Result<Command, CargoError> {
const PASSWORD: &str = "the password";
let mut cmd = Command::cargo_bin("doby")?;
cmd.arg("-p").arg(PASSWORD);
Ok(cmd)
}
#[test]
fn files() -> io::Result<()> {
const PLAINTEXT: &[u8] = b"the plaintext";
let tmp_dir = TempDir::new()?;
let tmp_path = PathBuf::from(tmp_dir.path());
let mut tmp_plaintext = NamedTempFile::new_in(&tmp_dir)?;
tmp_plaintext.write_all(PLAINTEXT)?;
let tmp_ciphertext = tmp_path.join("ciphertext");
doby_cmd().unwrap().arg(tmp_plaintext.path()).arg(&tmp_ciphertext).assert().success().stdout("").stderr("");
let tmp_decrypted = tmp_path.join("decryped");
doby_cmd().unwrap().arg(tmp_ciphertext).arg(&tmp_decrypted).assert().success().stdout("").stderr("");
let mut tmp_decrypted = File::open(tmp_decrypted).unwrap();
let mut buff = [0; PLAINTEXT.len()];
assert_eq!(tmp_decrypted.read(&mut buff)?, PLAINTEXT.len());
assert_eq!(buff, PLAINTEXT);
Ok(())
}
#[test]
fn argon2_params() -> io::Result<()> {
Command::cargo_bin("doby").unwrap().arg("-i").arg("0").assert().stderr("Invalid argon2 params: time cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-m").arg("0").assert().stderr("Invalid argon2 params: memory cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-t").arg("0").assert().stderr("Invalid argon2 params: too few lanes\n");
Ok(())
}