Compare commits

...

4 Commits

Author SHA1 Message Date
Matéo Duparc 10153f6316
Reduce HMAC output to 32 bytes 2021-11-14 13:33:16 +01:00
Matéo Duparc 1f50973381
Update help & examples in README 2021-11-14 13:33:09 +01:00
Matéo Duparc 311677d195
Overwrite by default 2021-11-14 11:22:53 +01:00
Matéo Duparc 2c8ab7e8ad
Password confirm 2021-11-14 11:22:03 +01:00
11 changed files with 275 additions and 279 deletions

2
Cargo.lock generated
View File

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

View File

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

View File

@ -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
@ -42,17 +42,17 @@ cat my-super-secret-logs-file.log | doby - logs.doby
Speicfy password from the command line:
```bash
doby -p "A super very ultra strong passphrase" my-super-secret-document.pdf document.doby
doby --password "A super very ultra strong passphrase" my-super-secret-document.pdf document.doby
```
Double encryption:
```bash
doby -p "first password" my-super-secret-database.db | doby -f - double-encrypted.doby
doby --password "first password" my-super-secret-database.db | doby -f - double-encrypted.doby
```
Increase password brute-force resistance:
```bash
echo "you-will-never-break-this" | doby --memory-cost 524288 --threads 16 --iterations 40 > my-super-secret-password.doby
echo "you-will-never-break-this" | doby --memory-cost 524288 --parallelism 16 --time-cost 40 > my-super-secret-password.doby
```
## Full Options
@ -63,14 +63,15 @@ USAGE:
FLAGS:
-f, --force-encrypt Encrypt even if doby format is recognized
-i, --interactive Prompt before overwriting files
-h, --help Prints help information
-V, --version Prints version information
OPTIONS:
-p, --password <password> Password used to derive encryption keys
-i, --iterations <iterations> Argon2 time cost [default: 10]
-m, --memory-cost <memory cost> Argon2 memory cost (in kilobytes) [default: 4096]
-t, --threads <threads> Argon2 parallelism (between 1 and 255) [default: 4]
--password <password> Password used to derive encryption keys
-t, --time-cost <iterations> Argon2 time cost [default: 10]
-m, --memory-cost <memory size> Argon2 memory cost (in kilobytes) [default: 4096]
-p, --parallelism <threads> Argon2 parallelism cost [default: 4]
-b, --block-size <blocksize> Size of the I/O buffer (in bytes) [default: 65536]
-c, --cipher <cipher> Encryption cipher to use [possible values: aes, xchacha20]
@ -151,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
@ -216,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>
@ -253,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.

View File

