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

63
Cargo.lock generated
View File

@@ -36,6 +36,30 @@ dependencies = [
"opaque-debug", "opaque-debug",
] ]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if 1.0.0",
"cipher 0.4.4",
"cpufeatures",
"zeroize",
]
[[package]]
name = "aes-gcm-stream"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d763ade72c376e5db452ec14f4c966fc907c036ef5cd2972a888e69fe2f811e5"
dependencies = [
"aes 0.8.4",
"cipher 0.4.4",
"ghash",
"zeroize",
]
[[package]] [[package]]
name = "aho-corasick" name = "aho-corasick"
version = "0.7.15" version = "0.7.15"
@@ -487,8 +511,9 @@ dependencies = [
[[package]] [[package]]
name = "card-cli" name = "card-cli"
version = "1.10.20" version = "1.10.21"
dependencies = [ dependencies = [
"aes-gcm-stream",
"authenticator 0.3.1", "authenticator 0.3.1",
"base64 0.21.7", "base64 0.21.7",
"bech32", "bech32",
@@ -1339,6 +1364,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.24.0" version = "0.24.0"
@@ -2662,6 +2697,18 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if 1.0.0",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]] [[package]]
name = "powerfmt" name = "powerfmt"
version = "0.2.0" version = "0.2.0"
@@ -3565,7 +3612,7 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "752ad8923d32fb5c117b8a9983266e294edc072401133720aa9af3959d63248d" checksum = "752ad8923d32fb5c117b8a9983266e294edc072401133720aa9af3959d63248d"
dependencies = [ dependencies = [
"aes", "aes 0.7.5",
"authenticator 0.4.1", "authenticator 0.4.1",
"base64 0.13.1", "base64 0.13.1",
"bcrypt-pbkdf", "bcrypt-pbkdf",
@@ -4199,6 +4246,16 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]] [[package]]
name = "untrusted" name = "untrusted"
version = "0.7.1" version = "0.7.1"
@@ -4752,7 +4809,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff64094218f27836b8683bd93fa7f257475d217449eb7629746fa8eaee068eee" checksum = "ff64094218f27836b8683bd93fa7f257475d217449eb7629746fa8eaee068eee"
dependencies = [ dependencies = [
"aes", "aes 0.7.5",
"bitflags 1.3.2", "bitflags 1.3.2",
"block-modes", "block-modes",
"hmac 0.11.0", "hmac 0.11.0",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "card-cli" name = "card-cli"
version = "1.10.20" version = "1.10.21"
authors = ["Hatter Jiang <jht5945@gmail.com>"] authors = ["Hatter Jiang <jht5945@gmail.com>"]
edition = "2018" edition = "2018"
@@ -52,6 +52,7 @@ der-parser = "9.0"
sshcerts = "0.13" sshcerts = "0.13"
swift-rs = { version = "1.0.7", optional = true } swift-rs = { version = "1.0.7", optional = true }
regex = "1.4.6" regex = "1.4.6"
aes-gcm-stream = "0.2.4"
#lazy_static = "1.4.0" #lazy_static = "1.4.0"
#ssh-key = "0.4.0" #ssh-key = "0.4.0"
#ctap-hid-fido2 = "2.1.3" #ctap-hid-fido2 = "2.1.3"

View File

@@ -1,10 +1,6 @@
use std::ops::Deref;
use clap::{App, Arg, ArgMatches, SubCommand}; use clap::{App, Arg, ArgMatches, SubCommand};
use rust_util::util_clap::{Command, CommandError}; use rust_util::util_clap::{Command, CommandError};
use rust_util::util_msg; use rust_util::util_msg;
use yubico_manager::config::{Config, Mode, Slot};
use yubico_manager::Yubico;
use crate::hmacutil; use crate::hmacutil;
@@ -29,28 +25,8 @@ impl Command for CommandImpl {
if json_output { util_msg::set_logger_std_out(false); } if json_output { util_msg::set_logger_std_out(false); }
let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?; let challenge_bytes = hmacutil::get_challenge_bytes(sub_arg_matches)?;
let hmac_result = hmacutil::compute_yubikey_hmac(&challenge_bytes)?;
let mut yubi = Yubico::new(); hmacutil::output_hmac_result(sub_arg_matches, json_output, challenge_bytes, &hmac_result);
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());
Ok(None) 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 yubikey::{Certificate, YubiKey};
use crate::ecdsautil::parse_ecdsa_to_rs; 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 = "."; const SEPARATOR: &str = ".";
@@ -45,6 +45,7 @@ impl Command for CommandImpl {
sub_arg_matches.value_of("private-key"), sub_arg_matches.value_of("private-key"),
"Private key PKCS#8 DER base64 encoded or PEM" "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 key_id = sub_arg_matches.value_of("key-id");
let claims = sub_arg_matches.values_of("claims"); 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); debugging!("Singed JWT: {}", token_string);
if json_output { if json_output {
json.insert("token", token_string.clone()); json.insert("token", token_string.clone());

View File

@@ -1,9 +1,100 @@
use std::collections::BTreeMap;
use clap::ArgMatches; use clap::ArgMatches;
use rust_util::XResult; 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::hmacmode::HmacKey;
use yubico_manager::sec::hmac_sha1; 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>> { 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") { 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_challconfig;
mod cmd_ecverify; mod cmd_ecverify;
mod cmd_hmac_sha1; mod cmd_hmac_sha1;
mod cmd_hmacencrypt;
mod cmd_hmacdecrypt;
mod cmd_list; mod cmd_list;
#[cfg(feature = "with-sequoia-openpgp")] #[cfg(feature = "with-sequoia-openpgp")]
mod cmd_pgp; mod cmd_pgp;
@@ -101,6 +103,8 @@ fn inner_main() -> CommandError {
Box::new(cmd_list::CommandImpl), Box::new(cmd_list::CommandImpl),
Box::new(cmd_chall::CommandImpl), Box::new(cmd_chall::CommandImpl),
Box::new(cmd_hmac_sha1::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_challconfig::CommandImpl),
Box::new(cmd_rsaencrypt::CommandImpl), Box::new(cmd_rsaencrypt::CommandImpl),
Box::new(cmd_rsadecrypt::CommandImpl), Box::new(cmd_rsadecrypt::CommandImpl),