From c9388063e8ac4862986e697eda95c316e1aa7e58 Mon Sep 17 00:00:00 2001 From: lbeder Date: Sat, 14 Sep 2024 22:57:37 -0400 Subject: [PATCH] UX fixes and updates --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 34 ++++++------- src/main.rs | 127 +++++++++++++++++++++++++------------------------ src/slowkey.rs | 1 + 5 files changed, 87 insertions(+), 79 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c17017d..8dd3072 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "slowkey" -version = "1.0.0-beta.2" +version = "1.0.1" dependencies = [ "base64 0.21.7", "better-panic", diff --git a/Cargo.toml b/Cargo.toml index c329c3c..4616903 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ authors = ["Leonid Beder "] edition = "2021" name = "slowkey" -version = "1.0.0-beta.2" +version = "1.0.1" [dependencies] better-panic = "0.3.0" diff --git a/README.md b/README.md index 6eaf600..5218af1 100755 --- a/README.md +++ b/README.md @@ -29,7 +29,7 @@ The SlowKey Key Derivation Scheme is defined as follows: ### Inputs - `password`: User's password. -- `salt`: Unique salt for hashing. Please note that the salt must be `16` bytes long, therefore shorter salts will be padded with 0s, while longer salts will be first SHA512 hashed and then truncated to `16` bytes. +- `salt`: Unique salt for hashing. Please note that the salt must be `16` bytes long, therefore shorter/longer salts will be SHA512 hashed and then truncated to `16` bytes. - `iterations`: Number of iterations the process should be repeated. ### Output @@ -107,7 +107,7 @@ Options: --checkpoint-dir Optional directory for storing encrypted checkpoints, each appended with an iteration-specific suffix. For each iteration i, the corresponding checkpoint file is named "checkpoint.i", indicating the iteration number at which the checkpoint was created --checkpoint-interval - Frequency of saving encrypted checkpoints to disk, specified as the number of iterations between each save. This argument is only required if --checkpoint-interval is provided [default: 0] + Frequency of saving encrypted checkpoints to disk, specified as the number of iterations between each save. This argument is only required if --checkpoint-interval is provided] --restore-from-checkpoint Path to an existing checkpoint from which to resume the derivation process --max-checkpoints-to-keep @@ -188,7 +188,7 @@ SlowKey: iterations: 100, length: 16, Scrypt: (n: 1048576, r: 8, p: 1), Argon2id Please input all data either in raw or hex format starting with the 0x prefix -✔ Enter your salt · saltsaltsaltsalt +✔ Enter your salt · ******** ✔ Enter your password · ******** @@ -198,38 +198,40 @@ Please input all data either in raw or hex format starting with the 0x prefix Final result: ```sh -✔ Enter your salt · saltsaltsaltsalt +✔ Enter your salt · ******** ✔ Enter your password · ******** -████████████████████████████████████████████████████████████████████████████████ 1/1 100% (0s) +████████████████████████████████████████████████████████████████████████████████ 100/100 100% (0s) Key (hex) is (please highlight to see): fd194763d687d50dafa952eec758df13 Finished in 8m 9s ``` -Please note that salt must be `16` bytes long, therefore shorter salts will be padded with 0s, while longer salts will be first SHA512 hashed and then truncated to `16` bytes: +Please note that salt must be `16` bytes long, therefore shorter/longer salts will be SHA512 hashed and then truncated to `16` bytes: ```sh -✔ Enter your salt · salt +✔ Enter your salt · ******** -Salt is shorter than 16 and will padded with 0s. Do you want to continue? [y/n] +Salt's size 4 is shorter than 16 and will be SHA512 hashed and then truncated to 16 bytes. Do you want to continue? [y/n] ``` ```sh -✔ Enter your salt · saltsaltsaltsaltsalt +✔ Enter your salt · ******** -Salt is longer than 16 and will first SHA512 hashed and then truncated to 16 bytes. Do you want to continue? [y/n] +Salt's size 20 is longer than 16 and will be SHA512 hashed and then truncated to 16 bytes. Do you want to continue? [y/n] ``` ### Checkpoints The tool also supports the creation of periodic checkpoints, which are securely encrypted and stored on the disk. Each checkpoint captures all parameters and the output from the last iteration, enabling you to resume computation from a previously established checkpoint. Additionally, the tool allows for the retention of multiple checkpoints. +Please note that even if the last checkpoint is done at the final iteration (in the case that the number of iterations divides by the checkpointing interval), the checkpoint still won't have the actual output until you complete the recovery process. + Please exercise caution when using this feature. Resuming computation from a compromised checkpoint may undermine your expectations regarding the duration of the key stretching process. -Please note that encryption key must be `32` bytes long, therefore shorter keys will be padded with 0s, while longer keys will be first SHA512 hashed and then truncated to `32` bytes: +Please note that encryption key must be `32` bytes long, therefore shorter/longer will be first SHA512 hashed and then truncated to `32` bytes: For instance, to elaborate on the previous example, suppose we want to create a checkpoint every `5` iterations forcefully terminate the execution at the `22nd` iteration: @@ -242,7 +244,7 @@ Please input all data either in raw or hex format starting with the 0x prefix Checkpoint will be created every 5 iterations and saved to the "~/checkpoints" checkpoints directory -✔ Enter your salt · saltsaltsaltsalt +✔ Enter your salt · ******** ✔ Enter your password · ******** @@ -253,7 +255,7 @@ SlowKey: iterations: 100, length: 16, Scrypt: (n: 1048576, r: 8, p: 1), Argon2id Created checkpoint #20 with data hash (salted) b4c0a8ef28897913854364bc80ab0676edb5e95384918c48700f1b5a57ac2c2c ``` -We can see that the last `checkpoint.020.b4c0a8ef28897913854364bc80ab0676edb5e95384918c48700f1b5a57ac2c2c` was retained in the `~/checkpoints` directory. Please note that file name contains iteration the checkpoint was taken at and a salted hash of the data. +We can see that the `checkpoint.020.b4c0a8ef28897913854364bc80ab0676edb5e95384918c48700f1b5a57ac2c2c` was retained in the `~/checkpoints` directory. Please note that file name contains iteration the checkpoint was taken at and a salted hash of the data. Let's use the `show-checkpoint` command to decrypt its contents and verify the parameters: @@ -280,7 +282,7 @@ Please input all data either in raw or hex format starting with the 0x prefix Checkpoint: iteration: 20, data (please highlight to see): 9edb1ad22baf39c9d7865e181caf7852 -✔ Enter your salt · saltsaltsaltsalt +✔ Enter your salt · ******** ✔ Enter your password · ******** @@ -310,7 +312,7 @@ SlowKey: iterations: 100, length: 16, Scrypt: (n: 1048576, r: 8, p: 1), Argon2id Please input all data either in raw or hex format starting with the 0x prefix -✔ Enter your salt · saltsaltsaltsalt +✔ Enter your salt · ******** ✔ Enter your password · ******** @@ -332,7 +334,7 @@ Please input all data either in raw or hex format starting with the 0x prefix ✔ Enter your checkpoint/output encryption key · ******** -✔ Enter your salt · saltsaltsaltsalt +✔ Enter your salt · ******** ✔ Enter your password · ******** diff --git a/src/main.rs b/src/main.rs index 0391f23..c868da2 100755 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,7 @@ use crate::{ use base64::{engine::general_purpose, Engine as _}; use clap::{Parser, Subcommand}; use crossterm::style::Stylize; -use dialoguer::{theme::ColorfulTheme, Confirm, Input, Password}; +use dialoguer::{theme::ColorfulTheme, Confirm, Password}; use humantime::format_duration; use indicatif::{MultiProgress, ProgressBar, ProgressStyle}; use mimalloc::MiMalloc; @@ -133,10 +133,9 @@ enum Commands { #[arg( long, requires = "checkpoint_path", - default_value = "0", help = "Frequency of saving encrypted checkpoints to disk, specified as the number of iterations between each save. This argument is only required if --checkpoint-interval is provided" )] - checkpoint_interval: usize, + checkpoint_interval: Option, #[arg( long, @@ -173,11 +172,14 @@ enum Commands { const HEX_PREFIX: &str = "0x"; fn get_salt() -> Vec { - let input: String = Input::with_theme(&ColorfulTheme::default()) + let input = Password::with_theme(&ColorfulTheme::default()) .with_prompt("Enter your salt") + .with_confirmation("Enter your salt again", "Error: salts don't match") .interact() .unwrap(); + println!(); + let mut salt = if input.starts_with(HEX_PREFIX) { hex::decode(input.strip_prefix(HEX_PREFIX).unwrap()).unwrap() } else { @@ -187,29 +189,31 @@ fn get_salt() -> Vec { let salt_len = salt.len(); match salt_len.cmp(&SlowKey::SALT_SIZE) { Ordering::Less => { - println!(); - let confirmation = Confirm::new() - .with_prompt(format!( - "Salt is shorter than {} and will padded with 0s. Do you want to continue?", - SlowKey::SALT_SIZE - )) - .wait_for_newline(true) - .interact() - .unwrap(); + .with_prompt(format!( + "Salt's length {} is shorter than {} and will be SHA512 hashed and then truncated to {} bytes. Do you want to continue?", + salt_len, + SlowKey::SALT_SIZE, SlowKey::SALT_SIZE + )) + .wait_for_newline(true) + .interact() + .unwrap(); if confirmation { - salt.resize(SlowKey::SALT_SIZE, 0) + let mut sha512 = Sha512::new(); + sha512.update(&salt); + salt = sha512.finalize().to_vec(); + + salt.truncate(SlowKey::SALT_SIZE); } else { panic!("Aborting"); } }, Ordering::Greater => { - println!(); - let confirmation = Confirm::new() .with_prompt(format!( - "Salt is longer than {} and will first SHA512 hashed and then truncated to {} bytes. Do you want to continue?", + "Salt's length {} is longer than {} and will be SHA512 hashed and then truncated to {} bytes. Do you want to continue?", + salt_len, SlowKey::SALT_SIZE, SlowKey::SALT_SIZE )) .wait_for_newline(true) @@ -250,22 +254,15 @@ fn get_password() -> Vec { } } -fn get_output_key(with_confirmation: bool) -> Vec { - let key = if with_confirmation { - Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your checkpoint/output encryption key") - .with_confirmation( - "Enter your checkpoint/output encryption key again", - "Error: keys don't match", - ) - .interact() - .unwrap() - } else { - Password::with_theme(&ColorfulTheme::default()) - .with_prompt("Enter your checkpoint/output encryption key") - .interact() - .unwrap() - }; +fn get_output_key() -> Vec { + let key = Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Enter your checkpoint/output encryption key") + .with_confirmation( + "Enter your checkpoint/output encryption key again", + "Error: keys don't match", + ) + .interact() + .unwrap(); let mut key = if key.starts_with(HEX_PREFIX) { hex::decode(key.strip_prefix(HEX_PREFIX).unwrap()).unwrap() @@ -279,16 +276,21 @@ fn get_output_key(with_confirmation: bool) -> Vec { println!(); let confirmation = Confirm::new() - .with_prompt(format!( - "Output encryption key is shorter than {} and will padded with 0s. Do you want to continue?", - ChaCha20Poly1305::KEY_SIZE - )) - .wait_for_newline(true) - .interact() - .unwrap(); + .with_prompt(format!( + "Output encryption key's length {} is shorter than {} and will be SHA512 hashed and then truncated to {} bytes. Do you want to continue?", + key_len, + ChaCha20Poly1305::KEY_SIZE, ChaCha20Poly1305::KEY_SIZE + )) + .wait_for_newline(true) + .interact() + .unwrap(); if confirmation { - key.resize(ChaCha20Poly1305::KEY_SIZE, 0) + let mut sha512 = Sha512::new(); + sha512.update(&key); + key = sha512.finalize().to_vec(); + + key.truncate(ChaCha20Poly1305::KEY_SIZE); } else { panic!("Aborting"); } @@ -298,8 +300,9 @@ fn get_output_key(with_confirmation: bool) -> Vec { let confirmation = Confirm::new() .with_prompt(format!( - "Output encryption key is longer than {} and will first SHA512 hashed and then truncated to {} bytes. Do you want to continue?", - SlowKey::SALT_SIZE, SlowKey::SALT_SIZE + "Output encryption key's length {} is longer than {} and will be SHA512 hashed and then truncated to {} bytes. Do you want to continue?", + key_len, + ChaCha20Poly1305::KEY_SIZE, ChaCha20Poly1305::KEY_SIZE )) .wait_for_newline(true) .interact() @@ -368,7 +371,7 @@ fn main() { if let Some(path) = restore_from_checkpoint { if output_key.is_none() { - output_key = Some(get_output_key(false)); + output_key = Some(get_output_key()); } let checkpoint_data = Checkpoint::get(&OpenCheckpointOptions { @@ -400,27 +403,27 @@ fn main() { let mut out: Option = None; if let Some(path) = output { if output_key.is_none() { - let key = get_output_key(true); + output_key = Some(get_output_key()); out = Some(Output::new(&OutputOptions { path, - key: key.clone(), + key: output_key.clone().unwrap(), slowkey: slowkey_opts.clone(), })) } } + let mut checkpointing_interval: usize = 0; + if let Some(dir) = checkpoint_dir { - if checkpoint_interval == 0 { - panic!("Invalid checkpoint interval") - } + checkpointing_interval = checkpoint_interval.unwrap(); - if output_key.as_ref().is_none() { - output_key = Some(get_output_key(true)); + if output_key.is_none() { + output_key = Some(get_output_key()); } checkpoint = Some(Checkpoint::new(&CheckpointOptions { - iterations, + iterations: slowkey_opts.iterations, dir: dir.to_owned(), key: output_key.clone().unwrap(), max_checkpoints_to_keep, @@ -429,7 +432,7 @@ fn main() { println!( "Checkpoint will be created every {} iterations and saved to the \"{}\" checkpoints directory", - checkpoint_interval.to_string().cyan(), + checkpointing_interval.to_string().cyan(), &dir.to_string_lossy().cyan() ); println!(); @@ -458,7 +461,7 @@ fn main() { let mb = MultiProgress::new(); let pb = mb - .add(ProgressBar::new(iterations as u64)) + .add(ProgressBar::new(slowkey_opts.iterations as u64)) .with_style( ProgressStyle::with_template("{bar:80.cyan/blue} {pos:>7}/{len:7} {percent}% ({eta})").unwrap(), ) @@ -468,11 +471,13 @@ fn main() { let mut cpb: Option = None; - if checkpoint.is_some() && checkpoint_interval != 0 { + if checkpoint.is_some() && checkpointing_interval != 0 { cpb = Some( - mb.add(ProgressBar::new((iterations / checkpoint_interval) as u64)) - .with_style(ProgressStyle::with_template("{msg}").unwrap()) - .with_position((offset / checkpoint_interval) as u64), + mb.add(ProgressBar::new( + (slowkey_opts.iterations / checkpointing_interval) as u64, + )) + .with_style(ProgressStyle::with_template("{msg}").unwrap()) + .with_position((offset / checkpointing_interval) as u64), ); if let Some(ref mut cpb) = &mut cpb { @@ -491,7 +496,7 @@ fn main() { offset, |current_iteration, current_data| { // Create a checkpoint if we've reached the checkpoint interval - if checkpoint_interval != 0 && (current_iteration + 1) % checkpoint_interval == 0 { + if checkpointing_interval != 0 && (current_iteration + 1) % checkpointing_interval == 0 { if let Some(checkpoint) = &mut checkpoint { checkpoint.create_checkpoint(&salt, current_iteration, current_data); } @@ -571,7 +576,7 @@ fn main() { ); println!(); - let output_key = get_output_key(false); + let output_key = get_output_key(); let checkpoint_data = Checkpoint::get(&OpenCheckpointOptions { key: output_key, @@ -613,7 +618,7 @@ fn main() { ); println!(); - let output_key = get_output_key(false); + let output_key = get_output_key(); let output_data = Output::get(&OpenOutputOptions { key: output_key, diff --git a/src/slowkey.rs b/src/slowkey.rs index 7858971..3e9264d 100755 --- a/src/slowkey.rs +++ b/src/slowkey.rs @@ -187,6 +187,7 @@ impl SlowKey { // Calculate the final SHA2 and SHA3 hashes (and trim the result, if required) self.double_hash(salt, password, &mut res); + res.truncate(self.length); res