@ -6,7 +6,7 @@ use std::{
};
use doby::{
encrypt, decrypt,
crypto::{ArgonParams, EncryptionParams, CipherAlgorithm, DobyCipher}
crypto::{EncryptionParams, CipherAlgorithm, DobyCipher}
};
const MAX_BLOCK_SIZE: usize = 1_073_741_824; //1GB
@ -33,11 +33,10 @@ fn main() -> io::Result<()> {
let input = File::open(&args[1])?;
let output = OpenOptions::new().create(true).truncate(true).write(true).open(&args[2])?;
let params = EncryptionParams::new(ArgonParams{
t_cost: 1,
m_cost: 8,
parallelism: 1,
}, CipherAlgorithm::AesCtr);
let params = EncryptionParams::new(
argon2::Params::new(8, 1, 1, None).unwrap(),
CipherAlgorithm::AesCtr
);
let mut best_encrypt_time = None;
let mut best_encrypt_block_size = None;
@ -49,7 +48,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.into(), &params).unwrap();
let cipher = DobyCipher::new(PASSWORD.as_bytes(), &params);
let t_encrypt = Instant::now();
encrypt(&mut reader, &mut writer, &params, cipher, block_size, None)?;
writer.flush()?;
@ -59,7 +58,7 @@ fn main() -> io::Result<()> {
reset(&mut reader)?;
reset(&mut writer)?;
let cipher = DobyCipher::new(PASSWORD.into(), &params).unwrap();
let cipher = DobyCipher::new(PASSWORD.as_bytes(), &params);
let t_decrypt = Instant::now();
decrypt(&mut reader, &mut writer, cipher, block_size)?;
writer.flush()?;

View File

@ -10,12 +10,12 @@ fn main() -> io::Result<()> {
if magic_bytes == MAGIC_BYTES {
match EncryptionParams::read(&mut file)? {
Some(params) => {
println!("Argon2 time cost: {}", params.argon2.t_cost);
println!("Argon2 memory cost: {}KB", params.argon2.m_cost);
println!("Argon2 parallelism: {}", params.argon2.parallelism);
println!("Argon2 time cost: {}", params.argon2.t_cost());
println!("Argon2 memory cost: {}KB", params.argon2.m_cost());
println!("Argon2 parallelism cost: {}", params.argon2.p_cost());
println!("Encryption cihpher: {}", params.cipher);
}
None => eprintln!("Invalid cipher")
None => eprintln!("Invalid parameters")
}
} else {
eprintln!("Doby format not recognized.");

View File

@ -1,25 +1,37 @@
use std::{
path::Path,
fs::File,
str::FromStr,
io::{stdin, stdout, Read},
};
use std::{fs::File, io::{self, Read, stdin, stdout}, path::Path, str::FromStr};
use clap::{crate_name, crate_version, App, Arg, AppSettings};
use crate::{LazyWriter, Password, crypto::{ArgonParams, CipherAlgorithm}};
use crate::{WrappedWriter, WrappedPassword, crypto::CipherAlgorithm};
cpufeatures::new!(aes_ni, "aes");
pub struct CliArgs {
pub password: Password,
pub password: WrappedPassword,
pub force_encrypt: bool,
pub argon2_params: ArgonParams,
pub argon2_params: argon2::Params,
pub cipher: CipherAlgorithm,
pub block_size: usize,
pub reader: Box<dyn Read>,
pub writer: LazyWriter<String>,
pub writer: WrappedWriter<String>,
}
pub fn parse() -> Option<CliArgs> {
pub struct ParseResult {
pub error: bool,
pub cli_args: Option<CliArgs>,
}
impl ParseResult {
fn exited() -> Self {
Self { error: false, cli_args: None }
}
}
impl From<CliArgs> for ParseResult {
fn from(args: CliArgs) -> Self {
ParseResult { error: false, cli_args: Some(args) }
}
}
pub fn parse() -> Option<ParseResult> {
let app = App::new(crate_name!())
.version(crate_version!())
.setting(AppSettings::ColoredHelp)
@ -27,22 +39,27 @@ pub fn parse() -> Option<CliArgs> {
.arg(Arg::with_name("INPUT").help("<PATH> | \"-\" or empty for stdin"))
.arg(Arg::with_name("OUTPUT").help("<PATH> | \"-\" or empty for stdout"))
.arg(
Arg::with_name("force-encrypt")
Arg::with_name("1_force_encrypt")
.short("f")
.long("force-encrypt")
.help(&format!("Encrypt even if {} format is recognized", crate_name!()))
)
.arg(
Arg::with_name("2_interactive")
.short("i")
.long("interactive")
.help("Prompt before overwriting files")
)
.arg(
Arg::with_name("1_password")
.short("p")
.long("password")
.value_name("password")
.help("Password used to derive encryption keys")
)
.arg(
Arg::with_name("2_t_cost")
.short("i")
.long("iterations")
.short("t")
.long("time-cost")
.value_name("iterations")
.help("Argon2 time cost")
.default_value("10")
@ -51,16 +68,16 @@ pub fn parse() -> Option<CliArgs> {
Arg::with_name("3_m_cost")
.short("m")
.long("memory-cost")
.value_name("memory cost")
.value_name("memory size")
.help("Argon2 memory cost (in kilobytes)")
.default_value("4096")
)
.arg(
Arg::with_name("4_parallelism")
.short("t")
.long("threads")
Arg::with_name("4_p_cost")
.short("p")
.long("parallelism")
.value_name("threads")
.help("Argon2 parallelism (between 1 and 255)")
.help("Argon2 parallelism cost")
.default_value("4")
)
.arg(
@ -85,14 +102,16 @@ pub fn parse() -> Option<CliArgs> {
let params = {
let t_cost = number(app.value_of("2_t_cost").unwrap())?;
let m_cost = number(app.value_of("3_m_cost").unwrap())?;
let parallelism = number(app.value_of("4_parallelism").unwrap())?;
let p_cost = number(app.value_of("4_p_cost").unwrap())?;
ArgonParams {
t_cost,
m_cost,
parallelism,
match argon2::Params::new(m_cost, t_cost, p_cost, None) {
Ok(params) => Some(params),
Err(e) => {
eprintln!("Invalid Argon2 parameters: {}", e);
None
}
}
};
}?;
let cipher = app
.value_of("cipher")
@ -125,28 +144,37 @@ pub fn parse() -> Option<CliArgs> {
None => Box::new(stdin())
};
let output = app
let wrapped_writer = match app
.value_of("OUTPUT")
.and_then(|s| if s == "-" { None } else { Some(s) })
.map(|s| {
if Path::new(s).exists() {
eprintln!("WARNING: {} already exists", s);
None
} else {
Some(LazyWriter::from_path(s.to_owned()))
.and_then(|s| if s == "-" { None } else { Some(s) }) {
Some(path) => {
if {
if app.is_present("2_interactive") && Path::new(path).exists() {
eprint!("Warning: {} already exists. Overwrite [y/N]? ", path);
let mut c = String::with_capacity(2);
io::stdin().read_line(&mut c).unwrap();
!c.is_empty() && c.chars().nth(0).unwrap() == 'y'
} else {
true
}
} {
WrappedWriter::from_path(path.to_string())
} else {
return Some(ParseResult::exited())
}
}
})
.unwrap_or_else(|| Some(LazyWriter::from_writer(stdout())))?;
None => WrappedWriter::from_writer(stdout())
};
Some(CliArgs {
password: app.value_of("1_password").into(),
force_encrypt: app.is_present("force-encrypt"),
force_encrypt: app.is_present("1_force_encrypt"),
argon2_params: params,
cipher,
block_size,
reader: input,
writer: output,
})
writer: wrapped_writer,
}.into())
}
fn number<T: FromStr>(val: &str) -> Option<T> {

View File

@ -1,34 +1,20 @@
use std::{convert::{TryFrom, TryInto}, fmt::{self, Display, Formatter}, io::{self, Read, Write}};
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;
use zeroize::Zeroize;
use crate::Password;
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(Debug, PartialEq, Eq, Clone, Copy)]
pub struct ArgonParams {
pub t_cost: u32,
pub m_cost: u32,
pub parallelism: u8,
}
impl TryFrom<ArgonParams> for argon2::Params {
type Error = argon2::Error;
fn try_from(params: ArgonParams) -> Result<Self, Self::Error> {
argon2::Params::new(params.m_cost, params.t_cost, params.parallelism.into(), None)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, TryFromPrimitive)]
#[repr(u8)]
pub enum CipherAlgorithm {
@ -57,14 +43,14 @@ impl Display for CipherAlgorithm {
#[derive(Debug, PartialEq, Eq)]
pub struct EncryptionParams {
salt: [u8; SALT_LEN],
pub argon2: ArgonParams,
pub argon2: argon2::Params,
pub cipher: CipherAlgorithm,
}
impl EncryptionParams {
pub const LEN: usize = SALT_LEN + 4*2 + 2;
pub const LEN: usize = SALT_LEN + 4*3 + 1;
pub fn new(argon2_params: ArgonParams, cipher: CipherAlgorithm) -> EncryptionParams {
pub fn new(argon2_params: argon2::Params, cipher: CipherAlgorithm) -> EncryptionParams {
let mut salt = [0; SALT_LEN];
OsRng.fill(&mut salt);
EncryptionParams {
@ -76,9 +62,9 @@ impl EncryptionParams {
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
writer.write_all(&self.salt)?;
writer.write_all(&self.argon2.t_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.t_cost().to_be_bytes())?;
writer.write_all(&self.argon2.m_cost().to_be_bytes())?;
writer.write_all(&self.argon2.p_cost().to_be_bytes())?;
writer.write_all(&(self.cipher as u8).to_be_bytes())?;
Ok(())
}
@ -90,97 +76,75 @@ impl EncryptionParams {
reader.read_exact(&mut t_cost)?;
let mut m_cost = [0; 4];
reader.read_exact(&mut m_cost)?;
let mut parallelism = [0; 1];
reader.read_exact(&mut parallelism)?;
let mut p_cost = [0; 4];
reader.read_exact(&mut p_cost)?;
let mut cipher_buff = [0; 1];
reader.read_exact(&mut cipher_buff)?;
match CipherAlgorithm::try_from(cipher_buff[0]) {
Ok(cipher) => {
let argon2_params = ArgonParams {
t_cost: u32::from_be_bytes(t_cost),
m_cost: u32::from_be_bytes(m_cost),
parallelism: u8::from_be_bytes(parallelism),
};
Ok(Some(EncryptionParams {
if let Ok(cipher) = CipherAlgorithm::try_from(cipher_buff[0]) {
if let Ok(argon2_params) = argon2::Params::new(
u32::from_be_bytes(m_cost),
u32::from_be_bytes(t_cost),
u32::from_be_bytes(p_cost),
None
) {
return Ok(Some(EncryptionParams {
salt,
argon2: argon2_params,
cipher,
}))
}));
}
Err(_) => Ok(None)
}
}
}
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
Ok(None)
}
}
pub struct DobyCipher {
cipher: Box<dyn StreamCipher>,
hmac: Hmac<blake2::Blake2b>,
hasher: VarBlake2b,
buffer: Vec<u8>,
}
impl DobyCipher {
pub fn new(mut password: Password, params: &EncryptionParams) -> Result<Self, argon2::Error> {
match params.argon2.try_into() {
Ok(argon2_params) => {
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, argon2_params);
let mut master_key = [0; KEY_LEN];
let password = password.unwrap_or_ask();
argon2.hash_password_into(password.as_bytes(), &params.salt, &mut master_key).zeroize(password)?;
let hkdf = Hkdf::<blake2::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();
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();
pub fn new(password: &[u8], params: &EncryptionParams) -> Self {
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::<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();
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();
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();
authentication_key.zeroize();
hmac.update(&encoded_params);
let mut encoded_params = Vec::with_capacity(EncryptionParams::LEN);
params.write(&mut encoded_params).unwrap();
let mut hasher = VarBlake2b::new_keyed(&authentication_key, HMAC_LEN);
authentication_key.zeroize();
hasher.update(&encoded_params);
let cipher: Box<dyn StreamCipher> = match params.cipher {
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();
let cipher: Box<dyn StreamCipher> = match params.cipher {
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();
Ok(Self {
cipher,
hmac,
buffer: Vec::new(),
})
}
Err(e) => {
password.zeroize();
Err(e)
}
Self {
cipher,
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
@ -189,44 +153,43 @@ 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::{ArgonParams, CipherAlgorithm, EncryptionParams, DobyCipher, HASH_LEN};
use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HMAC_LEN};
#[test]
fn encryption_params() {
let params = EncryptionParams::new(ArgonParams {
t_cost: 1,
m_cost: 8,
parallelism: 1,
}, CipherAlgorithm::XChaCha20);
let params = EncryptionParams::new(
argon2::Params::new(8, 1, 1, None).unwrap(),
CipherAlgorithm::XChaCha20
);
assert_eq!(EncryptionParams::LEN, 74);
assert_eq!(EncryptionParams::LEN, 77);
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[72..76], vec![0, 0, 0, 0x01]); //p_cost
assert_eq!(buff[76], CipherAlgorithm::XChaCha20 as u8);
let new_params = EncryptionParams::read(&mut buff.as_slice()).unwrap().unwrap();
assert_eq!(new_params, params);
@ -234,25 +197,24 @@ mod tests {
#[test]
fn doby_cipher() {
let params = EncryptionParams::new(ArgonParams {
t_cost: 1,
m_cost: 8,
parallelism: 1,
}, CipherAlgorithm::AesCtr);
let params = EncryptionParams::new(
argon2::Params::new(8, 1, 1, None).unwrap(),
CipherAlgorithm::AesCtr
);
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.into(), &params).unwrap();
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.into(), &params).unwrap();
let mut decrypted = vec![0; buff.len()+HASH_LEN];
let mut dec_cipher = DobyCipher::new(password.as_bytes(), &params);
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();

View File

@ -1,68 +1,68 @@
pub mod cli;
pub mod crypto;
use std::{fs::File, path::Path, io::{self, Read, Write}};
use std::{fmt::Display, fs::OpenOptions, io::{self, BufWriter, Read, Write}, path::Path};
use crypto::{DobyCipher, EncryptionParams};
use zeroize::Zeroize;
pub const MAGIC_BYTES: &[u8; 4] = b"DOBY";
pub struct Password(Option<String>);
pub struct WrappedPassword(Option<String>);
impl Password {
fn unwrap_or_ask(self) -> String {
self.0.unwrap_or_else(|| rpassword::read_password_from_tty(Some("Password: ")).unwrap())
impl WrappedPassword {
pub fn get(self, ask_confirm: bool) -> Option<String> {
self.0.or_else(|| {
let mut password = rpassword::read_password_from_tty(Some("Password: ")).ok()?;
if ask_confirm {
let mut password_confirm = rpassword::read_password_from_tty(Some("Password (confirm): ")).ok()?;
if password == password_confirm {
password_confirm.zeroize();
Some(password)
} else {
password.zeroize();
password_confirm.zeroize();
eprintln!("Passwords don't match");
None
}
} else {
Some(password)
}
})
}
}
impl From<Option<&str>> for Password {
impl From<Option<&str>> for WrappedPassword {
fn from(s: Option<&str>) -> Self {
Self(s.map(String::from))
}
}
impl From<&str> for Password {
fn from(s: &str) -> Self {
Some(s).into()
pub enum WrappedWriter<P: AsRef<Path>> {
PATH {
path: P
},
WRITER {
writer: Box<dyn Write>
}
}
impl Zeroize for Password {
fn zeroize(&mut self) {
self.0.zeroize()
}
}
pub struct LazyWriter<P: AsRef<Path>> {
path: Option<P>,
writer: Option<Box<dyn Write>>,
}
impl<P: AsRef<Path>> LazyWriter<P> {
impl<P: AsRef<Path> + Display> WrappedWriter<P> {
fn from_path(path: P) -> Self {
Self {
path: Some(path),
writer: None,
}
Self::PATH { path }
}
fn from_writer<T: 'static + Write>(writer: T) -> Self {
Self {
path: None,
writer: Some(Box::new(writer)),
}
}
}
impl<P: AsRef<Path>> Write for LazyWriter<P> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
if self.writer.is_none() {
self.writer = Some(Box::new(File::create(self.path.as_ref().unwrap()).unwrap()));
}
self.writer.as_mut().unwrap().write(buf)
Self::WRITER { writer: Box::new(writer) }
}
fn flush(&mut self) -> io::Result<()> {
self.writer.as_mut().unwrap().flush()
pub fn into_buf_writer(self) -> Option<BufWriter<Box<dyn Write>>> {
Some(BufWriter::new(match self {
Self::PATH { path } => Box::new(
OpenOptions::new().write(true).create(true).truncate(true).open(path.as_ref())
.map_err(|e| eprintln!("{}: {}", path, e))
.ok()?
) as Box<dyn Write>,
Self::WRITER { writer } => writer,
}))
}
}

View File

@ -1,4 +1,4 @@
use std::{process, io::{BufWriter, BufReader, Read}};
use std::{process, io::{BufReader, Read}};
use doby::{
cli,
crypto::{EncryptionParams, DobyCipher},
@ -6,63 +6,72 @@ use doby::{
decrypt,
encrypt,
};
use zeroize::Zeroize;
fn run() -> bool {
let mut success = false;
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);
if let Some(result) = cli::parse() {
if let Some(cli_args) = result.cli_args {
let mut reader = BufReader::new(cli_args.reader);
let mut magic_bytes = vec![0; MAGIC_BYTES.len()];
match reader.read(&mut magic_bytes) {
Ok(n) => {
if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt
match EncryptionParams::read(&mut reader) {
Ok(params) => {
match params {
Some(params) => {
match DobyCipher::new(cli_args.password, &params) {
Ok(cipher) => {
let mut magic_bytes = vec![0; MAGIC_BYTES.len()];
match reader.read(&mut magic_bytes) {
Ok(n) => {
if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt
match EncryptionParams::read(&mut reader) {
Ok(params) => {
if let Some(params) = params {
if let Some(mut password) = cli_args.password.get(false) {
if let Some(mut writer) = cli_args.writer.into_buf_writer() {
let cipher = DobyCipher::new(password.as_bytes(), &params);
password.zeroize();
match decrypt(&mut reader, &mut writer, cipher, cli_args.block_size) {
Ok(verified) => {
if verified {
success = true
} else {
eprintln!("WARNING: HMAC verification failed !\nEither your password is incorrect or the ciphertext has been corrupted.\nBe careful, the data could have been altered by an attacker.");
eprintln!("Warning: HMAC verification failed !\nEither your password is incorrect or the ciphertext has been corrupted.\nBe careful, the data could have been altered by an attacker.");
}
}
Err(e) => eprintln!("I/O error while decrypting: {}", e)
}
} else {
password.zeroize();
}
Err(e) => eprintln!("Invalid argon2 params: {}", e)
}
} else {
eprintln!("Invalid parameters")
}
None => eprintln!("Invalid cipher")
}
Err(e) => eprintln!("I/O error while reading headers: {}", e)
}
} else { //otherwise, encrypt
let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher);
if let Some(mut password) = cli_args.password.get(true) {
if let Some(mut writer) = cli_args.writer.into_buf_writer() {
let cipher = DobyCipher::new(password.as_bytes(), &params);
password.zeroize();
match encrypt(
&mut reader,
&mut writer,
&params,
cipher,
cli_args.block_size,
Some(&magic_bytes[..n])
) {
Ok(_) => success = true,
Err(e) => eprintln!("I/O error while encrypting: {}", e)
}
} else {
password.zeroize();
}
}
Err(e) => eprintln!("I/O error while reading headers: {}", e)
}
} else { //otherwise, encrypt
let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher);
match DobyCipher::new(cli_args.password, &params) {
Ok(cipher) => {
match encrypt(
&mut reader,
&mut writer,
&params,
cipher,
cli_args.block_size,
Some(&magic_bytes[..n])
) {
Ok(_) => success = true,
Err(e) => eprintln!("I/O error while encrypting: {}", e)
}
}
Err(e) => eprintln!("Invalid argon2 params: {}", e)
}
}
Err(e) => eprintln!("I/O error while reading magic bytes: {}", e),
}
Err(e) => eprintln!("I/O error while reading magic bytes: {}", e),
} else {
success = !result.error;
}
}
success

View File

@ -1,7 +1,6 @@
use rand::Rng;
use doby::{
crypto::{
ArgonParams,
CipherAlgorithm,
EncryptionParams,
DobyCipher,
@ -19,15 +18,14 @@ 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 CIPHERTEXT_SIZE: usize = PLAINTEXT.len()+113;
const PASSWORD: &str = "the password";
let params = EncryptionParams::new(ArgonParams {
t_cost: 1,
m_cost: 8,
parallelism: 1,
}, CipherAlgorithm::AesCtr);
let params = EncryptionParams::new(
argon2::Params::new(8, 1, 1, None).unwrap(),
CipherAlgorithm::AesCtr
);
let encrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap();
let encrypter = DobyCipher::new(PASSWORD.as_bytes(), &params);
let mut ciphertext = Vec::with_capacity(CIPHERTEXT_SIZE);
encrypt(&mut &PLAINTEXT[..], &mut ciphertext, &params, encrypter, BLOCK_SIZE, None).unwrap();
assert_eq!(ciphertext.len(), CIPHERTEXT_SIZE);
@ -38,13 +36,13 @@ fn authentication() {
compromised[i] = rand::thread_rng().gen();
}
assert_eq!(different_elements(&compromised, &ciphertext), 1);
let decrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap();
let decrypter = DobyCipher::new(PASSWORD.as_bytes(), &params);
let mut decrypted = Vec::with_capacity(PLAINTEXT.len());
let verified = decrypt(&mut &compromised[..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap();
assert_eq!(verified, false);
}
let decrypter = DobyCipher::new(PASSWORD.into(), &params).unwrap();
let decrypter = DobyCipher::new(PASSWORD.as_bytes(), &params);
let mut decrypted = Vec::with_capacity(PLAINTEXT.len());
let verified = decrypt(&mut &ciphertext[4+EncryptionParams::LEN..], &mut decrypted, decrypter, BLOCK_SIZE).unwrap();
assert_eq!(decrypted, PLAINTEXT);

View File

@ -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";
@ -22,7 +22,7 @@ fn setup_files<'a>() -> io::Result<(PathBuf, PathBuf, PathBuf)> {
fn doby_cmd() -> Result<Command, CargoError> {
let mut cmd = Command::cargo_bin("doby")?;
cmd.arg("-p").arg(PASSWORD);
cmd.arg("--password").arg(PASSWORD);
Ok(cmd)
}
@ -52,7 +52,7 @@ fn files() -> io::Result<()> {
fn stdout() -> io::Result<()> {
let (_, tmp_plaintext, tmp_ciphertext) = setup_files()?;
let shell_cmd = format!("{} -p \"{}\" {} > {}", cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_plaintext.to_str().unwrap(), tmp_ciphertext.to_str().unwrap());
let shell_cmd = format!("{} --password \"{}\" {} > {}", cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_plaintext.to_str().unwrap(), tmp_ciphertext.to_str().unwrap());
bash_cmd().arg(shell_cmd).assert().success().stdout("").stderr("");
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT);
@ -64,10 +64,10 @@ fn stdout() -> io::Result<()> {
fn stdin() -> io::Result<()> {
let (_, tmp_plaintext, tmp_ciphertext) = setup_files()?;
let mut shell_cmd = format!("cat {} | {} -p \"{}\" - {}", tmp_plaintext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_ciphertext.to_str().unwrap());
let mut shell_cmd = format!("cat {} | {} --password \"{}\" - {}", tmp_plaintext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD, tmp_ciphertext.to_str().unwrap());
bash_cmd().arg(shell_cmd).assert().success().stdout("").stderr("");
shell_cmd = format!("cat {} | {} -p \"{}\"", tmp_ciphertext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD);
shell_cmd = format!("cat {} | {} --password \"{}\"", tmp_ciphertext.to_str().unwrap(), cargo_bin("doby").to_str().unwrap(), PASSWORD);
bash_cmd().arg(shell_cmd).assert().success().stdout(PLAINTEXT);
Ok(())
@ -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("");
@ -107,8 +107,8 @@ fn test_cipher(cipher_str: &str, cipher_algorithm: CipherAlgorithm) -> io::Resul
doby_cmd().unwrap().arg("-c").arg(cipher_str).arg(tmp_plaintext).arg(&tmp_ciphertext).assert().success().stdout("").stderr("");
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);
assert_eq!(ciphertext[4+SALT_LEN+4*3], cipher_algorithm as u8);
assert_eq!(ciphertext.len(), PLAINTEXT.len()+17+SALT_LEN+HMAC_LEN);
doby_cmd().unwrap().arg(tmp_ciphertext).assert().success().stdout(PLAINTEXT).stderr("");
@ -127,17 +127,16 @@ fn aes_cipher() -> io::Result<()> {
Ok(())
}
#[test]
fn argon2_params() -> io::Result<()> {
Command::cargo_bin("doby").unwrap().arg("-i").arg("0").assert().failure().stderr("Invalid argon2 params: time cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-m").arg("0").assert().failure().stderr("Invalid argon2 params: memory cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-t").arg("0").assert().failure().stderr("Invalid argon2 params: not enough threads\n");
Command::cargo_bin("doby").unwrap().arg("-t").arg("0").assert().failure().stderr("Invalid Argon2 parameters: time cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-m").arg("0").assert().failure().stderr("Invalid Argon2 parameters: memory cost is too small\n");
Command::cargo_bin("doby").unwrap().arg("-p").arg("0").assert().failure().stderr("Invalid Argon2 parameters: not enough threads\n");
let ciphertext = doby_cmd().unwrap().arg("-i").arg("8").arg("-m").arg("2048").arg("-t").arg("8").assert().success().stderr("").get_output().stdout.clone();
let ciphertext = doby_cmd().unwrap().arg("-t").arg("8").arg("-m").arg("2048").arg("-p").arg("8").assert().success().stderr("").get_output().stdout.clone();
assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN..4+SALT_LEN+4].try_into().unwrap()), 8); //time cost
assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN+4..4+SALT_LEN+8].try_into().unwrap()), 2048); //memory cost
assert_eq!(u8::from_be_bytes([ciphertext[4+SALT_LEN+8]]), 8); //parallelism
assert_eq!(u32::from_be_bytes(ciphertext[4+SALT_LEN+8..4+SALT_LEN+12].try_into().unwrap()), 8); //parallelism
Ok(())
}