#![allow(clippy::as_conversions, clippy::indexing_slicing, clippy::expect_used)]
use std::{
io,
io::Write,
sync::{Arc, Mutex, atomic::AtomicUsize},
};
use clap::Parser;
use cosmian_kms_client::{
KmsClient,
cosmian_kmip::kmip_0::kmip_types::BlockCipherMode,
kmip_2_1::{
extra::BulkData,
kmip_operations::{Decrypt, Encrypt},
kmip_types::{CryptographicAlgorithm, CryptographicParameters, UniqueIdentifier},
},
};
use num_format::{CustomFormat, Grouping, ToFormattedString};
use zeroize::Zeroizing;
use crate::{
actions::kms::{
rsa::keys::{create_key_pair::CreateKeyPairAction, revoke_key::RevokeKeyAction},
symmetric::keys::create_key::CreateKeyAction,
},
error::{
KmsCliError,
result::{KmsCliResult, KmsCliResultHelper},
},
};
struct EncryptionResult {
batch_id: usize,
ciphertext: Zeroizing<Vec<u8>>,
encryption_time: u128,
}
struct FinalResult {
batch_id: usize,
encryption_time: u128,
decryption_time: u128,
}
#[derive(Parser, Debug)]
pub struct BenchAction {
#[clap(long = "number-of-threads", short = 't', default_value = "1")]
num_threads: usize,
#[clap(
long = "batch-size",
short = 'b',
default_value = "1",
verbatim_doc_comment
)]
batch_size: usize,
#[clap(long = "num-batches", short = 'n', default_value = "1")]
num_batches: usize,
#[clap(long = "wrapped-key", short = 'w', default_value = "false")]
wrapped_key: bool,
#[clap(long = "verbose", short = 'v', default_value = "false")]
verbose: bool,
}
impl BenchAction {
#[expect(clippy::print_stdout)]
pub async fn process(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> {
let version = kms_rest_client
.version()
.await
.with_context(|| "Can't execute the version query on the kms server")?;
println!("Server version: {version}");
println!(
"Running bench with {} threads, batch size {}, {} batches.",
self.num_threads, self.batch_size, self.num_batches
);
if self.wrapped_key {
println!("Algorithm: AES GCM using a 256 bit key wrapped by a 4096 bit RSA key");
} else {
println!("Algorithm: AES GCM using a 256 bit key");
}
let (key_id, wrapping_key) = self.create_keys(kms_rest_client.clone()).await?;
let format = CustomFormat::builder()
.grouping(Grouping::Standard)
.separator(" ")
.build()
.map_err(|e| KmsCliError::Default(format!("Failed to create the formatter: {e}")))?;
let data = if self.batch_size == 1 {
Zeroizing::new(vec![1_u8; 64])
} else {
BulkData::new(vec![Zeroizing::new(vec![1_u8; 64]); self.batch_size]).serialize()?
};
{
let mut stdout = io::stdout().lock();
write!(stdout, "Encrypting")?;
stdout.flush()?;
};
let amortized_encryption_time = std::time::Instant::now();
let counter = Arc::new(AtomicUsize::new(0));
let kms_client = Arc::new(kms_rest_client.clone());
let mut handles = Vec::new();
for _ in 0..self.num_threads {
let key_id = key_id.clone();
let data = data.clone();
let counter = counter.clone();
let num_batches = self.num_batches;
let kms_client = kms_client.clone();
let handle = tokio::spawn(async move {
encrypt(&kms_client, key_id, data, counter, num_batches).await
});
handles.push(handle);
}
let mut encryption_results = Vec::new();
for handle in handles {
match handle.await {
Ok(Ok(results)) => {
encryption_results.extend(results);
}
Ok(Err(e)) => return Err(e),
Err(e) => return Err(KmsCliError::Default(format!("Tokio Error: {e}"))),
}
}
let total_encryption_time_amortized = amortized_encryption_time.elapsed().as_micros();
{
let mut stdout = io::stdout().lock();
writeln!(
stdout,
": {}\u{b5}s",
total_encryption_time_amortized.to_formatted_string(&format)
)?;
};
{
let mut stdout = io::stdout().lock();
write!(stdout, "Decrypting")?;
stdout.flush()?;
};
let amortized_decryption_time = std::time::Instant::now();
let ciphertexts_to_process = Arc::new(Mutex::new(encryption_results));
let mut handles = Vec::new();
let kms_client = Arc::new(kms_rest_client.clone());
for _ in 0..self.num_threads {
let key_id = key_id.clone();
let ciphertexts_to_process = ciphertexts_to_process.clone();
let kms_client = kms_client.clone();
let handle =
tokio::spawn(
async move { decrypt(&kms_client, key_id, ciphertexts_to_process).await },
);
handles.push(handle);
}
let mut final_results = Vec::new();
for handle in handles {
match handle.await {
Ok(Ok(results)) => {
final_results.extend(results);
}
Ok(Err(e)) => return Err(e),
Err(e) => return Err(KmsCliError::Default(format!("Tokio Error: {e}"))),
}
}
let total_decryption_time_amortized = amortized_decryption_time.elapsed().as_micros();
{
let mut stdout = io::stdout().lock();
writeln!(
stdout,
": {}\u{b5}s",
total_decryption_time_amortized.to_formatted_string(&format)
)?;
stdout.flush()?;
};
self.revoke_keys(kms_rest_client, key_id, wrapping_key)
.await?;
final_results.sort_by_key(|r| r.batch_id);
let mut total_encryption_time = 0_u128;
let mut total_decryption_time = 0_u128;
if self.verbose {
for result in final_results {
total_encryption_time += result.encryption_time;
total_decryption_time += result.decryption_time;
if self.verbose {
println!(
"{}: encryption: {}\u{b5}s ({}\u{b5}s/v), decryption: {}\u{b5}s \
({}\u{b5}s/v)",
result.batch_id,
result.encryption_time.to_formatted_string(&format),
result.encryption_time / (self.batch_size as u128),
result.decryption_time.to_formatted_string(&format),
result.decryption_time / (self.batch_size as u128)
);
}
}
}
println!(
"Encryption time {}\u{b5}s => {}\u{b5}s/batch => {}\u{b5}s/value",
total_encryption_time.to_formatted_string(&format),
(total_encryption_time / (self.num_batches as u128)).to_formatted_string(&format),
total_encryption_time / (self.num_batches * self.batch_size) as u128
);
println!(
"Decryption time {}\u{b5}s => {}\u{b5}s/batch => {}\u{b5}s/value",
total_decryption_time.to_formatted_string(&format),
(total_decryption_time / self.num_batches as u128).to_formatted_string(&format),
total_decryption_time / (self.num_batches * self.batch_size) as u128
);
println!(
"Amortized encryption time ({} threads): {}\u{b5}s => {}\u{b5}s/batch => \
{}\u{b5}s/value",
self.num_threads,
total_encryption_time_amortized.to_formatted_string(&format),
(total_encryption_time_amortized / (self.num_batches as u128))
.to_formatted_string(&format),
total_encryption_time_amortized / (self.num_batches * self.batch_size) as u128
);
println!(
"Amortized decryption time ({} threads): {}\u{b5}s => {}\u{b5}s/batch => \
{}\u{b5}s/value",
self.num_threads,
total_decryption_time_amortized.to_formatted_string(&format),
(total_decryption_time_amortized / (self.num_batches as u128))
.to_formatted_string(&format),
total_decryption_time_amortized / (self.num_batches * self.batch_size) as u128
);
Ok(())
}
async fn create_keys(
&self,
kms_rest_client: KmsClient,
) -> KmsCliResult<(
UniqueIdentifier,
Option<(UniqueIdentifier, UniqueIdentifier)>,
)> {
if self.wrapped_key {
let (sk, pk) = CreateKeyPairAction {
tags: vec!["bench".to_owned()],
..Default::default()
}
.run(kms_rest_client.clone())
.await?;
let kk = CreateKeyAction {
number_of_bits: Some(256),
wrapping_key_id: Some(pk.to_string()),
tags: vec!["bench".to_owned()],
..Default::default()
}
.run(kms_rest_client)
.await?;
return Ok((kk, Some((sk, pk))));
}
let kk = CreateKeyAction {
number_of_bits: Some(256),
tags: vec!["bench".to_owned()],
..Default::default()
}
.run(kms_rest_client)
.await?;
Ok((kk, None))
}
async fn revoke_keys(
&self,
kms_rest_client: KmsClient,
symmetric_key: UniqueIdentifier,
wrapping_key: Option<(UniqueIdentifier, UniqueIdentifier)>,
) -> KmsCliResult<()> {
RevokeKeyAction {
revocation_reason: "Bench".to_owned(),
key_id: Some(symmetric_key.to_string()),
tags: None,
}
.run(kms_rest_client.clone())
.await?;
if let Some((sk, _pk)) = wrapping_key {
RevokeKeyAction {
revocation_reason: "Bench".to_owned(),
key_id: Some(sk.to_string()),
tags: None,
}
.run(kms_rest_client)
.await?;
}
Ok(())
}
}
async fn encrypt(
kms_rest_client: &KmsClient,
key_id: UniqueIdentifier,
data: Zeroizing<Vec<u8>>,
counter: Arc<AtomicUsize>,
num_batches: usize,
) -> KmsCliResult<Vec<EncryptionResult>> {
let mut results = Vec::new();
loop {
let next = counter.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if next >= num_batches {
break;
}
{
let mut stdout = io::stdout().lock();
write!(stdout, ".",)?;
stdout.flush()?;
};
let encrypt = Encrypt {
unique_identifier: Some(key_id.clone()),
cryptographic_parameters: Some(CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
block_cipher_mode: Some(BlockCipherMode::GCM),
..Default::default()
}),
data: Some(data.clone()),
..Default::default()
};
let start = std::time::Instant::now();
let response = kms_rest_client
.encrypt(encrypt)
.await
.with_context(|| "failed encrypting")?;
let elapsed = start.elapsed().as_micros();
let ciphertext = Zeroizing::new(
[
response.i_v_counter_nonce.unwrap_or_default(),
response.data.unwrap_or_default(),
response.authenticated_encryption_tag.unwrap_or_default(),
]
.concat(),
);
results.push(EncryptionResult {
batch_id: next,
ciphertext,
encryption_time: elapsed,
});
}
Ok(results)
}
async fn decrypt(
kms_rest_client: &KmsClient,
key_id: UniqueIdentifier,
encryptions: Arc<Mutex<Vec<EncryptionResult>>>,
) -> KmsCliResult<Vec<FinalResult>> {
let mut results = Vec::new();
loop {
let next = encryptions
.lock()
.expect("could not lock encryption results")
.pop();
let Some(next) = next else { break };
{
let mut stdout = io::stdout().lock();
write!(stdout, ".",)?;
stdout.flush()?;
};
let (iv, data, tag) = match BulkData::deserialize(next.ciphertext.as_ref()) {
Ok(_data) => (None, Some(next.ciphertext.as_slice()), None),
Err(_e) => {
let iv_len = 12;
let tag_len = 16;
let iv = &next.ciphertext[..iv_len];
let tag = &next.ciphertext[next.ciphertext.len() - tag_len..];
let data = &next.ciphertext[iv_len..next.ciphertext.len() - tag_len];
(Some(iv), Some(data), Some(tag))
}
};
let decrypt = Decrypt {
unique_identifier: Some(key_id.clone()),
cryptographic_parameters: Some(CryptographicParameters {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
block_cipher_mode: Some(BlockCipherMode::GCM),
..Default::default()
}),
i_v_counter_nonce: iv.map(Vec::from),
data: data.map(Vec::from),
authenticated_encryption_tag: tag.map(Vec::from),
..Default::default()
};
let start = std::time::Instant::now();
let _response = kms_rest_client
.decrypt(decrypt)
.await
.with_context(|| "failed encrypting")?;
let elapsed = start.elapsed().as_micros();
results.push(FinalResult {
batch_id: next.batch_id,
encryption_time: next.encryption_time,
decryption_time: elapsed,
});
}
Ok(results)
}