diff --git a/.gitignore b/.gitignore index 8a4a29f..fe09086 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +_tinyencrypt_config-rs.json *.tinyenc # ---> Rust # Generated by Cargo diff --git a/Cargo.lock b/Cargo.lock index 97646a7..681f560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2139,7 +2139,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "0.0.3" +version = "0.1.0" dependencies = [ "aes-gcm-stream", "base64", diff --git a/Cargo.toml b/Cargo.toml index 5884710..271527f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "0.0.3" +version = "0.1.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 8c7f685..22553a5 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -2,6 +2,7 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; use std::str::FromStr; +use std::time::Instant; use clap::Args; use openpgp_card::crypto_data::Cryptogram; @@ -25,10 +26,10 @@ pub struct CmdDecrypt { /// Files need to be decrypted pub paths: Vec, /// PIN - #[arg(long)] + #[arg(long, short = 'p')] pub pin: Option, /// SLOT - #[arg(long)] + #[arg(long, short = 's')] pub slot: Option, } @@ -49,11 +50,11 @@ pub fn decrypt_single(path: &PathBuf, pin: &Option, slot: &Option, slot: &Option, // Comment + #[arg(long, short = 'c')] pub comment: Option, // Comment + #[arg(long, short = 'C')] pub encrypted_comment: Option, // Encryption profile + #[arg(long, short = 'p')] pub profile: Option, + #[arg(long, short = 'x')] + pub compress: bool, } pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> { let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE)?; + debugging!("Found tiny encrypt config: {:?}", config); let envelops = config.find_envelops(&cmd_encrypt.profile)?; if envelops.is_empty() { return simple_error!("Cannot find any valid envelops"); } + debugging!("Found envelops: {:?}", envelops); debugging!("Cmd encrypt: {:?}", cmd_encrypt); for path in &cmd_encrypt.paths { - match encrypt_single(path, &envelops) { + match encrypt_single(path, &envelops, &cmd_encrypt) { Ok(_) => success!("Encrypt {} succeed", path.to_str().unwrap_or("N/A")), Err(e) => failure!("Encrypt {} failed: {}", path.to_str().unwrap_or("N/A"), e), } @@ -40,27 +52,92 @@ pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> { Ok(()) } -fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop]) -> XResult<()> { +fn encrypt_single(path: &PathBuf, envelops: &[&TinyEncryptConfigEnvelop], cmd_encrypt: &CmdEncrypt) -> XResult<()> { + let path_display = format!("{}", path.display()); + util::require_none_tiny_enc_file_and_exists(path)?; + + let mut file_in = opt_result!(File::open(path), "Open file: {} failed: {}", &path_display); + + let path_out = format!("{}{}", path_display, util::TINY_ENC_FILE_EXT); + util::require_file_not_exists(path_out.as_str())?; + let (key, nonce) = make_key256_and_nonce(); let envelops = encrypt_envelops(&key, &envelops)?; + let encrypted_comment = match &cmd_encrypt.encrypted_comment { + None => None, + Some(encrypted_comment) => Some(encode_base64( + &aes_gcm_encrypt(&key, &nonce, encrypted_comment.as_bytes())?)) + }; + let file_metadata = opt_result!(fs::metadata(path), "Read file: {} meta failed: {}", path.display()); let enc_metadata = EncMetadata { - comment: None, - encrypted_comment: None, + comment: cmd_encrypt.comment.clone(), + encrypted_comment, encrypted_meta: None, - compress: false, + compress: cmd_encrypt.compress, }; let encrypt_meta = TinyEncryptMeta::new(&file_metadata, &enc_metadata, &nonce, envelops); debugging!("Encrypted meta: {:?}", encrypt_meta); - // TODO write to file and do encrypt + + 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 encrypted_meta_bytes_len = encrypted_meta_bytes.len() as u32; + 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: {}"); + + let start = Instant::now(); + encrypt_file(&mut file_in, &mut file_out, &key, &nonce, cmd_encrypt.compress)?; + let encrypt_duration = start.elapsed(); + debugging!("Encrypt file: {} elapsed: {} ms", path_display, encrypt_duration.as_millis()); + zeroize(key); zeroize(nonce); Ok(()) } + +fn encrypt_file(file_in: &mut File, file_out: &mut File, key: &[u8], nonce: &[u8], compress: bool) -> XResult { + let mut total_len = 0; + let mut buffer = [0u8; 1024 * 8]; + let key = opt_result!(key.try_into(), "Key is not 32 bytes: {}"); + let mut gz_encoder = GzStreamEncoder::new_default(); + let mut encryptor = aes_gcm_stream::Aes256GcmStreamEncryptor::new(key, &nonce); + loop { + let len = opt_result!(file_in.read(&mut buffer), "Read file failed: {}"); + if len == 0 { + let last_block = if compress { + let last_compressed_buffer = opt_result!(gz_encoder.finalize(), "Decompress file failed: {}"); + let mut encrypted_block = encryptor.update(&last_compressed_buffer); + let (last_block, tag) = encryptor.finalize(); + encrypted_block.extend_from_slice(&last_block); + encrypted_block.extend_from_slice(&tag); + encrypted_block + } else { + let (mut last_block, tag) = encryptor.finalize(); + last_block.extend_from_slice(&tag); + last_block + }; + opt_result!(file_out.write_all(&last_block), "Write file failed: {}"); + success!("Decrypt finished, total bytes: {}", total_len); + break; + } else { + total_len += len; + let encrypted = if compress { + let compressed = opt_result!(gz_encoder.update(&buffer[0..len]), "Decompress file failed: {}"); + encryptor.update(&compressed) + } else { + encryptor.update(&buffer[0..len]) + }; + opt_result!(file_out.write_all(&encrypted), "Write file failed: {}"); + } + } + Ok(total_len) +} + fn encrypt_envelops(key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResult> { let mut encrypted_envelops = vec![]; for envelop in envelops { @@ -89,7 +166,7 @@ fn encrypt_envelop_ecdh(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResu header: WrapKeyHeader { kid: Some(envelop.kid.clone()), enc: ENC_AES256_GCM_P256.to_string(), - e_pub_key: encode_base64(&ephemeral_spki), + e_pub_key: encode_base64_url_no_pad(&ephemeral_spki), }, nonce, encrypted_data: encrypted_key, diff --git a/src/config.rs b/src/config.rs index 718dce4..4d3fb79 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,9 @@ +use std::cmp::Ordering; use std::collections::HashMap; use std::fs; + use rust_util::{debugging, opt_result, simple_error, XResult}; use rust_util::util_file::resolve_file_path; - use serde::{Deserialize, Serialize}; use crate::spec::TinyEncryptEnvelopType; @@ -15,7 +16,7 @@ use crate::spec::TinyEncryptEnvelopType; /// "type": "pgp", /// "kid": "KID-1", /// "desc": "this is key 001", -/// "public_key": "----- BEGIN OPENPGP ..." +/// "publicPart": "----- BEGIN OPENPGP ..." /// }, /// { /// "type": "ecdh", @@ -71,10 +72,17 @@ impl TinyEncryptConfig { }); } } - let envelops: Vec<_> = matched_envelops_map.values().map(|envelop| *envelop).collect(); + let mut envelops: Vec<_> = matched_envelops_map.values().map(|envelop| *envelop).collect(); if envelops.is_empty() { return simple_error!("Profile: {} has no valid envelopes found", profile); } + envelops.sort_by(|e1, e2| { + if e1.r#type < e2.r#type { return Ordering::Less; } + if e1.r#type > e2.r#type { return Ordering::Greater; } + if e1.kid < e2.kid { return Ordering::Less; } + if e1.kid > e2.kid { return Ordering::Greater; } + Ordering::Equal + }); debugging!("Found envelopes: {:#?}", envelops); Ok(envelops) } diff --git a/src/file.rs b/src/file.rs index 142bb7f..860d9db 100644 --- a/src/file.rs +++ b/src/file.rs @@ -3,6 +3,7 @@ use std::io::{Read, Write}; use rust_util::{opt_result, simple_error, XResult}; use crate::spec::TinyEncryptMeta; +use crate::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: {}"); @@ -26,7 +27,7 @@ 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 != 0x01 { + if tag != util::TINY_ENC_MAGIC_TAG { return simple_error!("Tag is not 0x01, but is: 0x{:x}", tag); } diff --git a/src/main.rs b/src/main.rs index 2a4afc0..05d7f04 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ extern crate core; use clap::{Parser, Subcommand}; -use rust_util::XResult; +use rust_util::{debugging, XResult}; use crate::cmd_decrypt::CmdDecrypt; use crate::cmd_encrypt::CmdEncrypt; @@ -46,12 +46,15 @@ fn main() -> XResult<()> { let args = Cli::parse(); match args.command { Commands::Encrypt(cmd_encrypt) => { + debugging!("Encrypt: {:?}", cmd_encrypt); cmd_encrypt::encrypt(cmd_encrypt) } Commands::Decrypt(cmd_decrypt) => { + debugging!("Decrypt: {:?}", cmd_decrypt); cmd_decrypt::decrypt(cmd_decrypt) } Commands::Info(cmd_info) => { + debugging!("Info: {:?}", cmd_info); cmd_info::info(cmd_info) } } diff --git a/src/spec.rs b/src/spec.rs index 9e547c6..4949e71 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -46,7 +46,7 @@ pub struct TinyEncryptEnvelop { } /// NOTICE: Kms and Age is not being supported in the future -#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, PartialOrd)] pub enum TinyEncryptEnvelopType { #[serde(rename = "pgp")] Pgp, @@ -96,7 +96,7 @@ impl TinyEncryptMeta { ecdh_point: None, envelop: None, envelops: Some(envelops), - encryption_algorithm: None, + encryption_algorithm: None, // use none default nonce: encode_base64(nonce), file_length: metadata.len(), file_last_modified: match metadata.modified() { diff --git a/src/util.rs b/src/util.rs index a2a6e3f..2bd98c2 100644 --- a/src/util.rs +++ b/src/util.rs @@ -12,6 +12,8 @@ pub const ENC_AES256_GCM_P256: &str = "aes256-gcm-p256"; pub const TINY_ENC_FILE_EXT: &str = ".tinyenc"; pub const TINY_ENC_CONFIG_FILE: &str = "~/.tinyencrypt/config-rs.json"; +pub const TINY_ENC_MAGIC_TAG: u16 = 0x01; + pub fn require_tiny_enc_file_and_exists(path: impl AsRef) -> XResult<()> { let path = path.as_ref(); let path_display = format!("{}", path.display()); @@ -22,6 +24,16 @@ pub fn require_tiny_enc_file_and_exists(path: impl AsRef) -> XResult<()> { Ok(()) } +pub fn require_none_tiny_enc_file_and_exists(path: impl AsRef) -> XResult<()> { + let path = path.as_ref(); + let path_display = format!("{}", path.display()); + if path_display.ends_with(TINY_ENC_FILE_EXT) { + return simple_error!("File is already tiny encrypt file: {}", &path_display); + } + require_file_exists(path)?; + Ok(()) +} + pub fn require_file_exists(path: impl AsRef) -> XResult<()> { let path = path.as_ref(); match fs::metadata(path) { diff --git a/src/util_ecdh.rs b/src/util_ecdh.rs index dbd4be0..5898699 100644 --- a/src/util_ecdh.rs +++ b/src/util_ecdh.rs @@ -4,22 +4,23 @@ use rust_util::{opt_result, XResult}; use p256::pkcs8::EncodePublicKey; use p256::{EncodedPoint, PublicKey}; -use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; +use p256::elliptic_curve::sec1::FromEncodedPoint; +// use p256::elliptic_curve::sec1::{FromEncodedPoint, ToEncodedPoint}; -#[derive(Debug)] -pub struct EphemeralKeyBytes(EncodedPoint); - -impl EphemeralKeyBytes { - pub fn from_public_key(epk: &PublicKey) -> Self { - EphemeralKeyBytes(epk.to_encoded_point(true)) - } - - pub fn decompress(&self) -> EncodedPoint { - // EphemeralKeyBytes is a valid compressed encoding by construction. - let p = PublicKey::from_encoded_point(&self.0).unwrap(); - p.to_encoded_point(false) - } -} +// #[derive(Debug)] +// pub struct EphemeralKeyBytes(EncodedPoint); +// +// impl EphemeralKeyBytes { +// pub fn from_public_key(epk: &PublicKey) -> Self { +// EphemeralKeyBytes(epk.to_encoded_point(true)) +// } +// +// pub fn decompress(&self) -> EncodedPoint { +// // EphemeralKeyBytes is a valid compressed encoding by construction. +// let p = PublicKey::from_encoded_point(&self.0).unwrap(); +// p.to_encoded_point(false) +// } +// } pub fn compute_shared_secret(public_key_point_hex: &str) -> XResult<(Vec, Vec)> { let public_key_point_bytes = opt_result!(hex::decode(public_key_point_hex), "Parse public key point hex failed: {}");