XChaCha20 support

This commit is contained in:
Matéo Duparc 2021-06-27 20:35:23 +02:00
parent 796a01376e
commit 4003185b8b
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
9 changed files with 302 additions and 66 deletions

151
Cargo.lock generated
View File

@ -15,6 +15,15 @@ dependencies = [
"opaque-debug", "opaque-debug",
] ]
[[package]]
name = "ansi_term"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "argon2" name = "argon2"
version = "0.2.1" version = "0.2.1"
@ -37,6 +46,17 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]] [[package]]
name = "base64ct" name = "base64ct"
version = "1.0.0" version = "1.0.0"
@ -93,6 +113,17 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chacha20"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fee7ad89dc1128635074c268ee661f90c3f7e83d9fd12910608c36b47d6c3412"
dependencies = [
"cfg-if 1.0.0",
"cipher",
"cpufeatures",
]
[[package]] [[package]]
name = "cipher" name = "cipher"
version = "0.3.0" version = "0.3.0"
@ -108,9 +139,13 @@ version = "2.33.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
dependencies = [ dependencies = [
"ansi_term",
"atty",
"bitflags", "bitflags",
"strsim",
"textwrap", "textwrap",
"unicode-width", "unicode-width",
"vec_map",
] ]
[[package]] [[package]]
@ -157,6 +192,17 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "derivative"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "digest" name = "digest"
version = "0.9.0" version = "0.9.0"
@ -173,9 +219,12 @@ dependencies = [
"aes", "aes",
"argon2", "argon2",
"blake3", "blake3",
"chacha20",
"clap", "clap",
"cpufeatures",
"hkdf", "hkdf",
"hmac", "hmac",
"num_enum",
"rand", "rand",
"rpassword", "rpassword",
"zeroize", "zeroize",
@ -202,6 +251,15 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "hkdf" name = "hkdf"
version = "0.11.0" version = "0.11.0"
@ -228,6 +286,28 @@ version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
[[package]]
name = "num_enum"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066"
dependencies = [
"derivative",
"num_enum_derive",
]
[[package]]
name = "num_enum_derive"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "opaque-debug" name = "opaque-debug"
version = "0.3.0" version = "0.3.0"
@ -251,6 +331,33 @@ version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857"
[[package]]
name = "proc-macro-crate"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785"
dependencies = [
"toml",
]
[[package]]
name = "proc-macro2"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
dependencies = [
"proc-macro2",
]
[[package]] [[package]]
name = "rand" name = "rand"
version = "0.8.4" version = "0.8.4"
@ -301,12 +408,35 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "serde"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
[[package]]
name = "strsim"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.0" version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2"
[[package]]
name = "syn"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]] [[package]]
name = "textwrap" name = "textwrap"
version = "0.11.0" version = "0.11.0"
@ -316,6 +446,15 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "toml"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "typenum" name = "typenum"
version = "1.13.0" version = "1.13.0"
@ -328,6 +467,18 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.3" version = "0.9.3"

View File

@ -15,9 +15,12 @@ codegen-units = 1
opt-level = 3 opt-level = 3
[dependencies] [dependencies]
clap = { version = "2.33", default-features = false } clap = "2.33"
rand = "0.8" rand = "0.8"
aes = { version = "0.7", features = ["ctr", "armv8"] } num_enum = "0.5"
cpufeatures = "0.1"
aes = { version = "0.7", features = ["ctr"] }
chacha20 = "0.7"
hmac = "0.11" hmac = "0.11"
blake3 = "0.3" blake3 = "0.3"
hkdf = "0.11" hkdf = "0.11"

View File

