feat: 1.10.21, add hmac-encrypt, hmac-decrypt

This commit is contained in:
2025-03-23 18:23:36 +08:00
parent 31e710d779
commit ea0b091414
8 changed files with 262 additions and 34 deletions

View File

@@ -1,10 +1,6 @@
use std::ops::Deref;
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::Yubico;
use crate::hmacutil;
@@ -29,28 +25,8 @@ impl Command for CommandImpl {
if json_output { util_msg::set_logger_std_out(false); }
let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?;
let mut yubi = Yubico::new();
let device = match yubi.find_yubikey() {
Ok(device) => device,
Err(_) => {
warning!("YubiKey not found");
return Ok(Some(1));
}
};
success!("Found key, Vendor ID: {:?}, Product ID: {:?}", device.vendor_id, device.product_id);
let config = Config::default()
.set_vendor_id(device.vendor_id)
.set_product_id(device.product_id)
.set_variable_size(true)
.set_mode(Mode::Sha1)
.set_slot(Slot::Slot2);
// In HMAC Mode, the result will always be the SAME for the SAME provided challenge
let hmac_result = opt_result!(yubi.challenge_response_hmac(&challenge_bytes, config), "Challenge HMAC failed: {}");
hmacutil::output_hmac_result(sub_arg_matches, json_output, challenge_bytes, hmac_result.deref());
let hmac_result = hmacutil::compute_yubikey_hmac(&challenge_bytes)?;
hmacutil::output_hmac_result(sub_arg_matches, json_output, challenge_bytes, &hmac_result);
Ok(None)
}

49
src/cmd_hmacdecrypt.rs Normal file
View File

@@ -0,0 +1,49 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
use crate::hmacutil;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-decrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Yubikey HMAC decrypt")
.arg(
Arg::with_name("ciphertext")
.long("ciphertext")
.takes_value(true)
.help("Ciphertext"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let ciphertext = sub_arg_matches.value_of("ciphertext").unwrap();
let plaintext = hmacutil::hmac_decrypt_to_string(&ciphertext)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("plaintext", plaintext);
println!(
"{}",
serde_json::to_string_pretty(&json).expect("Convert to JSON failed!")
);
} else {
success!("Plaintext: {}", plaintext);
}
Ok(None)
}
}

49
src/cmd_hmacencrypt.rs Normal file
View File

@@ -0,0 +1,49 @@
use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg;
use std::collections::BTreeMap;
use crate::hmacutil;
pub struct CommandImpl;
impl Command for CommandImpl {
fn name(&self) -> &str {
"hmac-encrypt"
}
fn subcommand<'a>(&self) -> App<'a, 'a> {
SubCommand::with_name(self.name())
.about("Yubikey HMAC encrypt")
.arg(
Arg::with_name("plaintext")
.long("plaintext")
.takes_value(true)
.help("Plaintext"),
)
.arg(Arg::with_name("json").long("json").help("JSON output"))
}
fn run(&self, _arg_matches: &ArgMatches, sub_arg_matches: &ArgMatches) -> CommandError {
let json_output = sub_arg_matches.is_present("json");
if json_output {
util_msg::set_logger_std_out(false);
}
let plaintext = sub_arg_matches.value_of("plaintext").unwrap();
let hmac_encrypt_ciphertext = hmacutil::hmac_encrypt_from_string(plaintext)?;
if json_output {
let mut json = BTreeMap::<&'_ str, String>::new();
json.insert("ciphertext", hmac_encrypt_ciphertext);
println!(
"{}",
serde_json::to_string_pretty(&json).expect("Convert to JSON failed!")
);
} else {
success!("HMAC encrypt ciphertext: {}", hmac_encrypt_ciphertext);
}
Ok(None)
}
}

View File

