diff --git a/Cargo.lock b/Cargo.lock index 47c537e..742361e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -31,9 +31,9 @@ dependencies = [ [[package]] name = "aes-gcm-stream" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75e8bcc55e692d9a680b8ab2223f26dfc2f2e3f6288ad01fcab0678b06c7e1ff" +checksum = "1702f71a8446da86c4fe4b2fa1a93e5a16880a81f0c1d08312367917dee0e3ee" dependencies = [ "aes", "cipher", @@ -469,9 +469,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" +dependencies = [ + "powerfmt", +] [[package]] name = "des" @@ -610,9 +613,9 @@ checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" [[package]] name = "flate2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" dependencies = [ "crc32fast", "miniz_oxide", @@ -648,6 +651,17 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-set-times" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd738b84894214045e8414eaded76359b4a5773f0a0a56b16575110739cdcf39" +dependencies = [ + "io-lifetimes", + "rustix", + "windows-sys", +] + [[package]] name = "futures-channel" version = "0.3.28" @@ -961,6 +975,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-lifetimes" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffb4def18c48926ccac55c1223e02865ce1a821751a95920448662696e7472c" + [[package]] name = "ipnet" version = "2.8.0" @@ -1396,6 +1416,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1632,9 +1658,9 @@ dependencies = [ [[package]] name = "rust_util" -version = "0.6.42" +version = "0.6.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc01275355fe567d95aacf1c243d0c2c51fe9f94271a498c3fe8335d3c2b1a01" +checksum = "c1934a25d960599cde936954c2b88942c63a7decf585cf12fa6aabcaa7e67644" dependencies = [ "lazy_static", "libc", @@ -1668,9 +1694,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.18" +version = "0.38.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a74ee2d7c2581cd139b42447d7d9389b889bdaad3a73f1ebb16f2a3237bb19c" +checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" dependencies = [ "bitflags 2.4.0", "errno", @@ -1795,18 +1821,18 @@ checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" [[package]] name = "serde" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" +checksum = "8e422a44e74ad4001bdc8eede9a4570ab52f71190e9c076d14369f38b9200537" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.188" +version = "1.0.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" +checksum = "1e48d1f918009ce3145511378cf68d613e3b3d9137d67272562080d68a2b32d5" dependencies = [ "proc-macro2", "quote", @@ -2089,12 +2115,13 @@ dependencies = [ [[package]] name = "time" -version = "0.3.29" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "426f806f4089c493dcac0d24c29c01e2c38baf8e30f1b716ee37e83d200b18fe" +checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" dependencies = [ "deranged", "itoa", + "powerfmt", "serde", "time-core", "time-macros", @@ -2117,13 +2144,14 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "0.3.1" +version = "0.3.2" dependencies = [ "aes-gcm-stream", "base64", "chrono", "clap", "flate2", + "fs-set-times", "hex", "openpgp-card", "openpgp-card-pcsc", @@ -2218,20 +2246,19 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.37" +version = "0.1.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +checksum = "ee2ef2af84856a50c1d430afce2fdded0a4ec7eda868db86409b4543df0797f9" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", ] diff --git a/Cargo.toml b/Cargo.toml index ca64ca9..a349409 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "0.3.1" +version = "0.3.2" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" @@ -13,6 +13,7 @@ base64 = "0.21" chrono = "0.4" clap = { version = "4.1", features = ["derive"] } flate2 = "1.0" +fs-set-times = "0.20.0" hex = "0.4" openpgp-card = "0.3" openpgp-card-pcsc = "0.3" diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index 5e67f80..24d758c 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -2,15 +2,17 @@ use std::{fs, io}; use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; -use std::time::Instant; +use std::time::{Instant, SystemTime}; use clap::Args; +use fs_set_times::SystemTimeSpec; use openpgp_card::{OpenPgp, OpenPgpTransaction}; use openpgp_card::crypto_data::Cryptogram; use rust_util::{ debugging, failure, iff, information, opt_result, simple_error, success, util_msg, util_term, warning, XResult, }; +use rust_util::util_time::UnixEpochTime; use x509_parser::prelude::FromDer; use x509_parser::x509::SubjectPublicKeyInfo; use yubikey::piv::{AlgorithmId, decrypt_data}; @@ -20,9 +22,9 @@ use zeroize::Zeroize; use crate::{card, file, util, 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::spec::{EncEncryptedMeta, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; -use crate::consts::{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)] @@ -102,46 +104,28 @@ pub fn decrypt_single(config: &Option, let key = try_decrypt_key(config, selected_envelop, pin, slot)?; let nonce = opt_result!(util::decode_base64(&meta.nonce), "Decode nonce failed: {}"); - debugging!("Decrypt key: {}", hex::encode(&key)); + // 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(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 comment: {}", message), - } - } - } - } + let enc_meta = parse_encrypted_meta(&meta, &key, &nonce)?; + parse_encrypted_comment(&meta, &key, &nonce)?; 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 mut file_out = File::create(path_out)?; 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_msg::clear_lastline(); + + update_out_file_time(enc_meta, path_out); + let encrypt_duration = start.elapsed(); + debugging!("Inner decrypt file: {} elapsed: {} ms", path_display, encrypt_duration.as_millis()); } util::zeroize(key); @@ -193,6 +177,63 @@ fn decrypt_file(file_in: &mut File, file_out: &mut File, key: &[u8], nonce: &[u8 Ok(total_len) } +fn update_out_file_time(enc_meta: Option, path_out: &str) { + if let Some(enc_meta) = &enc_meta { + let create_time = enc_meta.c_time.map(|t| SystemTime::from_millis(t)); + let modify_time = enc_meta.m_time.map(|t| SystemTime::from_millis(t)); + if create_time.is_some() || modify_time.is_some() { + let set_times_result = fs_set_times::set_times( + path_out, + create_time.map(|t| SystemTimeSpec::Absolute(t)), + modify_time.map(|t| SystemTimeSpec::Absolute(t)), + ); + match set_times_result { + Ok(_) => information!("Set file time succeed."), + Err(e) => warning!("Set file time failed: {}", e), + } + } + } +} + +fn parse_encrypted_comment(meta: &TinyEncryptMeta, 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) { + 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 comment: {}", message), + } + } + } + } + Ok(()) +} + +fn parse_encrypted_meta(meta: &TinyEncryptMeta, 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: {}"); + if let Some(filename) = &enc_meta.filename { + information!("Source filename: {}", filename); + } + let fmt = simpledateformat::fmt(DATE_TIME_FORMAT).unwrap(); + if let Some(c_time) = &enc_meta.c_time { + information!("Source file create time: {}", fmt.format_local(SystemTime::from_millis(*c_time))); + } + if let Some(m_time) = &enc_meta.c_time { + information!("Source file modified time: {}", fmt.format_local(SystemTime::from_millis(*m_time))); + } + Some(enc_meta) + } + }) +} + fn try_decrypt_key(config: &Option, envelop: &TinyEncryptEnvelop, pin: &Option, diff --git a/src/cmd_encrypt.rs b/src/cmd_encrypt.rs index 16ff511..1ba0727 100644 --- a/src/cmd_encrypt.rs +++ b/src/cmd_encrypt.rs @@ -8,15 +8,16 @@ 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::util_time::UnixEpochTime; use zeroize::Zeroize; use crate::{file, util, util_ecdh, util_p384, util_x25519}; use crate::compress::GzStreamEncoder; use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop}; +use crate::consts::{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_encrypt, aes_gcm_encrypt_with_salt}; use crate::crypto_rsa::parse_spki; use crate::spec::{EncEncryptedMeta, EncMetadata, TINY_ENCRYPT_VERSION_10, TinyEncryptEnvelop, TinyEncryptEnvelopType, TinyEncryptMeta}; -use crate::consts::{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, WrapKeyHeader}; #[derive(Debug, Args)] @@ -121,12 +122,14 @@ fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_en &aes_gcm_encrypt_with_salt(&key, &nonce, SALT_COMMENT, encrypted_comment.as_bytes())?)) }; + let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display()); let enc_encrypted_meta = EncEncryptedMeta { filename: Some(util::get_file_name(path)), + c_time: file_metadata.created().ok().map(|t| t.to_millis()).flatten(), + m_time: file_metadata.modified().ok().map(|t| t.to_millis()).flatten(), }; 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, diff --git a/src/cmd_info.rs b/src/cmd_info.rs index 44e18e1..1530a08 100644 --- a/src/cmd_info.rs +++ b/src/cmd_info.rs @@ -1,15 +1,15 @@ use std::cmp::max; use std::fs::File; -use std::ops::Add; use std::path::PathBuf; use std::time::{Duration, SystemTime}; use clap::Args; -use rust_util::{iff, opt_result, simple_error, success, util_time, warning, XResult}; +use rust_util::{util_time, iff, opt_result, simple_error, success, warning, XResult}; +use rust_util::util_time::UnixEpochTime; use simpledateformat::format_human2; +use crate::consts::{DATE_TIME_FORMAT, TINY_ENC_AES_GCM, TINY_ENC_FILE_EXT}; use crate::file; -use crate::consts::{TINY_ENC_AES_GCM, TINY_ENC_FILE_EXT}; #[derive(Debug, Args)] pub struct CmdInfo { @@ -57,15 +57,15 @@ pub fn info_single(path: &PathBuf, cmd_info: &CmdInfo) -> XResult<()> { ); let now_millis = util_time::get_current_millis() as u64; - let fmt = simpledateformat::fmt("EEE MMM dd HH:mm:ss z yyyy").unwrap(); + let fmt = simpledateformat::fmt(DATE_TIME_FORMAT).unwrap(); infos.push(format!("{}: {}, {} ago", header("Last modified"), - fmt.format_local(from_unix_epoch(meta.file_last_modified)), + fmt.format_local(SystemTime::from_millis(meta.file_last_modified)), format_human2(Duration::from_millis(now_millis - meta.file_last_modified)) )); infos.push(format!("{}: {}, {} ago", header("Created"), - fmt.format_local(from_unix_epoch(meta.created)), + fmt.format_local(SystemTime::from_millis(meta.created)), format_human2(Duration::from_millis(now_millis - meta.created)) )); @@ -101,10 +101,6 @@ pub fn info_single(path: &PathBuf, cmd_info: &CmdInfo) -> XResult<()> { Ok(()) } -fn from_unix_epoch(t: u64) -> SystemTime { - SystemTime::UNIX_EPOCH.add(Duration::from_millis(t)) -} - fn header(h: &str) -> String { let width = 21; h.to_string() + ".".repeat(max(width - h.len(), 0)).as_str() diff --git a/src/consts.rs b/src/consts.rs index 81f67f8..6488582 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -16,3 +16,5 @@ pub const TINY_ENC_COMPRESSED_MAGIC_TAG: u16 = 0x02; // Encryption nonce salt pub const SALT_COMMENT: &[u8] = b"salt:comment"; pub const SALT_META: &[u8] = b"salt:meta"; + +pub const DATE_TIME_FORMAT: &str = "EEE MMM dd HH:mm:ss z yyyy"; diff --git a/src/spec.rs b/src/spec.rs index 33715bb..71ff70d 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -97,6 +97,10 @@ impl TinyEncryptEnvelopType { pub struct EncEncryptedMeta { #[serde(skip_serializing_if = "Option::is_none")] pub filename: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub c_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub m_time: Option, } impl EncEncryptedMeta { diff --git a/src/wrap_key.rs b/src/wrap_key.rs index 0ce392f..49a1df0 100644 --- a/src/wrap_key.rs +++ b/src/wrap_key.rs @@ -14,6 +14,7 @@ pub struct WrapKey { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct WrapKeyHeader { + #[serde(skip_serializing_if = "Option::is_none")] pub kid: Option, pub enc: String, pub e_pub_key: String,