@ -5,7 +5,7 @@ Secure symmetric encryption from the command line
## Features ## Features
* 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)) * 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)
* Encryption from STDIN/STDOUT or from files * Encryption from STDIN/STDOUT or from files
@ -71,8 +71,9 @@ FLAGS:
-V, --version Prints version information -V, --version Prints version information
OPTIONS: OPTIONS:
-p, --password <password> -p, --password <password> Password used to derive encryption keys
-b, --block-size <blocksize> Size of file chunk (in bytes) [default: 65536] -b, --block-size <blocksize> Size of file chunk (in bytes) [default: 65536]
-c, --cipher <cipher> Encryption cipher to use [possible values: aes, xchacha20]
-m, --memory-cost <memory cost> Argon2 memory cost (in kilobytes) [default: 4096] -m, --memory-cost <memory cost> Argon2 memory cost (in kilobytes) [default: 4096]
-t, --threads <threads> Argon2 parallelism (between 1 and 255) [default: 4] -t, --threads <threads> Argon2 parallelism (between 1 and 255) [default: 4]
-i, --iterations <iterations> Argon2 time cost [default: 10] -i, --iterations <iterations> Argon2 time cost [default: 10]
@ -141,15 +142,16 @@ 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(random_hkdf_salt); hmac.update(random_hkdf_salt);
hmac.update(random_nonce); //16 bytes random nonce used in AES-CTR cipher 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 an AES-CTR cipher with `encryption_key` and `random_nonce` 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 `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.
```rust ```rust
let cipher = Aes256Ctr::new(encryption_key, random_nonce); let cipher = Aes256Ctr::new(encryption_key, random_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 {
@ -185,7 +187,7 @@ let master_key: [u8; 32] = argon2id(
Then, doby starts decryption. Then, doby starts decryption.
```rust ```rust
let cipher = Aes256Ctr::new(encryption_key, nonce_read_from_input); let cipher = XChaCha20::new(encryption_key, nonce_read_from_input); //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 {

9
src/bin/aes-ni.rs Normal file
View File

@ -0,0 +1,9 @@
cpufeatures::new!(aes_ni, "aes");
fn main() {
println!("AES-NI is {}supported on this device.", if aes_ni::get() {
""
} else {
"NOT "
});
}

View File

@ -6,7 +6,7 @@ use std::{
}; };
use doby::{ use doby::{
encrypt, decrypt, encrypt, decrypt,
crypto::{ArgonParams, EncryptionParams, Cipher} crypto::{ArgonParams, EncryptionParams, CipherAlgorithm, DobyCipher}
}; };
const MAX_BLOCK_SIZE: usize = 1_073_741_824; //1GB const MAX_BLOCK_SIZE: usize = 1_073_741_824; //1GB
@ -37,7 +37,7 @@ fn main() -> io::Result<()> {
t_cost: 1, t_cost: 1,
m_cost: 8, m_cost: 8,
parallelism: 1, parallelism: 1,
}); }, CipherAlgorithm::AesCtr);
let mut best_encrypt_time = None; let mut best_encrypt_time = None;
let mut best_encrypt_block_size = None; let mut best_encrypt_block_size = None;
@ -49,7 +49,7 @@ fn main() -> io::Result<()> {
let mut reader = BufReader::with_capacity(block_size, &input); let mut reader = BufReader::with_capacity(block_size, &input);
let mut writer = BufWriter::with_capacity(block_size, &output); let mut writer = BufWriter::with_capacity(block_size, &output);
let cipher = Cipher::new(PASSWORD, &params).unwrap(); let cipher = DobyCipher::new(PASSWORD, &params).unwrap();
let t_encrypt = Instant::now(); let t_encrypt = Instant::now();
encrypt(&mut reader, &mut writer, &params, cipher, block_size, None)?; encrypt(&mut reader, &mut writer, &params, cipher, block_size, None)?;
writer.flush()?; writer.flush()?;
@ -59,7 +59,7 @@ fn main() -> io::Result<()> {
reset(&mut reader)?; reset(&mut reader)?;
reset(&mut writer)?; reset(&mut writer)?;
let cipher = Cipher::new(PASSWORD, &params).unwrap(); let cipher = DobyCipher::new(PASSWORD, &params).unwrap();
let t_decrypt = Instant::now(); let t_decrypt = Instant::now();
decrypt(&mut reader, &mut writer, cipher, block_size)?; decrypt(&mut reader, &mut writer, cipher, block_size)?;
writer.flush()?; writer.flush()?;

View File

@ -4,13 +4,16 @@ use std::{
str::FromStr, str::FromStr,
io::{stdin, stdout, Read, Write}, io::{stdin, stdout, Read, Write},
}; };
use clap::{crate_name, crate_version, App, Arg}; use clap::{crate_name, crate_version, App, Arg, AppSettings};
use crate::crypto::ArgonParams; use crate::crypto::{ArgonParams, CipherAlgorithm};
cpufeatures::new!(aes_ni, "aes");
pub struct CliArgs { pub struct CliArgs {
pub password: String, pub password: String,
pub force_encrypt: bool, pub force_encrypt: bool,
pub argon2_params: ArgonParams, pub argon2_params: ArgonParams,
pub cipher: CipherAlgorithm,
pub block_size: usize, pub block_size: usize,
pub reader: Box<dyn Read>, pub reader: Box<dyn Read>,
pub writer: Box<dyn Write>, pub writer: Box<dyn Write>,
@ -19,6 +22,7 @@ pub struct CliArgs {
pub fn parse() -> Option<CliArgs> { pub fn parse() -> Option<CliArgs> {
let app = App::new(crate_name!()) let app = App::new(crate_name!())
.version(crate_version!()) .version(crate_version!())
.setting(AppSettings::ColoredHelp)
.arg(Arg::with_name("INPUT").help("<PATH> | \"-\" or empty for stdin")) .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("OUTPUT").help("<PATH> | \"-\" or empty for stdout"))
.arg( .arg(
@ -32,6 +36,7 @@ pub fn parse() -> Option<CliArgs> {
.short("p") .short("p")
.long("password") .long("password")
.value_name("password") .value_name("password")
.help("Password used to derive encryption keys")
) )
.arg( .arg(
Arg::with_name("t_cost") Arg::with_name("t_cost")
@ -64,6 +69,16 @@ pub fn parse() -> Option<CliArgs> {
.help("Size of file chunk (in bytes)") .help("Size of file chunk (in bytes)")
.default_value("65536") .default_value("65536")
) )
.arg(
Arg::with_name("cipher")
.short("c")
.long("cipher")
.value_name("cipher")
.help("Encryption cipher to use")
.long_help("Encryption cipher to use. By default, AES is selected if AES-NI is supported. Otherwise, XChaCha20 is used.")
.possible_values(&["aes", "xchacha20"])
.case_insensitive(true)
)
.get_matches(); .get_matches();
let params = { let params = {
@ -78,6 +93,21 @@ pub fn parse() -> Option<CliArgs> {
} }
}; };
let cipher = app
.value_of("cipher")
.and_then(|s| Some(if s.to_lowercase() == "aes" {
CipherAlgorithm::AesCtr
} else {
CipherAlgorithm::XChaCha20
})
)
.unwrap_or_else(|| if aes_ni::get() {
CipherAlgorithm::AesCtr
} else {
CipherAlgorithm::XChaCha20
}
);
let block_size = number(app.value_of("blocksize").unwrap())?; let block_size = number(app.value_of("blocksize").unwrap())?;
let input = app let input = app
@ -97,7 +127,7 @@ pub fn parse() -> Option<CliArgs> {
Some(Box::new(File::create(s).unwrap()) as Box<dyn Write>) Some(Box::new(File::create(s).unwrap()) as Box<dyn Write>)
} }
}) })
.unwrap_or_else(|| Some(Box::new(stdout()))); .unwrap_or_else(|| Some(Box::new(stdout())))?;
let password = match app.value_of("password") { let password = match app.value_of("password") {
Some(s) => s.to_string(), Some(s) => s.to_string(),
@ -108,9 +138,10 @@ pub fn parse() -> Option<CliArgs> {
password, password,
force_encrypt: app.is_present("force-encrypt"), force_encrypt: app.is_present("force-encrypt"),
argon2_params: params, argon2_params: params,
cipher,
block_size, block_size,
reader: input, reader: input,
writer: output?, writer: output,
}) })
} }

