diff --git a/Cargo.lock b/Cargo.lock index 3be734b..b669a4b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -572,25 +572,14 @@ dependencies = [ [[package]] name = "errno" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add4f07d43996f76ef320709726a556a9d4f965d9410d8d0271132d2f8293480" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" dependencies = [ - "errno-dragonfly", "libc", "windows-sys", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "fastrand" version = "2.0.1" @@ -1025,9 +1014,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45786cec4d5e54a224b15cb9f06751883103a27c19c93eda09b0b4f5f08fefac" +checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" [[package]] name = "log" @@ -1448,9 +1437,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b1106fec09662ec6dd98ccac0f81cef56984d0b49f75c92d8cbad76e20c005c" +checksum = "134c189feb4956b20f6f547d2cf727d4c0fe06722b20a0eec87ed445a97f92da" dependencies = [ "unicode-ident", ] @@ -1679,9 +1668,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.17" +version = "0.38.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f25469e9ae0f3d0047ca8b93fc56843f38e6774f0914a107ff8b41be8be8e0b7" +checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" dependencies = [ "bitflags 2.4.0", "errno", @@ -1800,9 +1789,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.19" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" @@ -2128,7 +2117,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "0.2.5" +version = "0.3.0" dependencies = [ "aes-gcm-stream", "base64", @@ -2173,9 +2162,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.32.0" +version = "1.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" +checksum = "4f38200e3ef7995e5ef13baec2f432a6da0aa9ac495b2c0e8f3b7eec2c92d653" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index fa6ede3..05f6341 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "0.2.5" +version = "0.3.0" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index 1ae320a..2672bef 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -20,12 +20,9 @@ use zeroize::Zeroize; use crate::{card, file, util, util_piv}; use crate::compress::GzStreamDecoder; use crate::config::TinyEncryptConfig; -use crate::crypto_aes::aes_gcm_decrypt; -use crate::spec::{TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; -use crate::util::{ - ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, - TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT, -}; +use crate::crypto_aes::{aes_gcm_decrypt, try_aes_gcm_decrypt_with_salt}; +use crate::spec::{EncEncryptedMeta, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; +use crate::util::{ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, SALT_COMMENT, TINY_ENC_CONFIG_FILE, TINY_ENC_FILE_EXT}; use crate::wrap_key::WrapKey; #[derive(Debug, Args)] @@ -41,6 +38,9 @@ pub struct CmdDecrypt { /// Remove source file #[arg(long, short = 'R')] pub remove_file: bool, + /// Skip decrypt file + #[arg(long)] + pub skip_decrypt_file: bool, } pub fn decrypt(cmd_decrypt: CmdDecrypt) -> XResult<()> { @@ -105,34 +105,48 @@ pub fn decrypt_single(config: &Option, debugging!("Decrypt key: {}", hex::encode(&key)); debugging!("Decrypt nonce: {}", hex::encode(&nonce)); + if let Some(enc_encrypted_meta) = &meta.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: {}"); + if let Some(filename) = &enc_meta.filename { + information!("Source filename: {}", filename); + } + } + if let Some(encrypted_comment) = &meta.encrypted_comment { match util::decode_base64(encrypted_comment) { Err(e) => warning!("Decode encrypted comment failed: {}", e), - Ok(encrypted_comment_based_bytes) => match aes_gcm_decrypt(&key, &nonce, &encrypted_comment_based_bytes) { + Ok(ec_bytes) => match try_aes_gcm_decrypt_with_salt(&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)), - Ok(message) => success!("Encrypted message: {}", message), + Ok(message) => success!("Encrypted comment: {}", message), } } } } - let mut file_out = File::create(path_out)?; + if cmd_decrypt.skip_decrypt_file { + information!("Decrypt file is skipped."); + } else { + let mut file_out = File::create(path_out)?; - let start = Instant::now(); - util_msg::print_lastline( - &format!("Decrypting file: {}{} ...", path_display, iff!(meta.compress, " [compressed]", "")) - ); - let _ = decrypt_file(&mut file_in, &mut file_out, &key, &nonce, meta.compress)?; - util_msg::clear_lastline(); - let encrypt_duration = start.elapsed(); - debugging!("Encrypt file: {} elapsed: {} ms", path_display, encrypt_duration.as_millis()); + let start = Instant::now(); + util_msg::print_lastline( + &format!("Decrypting file: {}{} ...", path_display, iff!(meta.compress, " [compressed]", "")) + ); + let _ = decrypt_file(&mut file_in, &mut file_out, &key, &nonce, meta.compress)?; + util_msg::clear_lastline(); + let encrypt_duration = start.elapsed(); + debugging!("Encrypt file: {} elapsed: {} ms", path_display, encrypt_duration.as_millis()); + drop(file_out); + } util::zeroize(key); util::zeroize(nonce); drop(file_in); - drop(file_out); if cmd_decrypt.remove_file { match fs::remove_file(path) { Err(e) => warning!("Remove file: {} failed: {}", path_display, e), diff --git a/src/cmd_encrypt.rs b/src/cmd_encrypt.rs index bf1b1b2..035e6c3 100644 --- a/src/cmd_encrypt.rs +++ b/src/cmd_encrypt.rs @@ -7,16 +7,16 @@ use std::time::Instant; use clap::Args; use flate2::Compression; use rsa::Pkcs1v15Encrypt; -use rust_util::{debugging, failure, information, opt_result, simple_error, success, util_msg, warning, XResult}; +use rust_util::{debugging, failure, iff, information, opt_result, simple_error, success, util_msg, warning, XResult}; use zeroize::Zeroize; -use crate::{util, util_ecdh, util_p384, util_x25519}; +use crate::{compress, util, util_ecdh, util_p384, util_x25519}; use crate::compress::GzStreamEncoder; use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop}; -use crate::crypto_aes::aes_gcm_encrypt; +use crate::crypto_aes::{aes_gcm_encrypt, aes_gcm_encrypt_with_salt}; use crate::crypto_rsa::parse_spki; -use crate::spec::{EncMetadata, TINY_ENCRYPT_VERSION_10, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; -use crate::util::{ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, TINY_ENC_CONFIG_FILE}; +use crate::spec::{EncEncryptedMeta, EncMetadata, TINY_ENCRYPT_VERSION_10, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; +use crate::util::{ENC_AES256_GCM_P256, ENC_AES256_GCM_P384, ENC_AES256_GCM_X25519, SALT_COMMENT, TINY_ENC_CONFIG_FILE}; use crate::wrap_key::{WrapKey, WrapKeyHeader}; #[derive(Debug, Args)] @@ -44,6 +44,9 @@ pub struct CmdEncrypt { /// Remove source file #[arg(long, short = 'R')] pub remove_file: bool, + /// Disable compress meta + #[arg(long)] + pub disable_compress_meta: bool, } pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> { @@ -115,14 +118,19 @@ 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(&key, &nonce, encrypted_comment.as_bytes())?)) + &aes_gcm_encrypt_with_salt(&key, &nonce, SALT_COMMENT, encrypted_comment.as_bytes())?)) }; + let enc_encrypted_meta = EncEncryptedMeta { + filename: Some(util::get_file_name(path)), + }; + let enc_encrypted_meta_bytes = opt_result!(enc_encrypted_meta.seal(&key, &nonce), "Seal enc-encrypted-meta failed: {}"); + let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display()); let enc_metadata = EncMetadata { comment: cmd_encrypt.comment.clone(), encrypted_comment, - encrypted_meta: None, + encrypted_meta: Some(util::encode_base64(&enc_encrypted_meta_bytes)), compress: cmd_encrypt.compress, }; @@ -150,10 +158,18 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en } } + let compress_meta = !cmd_encrypt.disable_compress_meta; + let mut file_out = File::create(&path_out)?; - opt_result!(file_out.write_all(&util::TINY_ENC_MAGIC_TAG.to_be_bytes()), "Write tag failed: {}"); - let encrypted_meta_bytes = opt_result!(serde_json::to_vec(&encrypt_meta), "Generate meta json bytes failed: {}"); + let tag = iff!(compress_meta, util::TINY_ENC_COMPRESSED_MAGIC_TAG, util::TINY_ENC_MAGIC_TAG); + opt_result!(file_out.write_all(&tag.to_be_bytes()), "Write tag failed: {}"); + let mut encrypted_meta_bytes = opt_result!(serde_json::to_vec(&encrypt_meta), "Generate meta json bytes failed: {}"); + if compress_meta { + encrypted_meta_bytes = opt_result!( + compress::compress(Compression::default(), &encrypted_meta_bytes), "Compress encrypted meta failed: {}"); + } let encrypted_meta_bytes_len = encrypted_meta_bytes.len() as u32; + debugging!("Encrypted meta len: {}", encrypted_meta_bytes_len); opt_result!(file_out.write_all(&encrypted_meta_bytes_len.to_be_bytes()), "Write meta len failed: {}"); opt_result!(file_out.write_all(&encrypted_meta_bytes), "Write meta failed: {}"); @@ -281,7 +297,7 @@ fn encrypt_envelop_shared_secret(key: &[u8], let wrap_key = WrapKey { header: WrapKeyHeader { - kid: Some(envelop.kid.clone()), + kid: None, // Some(envelop.kid.clone()), enc: enc_type.to_string(), e_pub_key: util::encode_base64_url_no_pad(&ephemeral_spki), }, diff --git a/src/compress.rs b/src/compress.rs index 8a44cc1..4bee024 100644 --- a/src/compress.rs +++ b/src/compress.rs @@ -4,6 +4,20 @@ use flate2::Compression; use flate2::write::{GzDecoder, GzEncoder}; use rust_util::{simple_error, XResult}; +pub fn compress(compression: Compression, message: &[u8]) -> XResult> { + let mut encoder = GzStreamEncoder::new(compression); + let mut buff = encoder.update(message)?; + buff.extend_from_slice(&encoder.finalize()?); + Ok(buff) +} + +pub fn decompress(message: &[u8]) -> XResult> { + let mut decoder = GzStreamDecoder::new(); + let mut buff = decoder.update(message)?; + buff.extend_from_slice(&decoder.finalize()?); + Ok(buff) +} + pub struct GzStreamEncoder { gz_encoder: GzEncoder>, } diff --git a/src/crypto_aes.rs b/src/crypto_aes.rs index 8a8bbb3..0b4fd73 100644 --- a/src/crypto_aes.rs +++ b/src/crypto_aes.rs @@ -1,25 +1,48 @@ use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor}; use rust_util::{opt_result, XResult}; +use zeroize::Zeroize; + +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> { - let key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}"); + let mut key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}"); let mut aes256_gcm = Aes256GcmStreamDecryptor::new(key, nonce); let mut b1 = aes256_gcm.update(message); let b2 = opt_result!(aes256_gcm.finalize(), "Invalid envelop: {}"); b1.extend_from_slice(&b2); + key.zeroize(); Ok(b1) } +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> { - let key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}"); + let mut key: [u8; 32] = opt_result!(key.try_into(), "Invalid envelop: {}"); let mut aes256_gcm = Aes256GcmStreamEncryptor::new(key, nonce); let mut b1 = aes256_gcm.update(message); let (b2, tag) = aes256_gcm.finalize(); b1.extend_from_slice(&b2); b1.extend_from_slice(&tag); + key.zeroize(); Ok(b1) } +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() { let data_key = hex::decode("0001020304050607080910111213141516171819202122232425262728293031").unwrap(); diff --git a/src/file.rs b/src/file.rs index 860d9db..50fe523 100644 --- a/src/file.rs +++ b/src/file.rs @@ -1,9 +1,9 @@ use std::io::{Read, Write}; -use rust_util::{opt_result, simple_error, XResult}; +use rust_util::{debugging, opt_result, simple_error, XResult}; use crate::spec::TinyEncryptMeta; -use crate::util; +use crate::{compress, util}; pub fn _write_tiny_encrypt_meta(w: &mut W, meta: &TinyEncryptMeta) -> XResult { let meta_json = opt_result!( serde_json::to_string(meta), "Meta to JSON failed: {}"); @@ -27,7 +27,9 @@ pub fn read_tiny_encrypt_meta(r: &mut R) -> XResult { let mut tag_buff = [0_u8; 2]; opt_result!(r.read_exact(&mut tag_buff), "Read tag failed: {}"); let tag = u16::from_be_bytes(tag_buff); - if tag != util::TINY_ENC_MAGIC_TAG { + let is_normal_tiny_enc = tag == util::TINY_ENC_MAGIC_TAG; + let is_compressed_tiny_enc = tag == util::TINY_ENC_COMPRESSED_MAGIC_TAG; + if !is_normal_tiny_enc && !is_compressed_tiny_enc { return simple_error!("Tag is not 0x01, but is: 0x{:x}", tag); } @@ -38,8 +40,15 @@ pub fn read_tiny_encrypt_meta(r: &mut R) -> XResult { return simple_error!("Meta too large: {}", length); } + debugging!("Encrypted meta len: {}", length); let mut meta_buff = vec![0; length as usize]; opt_result!(r.read_exact(meta_buff.as_mut_slice()), "Read meta failed: {}"); + debugging!("Tiny enc meta compressed: {}", is_compressed_tiny_enc); + if is_compressed_tiny_enc { + meta_buff = opt_result!(compress::decompress(&meta_buff), "Decompress meta failed: {}"); + } + debugging!("Encrypted meta: {}", String::from_utf8_lossy(&meta_buff)); + Ok(opt_result!(serde_json::from_slice(&meta_buff), "Parse meta failed: {}")) } diff --git a/src/spec.rs b/src/spec.rs index 8ccb927..3ff19a0 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,10 +1,12 @@ use std::fs::Metadata; +use flate2::Compression; -use rust_util::util_time; +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::util::{encode_base64, get_user_agent, TINY_ENC_AES_GCM}; +use crate::util::{encode_base64, get_user_agent, SALT_META, TINY_ENC_AES_GCM}; pub const TINY_ENCRYPT_VERSION_10: &'static str = "1.0"; pub const TINY_ENCRYPT_VERSION_11: &'static str = "1.1"; @@ -16,19 +18,30 @@ pub struct TinyEncryptMeta { pub version: String, pub created: u64, pub user_agent: String, + #[serde(skip_serializing_if = "Option::is_none")] pub comment: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub encrypted_comment: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub encrypted_meta: Option, // --------------------------------------- + #[serde(skip_serializing_if = "Option::is_none")] pub pgp_envelop: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub pgp_fingerprint: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub age_envelop: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub age_recipient: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub ecdh_envelop: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub ecdh_point: Option, + #[serde(skip_serializing_if = "Option::is_none")] pub envelop: Option, // --------------------------------------- pub envelops: Option>, + #[serde(skip_serializing_if = "Option::is_none")] pub encryption_algorithm: Option, pub nonce: String, pub file_length: u64, @@ -41,6 +54,7 @@ pub struct TinyEncryptMeta { pub struct TinyEncryptEnvelop { pub r#type: TinyEncryptEnvelopType, pub kid: String, + #[serde(skip_serializing_if = "Option::is_none")] pub desc: Option, pub encrypted_key: String, } @@ -78,6 +92,32 @@ impl TinyEncryptEnvelopType { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct EncEncryptedMeta { + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, +} + +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: {}"); + decrypted = opt_result!(compress::decompress(&decrypted), "Decode faield: {}"); + let meta = opt_result!( + serde_json::from_slice::(&decrypted), "Parse failed: {}"); + return Ok(meta); + } + + pub fn seal(&self, 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: {}"); + Ok(encrypted) + } +} + pub struct EncMetadata { pub comment: Option, pub encrypted_comment: Option, diff --git a/src/util.rs b/src/util.rs index 7e9fd27..802fb29 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,6 +1,6 @@ use std::{fs, io}; use std::io::Write; -use std::path::Path; +use std::path::{Path, PathBuf}; use base64::Engine; use base64::engine::general_purpose; @@ -17,6 +17,20 @@ pub const TINY_ENC_CONFIG_FILE: &str = "~/.tinyencrypt/config-rs.json"; pub const TINY_ENC_AES_GCM: &str = "AES/GCM"; pub const TINY_ENC_MAGIC_TAG: u16 = 0x01; +pub const TINY_ENC_COMPRESSED_MAGIC_TAG: u16 = 0x02; + +pub const SALT_COMMENT: &[u8] = b"salt:comment"; +pub const SALT_META: &[u8] = b"salt:meta"; + +pub fn get_file_name(path: &PathBuf) -> String { + let path_display = format!("{}", path.display()); + if path_display.contains("/") { + if let Some(p) = path_display.split("/").last() { + return p.to_string(); + } + } + path_display +} pub fn require_tiny_enc_file_and_exists(path: impl AsRef) -> XResult<()> { let path = path.as_ref();