diff --git a/Cargo.lock b/Cargo.lock index ac5b218..f976d2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1696,9 +1696,9 @@ dependencies = [ [[package]] name = "rust_util" -version = "0.6.41" +version = "0.6.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df24005feacce81f4ae340464b39c380f7e01e7225bfdef62d40cb44cb1c11d7" +checksum = "cc01275355fe567d95aacf1c243d0c2c51fe9f94271a498c3fe8335d3c2b1a01" dependencies = [ "lazy_static", "libc", diff --git a/Cargo.toml b/Cargo.toml index af2d8b0..a386cef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ openpgp-card = "0.3.7" openpgp-card-pcsc = "0.3.0" reqwest = { version = "0.11.14", features = ["blocking", "rustls", "rustls-tls"] } rpassword = "7.2.0" -rust_util = "0.6.41" +rust_util = "0.6.42" serde = { version = "1.0.152", features = ["derive"] } serde_json = "1.0.93" sha256 = "1.4.0" @@ -31,3 +31,5 @@ yubikey = { version = "0.8.0", features = ["untested"] } codegen-units = 1 opt-level = 'z' lto = true +panic = 'abort' +strip = true diff --git a/README.md b/README.md index bd2b840..ba4e082 100644 --- a/README.md +++ b/README.md @@ -4,3 +4,33 @@ Tiny encrypt for Rust > Tiny encrypt spec see: https://git.hatter.ink/hatter/tiny-encrypt-java +TODOs: +* Decrypt supports compress +* Encrypt subcommand + + +
+ +Encrypt config `~/.tinyencrypt/config-rs.json`: +```json +{ + "envelops": [ + { + "type": "pgp", + "kid": "KID-1", + "desc": "this is key 001", + "publicPart": "----- BEGIN OPENPGP ..." + }, + { + "type": "ecdh", + "kid": "KID-2", + "desc": "this is key 002", + "publicPart": "04..." + } + ], + "profiles": { + "default": ["KID-1", "KID-2"], + "leve2": ["KID-2"] + } + } +``` diff --git a/src/cmd_decrypt.rs b/src/cmd_decrypt.rs index 365945c..286f2a5 100644 --- a/src/cmd_decrypt.rs +++ b/src/cmd_decrypt.rs @@ -3,8 +3,8 @@ use std::fs::File; use std::io::{Read, Write}; use std::path::PathBuf; use std::str::FromStr; -use clap::Args; +use clap::Args; use openpgp_card::crypto_data::Cryptogram; use openpgp_card::OpenPgp; use rust_util::{debugging, failure, opt_result, simple_error, success, util_term, XResult}; @@ -32,7 +32,17 @@ pub struct CmdDecrypt { pub slot: Option, } -pub fn decrypt(path: &PathBuf, pin: &Option, slot: &Option) -> XResult<()> { +pub fn decrypt(cmd_decrypt: CmdDecrypt) -> XResult<()> { + for path in &cmd_decrypt.paths { + match decrypt_single(path, &cmd_decrypt.pin, &cmd_decrypt.slot) { + Ok(_) => success!("Decrypt {} succeed", path.to_str().unwrap_or("N/A")), + Err(e) => failure!("Decrypt {} failed: {}", path.to_str().unwrap_or("N/A"), e), + } + } + Ok(()) +} + +pub fn decrypt_single(path: &PathBuf, pin: &Option, slot: &Option) -> XResult<()> { let path_display = format!("{}", path.display()); if !path_display.ends_with(TINY_ENC_FILE_EXT) { return simple_error!("File is not tiny encrypt file: {}", &path_display); diff --git a/src/cmd_encrypt.rs b/src/cmd_encrypt.rs new file mode 100644 index 0000000..1c0be8f --- /dev/null +++ b/src/cmd_encrypt.rs @@ -0,0 +1,77 @@ +use std::path::PathBuf; + +use clap::Args; +use rust_util::{debugging, simple_error, XResult}; + +use crate::config::{TinyEncryptConfig, TinyEncryptConfigEnvelop}; +use crate::spec::{TinyEncryptEnvelop, TinyEncryptEnvelopType}; +use crate::util::TINY_ENC_CONFIG_FILE; + +#[derive(Debug, Args)] +pub struct CmdEncrypt { + /// Files need to be decrypted + pub paths: Vec, + // Comment + pub comment: Option, + // Encryption profile + pub profile: Option, +} + +pub fn encrypt(cmd_encrypt: CmdEncrypt) -> XResult<()> { + let config = TinyEncryptConfig::load(TINY_ENC_CONFIG_FILE)?; + let envelops = config.find_envelops(&cmd_encrypt.profile); + if envelops.is_empty() { + return simple_error!("Cannot find any valid envelops"); + } + + let (key, nonce) = make_key_and_nonce(); + let envelops = encrypt_envelops(&key, &envelops)?; + + debugging!("Envelops: {:?}", envelops); + + println!("Cmd encrypt: {:?}", cmd_encrypt); + Ok(()) +} + +fn encrypt_envelops(key: &[u8], envelops: &[&TinyEncryptConfigEnvelop]) -> XResult> { + let mut encrypted_envelops = vec![]; + for envelop in envelops { + match envelop.r#type { + TinyEncryptEnvelopType::Pgp => { + encrypted_envelops.push(encrypt_envelop_pgp(key, envelop)?); + } + TinyEncryptEnvelopType::Ecdh => { + encrypted_envelops.push(encrypt_envelop_ecdh(key, envelop)?); + } + _ => return simple_error!("Not supported type: {:?}", envelop.r#type), + } + } + Ok(encrypted_envelops) +} + +fn encrypt_envelop_ecdh(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult { + Ok(TinyEncryptEnvelop { + r#type: envelop.r#type, + kid: envelop.kid.clone(), + desc: envelop.desc.clone(), + encrypted_key: "".to_string(), // TODO ... + }) +} + + +fn encrypt_envelop_pgp(key: &[u8], envelop: &TinyEncryptConfigEnvelop) -> XResult { + Ok(TinyEncryptEnvelop { + r#type: envelop.r#type, + kid: envelop.kid.clone(), + desc: envelop.desc.clone(), + encrypted_key: "".to_string(), // TODO ... + }) +} + +fn make_key_and_nonce() -> (Vec, Vec) { + // TODO use random + let key = [0u8; 32]; + let nonce = [0u8; 12]; + + (key.into(), nonce.into()) +} \ No newline at end of file diff --git a/src/cmd_info.rs b/src/cmd_info.rs index 92a8c67..f134ddd 100644 --- a/src/cmd_info.rs +++ b/src/cmd_info.rs @@ -20,7 +20,7 @@ pub struct CmdInfo { pub raw_meta: bool, } -pub fn info(cmd_info: &CmdInfo) -> XResult<()> { +pub fn info(cmd_info: CmdInfo) -> XResult<()> { let path_display = format!("{}", cmd_info.path.display()); let mut file_in = opt_result!(File::open(&cmd_info.path), "Open file: {} failed: {}", &path_display); let meta = opt_result!(file::read_tiny_encrypt_meta_and_normalize(&mut file_in), "Read file: {}, failed: {}", &path_display); diff --git a/src/config.rs b/src/config.rs index 00de0b4..b823167 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,72 @@ +use std::collections::HashMap; +use std::fs; +use rust_util::{opt_result, XResult}; +use rust_util::util_file::resolve_file_path; + use serde::{Deserialize, Serialize}; +use crate::spec::TinyEncryptEnvelopType; + +/// Config file sample: +/// ~/.tinyencrypt/config-rs.json +/// { +/// "envelops": [ +/// { +/// "type": "pgp", +/// "kid": "KID-1", +/// "desc": "this is key 001", +/// "public_key": "----- BEGIN OPENPGP ..." +/// }, +/// { +/// "type": "ecdh", +/// "kid": "KID-2", +/// "desc": "this is key 002", +/// "publicPart": "04..." +/// } +/// ], +/// "profiles": { +/// "default": ["KID-1", "KID-2"], +/// "leve2": ["KID-2"] +/// } +/// } #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TinyEncryptConfig { - // card cli is not used by tiny-encrypt-rs - pub card_cli: String, - pub default_key_name: String, - pub local_private_key_pem_challenge: String, - pub local_private_key_pem_encrypted: String, - pub local_public_key_pem: String, - pub pgp_encrypt_public_key_pem: Option, + pub envelops: Vec, + pub profiles: HashMap>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TinyEncryptConfigEnvelop { + pub r#type: TinyEncryptEnvelopType, + pub kid: String, + pub desc: Option, + pub public_part: String, +} + +impl TinyEncryptConfig { + pub fn load(file: &str) -> XResult { + let resolved_file = resolve_file_path(file); + let config_contents = opt_result!(fs::read_to_string(&resolved_file), "Read file: {}, failed: {}", file); + // TODO replace with Human JSON + Ok(opt_result!(serde_json::from_str(&config_contents), "Parse file: {}, failed: {}", file)) + } + + pub fn find_envelops(&self, profile: &Option) -> Vec<&TinyEncryptConfigEnvelop> { + let profile = profile.as_ref().map(String::as_str).unwrap_or("default"); + let mut matched_envelops_map = HashMap::new(); + if let Some(key_ids) = self.profiles.get(profile) { + for key_id in key_ids { + self.envelops.iter().for_each(|envelop| { + let is_matched = (&envelop.kid == key_id) + || key_id == &format!("type:{}", &envelop.r#type.get_name()); + if is_matched { + matched_envelops_map.insert(&envelop.kid, envelop); + } + }); + } + } + matched_envelops_map.values().map(|envelop| *envelop).collect() + } } diff --git a/src/main.rs b/src/main.rs index 9a457e0..4bb0e0e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,10 @@ extern crate core; -use std::path::PathBuf; - use clap::{Parser, Subcommand}; -use rust_util::{failure, information, success, XResult}; +use rust_util::XResult; use crate::cmd_decrypt::CmdDecrypt; +use crate::cmd_encrypt::CmdEncrypt; use crate::cmd_info::CmdInfo; mod util; @@ -17,6 +16,7 @@ mod file; mod card; mod cmd_info; mod cmd_decrypt; +mod cmd_encrypt; #[derive(Debug, Parser)] #[command(name = "tiny-encrypt-rs")] @@ -30,10 +30,7 @@ struct Cli { enum Commands { /// Encrypt file(s) #[command(arg_required_else_help = true, short_flag = 'e')] - Encrypt { - /// Files need to be encrypted - paths: Vec, - }, + Encrypt(CmdEncrypt), /// Decrypt file(s) #[command(arg_required_else_help = true, short_flag = 'd')] Decrypt(CmdDecrypt), @@ -45,21 +42,14 @@ enum Commands { fn main() -> XResult<()> { let args = Cli::parse(); match args.command { - Commands::Encrypt { paths } => { - paths.iter().for_each(|f| information!("{:?}", f)); - Ok(()) + Commands::Encrypt(cmd_encrypt) => { + cmd_encrypt::encrypt(cmd_encrypt) } Commands::Decrypt(cmd_decrypt) => { - for path in &cmd_decrypt.paths { - match cmd_decrypt::decrypt(path, &cmd_decrypt.pin, &cmd_decrypt.slot) { - Ok(_) => success!("Decrypt {} succeed", path.to_str().unwrap_or("N/A")), - Err(e) => failure!("Decrypt {} failed: {}", path.to_str().unwrap_or("N/A"), e), - } - } - Ok(()) + cmd_decrypt::decrypt(cmd_decrypt) } - Commands::Info(command_info) => { - cmd_info::info(&command_info) + Commands::Info(cmd_info) => { + cmd_info::info(cmd_info) } } } \ No newline at end of file diff --git a/src/spec.rs b/src/spec.rs index e2771e7..9e547c6 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -1,11 +1,13 @@ +use std::fs::Metadata; + +use rust_util::util_time; +use rust_util::util_time::get_millis; use serde::{Deserialize, Serialize}; -// pub const TINY_ENCRYPT_VERSION: &'static str = "1.0"; +use crate::util::{encode_base64, get_user_agent}; -// pub const ENVELOP_TYPE_KMS: &'static str = "kms"; -// pub const ENVELOP_TYPE_PGP: &'static str = "pgp"; -// pub const ENVELOP_TYPE_AGE: &'static str = "age"; -// pub const ENVELOP_TYPE_ECDH: &'static str = "ecdh"; +// pub const TINY_ENCRYPT_VERSION_10: &'static str = "1.0"; +pub const TINY_ENCRYPT_VERSION_11: &'static str = "1.1"; /// Specification: [Tiny Encrypt Spec V1.1](https://git.hatter.ink/hatter/tiny-encrypt-java/src/branch/master/TinyEncryptSpecV1.1.md) #[derive(Clone, Debug, Serialize, Deserialize)] @@ -43,6 +45,7 @@ pub struct TinyEncryptEnvelop { pub encrypted_key: String, } +/// NOTICE: Kms and Age is not being supported in the future #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub enum TinyEncryptEnvelopType { #[serde(rename = "pgp")] @@ -69,7 +72,41 @@ impl TinyEncryptEnvelopType { } } +pub struct EncMetadata { + pub comment: Option, + pub encrypted_comment: Option, + pub encrypted_meta: Option, + pub compress: bool, +} + impl TinyEncryptMeta { + pub fn new(metadata: &Metadata, enc_metadata: &EncMetadata, nonce: &[u8], envelops: Vec) -> Self { + TinyEncryptMeta { + version: TINY_ENCRYPT_VERSION_11.to_string(), + created: util_time::get_current_millis() as u64, + user_agent: get_user_agent(), + comment: enc_metadata.comment.to_owned(), + encrypted_comment: enc_metadata.encrypted_comment.to_owned(), + encrypted_meta: enc_metadata.encrypted_meta.to_owned(), + pgp_envelop: None, + pgp_fingerprint: None, + age_envelop: None, + age_recipient: None, + ecdh_envelop: None, + ecdh_point: None, + envelop: None, + envelops: Some(envelops), + encryption_algorithm: None, + nonce: encode_base64(nonce), + file_length: metadata.len(), + file_last_modified: match metadata.modified() { + Ok(modified) => get_millis(&modified) as u64, + Err(_) => 0, + }, + compress: enc_metadata.compress, + } + } + pub fn normalize(&mut self) { if self.envelops.is_none() { self.envelops = Some(vec![]); @@ -84,7 +121,7 @@ impl TinyEncryptMeta { if let (Some(envelop), Some(envelops)) = (&self.envelop, &mut self.envelops) { envelops.push(TinyEncryptEnvelop { r#type: TinyEncryptEnvelopType::Kms, - kid: "".into(), + kid: "".to_string(), desc: None, encrypted_key: envelop.into(), }); diff --git a/src/util.rs b/src/util.rs index 2ce470d..076eae1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,6 +7,7 @@ use rust_util::{warning, XResult}; 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 fn simple_kdf(input: &[u8]) -> Vec { let input = hex::decode(sha256::digest(input)).unwrap(); @@ -24,6 +25,10 @@ pub fn decode_base64(input: &str) -> XResult> { Ok(general_purpose::STANDARD.decode(input)?) } +pub fn encode_base64(input: &[u8]) -> String { + general_purpose::STANDARD.encode(input) +} + pub fn decode_base64_url_no_pad(input: &str) -> XResult> { Ok(general_purpose::URL_SAFE_NO_PAD.decode(input)?) } @@ -46,7 +51,7 @@ pub fn read_number(hint: &str, from: usize, to: usize) -> usize { } } -pub fn _get_user_agent() -> String { +pub fn get_user_agent() -> String { format!("TinyEncrypt-rs v{}@{}", env!("CARGO_PKG_VERSION"), if cfg!(target_os = "macos") { "MacOS"