View File

@ -1,4 +1,6 @@
use std::io::{self, Read, Write}; use std::{convert::TryFrom, io::{self, Read, Write}};
use num_enum::TryFromPrimitive;
use chacha20::XChaCha20;
use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}}; use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}};
use hmac::{Hmac, Mac, NewMac}; use hmac::{Hmac, Mac, NewMac};
use rand::{Rng, rngs::OsRng}; use rand::{Rng, rngs::OsRng};
@ -7,7 +9,8 @@ use hkdf::Hkdf;
use zeroize::Zeroize; use zeroize::Zeroize;
const SALT_LEN: usize = 64; const SALT_LEN: usize = 64;
const NONCE_LEN: usize = 16; const AES_NONCE_LEN: usize = 16;
const XCHACHA20_NONCE_LEN: usize = 24;
const HASH_LEN: usize = 32; const HASH_LEN: usize = 32;
const KEY_LEN: usize = HASH_LEN; const KEY_LEN: usize = HASH_LEN;
@ -17,28 +20,48 @@ pub struct ArgonParams {
pub parallelism: u8, pub parallelism: u8,
} }
#[derive(Clone, Copy, Debug, TryFromPrimitive)]
#[repr(u8)]
pub enum CipherAlgorithm {
AesCtr = 0,
XChaCha20 = 1,
}
impl CipherAlgorithm {
fn get_nonce_size(&self) -> usize {
match self {
CipherAlgorithm::AesCtr => AES_NONCE_LEN,
CipherAlgorithm::XChaCha20 => XCHACHA20_NONCE_LEN,
}
}
}
pub struct EncryptionParams { pub struct EncryptionParams {
password_salt: [u8; SALT_LEN], password_salt: [u8; SALT_LEN],
argon2: ArgonParams, argon2: ArgonParams,
hkdf_salt: [u8; SALT_LEN], hkdf_salt: [u8; SALT_LEN],
nonce: [u8; NONCE_LEN], nonce: Vec<u8>,
cipher: CipherAlgorithm,
} }
impl EncryptionParams { impl EncryptionParams {
const PARAMS_LEN: usize = SALT_LEN*2 + 4*2 + 1 + NONCE_LEN; fn get_params_len(&self) -> usize {
SALT_LEN*2 + 4*2 + 1 + self.cipher.get_nonce_size()
}
pub fn new(argon2_params: ArgonParams) -> EncryptionParams { pub fn new(argon2_params: ArgonParams, cipher: CipherAlgorithm) -> EncryptionParams {
let mut password_salt = [0; SALT_LEN]; let mut password_salt = [0; SALT_LEN];
OsRng.fill(&mut password_salt); OsRng.fill(&mut password_salt);
let mut hkdf_salt = [0; SALT_LEN]; let mut hkdf_salt = [0; SALT_LEN];
OsRng.fill(&mut hkdf_salt); OsRng.fill(&mut hkdf_salt);
let mut nonce = [0; NONCE_LEN]; let mut nonce = vec![0; cipher.get_nonce_size()];
OsRng.fill(&mut nonce); OsRng.fill(&mut nonce[..]);
EncryptionParams { EncryptionParams {
password_salt, password_salt,
argon2: argon2_params, argon2: argon2_params,
hkdf_salt, hkdf_salt,
nonce, nonce,
cipher,
} }
} }
pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> { pub fn write<W: Write>(&self, writer: &mut W) -> io::Result<()> {
@ -47,45 +70,54 @@ 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.hkdf_salt)?; writer.write_all(&self.hkdf_salt)?;
writer.write_all(&(self.cipher as u8).to_be_bytes())?;
writer.write_all(&self.nonce)?; writer.write_all(&self.nonce)?;
Ok(()) Ok(())
} }
pub fn read<R: Read>(reader: &mut R) -> io::Result<Self> { pub fn read<R: Read>(reader: &mut R) -> io::Result<Option<Self>> {
let mut password_salt = [0; SALT_LEN]; let mut password_salt = [0; SALT_LEN];
reader.read_exact(&mut password_salt)?; reader.read_exact(&mut password_salt)?;
let mut t_cost_buf = [0; 4]; let mut t_cost = [0; 4];
reader.read_exact(&mut t_cost_buf)?; reader.read_exact(&mut t_cost)?;
let mut m_cost_buf = [0; 4]; let mut m_cost = [0; 4];
reader.read_exact(&mut m_cost_buf)?; reader.read_exact(&mut m_cost)?;
let mut parallelism_buf = [0; 1]; let mut parallelism = [0; 1];
reader.read_exact(&mut parallelism_buf)?; reader.read_exact(&mut parallelism)?;
let mut hkdf_salt = [0; SALT_LEN]; let mut hkdf_salt = [0; SALT_LEN];
reader.read_exact(&mut hkdf_salt)?; reader.read_exact(&mut hkdf_salt)?;
let mut nonce = [0; NONCE_LEN]; let mut cipher_buff = [0; 1];
reader.read_exact(&mut nonce)?; reader.read_exact(&mut cipher_buff)?;
match CipherAlgorithm::try_from(cipher_buff[0]) {
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_buf), t_cost: u32::from_be_bytes(t_cost),
m_cost: u32::from_be_bytes(m_cost_buf), m_cost: u32::from_be_bytes(m_cost),
parallelism: u8::from_be_bytes(parallelism_buf), parallelism: u8::from_be_bytes(parallelism),
}; };
Ok(EncryptionParams { Ok(Some(EncryptionParams {
password_salt, password_salt,
argon2: argon2_params, argon2: argon2_params,
hkdf_salt, hkdf_salt,
nonce, nonce,
}) cipher,
}))
}
Err(_) => Ok(None)
}
} }
} }
pub struct Cipher { pub struct DobyCipher {
cipher: Aes256Ctr, cipher: Box<dyn StreamCipher>,
hmac: Hmac<blake3::Hasher>, hmac: Hmac<blake3::Hasher>,
buffer: Vec<u8>, buffer: Vec<u8>,
} }
impl Cipher { impl DobyCipher {
pub fn new(password: &[u8], params: &EncryptionParams) -> Result<Self, argon2::Error> { 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 argon = Argon2::new(None, params.argon2.t_cost, params.argon2.m_cost, params.argon2.parallelism.into(), Version::V0x13)?;
@ -99,13 +131,16 @@ impl Cipher {
hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap(); hkdf.expand(b"doby_authentication_key", &mut authentication_key).unwrap();
master_key.zeroize(); master_key.zeroize();
let mut encoded_params = Vec::with_capacity(EncryptionParams::PARAMS_LEN); let mut encoded_params = Vec::with_capacity(params.get_params_len());
params.write(&mut encoded_params).unwrap(); params.write(&mut encoded_params).unwrap();
let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap(); let mut hmac = Hmac::new_from_slice(&authentication_key).unwrap();
hmac.update(&encoded_params); hmac.update(&encoded_params);
Ok(Cipher { Ok(Self {
cipher: Aes256Ctr::new_from_slices(&encryption_key, &params.nonce).unwrap(), 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, hmac,
buffer: Vec::new(), buffer: Vec::new(),
}) })
@ -127,8 +162,8 @@ impl Cipher {
buff[..buffer_len].clone_from_slice(&self.buffer); buff[..buffer_len].clone_from_slice(&self.buffer);
let read = reader.read(&mut buff[buffer_len..])?; let read = reader.read(&mut buff[buffer_len..])?;
self.buffer.clear();
let n = if buffer_len + read >= HASH_LEN { let n = if buffer_len + read >= HASH_LEN {
self.buffer.clear();
buffer_len + read - HASH_LEN buffer_len + read - HASH_LEN
} else { } else {
0 0

View File

@ -2,11 +2,11 @@ pub mod cli;
pub mod crypto; pub mod crypto;
use std::io::{self, Read, Write}; use std::io::{self, Read, Write};
use crypto::{Cipher, EncryptionParams}; use crypto::{DobyCipher, EncryptionParams};
pub const MAGIC_BYTES: &[u8; 4] = b"DOBY"; pub const MAGIC_BYTES: &[u8; 4] = b"DOBY";
pub fn encrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, params: &EncryptionParams, mut cipher: Cipher, block_size: usize, already_read: Option<Vec<u8>>) -> io::Result<()> { 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)?; writer.write_all(MAGIC_BYTES)?;
params.write(writer)?; params.write(writer)?;
let mut buff = vec![0; block_size]; let mut buff = vec![0; block_size];
@ -30,7 +30,7 @@ pub fn encrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, params: &Encry
Ok(()) Ok(())
} }
pub fn decrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, mut cipher: Cipher, block_size: usize) -> io::Result<bool> { pub fn decrypt<R: Read, W: Write>(reader: &mut R, writer: &mut W, mut cipher: DobyCipher, block_size: usize) -> io::Result<bool> {
let mut buff = vec![0; block_size]; let mut buff = vec![0; block_size];
loop { loop {
let n = cipher.decrypt_chunk(reader, &mut buff)?; let n = cipher.decrypt_chunk(reader, &mut buff)?;

View File

@ -2,7 +2,7 @@ use std::io::{BufWriter, BufReader, Read};
use zeroize::Zeroize; use zeroize::Zeroize;
use doby::{ use doby::{
cli, cli,
crypto::{EncryptionParams, Cipher}, crypto::{EncryptionParams, DobyCipher},
MAGIC_BYTES, MAGIC_BYTES,
decrypt, decrypt,
encrypt, encrypt,
@ -22,25 +22,30 @@ fn main() {
if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt if magic_bytes == MAGIC_BYTES && !cli_args.force_encrypt { //we probably want to decrypt
match EncryptionParams::read(&mut reader) { match EncryptionParams::read(&mut reader) {
Ok(params) => { Ok(params) => {
match Cipher::new(cli_args.password.as_bytes(), &params) { match params {
Ok(cipher) => { Some(params) => {
match decrypt(&mut reader, &mut writer, cipher, cli_args.block_size) { match DobyCipher::new(cli_args.password.as_bytes(), &params) {
Ok(verified) => { Ok(cipher) => {
if !verified { match decrypt(&mut reader, &mut writer, cipher, cli_args.block_size) {
eprintln!("WARNING: HMAC verification failed !\nEither your password is incorrect or the file has been corrupted.\nOpen with caution, it could have been infected by an attacker."); Ok(verified) => {
if !verified {
eprintln!("WARNING: HMAC verification failed !\nEither your password is incorrect or the file has been corrupted.\nOpen with caution, it could have been infected by an attacker.");
}
}
Err(e) => eprintln!("I/O error while decrypting: {}", e)
} }
} }
Err(e) => eprintln!("I/O error while decrypting: {}", e) Err(e) => eprintln!("Invalid argon2 params: {}", e)
} }
} }
Err(e) => eprintln!("Invalid argon2 params: {}", e) None => eprintln!("Invalid cipher")
} }
} }
Err(e) => eprintln!("I/O error while reading headers: {}", e) Err(e) => eprintln!("I/O error while reading headers: {}", e)
} }
} else { //otherwise, encrypt } else { //otherwise, encrypt
let params = EncryptionParams::new(cli_args.argon2_params); let params = EncryptionParams::new(cli_args.argon2_params, cli_args.cipher);
match Cipher::new(cli_args.password.as_bytes(), &params) { match DobyCipher::new(cli_args.password.as_bytes(), &params) {
Ok(cipher) => { Ok(cipher) => {
if let Err(e) = encrypt( if let Err(e) = encrypt(
&mut reader, &mut reader,