From 6c6c22b2dd89c8e4119e7f11e27d62e998d9fd17 Mon Sep 17 00:00:00 2001 From: Michele Esposito Date: Tue, 19 Dec 2023 19:33:39 +0100 Subject: [PATCH] feat: complete cli additions --- README.md | 115 +++++++++++++++++------ benches/Cargo.toml | 3 - enclave/README.md | 2 +- enclave/src/lib.rs | 34 ++++++- src/main.rs | 41 +++++++-- src/utils.rs | 224 ++++++++++++++++++++++++++++----------------- 6 files changed, 288 insertions(+), 131 deletions(-) diff --git a/README.md b/README.md index 3482d43..ed2ad50 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # secured -Secured is a versatile Rust package that provides robust encryption and decryption capabilities. It can be seamlessly integrated as a library in other Rust applications or used as a standalone command-line interface (CLI) tool. +A very fast CLI tool for encryption and decryption of large amounts of data > [!WARNING] -> This crate is under development and APIs are rapidly changing (including this README!). Make sure to lock to a specific crate version to avoid updates. +> As this crate is under early development, APIs are rapidly changing, and so is the documentation. ## Features -- **Encryption and Decryption**: Easily encrypt and decrypt files with password, using [the `ChaCha20` and `Poly1305` algorithms combined](cipher/README.md). -- **Cli & Library**: Use as a standalone CLI tool or integrate as a library in your Rust applications. +- **Encryption and Decryption**: Easily encrypt and decrypt files with password or a pre-generated encryption key. +- **Key Derivation**: Generate encryption keys from passwords with customizable iterations and salt. +- **File Inspection**: Inspect details of secured files. ## Installation @@ -28,49 +29,100 @@ cargo add secured ## Usage -### As a CLI Tool +### Encrypting a Single File -Secured is straightforward to use from the command line. Here are the basic commands: +Encrypt a single file with a password. If no password is provided, the tool will prompt you for it. -1. **Encryption** +```sh +secured encrypt secret.txt +``` + +### Decrypting a Single File - ```sh - secured encrypt [PASSWORD] - ``` +Decrypt a single file with a password. If no password is provided, the tool will prompt you for it. - Encrypts the specified ``. An optional `[PASSWORD]` can be passed directly to the command. +```sh +secured decrypt secret.txt.secured +``` -2. **Decryption** - ```sh - secured decrypt [PASSWORD] - ``` - Decrypts the specified ``. An optional `[PASSWORD]` can be passed directly to the command. Obviously, the password must be the same used during encryption. +### Encrypting/Decrypting Multiple Files with Glob Patterns -### As a Library +Use glob patterns to encrypt or decrypt multiple files with a single command. + +```sh +secured encrypt data/*.txt +secured decrypt data/*.txt.secured +``` + +### Generating Encryption Key + +Generate an encryption key from a password with customizable iterations and salt. + +```sh +secured key --password my_secret_password --iterations 1000000 --salt abcdef1234567890 +``` + +### Inspecting Secured Files + +Inspect details of one or more secured files. + +```sh +secured inspect secret.txt.secured +secured inspect data/*.txt.secured +``` + +## Examples + +### Encrypting a Single File -To use Secured as a library in your Rust application, simply import the package and utilize its encryption and decryption functions as per your requirements. +```sh +secured encrypt secret.txt +``` + +### Decrypting a Single File -#### Encrypting Data +```sh +secured decrypt secret.txt.secured +``` -```rust -use secured_enclave::{Enclave, Encryptable, KeyDerivationStrategy}; +### Encrypting/Decrypting Multiple Files with Glob Patterns -let password = "strong_password"; -let encrypted_string = "Hello, world!".encrypt(password.to_string(), KeyDerivationStrategy::default()); +```sh +secured encrypt data/*.txt +secured decrypt data/*.txt.secured ``` -#### Decrypting Data +### Generating Encryption Key -```rust -use secured_enclave::{Decryptable, EnclaveError}; +```sh +secured key --password my_secret_password --iterations 1000000 --salt abcdef1234567890 +``` -let password = "strong_password"; -let decrypted_result = encrypted_data.decrypt(password.to_string()); +### Inspecting Secured Files -println!("Decrypted data: {:?}", String::from_utf8(decrypted_data).unwrap()) +```sh +secured inspect secret.txt.secured +secured inspect data/*.txt.secured ``` -See [Enclave documentation](enclave/README.md) for more advanced usage +## Advanced Usage + +### Customizing Key Derivation + +For advanced users, customize key derivation options during key generation. + +```sh +secured key --password my_secret_password --iterations 1500000 --salt 1a2b3c4d... +``` + +### Integrating with Scripts + +Integrate **secured** into your scripts for automated encryption and decryption tasks. + +```sh +#!/bin/bash +secured encrypt data/*.csv --key $ENCRYPTION_KEY +``` ## Contributing @@ -79,3 +131,6 @@ Contributions are welcome! Feel free to open issues or submit pull requests. ## License Secured is distributed under the MIT License. See `LICENSE` for more information. +``` + +This version of the README emphasizes examples with single files and no options, showcasing the tool's ability to prompt for a password when needed. It then introduces examples with glob patterns for handling multiple files in a single command. diff --git a/benches/Cargo.toml b/benches/Cargo.toml index 9960f6a..aacc25b 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -16,6 +16,3 @@ secured-cipher = { path = "../cipher/" } name = "chacha20" path = "src/chacha20.rs" harness = false - -[profile.bench] -debug = true diff --git a/enclave/README.md b/enclave/README.md index 75ed5b1..30c76fe 100644 --- a/enclave/README.md +++ b/enclave/README.md @@ -17,7 +17,7 @@ Add the following line to your `Cargo.toml` file: ```toml [dependencies] -enclave = "0.5.0" +secured-enclave = "0.5.0" ``` ## Usage diff --git a/enclave/src/lib.rs b/enclave/src/lib.rs index a6540c5..2c44fb9 100644 --- a/enclave/src/lib.rs +++ b/enclave/src/lib.rs @@ -25,10 +25,10 @@ pub struct Enclave { pub metadata: T, /// The encrypted data. - encrypted_bytes: Box<[u8]>, + pub encrypted_bytes: Box<[u8]>, /// The nonce used in the encryption process, 8 bytes long (ChaCha20). - nonce: [u8; NONCE_SIZE], + pub nonce: [u8; NONCE_SIZE], } impl Enclave @@ -81,6 +81,28 @@ where .decrypt_and_verify(&envelope)?, ) } + + /// Recovers the key used to encrypt the enclave using a provided password. + /// + /// # Arguments + /// * `encrypted_bytes`: The encrypted enclave. + /// + /// # Returns + /// A `Result` containing the recovered key, or an error string if recovery fails. + pub fn recover_key( + encrypted_bytes: &[u8], + password: &[u8], + ) -> Result, EnclaveError> { + let strategy = KeyDerivationStrategy::try_from( + encrypted_bytes[encrypted_bytes.len() - 9..encrypted_bytes.len()].to_vec(), + )?; + let salt: [u8; 16] = encrypted_bytes[encrypted_bytes.len() - 25..encrypted_bytes.len() - 9] + .try_into() + .unwrap(); + let key = Key::::with_salt(password, salt, strategy); + + Ok(key) + } } impl From> for Vec @@ -121,7 +143,15 @@ where /// # Returns /// A `Result` containing the deserialized `Enclave` instance, or an `EnclaveError` if deserialization fails. fn try_from(bytes: Vec) -> Result { + if bytes.len() == 0 { + return Err(EnclaveError::Deserialization("No bytes found".to_string())); + } let metadata_len = bytes[0]; + if usize::from(metadata_len) > bytes.len() { + return Err(EnclaveError::Deserialization( + "unexpected metadata length".to_string(), + )); + } let metadata = T::try_from(bytes[1..metadata_len as usize + 1].to_vec()).or(Err( EnclaveError::Deserialization("error deserializing metadata".to_string()), ))?; diff --git a/src/main.rs b/src/main.rs index 03e5dda..fb36ba6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,9 @@ use clap::{Parser, Subcommand}; mod utils; pub use utils::{decrypt_files, encrypt_files}; -use utils::{generate_encryption_key_with_options, get_password_or_prompt, Credentials}; +use utils::{ + generate_encryption_key_with_options, get_password_or_prompt, inspect_files, Credentials, +}; /// Defines command line subcommands for the application. #[derive(Debug, Subcommand)] @@ -20,7 +22,12 @@ enum Command { /// If provided, the password will be ignored. #[arg(short, long)] key: Option, + + /// Wipe the original file after encryption. + #[arg(short, long)] + wipe: bool, }, + /// Decrypts a specified file. Decrypt { /// Path to the files to be decrypted. Supports glob patterns. @@ -34,7 +41,12 @@ enum Command { /// If provided, the password will be ignored. #[arg(short, long)] key: Option, + + /// Wipe the encrypted file after decryption. + #[arg(short, long)] + wipe: bool, }, + /// Derives a key from a given password. Key { /// Optional password. If not provided, it will be prompted for. @@ -51,6 +63,12 @@ enum Command { #[arg(short, long)] salt: Option, }, + + /// Inspects a specified `.secured` file. + Inspect { + /// Path to the files to be inspected. Supports glob patterns. + path: Vec, + }, } /// Defines the command line arguments structure. @@ -70,31 +88,34 @@ fn main() { path, password, key, + wipe, } => match key { - Some(key) => encrypt_files(&Credentials::HexKey(key), path), + Some(key) => encrypt_files(&Credentials::HexKey(key), path, wipe), None => { - let password = get_password_or_prompt(password); - encrypt_files(&Credentials::Password(password), path); + let password = get_password_or_prompt(password, true); + encrypt_files(&Credentials::Password(password), path, wipe); } }, Command::Decrypt { path, password, key, - } => match key { - Some(key) => decrypt_files(&Credentials::HexKey(key), path), + wipe, + } => match key { + Some(key) => decrypt_files(&Credentials::HexKey(key), path, wipe), None => { - let password = get_password_or_prompt(password); - decrypt_files(&Credentials::Password(password), path); + let password = get_password_or_prompt(password, false); + decrypt_files(&Credentials::Password(password), path, wipe); } - } + }, Command::Key { password, iterations, salt, } => { - let password = get_password_or_prompt(password); + let password = get_password_or_prompt(password, true); generate_encryption_key_with_options(&password, iterations, salt); } + Command::Inspect { path } => inspect_files(path), } } diff --git a/src/utils.rs b/src/utils.rs index c268634..c35f3c9 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -5,8 +5,8 @@ use std::fs::{metadata, File}; use std::io::{Read, Write}; use std::time::Duration; -use cipher::{Key, KeyDerivationStrategy}; -use enclave::{Decryptable, Encryptable}; +use cipher::{Key, KeyDerivationStrategy, SignedEnvelope}; +use enclave::{Decryptable, Enclave, Encryptable}; pub(crate) const LOADERS: [&str; 7] = [ "▹▹▹▹▹", @@ -23,33 +23,40 @@ pub enum Credentials { HexKey(String), } +enum CachedCredentials { + Key(Key<32, 16>), + KeyBytes([u8; 32]), +} + /// Encrypts files with a given password. /// /// # Arguments /// * `credentials` - The password used for encryption. /// * `path` - The path of the files to be encrypted. -pub fn encrypt_files(credentials: &Credentials, path: Vec) { +/// * `wipe` - Whether or not to wipe the original file after encryption. +pub fn encrypt_files(credentials: &Credentials, path: Vec, wipe: bool) { let plaintext_files = path .iter() .map(|p| glob(&p).expect("Invalid file pattern").collect::>()) .flatten() .collect::>(); - match credentials { + let encryption_key = match credentials { Credentials::Password(password) => { - encrypt_multiple_with_password(password.to_string(), plaintext_files) + CachedCredentials::Key(generate_encryption_key_with_progress(password)) } Credentials::HexKey(hex_key) => { - encrypt_multiple_with_hex_key(hex_key.to_string(), plaintext_files) + let encryption_key: [u8; 32] = hex::decode(hex_key) + .expect("Not a valid hex value") + .try_into() + .expect("Not a valid 32-byte key"); + + CachedCredentials::KeyBytes(encryption_key) } }; -} -fn encrypt_multiple_with_password( - password: String, - plaintext_files: Vec>, -) { let counter = std::time::Instant::now(); + let mut bytes_counter: usize = 0; let progress = ProgressBar::new_spinner(); progress.set_style( ProgressStyle::with_template("{spinner:.yellow} {msg}") @@ -57,8 +64,6 @@ fn encrypt_multiple_with_password( .tick_strings(&LOADERS), ); - let encryption_key = generate_encryption_key_with_progress(&password); - for (i, entry) in plaintext_files.iter().enumerate() { match entry { Ok(path) => { @@ -72,62 +77,20 @@ fn encrypt_multiple_with_password( )); if metadata(path).unwrap().is_file() { - let encrypted_bytes = get_file_as_byte_vec(&filename).encrypt_with_key(&encryption_key); + let file_contents = get_file_as_byte_vec(&filename); + let encrypted_bytes = match &encryption_key { + CachedCredentials::Key(key) => file_contents.encrypt_with_key(&key), + CachedCredentials::KeyBytes(key) => file_contents.encrypt_with_raw_key(*key), + }; + bytes_counter += encrypted_bytes.len(); File::create(format!("{}.secured", filename)) .expect("Unable to create file") .write_all(&encrypted_bytes) .expect("Unable to write data"); - } - - progress.tick(); - } - Err(e) => println!("{:?}", e), - } - } - - progress.finish_with_message(format!( - "✅ > {} files secured in {}ms", - plaintext_files.len(), - counter.elapsed().as_millis() - )); -} - -fn encrypt_multiple_with_hex_key( - hex_key: String, - plaintext_files: Vec>, -) { - let counter = std::time::Instant::now(); - let progress = ProgressBar::new_spinner(); - progress.set_style( - ProgressStyle::with_template("{spinner:.yellow} {msg}") - .unwrap() - .tick_strings(&LOADERS), - ); - - let encryption_key: [u8; 32] = hex::decode(hex_key) - .expect("Not a valid hex value") - .try_into() - .expect("Not a valid 32-byte key"); - - for (i, entry) in plaintext_files.iter().enumerate() { - match entry { - Ok(path) => { - let filename = path.to_str().unwrap().to_string(); - - progress.set_message(format!( - "[{}/{}] {}", - i + 1, - plaintext_files.len(), - filename.clone() - )); - if metadata(path).unwrap().is_file() { - let encrypted_bytes = - get_file_as_byte_vec(&filename).encrypt_with_raw_key(encryption_key); - File::create(format!("{}.secured", filename)) - .expect("Unable to create file") - .write_all(&encrypted_bytes) - .expect("Unable to write data"); + if wipe { + std::fs::remove_file(filename).expect("Unable to remove file"); + } } progress.tick(); @@ -137,8 +100,9 @@ fn encrypt_multiple_with_hex_key( } progress.finish_with_message(format!( - "✅ > {} files secured in {}ms", + "✅ > {} files secured ({}MB in {}ms)", plaintext_files.len(), + bytes_counter / 1024 / 1024, counter.elapsed().as_millis() )); } @@ -148,7 +112,8 @@ fn encrypt_multiple_with_hex_key( /// # Arguments /// * `password` - The password used for decryption. /// * `path` - The path of the files to be decrypted. -pub fn decrypt_files(credentials: &Credentials, path: Vec) { +/// * `wipe` - Whether or not to wipe the encrypted file after decryption. +pub fn decrypt_files(credentials: &Credentials, path: Vec, wipe: bool) { let encrypted_files = path .iter() .map(|p| glob(&p).expect("Invalid file pattern").collect::>()) @@ -163,6 +128,22 @@ pub fn decrypt_files(credentials: &Credentials, path: Vec) { .tick_strings(&LOADERS), ); + let mut cached_encryption_key = match credentials { + // we skip the password derivation step in this case + // because we first need the key salt and iterations + // from the enclave + Credentials::Password(_) => [0u8; 32], + // in this case we already have the key, super fast! + Credentials::HexKey(hex_key) => { + let encryption_key: [u8; 32] = hex::decode(hex_key) + .expect("Not a valid hex value") + .try_into() + .expect("Not a valid 32-byte key"); + + encryption_key + } + }; + for (i, entry) in encrypted_files.iter().enumerate() { match entry { Ok(path) => { @@ -175,25 +156,43 @@ pub fn decrypt_files(credentials: &Credentials, path: Vec) { filename.clone() )); - let encrypted_bytes = get_file_as_byte_vec(&filename); - let recovered_bytes = match credentials { - Credentials::Password(password) => { - encrypted_bytes.decrypt(password.clone()) - } - Credentials::HexKey(hex_key) => { - let encryption_key: [u8; 32] = hex::decode(hex_key) - .expect("Not a valid hex value") - .try_into() - .expect("Not a valid 32-byte key"); + if metadata(path).unwrap().is_file() { + let encrypted_bytes = get_file_as_byte_vec(&filename); + let recovered_bytes = match credentials { + Credentials::Password(password) => { + // this step is optimistic + let enclave = Enclave::>::try_from( + encrypted_bytes[..encrypted_bytes.len() - 25].to_vec(), + ) + .expect("Unable to parse enclave"); + let result = enclave.decrypt(cached_encryption_key); + if result.is_ok() { + result + } else { + // if the optimistic decryption fails, we try again with the password + cached_encryption_key = + Enclave::>::recover_key(&encrypted_bytes, password.as_bytes()) + .expect("Unable to recover encryption key") + .pubk; + let enclave = Enclave::>::try_from( + encrypted_bytes[..encrypted_bytes.len() - 25].to_vec(), + ) + .expect("Unable to parse enclave"); + enclave.decrypt(cached_encryption_key) + } + } + _ => encrypted_bytes.decrypt_with_key(cached_encryption_key), + }; + + File::create(filename.replace(".secured", "")) + .expect("Unable to create file") + .write_all(&recovered_bytes.unwrap()) + .expect("Unable to write data"); - encrypted_bytes.decrypt_with_key(encryption_key) + if wipe { + std::fs::remove_file(filename).expect("Unable to remove file"); } - }; - - File::create(filename.replace(".secured", "")) - .expect("Unable to create file") - .write_all(&recovered_bytes.unwrap()) - .expect("Unable to write data"); + } progress.tick(); } @@ -231,10 +230,24 @@ pub(crate) fn get_file_as_byte_vec(filename: &String) -> Vec { /// /// # Returns /// A `String` containing the password. -pub(crate) fn get_password_or_prompt(password: Option) -> String { +pub(crate) fn get_password_or_prompt(password: Option, confirmation: bool) -> String { match password { Some(password) => password, - None => prompt_password("Enter password: ").expect("Unable to read password"), + None => { + let password = prompt_password("Enter password: ").expect("Unable to read password"); + if !confirmation { + return password; + } + + let password_confirmation = + prompt_password("Confirm password: ").expect("Unable to read password"); + + if password != password_confirmation { + panic!("Passwords do not match"); + } + + password + }, } } @@ -299,3 +312,44 @@ pub(crate) fn generate_encryption_key_with_options( println!("🔑 > Key: {}", hex::encode(encryption_key.pubk)); println!("🧂 > Salt: {}", hex::encode(encryption_key.salt)); } + +pub(crate) fn inspect_files(path: Vec) { + let files = path + .iter() + .map(|p| glob(&p).expect("Invalid file pattern").collect::>()) + .flatten() + .collect::>(); + + for entry in files { + match entry { + Ok(path) => { + let filename = path.to_str().unwrap().to_string(); + let file_contents = get_file_as_byte_vec(&filename); + + let enclave: Result>, _> = Enclave::try_from(file_contents); + + if enclave.is_err() { + println!(" ------------------------------ "); + println!("📦 > File\t\t\t {}", filename); + println!("🚫 > Not a valid enclave.\n\n\n"); + continue; + } + + let enclave = enclave.unwrap(); + let envelope = SignedEnvelope::try_from(enclave.encrypted_bytes.to_vec()).expect("Invalid envelope"); + + println!(" ------------------------------ "); + println!("📦 > File\t\t\t {}\n", filename); + println!("ℹ️ > Signed envelope headers\t\t\t {}", hex::encode(envelope.header)); + println!( + "🤐 > Ciphertext\t\t\t {}", + hex::encode(envelope.data) + ); + println!("✍️ > Signature\t\t\t {}", hex::encode(envelope.mac)); + println!("🎲 > Nonce\t\t\t {}", hex::encode(enclave.nonce)); + println!("👓 > Enclave Metadata\t {}", hex::encode(enclave.metadata)); + } + Err(e) => println!("{:?}", e), + } + } +}