@@ -11,7 +11,7 @@ use yubikey::piv::{sign_data, AlgorithmId, SlotId};
use yubikey::{Certificate, YubiKey};
use crate::ecdsautil::parse_ecdsa_to_rs;
use crate::{cmd_signjwt, digest, ecdsautil, pivutil, rsautil, util};
use crate::{cmd_signjwt, digest, ecdsautil, hmacutil, pivutil, rsautil, util};
const SEPARATOR: &str = ".";
@@ -45,6 +45,7 @@ impl Command for CommandImpl {
sub_arg_matches.value_of("private-key"),
"Private key PKCS#8 DER base64 encoded or PEM"
);
let private_key = hmacutil::try_hmac_decrypt_to_string(private_key)?;
let key_id = sub_arg_matches.value_of("key-id");
let claims = sub_arg_matches.values_of("claims");
@@ -117,7 +118,7 @@ impl Command for CommandImpl {
}
}
let token_string = sign_jwt(private_key, header, &payload, &jwt_claims)?;
let token_string = sign_jwt(&private_key, header, &payload, &jwt_claims)?;
debugging!("Singed JWT: {}", token_string);
if json_output {
json.insert("token", token_string.clone());

View File

@@ -1,9 +1,100 @@
use std::collections::BTreeMap;
use clap::ArgMatches;
use rust_util::XResult;
use std::collections::BTreeMap;
use std::ops::Deref;
use aes_gcm_stream::{Aes256GcmStreamDecryptor, Aes256GcmStreamEncryptor};
use rand::random;
use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::hmacmode::HmacKey;
use yubico_manager::sec::hmac_sha1;
use yubico_manager::Yubico;
use crate::digest::{copy_sha256, sha256, sha256_bytes};
use crate::util::{base64_decode, base64_encode};
const HMAC_ENC_PREFIX: &str = "hmac_enc:";
// hmac encrypt, format: hmac_enc:<HMAC-NONCE>:<AES-GCM-NONCE>:<ENCRYPTED>
pub fn hmac_encrypt_from_string(plaintext: &str) -> XResult<String> {
hmac_encrypt(plaintext.as_bytes())
}
pub fn hmac_encrypt(plaintext: &[u8]) -> XResult<String> {
let hmac_nonce: [u8; 8] = random();
let aes_gcm_nonce: [u8; 16] = random();
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
let key = copy_sha256(&sha256_bytes(&hmac_key))?;
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!("{}{}:{}:{}",
HMAC_ENC_PREFIX,
hex::encode(&hmac_nonce),
hex::encode(&aes_gcm_nonce),
base64_encode(&ciphertext)
))
}
pub fn is_hmac_encrypted(ciphertext: &str) -> bool {
ciphertext.starts_with(HMAC_ENC_PREFIX)
}
pub fn try_hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
if is_hmac_encrypted(ciphertext) {
hmac_decrypt_to_string(ciphertext)
} else {
Ok(ciphertext.to_string())
}
}
pub fn hmac_decrypt_to_string(ciphertext: &str) -> XResult<String> {
let plaintext = hmac_decrypt(ciphertext)?;
Ok(String::from_utf8(plaintext)?)
}
pub fn hmac_decrypt(ciphertext: &str) -> XResult<Vec<u8>> {
if !is_hmac_encrypted(ciphertext) {
return simple_error!("Invalid ciphertext: {}", ciphertext);
}
let parts = ciphertext.split(":").collect::<Vec<_>>();
let hmac_nonce = hex::decode(parts[1])?;
let aes_gcm_nonce = hex::decode(parts[2])?;
let ciphertext = base64_decode(parts[3])?;
let hmac_key = compute_yubikey_hmac(&hmac_nonce)?;
let key = copy_sha256(&sha256_bytes(&hmac_key))?;
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 compute_yubikey_hmac(challenge_bytes: &[u8]) -> XResult<Vec<u8>> {
let mut yubi = Yubico::new();
let device = match yubi.find_yubikey() {
Ok(device) => device,
Err(_) => {
return simple_error!("YubiKey not found");
}
};
debugging!("Found key, Vendor ID: {:?}, Product ID: {:?}", device.vendor_id, device.product_id);
let config = Config::default()
.set_vendor_id(device.vendor_id)
.set_product_id(device.product_id)
.set_variable_size(true)
.set_mode(Mode::Sha1)
.set_slot(Slot::Slot2);
let hmac_result = opt_result!(yubi.challenge_response_hmac(&challenge_bytes, config), "Challenge HMAC failed: {}");
Ok(hmac_result.deref().to_vec())
}
pub fn get_challenge_bytes(sub_arg_matches: &ArgMatches) -> XResult<Vec<u8>> {
let challenge_bytes: Vec<u8> = if let Some(challenge) = sub_arg_matches.value_of("challenge") {

View File

@@ -9,6 +9,8 @@ mod cmd_chall;
mod cmd_challconfig;
mod cmd_ecverify;
mod cmd_hmac_sha1;
mod cmd_hmacencrypt;
mod cmd_hmacdecrypt;
mod cmd_list;
#[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgp;
@@ -101,6 +103,8 @@ fn inner_main() -> CommandError {
Box::new(cmd_list::CommandImpl),
Box::new(cmd_chall::CommandImpl),
Box::new(cmd_hmac_sha1::CommandImpl),
Box::new(cmd_hmacencrypt::CommandImpl),
Box::new(cmd_hmacdecrypt::CommandImpl),
Box::new(cmd_challconfig::CommandImpl),
Box::new(cmd_rsaencrypt::CommandImpl),
Box::new(cmd_rsadecrypt::CommandImpl),