From bb07aec89621e075ea7ac9aa338bc291dc8de70e Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sun, 22 Oct 2023 19:05:38 +0800 Subject: [PATCH] feat: v0.5.0, supports ChaCha20/Poly1305 --- Cargo.lock | 2 +- Cargo.toml | 4 +- src/cmd_decrypt.rs | 26 ++++++++----- src/cmd_encrypt.rs | 45 ++++++++++++++++------ src/cmd_info.rs | 2 +- src/crypto_aes.rs | 89 ------------------------------------------- src/crypto_cryptor.rs | 8 ++++ src/crypto_simple.rs | 31 +++++++++++++++ src/main.rs | 2 +- src/spec.rs | 21 +++++----- 10 files changed, 105 insertions(+), 125 deletions(-) delete mode 100644 src/crypto_aes.rs create mode 100644 src/crypto_simple.rs diff --git a/Cargo.lock b/Cargo.lock index 3234dab..a1e87e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1729,7 +1729,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "0.4.4" +version = "0.5.0" dependencies = [ "aes-gcm-stream", "base64", diff --git a/Cargo.toml b/Cargo.toml index 9c7d84c..1566a0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "0.4.4" +version = "0.5.0" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" @@ -39,7 +39,7 @@ yubikey = { version = "0.8", features = ["untested"] } zeroize = "1.6" [patch.crates-io] -rust-crypto = { git="https://github.com/jht5945/rust-crypto.git" } +rust-crypto = { git = "https://github.com/jht5945/rust-crypto.git" } [profile.release] codegen-units = 1 diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index 15d3641..d99ca39 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -16,14 +16,14 @@ use yubikey::piv::{AlgorithmId, decrypt_data}; use yubikey::YubiKey; use zeroize::Zeroize; -use crate::{util, util_enc_file, util_envelop, util_file, util_pgp, util_piv}; +use crate::{consts, crypto_simple, util, util_enc_file, util_envelop, util_file, util_pgp, util_piv}; use crate::compress::GzStreamDecoder; use crate::config::TinyEncryptConfig; use crate::consts::{ DATE_TIME_FORMAT, ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, SALT_COMMENT, TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT, }; -use crate::crypto_aes::{aes_gcm_decrypt, try_aes_gcm_decrypt_with_salt}; +use crate::crypto_cryptor::Cryptor; use crate::spec::{EncEncryptedMeta, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; use crate::util::SecVec; use crate::util_digest::DigestWrite; @@ -109,6 +109,10 @@ pub fn decrypt_single(config: &Option, util_enc_file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display); debugging!("Found meta: {}", serde_json::to_string_pretty(&meta).unwrap()); + let encryption_algorithm = meta.encryption_algorithm + .as_ref().map(String::as_str).unwrap_or(consts::TINY_ENC_AES_GCM); + let cryptor = Cryptor::from(encryption_algorithm)?; + let do_skip_file_out = cmd_decrypt.skip_decrypt_file || cmd_decrypt.direct_print || cmd_decrypt.digest_file; let path_out = &path_display[0..path_display.len() - TINY_ENC_FILE_EXT.len()]; if !do_skip_file_out { util::require_file_not_exists(path_out)?; } @@ -127,8 +131,8 @@ pub fn decrypt_single(config: &Option, // debugging!("Decrypt key: {}", hex::encode(&key.0)); debugging!("Decrypt nonce: {}", hex::encode(&nonce.0)); - let enc_meta = parse_encrypted_meta(&meta, &key.0, &nonce.0)?; - parse_encrypted_comment(&meta, &key.0, &nonce.0)?; + let enc_meta = parse_encrypted_meta(&meta, cryptor, &key.0, &nonce.0)?; + parse_encrypted_comment(&meta, cryptor, &key.0, &nonce.0)?; // Decrypt to output if cmd_decrypt.direct_print { @@ -223,11 +227,11 @@ fn decrypt_file(file_in: &mut File, file_len: u64, file_out: &mut impl Write, Ok(total_len) } -fn parse_encrypted_comment(meta: &TinyEncryptMeta, key: &[u8], nonce: &[u8]) -> XResult<()> { +fn parse_encrypted_comment(meta: &TinyEncryptMeta, crypto: Cryptor, key: &[u8], nonce: &[u8]) -> XResult<()> { if let Some(encrypted_comment) = &meta.encrypted_comment { match util::decode_base64(encrypted_comment) { Err(e) => warning!("Decode encrypted comment failed: {}", e), - Ok(ec_bytes) => match try_aes_gcm_decrypt_with_salt(key, nonce, SALT_COMMENT, &ec_bytes) { + Ok(ec_bytes) => match crypto_simple::try_decrypt_with_salt(crypto, key, nonce, SALT_COMMENT, &ec_bytes) { Err(e) => warning!("Decrypt encrypted comment failed: {}", e), Ok(decrypted_comment_bytes) => match String::from_utf8(decrypted_comment_bytes.clone()) { Err(_) => success!("Encrypted message hex: {}", hex::encode(&decrypted_comment_bytes)), @@ -239,14 +243,14 @@ fn parse_encrypted_comment(meta: &TinyEncryptMeta, key: &[u8], nonce: &[u8]) -> Ok(()) } -fn parse_encrypted_meta(meta: &TinyEncryptMeta, key: &[u8], nonce: &[u8]) -> XResult> { +fn parse_encrypted_meta(meta: &TinyEncryptMeta, cryptor: Cryptor, key: &[u8], nonce: &[u8]) -> XResult> { Ok(match &meta.encrypted_meta { None => None, Some(enc_encrypted_meta) => { let enc_encrypted_meta_bytes = opt_result!( util::decode_base64(enc_encrypted_meta), "Decode enc-encrypted-meta failed: {}"); let enc_meta = opt_result!( - EncEncryptedMeta::unseal(key, nonce, &enc_encrypted_meta_bytes), "Unseal enc-encrypted-meta failed: {}"); + EncEncryptedMeta::unseal(cryptor, key, nonce, &enc_encrypted_meta_bytes), "Unseal enc-encrypted-meta failed: {}"); debugging!("Encrypted meta: {:?}", enc_meta); if let Some(filename) = &enc_meta.filename { information!("Source filename: {}", filename); @@ -307,7 +311,8 @@ fn try_decrypt_key_ecdh(config: &Option, slot_id, ), "Decrypt via PIV card failed: {}"); let key = util::simple_kdf(shared_secret.as_slice()); - let decrypted_key = aes_gcm_decrypt(&key, &wrap_key.nonce, &wrap_key.encrypted_data)?; + let decrypted_key = crypto_simple::decrypt( + Cryptor::Aes256Gcm, &key, &wrap_key.nonce, &wrap_key.encrypted_data)?; util::zeroize(key); util::zeroize(shared_secret); Ok(decrypted_key) @@ -329,7 +334,8 @@ fn try_decrypt_key_ecdh_pgp_x25519(envelop: &TinyEncryptEnvelop, pin: &Option, } pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> { @@ -116,6 +119,16 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en return Ok(0); } + let encryption_algorithm = cmd_encrypt.encryption_algorithm.as_ref() + .map(String::as_str).unwrap_or(consts::TINY_ENC_AES_GCM) + .to_lowercase(); + let cryptor = match encryption_algorithm.as_str() { + "aes" | "aes/gcm" => Cryptor::Aes256Gcm, + "chacha20" | "chacha20/poly1305" => Cryptor::ChaCha20Poly1305, + _ => return simple_error!("Unknown encryption algorithm: {}, should be AES or CHACHA20", encryption_algorithm), + }; + information!("Using encryption algorithm: {}", cryptor.get_name()); + util::require_file_exists(path)?; let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display); @@ -129,7 +142,8 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en let encrypted_comment = match &cmd_encrypt.encrypted_comment { None => None, Some(encrypted_comment) => Some(util::encode_base64( - &aes_gcm_encrypt_with_salt(&key.0, &nonce.0, SALT_COMMENT, encrypted_comment.as_bytes())?)) + &crypto_simple::encrypt_with_salt( + cryptor, &key.0, &nonce.0, SALT_COMMENT, encrypted_comment.as_bytes())?)) }; let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display()); @@ -138,7 +152,8 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en c_time: file_metadata.created().ok().and_then(|t| t.to_millis()), m_time: file_metadata.modified().ok().and_then(|t| t.to_millis()), }; - let enc_encrypted_meta_bytes = opt_result!(enc_encrypted_meta.seal(&key.0, &nonce.0), "Seal enc-encrypted-meta failed: {}"); + let enc_encrypted_meta_bytes = opt_result!(enc_encrypted_meta.seal( + cryptor, &key.0, &nonce.0), "Seal enc-encrypted-meta failed: {}"); let enc_metadata = EncMetadata { comment: cmd_encrypt.comment.clone(), @@ -147,11 +162,21 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en compress: cmd_encrypt.compress, }; - let mut encrypt_meta = TinyEncryptMeta::new(&file_metadata, &enc_metadata, &nonce.0, envelops); + let mut encrypt_meta = TinyEncryptMeta::new( + &file_metadata, &enc_metadata, cryptor, &nonce.0, envelops); debugging!("Encrypted meta: {:?}", encrypt_meta); if cmd_encrypt.compatible_with_1_0 { - encrypt_meta = process_compatible_with_1_0(cmd_encrypt, encrypt_meta)?; + if !cmd_encrypt.disable_compress_meta { + return simple_error!("Compatible with 1.0 mode must turns --disable-compress-meta on."); + } + if let Cryptor::Aes256Gcm = Cryptor::Aes256Gcm {} else { + return simple_error!("Compatible with 1.0 mode must use AES/GCM."); + } + encrypt_meta = process_compatible_with_1_0(encrypt_meta)?; + if encrypt_meta.pgp_envelop.is_none() && encrypt_meta.ecdh_envelop.is_none() { + return simple_error!("Compatible with 1.0 mode must contains PGP or ECDH Envelop."); + } } let mut file_out = File::create(&path_out)?; @@ -172,10 +197,7 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en Ok(file_metadata.len()) } -fn process_compatible_with_1_0(cmd_encrypt: &CmdEncrypt, mut encrypt_meta: TinyEncryptMeta) -> XResult { - if !cmd_encrypt.disable_compress_meta { - return simple_error!("Compatible with 1.0 mode must turns --disable-compress-meta on."); - } +fn process_compatible_with_1_0(mut encrypt_meta: TinyEncryptMeta) -> XResult { if let Some(envelops) = encrypt_meta.envelops { let mut filter_envelops = vec![]; for envelop in envelops { @@ -309,7 +331,8 @@ fn encrypt_envelop_shared_secret(key: &[u8], let shared_key = util::simple_kdf(shared_secret); let (_, nonce) = util::make_key256_and_nonce(); - let encrypted_key = aes_gcm_encrypt(&shared_key, &nonce.0, key)?; + let encrypted_key = crypto_simple::encrypt( + Cryptor::Aes256Gcm, &shared_key, &nonce.0, key)?; let wrap_key = WrapKey { header: WrapKeyHeader { diff --git a/src/cmd_info.rs b/src/cmd_info.rs index 13cd256..e0af230 100644 --- a/src/cmd_info.rs +++ b/src/cmd_info.rs @@ -17,7 +17,7 @@ pub struct CmdInfo { /// File pub paths: Vec, /// Show raw meta - #[arg(long, default_value_t = false)] + #[arg(long, short = 'R', default_value_t = false)] pub raw_meta: bool, } diff --git a/src/crypto_aes.rs b/src/crypto_aes.rs deleted file mode 100644 index a5b1f5a..0000000 --- a/src/crypto_aes.rs +++ /dev/null @@ -1,89 +0,0 @@ -use rust_util::{opt_result, XResult}; - -pub fn try_aes_gcm_decrypt_with_salt(key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult> { - let new_nonce = build_salted_nonce(nonce, salt); - if let Ok(decrypted) = aes_gcm_decrypt(key, &new_nonce, message) { - return Ok(decrypted); - } - aes_gcm_decrypt(key, nonce, message) -} - -pub fn aes_gcm_decrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult> { - Ok(opt_result!(aes_gcm_stream::aes_256_gcm_decrypt(key, nonce, message), "Bad key or cipher text: {}")) -} - -pub fn aes_gcm_encrypt_with_salt(key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult> { - let new_nonce = build_salted_nonce(nonce, salt); - aes_gcm_encrypt(key, &new_nonce, message) -} - -pub fn aes_gcm_encrypt(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult> { - Ok(opt_result!(aes_gcm_stream::aes_256_gcm_encrypt(key, nonce, message), "Bad key length: {}")) -} - -fn build_salted_nonce(nonce: &[u8], salt: &[u8]) -> Vec { - let mut nonce_with_salt = nonce.to_vec(); - nonce_with_salt.extend_from_slice(salt); - let input = hex::decode(sha256::digest(nonce_with_salt)).unwrap(); - input[0..12].to_vec() -} - -#[test] -fn test_aes_gcm_01() { - use aes_gcm_stream::Aes256GcmStreamEncryptor; - let data_key = hex::decode("0001020304050607080910111213141516171819202122232425262728293031").unwrap(); - let nonce = hex::decode("000102030405060708091011").unwrap(); - - let plain_text1 = "Hello world!".as_bytes(); - let encrypted_text1 = "dce9511866417cff5123fa08c9e92cf156c5fc8bf6108ff28816fb58"; - - let plain_text2 = "This is a test message.".as_bytes(); - let encrypted_text2 = "c0e45407290878b0426fea4c09597ce323b056f975c63cce6c8da516c2a78c7d71b590c869cf92"; - - let key256: [u8; 32] = data_key.as_slice().try_into().unwrap(); - { - let mut encryptor = Aes256GcmStreamEncryptor::new(key256.clone(), &nonce); - let mut encrypted = encryptor.update(plain_text1); - let (last_block, tag) = encryptor.finalize(); - encrypted.extend_from_slice(&last_block); - encrypted.extend_from_slice(&tag); - assert_eq!(encrypted_text1, hex::encode(&encrypted)); - } - { - let mut encryptor = Aes256GcmStreamEncryptor::new(key256.clone(), &nonce); - let mut encrypted = encryptor.update(plain_text2); - let (last_block, tag) = encryptor.finalize(); - encrypted.extend_from_slice(&last_block); - encrypted.extend_from_slice(&tag); - assert_eq!(encrypted_text2, hex::encode(&encrypted)); - } -} - -#[test] -fn test_aes_gcm_02() { - use aes_gcm_stream::Aes256GcmStreamDecryptor; - let data_key = hex::decode("aa01020304050607080910111213141516171819202122232425262728293031").unwrap(); - let nonce = hex::decode("aa0102030405060708091011").unwrap(); - - let plain_text1 = hex::encode("Hello world!".as_bytes()); - let encrypted_text1 = hex::decode("42b625d2bacb8a514076f14002f02770e9ccd98c90e556dc267aca30").unwrap(); - - let plain_text2 = hex::encode("This is a test message.".as_bytes()); - let encrypted_text2 = hex::decode("5ebb20cdf5828e1e533ae1043ce6703cfa51574a83a069700aedefdbe2c735b01b74da214cba4a").unwrap(); - - let key256: [u8; 32] = data_key.as_slice().try_into().unwrap(); - { - let mut decryptor = Aes256GcmStreamDecryptor::new(key256.clone(), &nonce); - let mut plain_text = decryptor.update(encrypted_text1.as_slice()); - let last_block = decryptor.finalize().unwrap(); - plain_text.extend_from_slice(&last_block); - assert_eq!(plain_text1, hex::encode(&plain_text)); - } - { - let mut decryptor = Aes256GcmStreamDecryptor::new(key256.clone(), &nonce); - let mut plain_text = decryptor.update(encrypted_text2.as_slice()); - let last_block = decryptor.finalize().unwrap(); - plain_text.extend_from_slice(&last_block); - assert_eq!(plain_text2, hex::encode(&plain_text)); - } -} \ No newline at end of file diff --git a/src/crypto_cryptor.rs b/src/crypto_cryptor.rs index 66edfb6..9e34ed6 100644 --- a/src/crypto_cryptor.rs +++ b/src/crypto_cryptor.rs @@ -20,6 +20,14 @@ impl Cryptor { } } + pub fn get_name(&self) -> String { + let name = match self { + Cryptor::Aes256Gcm => consts::TINY_ENC_AES_GCM, + Cryptor::ChaCha20Poly1305 => consts::TINY_ENC_CHACHA20_POLY1305, + }; + name.to_string() + } + pub fn encryptor(self, key: &[u8], nonce: &[u8]) -> XResult> { get_encryptor(self, key, nonce) } diff --git a/src/crypto_simple.rs b/src/crypto_simple.rs new file mode 100644 index 0000000..f27e002 --- /dev/null +++ b/src/crypto_simple.rs @@ -0,0 +1,31 @@ +use rust_util::XResult; + +use crate::crypto_cryptor::Cryptor; + +pub fn try_decrypt_with_salt(crypto: Cryptor, key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult> { + let new_nonce = build_salted_nonce(nonce, salt); + if let Ok(decrypted) = decrypt(crypto, key, &new_nonce, message) { + return Ok(decrypted); + } + decrypt(crypto, key, nonce, message) +} + +pub fn decrypt(crypto: Cryptor, key: &[u8], nonce: &[u8], message: &[u8]) -> XResult> { + crypto.decryptor(key, nonce)?.decrypt(message) +} + +pub fn encrypt_with_salt(crypto: Cryptor, key: &[u8], nonce: &[u8], salt: &[u8], message: &[u8]) -> XResult> { + let new_nonce = build_salted_nonce(nonce, salt); + encrypt(crypto, key, &new_nonce, message) +} + +pub fn encrypt(crypto: Cryptor, key: &[u8], nonce: &[u8], message: &[u8]) -> XResult> { + Ok(crypto.encryptor(key, nonce)?.encrypt(message)) +} + +fn build_salted_nonce(nonce: &[u8], salt: &[u8]) -> Vec { + let mut nonce_with_salt = nonce.to_vec(); + nonce_with_salt.extend_from_slice(salt); + let input = hex::decode(sha256::digest(nonce_with_salt)).unwrap(); + input[0..12].to_vec() +} diff --git a/src/main.rs b/src/main.rs index 5e4e65e..5981822 100644 --- a/src/main.rs +++ b/src/main.rs @@ -21,7 +21,7 @@ mod util_x25519; mod compress; mod config; mod spec; -mod crypto_aes; +mod crypto_simple; mod crypto_rsa; mod crypto_cryptor; mod wrap_key; diff --git a/src/spec.rs b/src/spec.rs index 71ff70d..491aa9f 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -5,8 +5,9 @@ use rust_util::{opt_result, util_time, XResult}; use rust_util::util_time::get_millis; use serde::{Deserialize, Serialize}; -use crate::{compress, crypto_aes}; -use crate::consts::{SALT_META, TINY_ENC_AES_GCM}; +use crate::{compress, crypto_simple}; +use crate::consts::SALT_META; +use crate::crypto_cryptor::Cryptor; use crate::util::{encode_base64, get_user_agent}; pub const TINY_ENCRYPT_VERSION_10: &str = "1.0"; @@ -104,21 +105,21 @@ pub struct EncEncryptedMeta { } impl EncEncryptedMeta { - pub fn unseal(key: &[u8], nonce: &[u8], message: &[u8]) -> XResult { - let mut decrypted = opt_result!(crypto_aes::try_aes_gcm_decrypt_with_salt( - key, nonce, SALT_META, message), "Decrypt failed: {}"); + pub fn unseal(crypto: Cryptor, key: &[u8], nonce: &[u8], message: &[u8]) -> XResult { + let mut decrypted = opt_result!(crypto_simple::try_decrypt_with_salt( + crypto, key, nonce, SALT_META, message), "Decrypt failed: {}"); decrypted = opt_result!(compress::decompress(&decrypted), "Decode faield: {}"); let meta = opt_result!( serde_json::from_slice::(&decrypted), "Parse failed: {}"); Ok(meta) } - pub fn seal(&self, key: &[u8], nonce: &[u8]) -> XResult> { + pub fn seal(&self, crypto: Cryptor, key: &[u8], nonce: &[u8]) -> XResult> { let mut encrypted_meta_json = serde_json::to_vec(self).unwrap(); encrypted_meta_json = opt_result!( compress::compress(Compression::default(), &encrypted_meta_json), "Compress failed: {}"); - let encrypted = opt_result!(crypto_aes::aes_gcm_encrypt_with_salt( - key, nonce, SALT_META, encrypted_meta_json.as_slice()), "Encrypt failed: {}"); + let encrypted = opt_result!(crypto_simple::encrypt_with_salt( + crypto, key, nonce, SALT_META, encrypted_meta_json.as_slice()), "Encrypt failed: {}"); Ok(encrypted) } } @@ -131,7 +132,7 @@ pub struct EncMetadata { } impl TinyEncryptMeta { - pub fn new(metadata: &Metadata, enc_metadata: &EncMetadata, nonce: &[u8], envelops: Vec) -> Self { + pub fn new(metadata: &Metadata, enc_metadata: &EncMetadata, cryptor: Cryptor, nonce: &[u8], envelops: Vec) -> Self { TinyEncryptMeta { version: TINY_ENCRYPT_VERSION_11.to_string(), created: util_time::get_current_millis() as u64, @@ -147,7 +148,7 @@ impl TinyEncryptMeta { ecdh_point: None, envelop: None, envelops: Some(envelops), - encryption_algorithm: Some(TINY_ENC_AES_GCM.to_string()), + encryption_algorithm: Some(cryptor.get_name()), nonce: encode_base64(nonce), file_length: metadata.len(), file_last_modified: match metadata.modified() {