Password confirm

This commit is contained in:
Matéo Duparc 2021-11-13 19:08:28 +01:00
parent 382ce3c389
commit 2c8ab7e8ad
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
8 changed files with 154 additions and 192 deletions

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

@ -5,14 +5,14 @@ use std::{
io::{stdin, stdout, Read},
};
use clap::{crate_name, crate_version, App, Arg, AppSettings};
use crate::{LazyWriter, Password, crypto::{ArgonParams, CipherAlgorithm}};
use crate::{LazyWriter, 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>,
@ -34,16 +34,15 @@ pub fn parse() -> Option<CliArgs> {
)
.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")
.value_name("iterations")
.short("t")
.long("time-cost")
.value_name("number of iterations")
.help("Argon2 time cost")
.default_value("10")
)
@ -56,11 +55,11 @@ pub fn parse() -> Option<CliArgs> {
.default_value("4096")
)
.arg(
Arg::with_name("4_parallelism")
.short("t")
.long("threads")
.value_name("threads")
.help("Argon2 parallelism (between 1 and 255)")
Arg::with_name("4_p_cost")
.short("p")
.long("parallelism")
.value_name("degree of parallelism")
.help("Argon2 parallelism cost")
.default_value("4")
)
.arg(
@ -85,14 +84,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")

View File

@ -1,4 +1,4 @@
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 num_enum::TryFromPrimitive;
use chacha20::XChaCha20;
use aes::{Aes256Ctr, cipher::{NewCipher, StreamCipher}};
@ -7,7 +7,6 @@ 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;
@ -15,20 +14,6 @@ const XCHACHA20_NONCE_LEN: usize = 24;
pub const HASH_LEN: usize = 64;
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 +42,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 +61,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,37 +75,25 @@ 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)
}
}
@ -131,13 +104,10 @@ pub struct DobyCipher {
}
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);
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];
let password = password.unwrap_or_ask();
argon2.hash_password_into(password.as_bytes(), &params.salt, &mut master_key).zeroize(password)?;
argon2.hash_password_into(password, &params.salt, &mut master_key).unwrap();
let hkdf = Hkdf::<blake2::Blake2b>::new(Some(&params.salt), &master_key);
master_key.zeroize();
let mut nonce = vec![0; params.cipher.get_nonce_size()];
@ -159,16 +129,10 @@ impl DobyCipher {
};
encryption_key.zeroize();
Ok(Self {
Self {
cipher,
hmac,
buffer: Vec::new(),
})
}
Err(e) => {
password.zeroize();
Err(e)
}
}
}
@ -209,24 +173,23 @@ impl DobyCipher {
#[cfg(test)]
mod tests {
use super::{ArgonParams, CipherAlgorithm, EncryptionParams, DobyCipher, HASH_LEN};
use super::{CipherAlgorithm, EncryptionParams, DobyCipher, HASH_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,24 +197,23 @@ 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 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);
let mut dec_cipher = DobyCipher::new(password.into(), &params).unwrap();
let mut dec_cipher = DobyCipher::new(password.as_bytes(), &params);
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

@ -7,32 +7,36 @@ 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()
}
}
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>>,

View File

@ -6,6 +6,7 @@ use doby::{
decrypt,
encrypt,
};
use zeroize::Zeroize;
fn run() -> bool {
let mut success = false;
@ -21,8 +22,9 @@ fn run() -> bool {
Ok(params) => {
match params {
Some(params) => {
match DobyCipher::new(cli_args.password, &params) {
Ok(cipher) => {
if let Some(mut password) = cli_args.password.get(false) {
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 {
@ -34,18 +36,17 @@ fn run() -> bool {
Err(e) => eprintln!("I/O error while decrypting: {}", e)
}
}
Err(e) => eprintln!("Invalid argon2 params: {}", e)
}
}
None => eprintln!("Invalid cipher")
None => eprintln!("Invalid parameters")
}
}
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) => {
if let Some(mut password) = cli_args.password.get(true) {
let cipher = DobyCipher::new(password.as_bytes(), &params);
password.zeroize();
match encrypt(
&mut reader,
&mut writer,
@ -58,8 +59,6 @@ fn run() -> bool {
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),

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()+145;
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

@ -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(())
@ -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+HASH_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(())
}