From 67568f8f15134877a512bccd3749a06034faa0c7 Mon Sep 17 00:00:00 2001 From: Hatter Jiang Date: Mon, 5 May 2025 23:22:16 +0800 Subject: [PATCH] feat: v1.12.7 --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/cmd_hmac_decrypt.rs | 14 ++++-- src/cmd_hmac_encrypt.rs | 14 ++++-- src/hmacutil.rs | 2 +- src/main.rs | 1 + src/pbeutil.rs | 101 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 126 insertions(+), 10 deletions(-) create mode 100644 src/pbeutil.rs diff --git a/Cargo.lock b/Cargo.lock index 73323f1..8672d10 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "card-cli" -version = "1.12.6" +version = "1.12.7" dependencies = [ "aes-gcm-stream", "authenticator 0.3.1", diff --git a/Cargo.toml b/Cargo.toml index 4cc694c..ca7e8eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "card-cli" -version = "1.12.6" +version = "1.12.7" authors = ["Hatter Jiang "] edition = "2018" diff --git a/src/cmd_hmac_decrypt.rs b/src/cmd_hmac_decrypt.rs index ae6955a..c8bc792 100644 --- a/src/cmd_hmac_decrypt.rs +++ b/src/cmd_hmac_decrypt.rs @@ -2,7 +2,7 @@ use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; use std::collections::BTreeMap; -use crate::{cmdutil, hmacutil, util}; +use crate::{cmdutil, hmacutil, pbeutil, util}; pub struct CommandImpl; @@ -21,6 +21,7 @@ impl Command for CommandImpl { .required(true) .help("Ciphertext"), ) + .arg(Arg::with_name("auto-pbe").long("auto-pbe").help("Auto PBE decryption")) .arg(cmdutil::build_json_arg()) } @@ -28,15 +29,20 @@ impl Command for CommandImpl { let json_output = cmdutil::check_json_output(sub_arg_matches); let ciphertext = sub_arg_matches.value_of("ciphertext").unwrap(); - let plaintext = hmacutil::hmac_decrypt_to_string(ciphertext)?; + let mut text = hmacutil::hmac_decrypt_to_string(ciphertext)?; + + let auto_pbe = sub_arg_matches.is_present("auto-pbe"); + if auto_pbe && pbeutil::is_simple_pbe_encrypted(&text) { + text = pbeutil::simple_pbe_decrypt_with_prompt_to_string(&text)?; + } if json_output { let mut json = BTreeMap::<&'_ str, String>::new(); - json.insert("plaintext", plaintext); + json.insert("plaintext", text); util::print_pretty_json(&json); } else { - success!("Plaintext: {}", plaintext); + success!("Plaintext: {}", text); } Ok(None) diff --git a/src/cmd_hmac_encrypt.rs b/src/cmd_hmac_encrypt.rs index d36e87c..760ffe9 100644 --- a/src/cmd_hmac_encrypt.rs +++ b/src/cmd_hmac_encrypt.rs @@ -2,7 +2,7 @@ use clap::{App, Arg, ArgMatches, SubCommand}; use rust_util::util_clap::{Command, CommandError}; use std::collections::BTreeMap; -use crate::{cmdutil, hmacutil, util}; +use crate::{cmdutil, hmacutil, pbeutil, util}; pub struct CommandImpl; @@ -21,14 +21,22 @@ impl Command for CommandImpl { .required(true) .help("Plaintext"), ) + .arg(Arg::with_name("with-pbe").long("with-pbe").help("With PBE encryption")) + .arg(Arg::with_name("pbe-iteration").long("pbe-iteration").takes_value(true).help("PBE iteration, default 100000")) .arg(cmdutil::build_json_arg()) } fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError { let json_output = cmdutil::check_json_output(sub_arg_matches); - let plaintext = sub_arg_matches.value_of("plaintext").unwrap(); - let hmac_encrypt_ciphertext = hmacutil::hmac_encrypt_from_string(plaintext)?; + let mut text = sub_arg_matches.value_of("plaintext").unwrap().to_string(); + let with_pbe = sub_arg_matches.is_present("with-pbe"); + if with_pbe { + let iteration = sub_arg_matches.value_of("pbe-iteration") + .map(|x| x.parse::().unwrap()).unwrap_or(100000); + text = pbeutil::simple_pbe_encrypt_with_prompt_from_string(iteration, &text)?; + } + let hmac_encrypt_ciphertext = hmacutil::hmac_encrypt_from_string(&text)?; if json_output { let mut json = BTreeMap::<&'_ str, String>::new(); diff --git a/src/hmacutil.rs b/src/hmacutil.rs index 76be399..4a79503 100644 --- a/src/hmacutil.rs +++ b/src/hmacutil.rs @@ -77,7 +77,7 @@ pub fn hmac_decrypt(ciphertext: &str) -> XResult> { Ok(plaintext) } -fn try_decode_hmac_val(s: &str) -> XResult> { +pub fn try_decode_hmac_val(s: &str) -> XResult> { match hex::decode(s) { Ok(v) => Ok(v), Err(e) => match base64_uri_decode(s) { diff --git a/src/main.rs b/src/main.rs index 2d724dd..14f5d5f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -79,6 +79,7 @@ mod signfile; mod sshutil; mod util; mod yubikeyutil; +mod pbeutil; pub struct DefaultCommandImpl; diff --git a/src/pbeutil.rs b/src/pbeutil.rs new file mode 100644 index 0000000..38b938f --- /dev/null +++ b/src/pbeutil.rs @@ -0,0 +1,101 @@ +use crate::digestutil::{copy_sha256, sha256_bytes}; +use crate::pinutil; +use crate::util::{base64_decode, base64_encode, base64_encode_url_safe_no_pad}; +use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor}; +use rand::random; +use rust_util::XResult; + +const PBE_ENC_PREFIX: &str = "pbe_enc:"; + +pub fn simple_pbe_encrypt_with_prompt_from_string(iteration: u32, plaintext: &str) -> XResult { + simple_pbe_encrypt_with_prompt(iteration, plaintext.as_bytes()) +} + +pub fn simple_pbe_decrypt_with_prompt_to_string(ciphertext: &str) -> XResult { + let plaintext = simple_pbe_decrypt_with_prompt(ciphertext)?; + Ok(String::from_utf8(plaintext)?) +} + +pub fn simple_pbe_encrypt_with_prompt(iteration: u32, plaintext: &[u8]) -> XResult { + let pin = opt_value_result!(pinutil::get_pin(None), "Simple PBE password required"); + simple_pbe_encrypt(&pin, iteration, plaintext) +} + +pub fn simple_pbe_decrypt_with_prompt(ciphertext: &str) -> XResult> { + let pin = opt_value_result!(pinutil::get_pin(None), "Simple PBE password required"); + simple_pbe_decrypt(&pin, ciphertext) +} + +// pub fn simple_pbe_encrypt_from_string( +// password: &str, +// iteration: u32, +// plaintext: &str, +// ) -> XResult { +// simple_pbe_encrypt(password, iteration, plaintext.as_bytes()) +// } + +// pub fn simple_pbe_decrypt_to_string(password: &str, ciphertext: &str) -> XResult { +// let plaintext = simple_pbe_decrypt(password, ciphertext)?; +// Ok(String::from_utf8(plaintext)?) +// } + +pub fn simple_pbe_encrypt(password: &str, iteration: u32, plaintext: &[u8]) -> XResult { + let pbe_salt: [u8; 32] = random(); + let key = simple_pbe_kdf(password, &pbe_salt, iteration)?; + let aes_gcm_nonce: [u8; 16] = random(); + + let mut encryptor = Aes256GcmStreamEncryptor::new(key, &aes_gcm_nonce); + let mut ciphertext = encryptor.update(plaintext); + let (final_part, tag) = encryptor.finalize(); + ciphertext.extend_from_slice(&final_part); + ciphertext.extend_from_slice(&tag); + + Ok(format!( + "{}{}:{}:{}:{}", + PBE_ENC_PREFIX, + iteration, + base64_encode_url_safe_no_pad(pbe_salt), + base64_encode_url_safe_no_pad(aes_gcm_nonce), + base64_encode(&ciphertext) + )) +} + +pub fn simple_pbe_decrypt(password: &str, ciphertext: &str) -> XResult> { + if !is_simple_pbe_encrypted(ciphertext) { + return simple_error!("Invalid ciphertext: {}", ciphertext); + } + let parts = ciphertext.split(":").collect::>(); + let iteration: u32 = parts[1].parse()?; + let pbe_salt = crate::hmacutil::try_decode_hmac_val(parts[2])?; + let aes_gcm_nonce = crate::hmacutil::try_decode_hmac_val(parts[3])?; + let ciphertext = base64_decode(parts[4])?; + + let key = simple_pbe_kdf(password, &pbe_salt, iteration)?; + + let mut decryptor = Aes256GcmStreamDecryptor::new(key, &aes_gcm_nonce); + let mut plaintext = decryptor.update(&ciphertext); + let final_part = decryptor.finalize()?; + plaintext.extend_from_slice(&final_part); + + Ok(plaintext) +} + +pub fn is_simple_pbe_encrypted(ciphertext: &str) -> bool { + ciphertext.starts_with(PBE_ENC_PREFIX) +} + +fn simple_pbe_kdf(password: &str, pbe_salt: &[u8], iteration: u32) -> XResult<[u8; 32]> { + let mut init_data = password.as_bytes().to_vec(); + init_data.extend_from_slice(&pbe_salt); + let mut loop_hash = sha256_bytes(&init_data); + for i in 0..iteration { + let i_to_bytes = i.to_be_bytes(); + for x in 0..4 { + loop_hash[x] = i_to_bytes[x]; + } + loop_hash = sha256_bytes(&loop_hash); + } + let key = copy_sha256(&sha256_bytes(&loop_hash))?; + + Ok(key) +}