use std::{
fs::File,
io::{Read, Write},
path::{Path, PathBuf},
};
use clap::Parser;
use cosmian_kms_client::{
ExportObjectParams, KmsClient, export_object,
kmip_2_1::{
kmip_attributes::Attributes,
kmip_data_structures::{KeyValue, KeyWrappingData},
kmip_types::{
CryptographicAlgorithm, CryptographicParameters, EncodingOption, KeyFormatType,
},
requests::{create_symmetric_key_kmip_object, decrypt_request},
},
read_bytes_from_file,
reexport::cosmian_kms_client_utils::symmetric_utils::{
DataEncryptionAlgorithm, parse_decrypt_elements,
},
};
use cosmian_kms_crypto::crypto::{
symmetric::symmetric_ciphers::{Mode, SymCipher, decrypt},
wrap::unwrap_key_block,
};
use cosmian_logger::trace;
use zeroize::Zeroizing;
use crate::{
actions::kms::{
console, labels::KEY_ID, shared::get_key_uid, symmetric::KeyEncryptionAlgorithm,
},
cli_bail,
error::{
KmsCliError,
result::{KmsCliResult, KmsCliResultHelper},
},
};
#[derive(Parser, Debug, Default)]
#[clap(verbatim_doc_comment)]
pub struct DecryptAction {
#[clap(required = true, name = "FILE")]
pub(crate) input_file: PathBuf,
#[clap(long = KEY_ID, short = 'k', group = "key-tags")]
pub(crate) key_id: Option<String>,
#[clap(long = "tag", short = 't', value_name = "TAG", group = "key-tags")]
pub(crate) tags: Option<Vec<String>>,
#[clap(
long = "data-encryption-algorithm",
short = 'd',
default_value = "aes-gcm",
verbatim_doc_comment
)]
pub(crate) data_encryption_algorithm: DataEncryptionAlgorithm,
#[clap(long = "key-encryption-algorithm", short = 'e', verbatim_doc_comment)]
pub(crate) key_encryption_algorithm: Option<KeyEncryptionAlgorithm>,
#[clap(long, short = 'o')]
pub(crate) output_file: Option<PathBuf>,
#[clap(long, short = 'a')]
pub(crate) authentication_data: Option<String>,
}
impl DecryptAction {
pub(crate) async fn run(&self, kms_rest_client: KmsClient) -> KmsCliResult<()> {
let id = get_key_uid(self.key_id.as_ref(), self.tags.as_ref(), KEY_ID)?;
let output_file_name = self
.output_file
.clone()
.unwrap_or_else(|| self.input_file.clone().with_extension("plain"));
let mut output_file =
File::create(&output_file_name).context("Fail to write the plaintext file")?;
if let Some(key_encryption_algorithm) = self.key_encryption_algorithm {
self.client_side_decrypt_with_file(
kms_rest_client,
key_encryption_algorithm,
self.data_encryption_algorithm,
&id,
&self.input_file,
&mut output_file,
self.authentication_data
.as_deref()
.map(hex::decode)
.transpose()?,
)
.await?;
} else {
let ciphertext = read_bytes_from_file(&self.input_file)
.with_context(|| "Cannot read bytes from the file to decrypt")?;
let plaintext = self
.server_side_decrypt(
&kms_rest_client,
self.data_encryption_algorithm.into(),
&id,
ciphertext,
self.authentication_data
.as_deref()
.map(hex::decode)
.transpose()?,
)
.await?;
output_file
.write_all(&plaintext)
.context("failed to write the plaintext file")?;
}
let stdout = format!(
"The decrypted file is available at {}",
output_file_name.display()
);
let mut stdout = console::Stdout::new(&stdout);
stdout.set_tags(self.tags.as_ref());
stdout.write()?;
Ok(())
}
pub async fn server_side_decrypt(
&self,
kms_rest_client: &KmsClient,
cryptographic_parameters: CryptographicParameters,
key_id: &str,
ciphertext: Vec<u8>,
aad: Option<Vec<u8>>,
) -> KmsCliResult<Zeroizing<Vec<u8>>> {
let (ciphertext, nonce, tag) =
parse_decrypt_elements(&cryptographic_parameters, ciphertext)?;
let decrypt_request = decrypt_request(
key_id,
Some(nonce),
ciphertext,
Some(tag),
aad,
Some(cryptographic_parameters),
);
let decrypt_response = kms_rest_client
.decrypt(decrypt_request)
.await
.context("Can't execute the query on the kms server")?;
decrypt_response.data.context("the plain text is empty")
}
#[expect(clippy::too_many_arguments, clippy::indexing_slicing)]
async fn client_side_decrypt_with_file(
&self,
kms_rest_client: KmsClient,
key_encryption_algorithm: KeyEncryptionAlgorithm,
data_encryption_algorithm: DataEncryptionAlgorithm,
key_id: &str,
input_file_name: &Path,
output_file: &mut File,
aad: Option<Vec<u8>>,
) -> KmsCliResult<()> {
let aad = match data_encryption_algorithm {
DataEncryptionAlgorithm::AesXts => vec![],
DataEncryptionAlgorithm::AesCbc | DataEncryptionAlgorithm::AesGcm => {
aad.unwrap_or_default()
}
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::AesGcmSiv | DataEncryptionAlgorithm::Chacha20Poly1305 => {
aad.unwrap_or_default()
}
};
let mut input_file = File::open(input_file_name)?;
let encaps_length = leb128::read::unsigned(&mut input_file).map_err(|e| {
KmsCliError::Default(format!(
"Failed to read the encapsulation length from the encrypted file: {e}"
))
})?;
let mut encapsulation = vec![0; usize::try_from(encaps_length)?];
input_file.read_exact(&mut encapsulation)?;
let dek = self
.server_side_decrypt(
&kms_rest_client,
key_encryption_algorithm.into(),
key_id,
encapsulation,
None,
)
.await?;
let dem_cryptographic_parameters: CryptographicParameters =
data_encryption_algorithm.into();
trace!("dek length {}", dek.len());
let cipher = SymCipher::from_algorithm_and_key_size(
dem_cryptographic_parameters
.cryptographic_algorithm
.unwrap_or(CryptographicAlgorithm::AES),
dem_cryptographic_parameters.block_cipher_mode,
dek.len(),
)?;
let mut nonce = vec![0; cipher.nonce_size()];
input_file.read_exact(&mut nonce)?;
let mut stream_cipher = cipher.stream_cipher(Mode::Decrypt, &dek, &nonce, &aad)?;
let tag_size = cipher.tag_size();
let mut chunk = vec![0; 2 ^ 16]; let mut read_buffer = vec![];
loop {
let bytes_read = input_file.read(&mut chunk)?;
if bytes_read == 0 {
break;
}
chunk.truncate(bytes_read);
let available_bytes = [read_buffer.as_slice(), &chunk].concat();
if available_bytes.len() > tag_size {
let num_bytes_to_process = available_bytes.len() - tag_size;
let output = stream_cipher.update(&available_bytes[..num_bytes_to_process])?;
output_file.write_all(&output)?;
read_buffer = available_bytes[num_bytes_to_process..].to_vec();
} else {
read_buffer = available_bytes;
}
}
if read_buffer.len() < tag_size {
cli_bail!("The tag is missing from the encrypted file")
}
let remaining = &read_buffer[..read_buffer.len() - cipher.tag_size()];
if !remaining.is_empty() {
let output = stream_cipher.update(remaining)?;
output_file.write_all(&output)?;
}
let tag = &read_buffer[read_buffer.len() - cipher.tag_size()..];
output_file.write_all(&stream_cipher.finalize_decryption(tag)?)?;
Ok(())
}
pub async fn client_side_decrypt_with_buffer(
&self,
kms_rest_client: &KmsClient,
data_encryption_algorithm: DataEncryptionAlgorithm,
key_encapsulation_key_id: &str,
ciphertext: &[u8],
aad: Option<Vec<u8>>,
) -> KmsCliResult<Vec<u8>> {
trace!(
"encryption algorithm {:?}, key id {:?}, ciphertext (len={}): {:?}",
data_encryption_algorithm,
key_encapsulation_key_id,
ciphertext.len(),
ciphertext
);
let unwrapping_key = export_object(
kms_rest_client,
key_encapsulation_key_id,
ExportObjectParams {
key_format_type: Some(KeyFormatType::TransparentSymmetricKey),
..ExportObjectParams::default()
},
)
.await?
.1;
let mut ct = ciphertext;
let aad = match data_encryption_algorithm {
DataEncryptionAlgorithm::AesXts | DataEncryptionAlgorithm::AesCbc => vec![],
DataEncryptionAlgorithm::AesGcm => aad.unwrap_or_default(),
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::AesGcmSiv | DataEncryptionAlgorithm::Chacha20Poly1305 => {
aad.unwrap_or_default()
}
};
let encaps_length = leb128::read::unsigned(&mut ct).map_err(|e| {
KmsCliError::Default(format!(
"Failed to read the encapsulation length from the encrypted file: {e}"
))
})?;
let mut encapsulation = vec![0; usize::try_from(encaps_length)?];
trace!("encapsulation length {}", encaps_length);
ct.read_exact(&mut encapsulation)?;
let mut dek_object = create_symmetric_key_kmip_object(
kms_rest_client.config.vendor_id.as_str(),
&[],
&Attributes {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
..Default::default()
},
)?;
let dek_key_block = dek_object.key_block_mut()?;
dek_key_block.key_value = Some(KeyValue::ByteString(Zeroizing::new(encapsulation)));
dek_key_block.key_wrapping_data = Some(KeyWrappingData {
encoding_option: Some(EncodingOption::NoEncoding),
..Default::default()
});
unwrap_key_block(dek_object.key_block_mut()?, &unwrapping_key)?;
let dek = dek_object.key_block()?.key_bytes()?;
let dem_cryptographic_parameters: CryptographicParameters =
data_encryption_algorithm.into();
trace!("dek length {}", dek.len());
let sym_cipher = SymCipher::from_algorithm_and_key_size(
dem_cryptographic_parameters
.cryptographic_algorithm
.unwrap_or(CryptographicAlgorithm::AES),
dem_cryptographic_parameters.block_cipher_mode,
dek.len(),
)?;
let mut nonce = vec![0; sym_cipher.nonce_size()];
ct.read_exact(&mut nonce)?;
let tag_size = sym_cipher.tag_size();
let (ciphertext, tag) = ct.split_at(ct.len() - tag_size);
let cleartext = decrypt(sym_cipher, &dek, &nonce, &aad, ciphertext, tag, None)?;
Ok(cleartext.to_vec())
}
}