From a7802a375028fd83236c2f0033798374f1d4e0f4 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Sun, 26 Jan 2025 00:22:16 +0800 Subject: [PATCH] feat: v1.8.5, support simple PBKDF encryption --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cmd_simple_encrypt_decrypt.rs | 39 ++++++- src/lib.rs | 1 + src/util.rs | 21 ++++ src/util_simple_pbe.rs | 175 ++++++++++++++++++++++++++++++ 6 files changed, 235 insertions(+), 5 deletions(-) create mode 100644 src/util_simple_pbe.rs diff --git a/Cargo.lock b/Cargo.lock index cfbfcf2..c34b229 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1867,7 +1867,7 @@ dependencies = [ [[package]] name = "tiny-encrypt" -version = "1.8.4" +version = "1.8.5" dependencies = [ "aes-gcm-stream", "base64 0.22.1", diff --git a/Cargo.toml b/Cargo.toml index 1404a17..e2dfe3e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tiny-encrypt" -version = "1.8.4" +version = "1.8.5" edition = "2021" license = "MIT" description = "A simple and tiny file encrypt tool" diff --git a/src/cmd_simple_encrypt_decrypt.rs b/src/cmd_simple_encrypt_decrypt.rs index 5c4b2cd..2b7007c 100644 --- a/src/cmd_simple_encrypt_decrypt.rs +++ b/src/cmd_simple_encrypt_decrypt.rs @@ -9,6 +9,7 @@ use serde::Serialize; use std::io; use std::io::Write; use std::process::exit; +use crate::util_simple_pbe::SimplePbkdfEncryptionV1; // Reference: https://git.hatter.ink/hatter/tiny-encrypt-rs/issues/3 const SIMPLE_ENCRYPTION_HEADER: &str = "tinyencrypt-dir"; @@ -40,6 +41,14 @@ pub struct CmdSimpleEncrypt { #[arg(long)] pub value_hex: Option, + /// With PBKDF encryption + #[arg(long, short = 'P')] + pub with_pbkdf_encryption: bool, + + /// PBKDF encryption password + #[arg(long, short = 'A')] + pub password: Option, + /// Direct output result value #[arg(long)] pub direct_output: bool, @@ -71,6 +80,10 @@ pub struct CmdSimpleDecrypt { #[arg(long, short = 'o')] pub output_format: Option, + /// PBKDF encryption password + #[arg(long, short = 'A')] + pub password: Option, + /// Direct output result value #[arg(long)] pub direct_output: bool, @@ -186,11 +199,17 @@ pub fn inner_simple_encrypt(cmd_simple_encrypt: CmdSimpleEncrypt) -> XResult<()> let envelops = cmd_encrypt::encrypt_envelops(cryptor, &value, &envelops)?; let envelops_json = serde_json::to_string(&envelops)?; - let simple_encrypt_result = format!("{}.{}", + let mut simple_encrypt_result = format!("{}.{}", SIMPLE_ENCRYPTION_HEADER, URL_SAFE_NO_PAD.encode(envelops_json.as_bytes()) ); + let with_pbkdf_encryption = cmd_simple_encrypt.with_pbkdf_encryption || cmd_simple_encrypt.password.is_some(); + if with_pbkdf_encryption { + let password = util::read_password(&cmd_simple_encrypt.password)?; + simple_encrypt_result = SimplePbkdfEncryptionV1::encrypt(&password, simple_encrypt_result.as_bytes())?.to_string(); + } + CmdResult::success(&simple_encrypt_result).print_exit(cmd_simple_encrypt.direct_output); } @@ -207,10 +226,18 @@ pub fn inner_simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()> _ => return simple_error!("not supported output format: {}", output_format), }; - let value = match cmd_simple_decrypt.get_value()? { + let mut value = match cmd_simple_decrypt.get_value()? { None => return simple_error!("--value-stdin/value must assign one"), Some(value) => value, }; + + if SimplePbkdfEncryptionV1::matches(&value) { + let simple_pbkdf_encryption_v1: SimplePbkdfEncryptionV1 = value.as_str().try_into()?; + let password = util::read_password(&cmd_simple_decrypt.password)?; + let plaintext_bytes = simple_pbkdf_encryption_v1.decrypt(&password)?; + value = opt_result!(String::from_utf8(plaintext_bytes), "Decrypt PBKDF encryption failed: {}"); + } + let value_parts = value.trim().split(SIMPLE_ENCRYPTION_DOT).collect::>(); if value_parts.len() != 2 { return simple_error!("bad value format: {}", value); @@ -234,7 +261,13 @@ pub fn inner_simple_decrypt(cmd_simple_decrypt: CmdSimpleDecrypt) -> XResult<()> return simple_error!("no envelops found: {:?}", cmd_simple_decrypt.key_id); } if filter_envelops.len() > 1 { - return simple_error!("too many envelops: {:?}, len: {}", cmd_simple_decrypt.key_id, filter_envelops.len()); + let mut kids = vec![]; + debugging!("Found {} envelopes", filter_envelops.len()); + for envelop in &filter_envelops { + kids.push(envelop.kid.clone()); + debugging!("- {} {}", envelop.kid, envelop.r#type.get_name()); + } + return simple_error!("too many envelops: {:?}, len: {}, matched kids: [{}]", cmd_simple_decrypt.key_id, filter_envelops.len(), kids.join(",")); } let value = crate::cmd_decrypt::try_decrypt_key(&config, filter_envelops[0], &pin, &slot, false)?; if cmd_simple_decrypt.direct_output && output_format == "plain" { diff --git a/src/lib.rs b/src/lib.rs index 9fbdfd5..e64649d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -75,4 +75,5 @@ mod util_keychainstatic; mod cmd_execenv; #[cfg(feature = "secure-enclave")] mod util_keychainkey; +mod util_simple_pbe; diff --git a/src/util.rs b/src/util.rs index b048ac3..c6876c1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -62,6 +62,27 @@ pub fn read_pin(pin: &Option) -> XResult { Ok(rpin) } +pub fn read_password(password: &Option) -> XResult { + let rpassword = match password { + Some(pin) => pin.to_string(), + None => { + let pin_entry = util_env::get_pin_entry().unwrap_or_else(|| "pinentry".to_string()); + if let Some(mut input) = PassphraseInput::with_binary(pin_entry) { + let secret = input + .with_description("Please input your password.") + .with_prompt("Password:") + .interact(); + opt_result!(secret, "Read password from pinentry failed: {}") + .expose_secret() + .to_string() + } else { + opt_result!(rpassword::prompt_password("Please input password: "), "Read password failed: {}") + } + } + }; + Ok(rpassword) +} + pub fn is_use_default_pin() -> bool { if util_env::get_no_default_pin_hint() { return false; diff --git a/src/util_simple_pbe.rs b/src/util_simple_pbe.rs new file mode 100644 index 0000000..3364db7 --- /dev/null +++ b/src/util_simple_pbe.rs @@ -0,0 +1,175 @@ +use crate::util_digest; +use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor}; +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use rand::random; +use rust_util::{opt_result, simple_error, SimpleError, XResult}; +use std::fmt::Display; + +const SIMPLE_PBKDF_ENCRYPTION_PREFIX: &str = "tinyencrypt-pbkdf-encryption-v1"; +// FORMAT +// ...... + +pub struct SimplePbkdfEncryptionV1 { + pub repetition: u32, + pub iterations: u32, + pub salt: Vec, + pub nonce: Vec, + pub ciphertext: Vec, + pub tag: Vec, +} + +impl SimplePbkdfEncryptionV1 { + pub fn matches(enc: &str) -> bool { + enc.starts_with(&format!("{SIMPLE_PBKDF_ENCRYPTION_PREFIX}.")) + } + + pub fn encrypt(password: &str, plaintext: &[u8]) -> XResult { + let salt: [u8; 12] = random(); + let repetition = 1000; + let iterations = 10000; + let key = simple_pbkdf(password.as_bytes(), &salt, repetition, iterations); + + let key_bytes: [u8; 32] = opt_result!(key.try_into(), "Bad AES 256 key: {:?}"); + let nonce: [u8; 12] = random(); + let mut ciphertext = vec![]; + + let mut aes256_gcm_stream_encryptor = Aes256GcmStreamEncryptor::new(key_bytes, &nonce); + ciphertext.extend_from_slice(&aes256_gcm_stream_encryptor.update(plaintext)); + let (last_ciphertext, tag) = aes256_gcm_stream_encryptor.finalize(); + ciphertext.extend_from_slice(&last_ciphertext); + + Ok(SimplePbkdfEncryptionV1 { + repetition, + iterations, + salt: salt.to_vec(), + nonce: nonce.to_vec(), + ciphertext, + tag, + }) + } + + pub fn decrypt(&self, password: &str) -> XResult> { + let key = simple_pbkdf( + password.as_bytes(), + &self.salt, + self.repetition, + self.iterations, + ); + let key_bytes: [u8; 32] = opt_result!(key.try_into(), "Bad AES 256 key: {:?}"); + let mut plaintext = vec![]; + + let mut aes256_gcm_stream_decryptor = Aes256GcmStreamDecryptor::new(key_bytes, &self.nonce); + plaintext.extend_from_slice(&aes256_gcm_stream_decryptor.update(&self.ciphertext)); + plaintext.extend_from_slice(&aes256_gcm_stream_decryptor.update(&self.tag)); + plaintext.extend_from_slice(&opt_result!( + aes256_gcm_stream_decryptor.finalize(), + "Decrypt failed: {}" + )); + + Ok(plaintext) + } +} + +impl TryFrom for SimplePbkdfEncryptionV1 { + type Error = SimpleError; + + fn try_from(enc: String) -> Result { + TryFrom::<&str>::try_from(enc.as_str()) + } +} + +impl TryFrom<&str> for SimplePbkdfEncryptionV1 { + type Error = SimpleError; + + fn try_from(enc: &str) -> Result { + if !Self::matches(enc) { + return simple_error!("Not simple PBKDF encryption: {enc}"); + } + let parts = enc.split(".").collect::>(); + + let repetition: u32 = opt_result!( + parts[1].parse(), + "Parse simple PBKDF failed, invalid repetition: {}, error: {}", + parts[1] + ); + let iterations: u32 = opt_result!( + parts[2].parse(), + "Parse simple PBKDF failed, invalid iterations: {}, error: {}", + parts[2] + ); + let salt = opt_result!( + URL_SAFE_NO_PAD.decode(parts[3]), + "Parse simple PBKDF failed, invalid salt: {}, error: {}", + parts[3] + ); + let nonce = opt_result!( + URL_SAFE_NO_PAD.decode(parts[4]), + "Parse simple PBKDF failed, invalid nonce: {}, error: {}", + parts[4] + ); + let ciphertext = opt_result!( + URL_SAFE_NO_PAD.decode(parts[5]), + "Parse simple PBKDF failed, invalid ciphertext: {}, error: {}", + parts[5] + ); + let tag = opt_result!( + URL_SAFE_NO_PAD.decode(parts[6]), + "Parse simple PBKDF failed, invalid tag: {}, error: {}", + parts[6] + ); + + Ok(Self { + repetition, + iterations, + salt, + nonce, + ciphertext, + tag, + }) + } +} + +impl Display for SimplePbkdfEncryptionV1 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut enc = String::with_capacity(1024); + enc.push_str(SIMPLE_PBKDF_ENCRYPTION_PREFIX); + enc.push('.'); + enc.push_str(&self.repetition.to_string()); + enc.push('.'); + enc.push_str(&self.iterations.to_string()); + enc.push('.'); + enc.push_str(&URL_SAFE_NO_PAD.encode(&self.salt)); + enc.push('.'); + enc.push_str(&URL_SAFE_NO_PAD.encode(&self.nonce)); + enc.push('.'); + enc.push_str(&URL_SAFE_NO_PAD.encode(&self.ciphertext)); + enc.push('.'); + enc.push_str(&URL_SAFE_NO_PAD.encode(&self.tag)); + write!(f, "{}", enc) + } +} + +fn simple_pbkdf(password: &[u8], salt: &[u8], repetition: u32, iterations: u32) -> Vec { + let mut input = password.to_vec(); + for it in 0..iterations { + let mut message = Vec::with_capacity((input.len() + salt.len() + 4) * repetition as usize); + for _ in 0..repetition { + message.extend_from_slice(&it.to_be_bytes()); + message.extend_from_slice(&input); + message.extend_from_slice(&salt); + } + input = util_digest::sha256_digest(&message); + } + input +} + +#[test] +fn test() { + let enc = SimplePbkdfEncryptionV1::encrypt("helloworld", "test".as_bytes()).unwrap(); + let enc_str = enc.to_string(); + let enc2: SimplePbkdfEncryptionV1 = enc_str.try_into().unwrap(); + assert_eq!(enc.to_string(), enc2.to_string()); + let plain = enc2.decrypt("helloworld").unwrap(); + assert_eq!(b"test", plain.as_slice()); +}