use std::{
fs::File,
io::prelude::*,
path::{Path, PathBuf},
};
use clap::Parser;
use cosmian_kms_client::{
ExportObjectParams, KmsClient, export_object,
kmip_2_1::{
kmip_attributes::Attributes,
kmip_data_structures::KeyWrappingSpecification,
kmip_types::{
CryptographicAlgorithm, CryptographicParameters, EncodingOption, KeyFormatType,
},
requests::{create_symmetric_key_kmip_object, encrypt_request},
},
read_bytes_from_file,
reexport::cosmian_kms_client_utils::symmetric_utils::DataEncryptionAlgorithm,
};
use cosmian_kms_crypto::crypto::{
symmetric::symmetric_ciphers::{Mode, SymCipher, encrypt, random_key, random_nonce},
wrap::wrap_object_with_key,
};
use cosmian_logger::trace;
use zeroize::Zeroizing;
use crate::{
actions::kms::{
console, labels::KEY_ID, shared::get_key_uid, symmetric::KeyEncryptionAlgorithm,
},
error::{
KmsCliError,
result::{KmsCliResult, KmsCliResultHelper},
},
};
#[derive(Parser, Debug, Default)]
#[clap(verbatim_doc_comment)]
pub struct EncryptAction {
#[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 = "data-encryption-algorithm",
short = 'd',
default_value = "aes-gcm"
)]
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 = "tag", short = 't', value_name = "TAG", group = "key-tags")]
pub(crate) tags: Option<Vec<String>>,
#[clap(required = false, long, short = 'o')]
pub(crate) output_file: Option<PathBuf>,
#[clap(required = false, long, short = 'n')]
pub(crate) nonce: Option<String>,
#[clap(required = false, long, short = 'a')]
pub(crate) authentication_data: Option<String>,
}
impl EncryptAction {
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 nonce = self
.nonce
.as_deref()
.map(hex::decode)
.transpose()
.with_context(|| "failed to decode the nonce")?;
let authentication_data = self
.authentication_data
.as_deref()
.map(hex::decode)
.transpose()
.with_context(|| "failed to decode the authentication data")?;
let output_file_name = self
.output_file
.clone()
.unwrap_or_else(|| self.input_file.with_extension("enc"));
let mut output_file = File::create(&output_file_name)
.with_context(|| "failed to write the encrypted file")?;
if let Some(key_encryption_algorithm) = self.key_encryption_algorithm {
self.client_side_encrypt_with_file(
kms_rest_client,
&id,
key_encryption_algorithm,
self.data_encryption_algorithm,
nonce,
&self.input_file,
&mut output_file,
authentication_data,
)
.await?;
} else {
let plaintext = read_bytes_from_file(&self.input_file)
.with_context(|| "Cannot read bytes from the file to encrypt")?;
let (nonce, data, tag) = self
.server_side_encrypt(
&kms_rest_client,
&id,
self.data_encryption_algorithm.into(),
nonce,
plaintext,
authentication_data,
)
.await?;
if let Some(nonce) = nonce {
output_file
.write_all(&nonce)
.with_context(|| "failed to write the nonce")?;
}
output_file
.write_all(&data)
.context("failed to write the ciphertext")?;
if let Some(tag) = tag {
output_file
.write_all(&tag)
.context("failed to write the authentication tag")?;
}
}
let stdout = format!(
"The encrypted file is available at {}",
output_file_name.display()
);
console::Stdout::new(&stdout).write()?;
Ok(())
}
pub async fn server_side_encrypt(
&self,
kms_rest_client: &KmsClient,
data_encryption_key_id: &str,
cryptographic_parameters: CryptographicParameters,
nonce: Option<Vec<u8>>,
plaintext: Vec<u8>,
authenticated_data: Option<Vec<u8>>,
) -> Result<(Option<Vec<u8>>, Vec<u8>, Option<Vec<u8>>), KmsCliError> {
let encrypt_request = encrypt_request(
data_encryption_key_id,
None,
plaintext,
nonce,
authenticated_data,
Some(cryptographic_parameters),
)?;
let encrypt_response = kms_rest_client
.encrypt(encrypt_request)
.await
.with_context(|| "Can't execute the query on the kms server")?;
let nonce = encrypt_response.i_v_counter_nonce;
let data = encrypt_response
.data
.context("The encrypted data is empty")?;
let authentication_tag = encrypt_response.authenticated_encryption_tag; Ok((nonce, data, authentication_tag))
}
#[expect(clippy::too_many_arguments)]
async fn client_side_encrypt_with_file(
&self,
kms_rest_client: KmsClient,
kek_id: &str,
key_encryption_algorithm: KeyEncryptionAlgorithm,
data_encryption_algorithm: DataEncryptionAlgorithm,
nonce: Option<Vec<u8>>,
input_file_name: &Path,
output_file: &mut File,
aad: Option<Vec<u8>>,
) -> KmsCliResult<Zeroizing<Vec<u8>>> {
let aad = match data_encryption_algorithm {
DataEncryptionAlgorithm::AesXts | DataEncryptionAlgorithm::AesCbc => vec![],
DataEncryptionAlgorithm::AesGcm => aad.unwrap_or_default(),
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::Chacha20Poly1305 | DataEncryptionAlgorithm::AesGcmSiv => {
aad.unwrap_or_default()
}
};
let (dek, encapsulation) = self
.server_side_kem_encapsulation(
kms_rest_client,
kek_id,
key_encryption_algorithm,
data_encryption_algorithm,
)
.await?;
leb128::write::unsigned(output_file, u64::try_from(encapsulation.len())?)?;
output_file.write_all(&encapsulation)?;
let cryptographic_parameters: CryptographicParameters = data_encryption_algorithm.into();
let cipher = SymCipher::from_algorithm_and_key_size(
cryptographic_parameters
.cryptographic_algorithm
.ok_or_else(|| {
KmsCliError::Default(
"No data encryption cryptographic algorithm specified".to_owned(),
)
})?,
cryptographic_parameters.block_cipher_mode,
dek.len(),
)?;
let nonce = match nonce {
Some(n) => n,
None => random_nonce(cipher)?,
};
output_file.write_all(&nonce)?;
let mut stream_cipher = cipher.stream_cipher(Mode::Encrypt, &dek, &nonce, &aad)?;
let mut file = File::open(input_file_name)?;
let mut chunk = vec![0; 2 ^ 16]; loop {
let bytes_read = file.read(&mut chunk)?;
if bytes_read == 0 {
break;
}
chunk.truncate(bytes_read);
let ciphertext = stream_cipher.update(&chunk)?;
output_file.write_all(&ciphertext)?;
}
let (remaining, tag) = stream_cipher.finalize_encryption()?;
output_file.write_all(&remaining)?;
output_file.write_all(&tag)?;
output_file.flush()?;
Ok(dek)
}
pub async fn server_side_kem_encapsulation(
&self,
kms_rest_client: KmsClient,
kek_id: &str,
key_encryption_algorithm: KeyEncryptionAlgorithm,
data_encryption_algorithm: DataEncryptionAlgorithm,
) -> KmsCliResult<(Zeroizing<Vec<u8>>, Vec<u8>)> {
let dek = match data_encryption_algorithm {
DataEncryptionAlgorithm::AesGcm => random_key(SymCipher::Aes256Gcm)?,
DataEncryptionAlgorithm::AesCbc => random_key(SymCipher::Aes256Cbc)?,
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::Chacha20Poly1305 => random_key(SymCipher::Chacha20Poly1305)?,
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::AesGcmSiv => random_key(SymCipher::Aes256Gcm)?,
DataEncryptionAlgorithm::AesXts => random_key(SymCipher::Aes256Xts)?,
};
let (kem_nonce, kem_ciphertext, kem_tag) = self
.server_side_encrypt(
&kms_rest_client,
kek_id,
key_encryption_algorithm.into(),
None,
dek.to_vec(),
None,
)
.await?;
let encapsulation: Vec<u8> = [
kem_nonce.unwrap_or_default(),
kem_ciphertext,
kem_tag.unwrap_or_default(),
]
.concat();
Ok((dek, encapsulation))
}
pub async fn client_side_kem_encapsulation(
&self,
kms_rest_client: &KmsClient,
kek_id: &str,
data_encryption_algorithm: DataEncryptionAlgorithm,
) -> KmsCliResult<(Zeroizing<Vec<u8>>, Vec<u8>)> {
trace!("data_encryption_algorithm: {data_encryption_algorithm}");
let dek: Zeroizing<Vec<u8>> = match data_encryption_algorithm {
DataEncryptionAlgorithm::AesCbc => random_key(SymCipher::Aes256Cbc)?,
DataEncryptionAlgorithm::AesGcm => random_key(SymCipher::Aes256Gcm)?,
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::Chacha20Poly1305 => random_key(SymCipher::Chacha20Poly1305)?,
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::AesGcmSiv => random_key(SymCipher::Aes256Gcm)?,
DataEncryptionAlgorithm::AesXts => random_key(SymCipher::Aes256Xts)?,
};
trace!("dek (len={}): {dek:?}", dek.len());
let wrapping_key = export_object(
kms_rest_client,
kek_id,
ExportObjectParams {
key_format_type: Some(KeyFormatType::TransparentSymmetricKey),
..ExportObjectParams::default()
},
)
.await?
.1;
let mut dek_object = create_symmetric_key_kmip_object(
kms_rest_client.config.vendor_id.as_str(),
&dek,
&Attributes {
cryptographic_algorithm: Some(CryptographicAlgorithm::AES),
..Default::default()
},
)?;
wrap_object_with_key(
&mut dek_object,
&wrapping_key,
&KeyWrappingSpecification {
encoding_option: Some(EncodingOption::NoEncoding),
..Default::default()
},
)?;
let encapsulation = dek_object.key_block()?.wrapped_key_bytes()?;
Ok((dek, encapsulation.to_vec()))
}
pub fn client_side_encrypt_with_buffer(
&self,
dek: &Zeroizing<Vec<u8>>,
encapsulation: &[u8],
data_encryption_algorithm: DataEncryptionAlgorithm,
nonce: Option<Vec<u8>>,
plaintext: &[u8],
aad: Option<Vec<u8>>,
) -> KmsCliResult<Vec<u8>> {
let aad = match data_encryption_algorithm {
DataEncryptionAlgorithm::AesXts | DataEncryptionAlgorithm::AesCbc => vec![],
DataEncryptionAlgorithm::AesGcm => aad.unwrap_or_default(),
#[cfg(feature = "non-fips")]
DataEncryptionAlgorithm::Chacha20Poly1305 | DataEncryptionAlgorithm::AesGcmSiv => {
aad.unwrap_or_default()
}
};
let mut output_buffer = Vec::with_capacity(encapsulation.len() + 2 * plaintext.len());
let encapsulation_len = u64::try_from(encapsulation.len())?;
leb128::write::unsigned(&mut output_buffer, encapsulation_len)?;
output_buffer.write_all(encapsulation)?;
let cryptographic_parameters: CryptographicParameters = data_encryption_algorithm.into();
let sym_cipher = SymCipher::from_algorithm_and_key_size(
cryptographic_parameters
.cryptographic_algorithm
.ok_or_else(|| {
KmsCliError::Default(
"No data encryption cryptographic algorithm specified".to_owned(),
)
})?,
cryptographic_parameters.block_cipher_mode,
dek.len(),
)?;
let nonce = match nonce {
Some(n) => n,
None => random_nonce(sym_cipher)?,
};
output_buffer.write_all(&nonce)?;
let (ciphertext, tag) = encrypt(sym_cipher, dek, &nonce, &aad, plaintext, None)?;
output_buffer.write_all(&ciphertext)?;
output_buffer.write_all(&tag)?;
output_buffer.flush()?;
Ok(output_buffer)
}
}