diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5b88715e..6fbb2a43 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -exclude: crate/cli/test_data|documentation/pandoc|documentation/overrides|enclave|crate/server/src/tests/test_utils.rs|crate/cli/src/tests/utils/test_utils.rs|crate/client/src/lib.rs|crate/cli/src/tests/certificates/openssl.rs|crate/client/src/kms_rest_client.rs|.pre-commit-config.yaml|crate/server/src/routes/google_cse/jwt.rs|crate/server/src/routes/google_cse/python/openssl|documentation/docs/google_cse|crate/pkcs11/sys +exclude: crate/cli/test_data|documentation/pandoc|documentation/overrides|enclave|crate/server/src/tests/test_utils.rs|crate/cli/src/tests/utils/test_utils.rs|crate/client/src/lib.rs|crate/cli/src/tests/certificates/openssl.rs|crate/client/src/kms_rest_client.rs|.pre-commit-config.yaml|crate/server/src/routes/google_cse/jwt.rs|crate/server/src/routes/google_cse/python/openssl|documentation/docs/google_cse|crate/pkcs11/sys|documentation/docs/drawings repos: - repo: https://github.com/compilerla/conventional-pre-commit rev: v3.2.0 @@ -16,16 +16,6 @@ repos: stages: [commit-msg] args: [] # optional: list of Conventional Commits types to allow e.g. [feat, fix, ci, chore, test] - # - repo: https://github.com/pre-commit/mirrors-prettier - # rev: v4.0.0-alpha.8 - # hooks: - # - id: prettier - # stages: [commit] - # exclude_types: - # - yaml - # - markdown - # exclude: documentation/theme_overrides|.cargo_check - - repo: https://github.com/igorshubovych/markdownlint-cli rev: v0.40.0 hooks: diff --git a/.vale.ini b/.vale.ini index 931bd5cf..7e20a204 100644 --- a/.vale.ini +++ b/.vale.ini @@ -4,3 +4,4 @@ MinAlertLevel = suggestion [*.md] BasedOnStyles = Vale +Vale.Spelling = NO diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e590040..0dd6f713 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,15 +6,20 @@ All notable changes to this project will be documented in this file. ### 🚀 Features -- Google Workspace Client-Side-Encryption (CSE) updates ([#319](https://github.com/Cosmian/kms/pull/319)) - - Generate Google S/MIME key-pairs and identities and upload them to Gmail API from ckms CLI ([#270](https://github.com/Cosmian/kms/issues/270)) +- Google Workspace Client-Side-Encryption (CSE) + updates ([#319](https://github.com/Cosmian/kms/pull/319)) + - Generate Google S/MIME key-pairs and identities and upload them to Gmail API from ckms + CLI ([#270](https://github.com/Cosmian/kms/issues/270)) - Server-side, export cert at PKCS7 format - Implement missing CSE endpoints - Wrap/unwrap CSE elements with authenticated encryption - Export wrapped keys from KMS specifying the cipher mode - Handle auth for guest users ([#271](https://github.com/Cosmian/kms/issues/271)) - Add SetAttribute/DeleteAttribute KMIP operations ([#303](https://github.com/Cosmian/kms/pull/303)) -- Reenable wrap/unwrap on ckms by linking statically on openssl ([#317](https://github.com/Cosmian/kms/pull/317)) +- Re-enable wrap/unwrap on ckms by linking statically on openssl ([#317](. + com/Cosmian/kms/pull/317)) +- Added AES GCM-SIV and AES XTS +- Added the ability to client side encrypt files with `ckms` and a hybrid scheme ### Documentation diff --git a/Cargo.lock b/Cargo.lock index 2549663d..6b0bd17a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -301,6 +301,21 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aes-gcm-siv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + [[package]] name = "ahash" version = "0.7.8" @@ -1262,6 +1277,7 @@ dependencies = [ name = "cosmian_kmip" version = "4.19.0" dependencies = [ + "aes-gcm-siv", "argon2", "base64 0.21.7", "bitflags 2.6.0", @@ -1304,6 +1320,7 @@ dependencies = [ "hex", "jwt-simple", "kms_test_server", + "leb128", "oauth2", "openssl", "pem", diff --git a/Cargo.toml b/Cargo.toml index 0d361969..ecc4c9e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ cloudproof = "3.0" der = { version = "0.7", default-features = false } env_logger = "0.11" hex = { version = "0.4", default-features = false } +leb128 = "0.2.5" log = { version = "0.4", default-features = false } native-tls = "0.2" num_cpus = "1.13" diff --git a/crate/cli/Cargo.toml b/crate/cli/Cargo.toml index 13fd5528..a9b93fdc 100644 --- a/crate/cli/Cargo.toml +++ b/crate/cli/Cargo.toml @@ -41,6 +41,7 @@ hex = { workspace = true } jwt-simple = { version = "0.12", default-features = false, features = [ "pure-rust", ] } +leb128 = { workspace = true } oauth2 = { version = "4.4", features = ["reqwest"] } pem = { workspace = true } reqwest = { workspace = true } @@ -60,7 +61,7 @@ actix-rt = { workspace = true } actix-server = { workspace = true } assert_cmd = "2.0" const-oid = { version = "0.9", features = ["db"] } -kms_test_server = { path = "../test_server"} +kms_test_server = { path = "../test_server" } openssl = { workspace = true } predicates = "3.1" regex = { version = "1.10", default-features = false } diff --git a/crate/cli/src/actions/cover_crypt/encrypt.rs b/crate/cli/src/actions/cover_crypt/encrypt.rs index 46fd804c..74073c1a 100644 --- a/crate/cli/src/actions/cover_crypt/encrypt.rs +++ b/crate/cli/src/actions/cover_crypt/encrypt.rs @@ -82,6 +82,7 @@ impl EncryptAction { Some(self.encryption_policy.to_string()), data, None, + None, self.authentication_data .as_deref() .map(|s| s.as_bytes().to_vec()), diff --git a/crate/cli/src/actions/elliptic_curves/encrypt.rs b/crate/cli/src/actions/elliptic_curves/encrypt.rs index cdc832a2..bb92c452 100644 --- a/crate/cli/src/actions/elliptic_curves/encrypt.rs +++ b/crate/cli/src/actions/elliptic_curves/encrypt.rs @@ -62,6 +62,7 @@ impl EncryptAction { None, data, None, + None, self.authentication_data .as_deref() .map(|s| s.as_bytes().to_vec()), diff --git a/crate/cli/src/actions/rsa/encrypt.rs b/crate/cli/src/actions/rsa/encrypt.rs index 3e54ca38..86c61d99 100644 --- a/crate/cli/src/actions/rsa/encrypt.rs +++ b/crate/cli/src/actions/rsa/encrypt.rs @@ -96,6 +96,7 @@ impl EncryptAction { data, None, None, + None, Some(to_cryptographic_parameters( self.encryption_algorithm, self.hash_fn, diff --git a/crate/cli/src/actions/symmetric/decrypt.rs b/crate/cli/src/actions/symmetric/decrypt.rs index 7fe817bd..819901db 100644 --- a/crate/cli/src/actions/symmetric/decrypt.rs +++ b/crate/cli/src/actions/symmetric/decrypt.rs @@ -1,25 +1,60 @@ -use std::{fs::File, io::Write, path::PathBuf}; +use std::{ + fs::File, + io::{Read, Write}, + path::{Path, PathBuf}, +}; use clap::Parser; -use cosmian_kms_client::{ - cosmian_kmip::crypto::generic::kmip_requests::build_decryption_request, read_bytes_from_file, - KmsClient, +#[cfg(not(feature = "fips"))] +use cosmian_kms_client::cosmian_kmip::crypto::symmetric::symmetric_ciphers::{ + CHACHA20_POLY1305_IV_LENGTH, CHACHA20_POLY1305_MAC_LENGTH, }; +use cosmian_kms_client::{ + cosmian_kmip::crypto::{ + generic::kmip_requests::build_decryption_request, + symmetric::symmetric_ciphers::{ + Mode, SymCipher, AES_128_GCM_IV_LENGTH, AES_128_GCM_MAC_LENGTH, AES_128_XTS_MAC_LENGTH, + AES_128_XTS_TWEAK_LENGTH, RFC5649_16_IV_LENGTH, RFC5649_16_MAC_LENGTH, + }, + }, + kmip::kmip_types::{BlockCipherMode, CryptographicAlgorithm, CryptographicParameters}, + read_bytes_from_file, KmsClient, +}; +use zeroize::Zeroizing; use crate::{ - actions::console, + actions::{ + console, + symmetric::{DataEncryptionAlgorithm, KeyEncryptionAlgorithm}, + }, cli_bail, - error::result::{CliResult, CliResultHelper}, + error::{ + result::{CliResult, CliResultHelper}, + CliError, + }, }; -/// Decrypts a file using AES GCM +/// Decrypt a file using a symmetric key. /// -/// The content of the file must be the concatenation of -/// - the nonce (12 bytes) +/// Decryption can happen in two ways: +/// - server side: the data is sent to the server and decrypted server side. +/// - client side: The encapsulated/wrapped data encryption key (DEK) is read from the input file +/// and decrypted server side using the key encryption algorithm and the key encryption key (KEK) +/// identified by `--key-id`. Once the DEK is recovered, the data is decrypted client side +/// using the data encryption algorithm. +/// +/// To decrypt the data server side, do not specify the key encryption algorithm. +/// +/// The bytes written from the input are expected to be the concatenation of +/// - if client side decryption is used: +/// - the length of the encapsulated DEK as an unsigned LEB128 integer +/// - the encapsulated DEK +/// - the nonce used for data encryption (or tweak for XTS) /// - the encrypted data (same size as the plaintext) -/// - the authentication tag (16 bytes) +/// - the authentication tag generated by the data encryption algorithm (none, for XTS) /// -/// This is not a streaming call: the file is entirely loaded in memory before being sent for decryption. +/// Note: server side decryption is not a streaming call: +/// the data is entirely loaded in memory before being encrypted. #[derive(Parser, Debug)] #[clap(verbatim_doc_comment)] pub struct DecryptAction { @@ -32,6 +67,32 @@ pub struct DecryptAction { #[clap(long = "key-id", short = 'k', group = "key-tags")] key_id: Option, + /// The data encryption algorithm. + /// If not specified, aes-gcm is used. + /// + /// If no key encryption algorithm is specified, the data will be sent to the server + /// and will be decrypted server side. + #[clap( + long = "data-encryption-algorithm", + short = 'd', + default_value = "aes-gcm" + )] + data_encryption_algorithm: DataEncryptionAlgorithm, + + /// The optional key encryption algorithm used to decrypt the data encryption key. + /// + /// If not specified: + /// - the decryption of the data is performed server side using the key identified by + /// `--key-id` + /// + /// If specified: + /// - the data encryption key (DEK) is unwrapped (i.e., decrypted) server side + /// using the key encryption algorithm and the key identified by `--key-id`. + /// - the data is decrypted client side with the data encryption algorithm and using + /// the DEK. + #[clap(long = "key-encryption-algorithm", short = 'e', verbatim_doc_comment)] + key_encryption_algorithm: Option, + /// Tag to use to retrieve the key when no key id is specified. /// To specify multiple tags, use the option multiple times. #[clap(long = "tag", short = 't', value_name = "TAG", group = "key-tags")] @@ -41,17 +102,13 @@ pub struct DecryptAction { #[clap(required = false, long, short = 'o')] output_file: Option, - /// Optional authentication data that was supplied during encryption. + /// Optional authentication data that was supplied during encryption as a hex string. #[clap(required = false, long, short)] authentication_data: Option, } impl DecryptAction { pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { - // Read the file to decrypt - let mut data = read_bytes_from_file(&self.input_file) - .with_context(|| "Cannot read bytes from the file to decrypt")?; - // Recover the unique identifier or set of tags let id = if let Some(key_id) = &self.key_id { key_id.clone() @@ -61,20 +118,103 @@ impl DecryptAction { cli_bail!("Either `--key-id` or one or more `--tag` must be specified") }; - // Extract the nonce, the encrypted data and the tag - let nonce = data.drain(..12).collect::>(); - let tag = data.drain(data.len() - 16..).collect::>(); + // Write the decrypted file + 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( + 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 { + // Read the file to decrypt + let ciphertext = read_bytes_from_file(&self.input_file) + .with_context(|| "Cannot read bytes from the file to decrypt")?; + // Decrypt the ciphertext server side + 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")?; + } + + // Print the output file name to the console and return + let stdout = format!("The decrypted file is available at {output_file_name:?}"); + let mut stdout = console::Stdout::new(&stdout); + stdout.set_tags(self.tags.as_ref()); + stdout.write()?; + + Ok(()) + } + + async fn server_side_decrypt( + &self, + kms_rest_client: &KmsClient, + cryptographic_parameters: CryptographicParameters, + key_id: &str, + mut ciphertext: Vec, + aad: Option>, + ) -> CliResult>> { + // Extract the nonce, the encrypted data, and the tag + let (nonce_size, tag_size) = match &cryptographic_parameters + .cryptographic_algorithm + .unwrap_or(CryptographicAlgorithm::AES) + { + CryptographicAlgorithm::AES => match cryptographic_parameters + .block_cipher_mode + .unwrap_or(BlockCipherMode::GCM) + { + BlockCipherMode::GCM | BlockCipherMode::GCMSIV => { + (AES_128_GCM_IV_LENGTH, AES_128_GCM_MAC_LENGTH) + } + BlockCipherMode::XTS => (AES_128_XTS_TWEAK_LENGTH, AES_128_XTS_MAC_LENGTH), + BlockCipherMode::NISTKeyWrap => (RFC5649_16_IV_LENGTH, RFC5649_16_MAC_LENGTH), + _ => cli_bail!("Unsupported block cipher mode"), + }, + #[cfg(not(feature = "fips"))] + CryptographicAlgorithm::ChaCha20Poly1305 | CryptographicAlgorithm::ChaCha20 => { + (CHACHA20_POLY1305_IV_LENGTH, CHACHA20_POLY1305_MAC_LENGTH) + } + a => cli_bail!("Unsupported cryptographic algorithm: {:?}", a), + }; + let nonce = ciphertext.drain(..nonce_size).collect::>(); + let tag = ciphertext + .drain(ciphertext.len() - tag_size..) + .collect::>(); // Create the kmip query let decrypt_request = build_decryption_request( - &id, + key_id, Some(nonce), - data, + ciphertext, Some(tag), - self.authentication_data - .as_deref() - .map(|s| s.as_bytes().to_vec()), - None, + aad, + Some(cryptographic_parameters), ); // Query the KMS with your kmip data and get the key pair ids @@ -83,23 +223,103 @@ impl DecryptAction { .await .context("Can't execute the query on the kms server")?; - let plaintext = decrypt_response.data.context("the plain text is empty")?; - - // Write the decrypted file - let output_file = self - .output_file - .clone() - .unwrap_or_else(|| self.input_file.clone().with_extension(".plain")); - let mut buffer = File::create(&output_file).context("Fail to write the plaintext file")?; - buffer - .write_all(&plaintext) - .context("failed to write the plaintext file")?; - - let stdout = format!("The decrypted file is available at {output_file:?}"); - let mut stdout = console::Stdout::new(&stdout); - stdout.set_tags(self.tags.as_ref()); - stdout.write()?; + decrypt_response.data.context("the plain text is empty") + } + #[allow(clippy::too_many_arguments)] + async fn client_side_decrypt( + &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>, + ) -> CliResult<()> { + // Additional authenticated data (AAD) for AEAD ciphers + // (empty for XTS) + let aad = match data_encryption_algorithm { + DataEncryptionAlgorithm::AesXts => vec![], + DataEncryptionAlgorithm::AesGcm => aad.unwrap_or_default(), + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::AesGcmSiv | DataEncryptionAlgorithm::Chacha20Poly1305 => { + aad.unwrap_or_default() + } + }; + // Open the input file + let mut input_file = File::open(input_file_name)?; + // read the encapsulation length as a LEB128 encoded u64 + let encaps_length = leb128::read::unsigned(&mut input_file).map_err(|e| { + CliError::Default(format!( + "Failed to read the encapsulation length from the encrypted file: {e}" + )) + })?; + // read the encapsulated data + #[allow(clippy::cast_possible_truncation)] + let mut encapsulation = vec![0; encaps_length as usize]; + input_file.read_exact(&mut encapsulation)?; + // recover the DEK + let dek = self + .server_side_decrypt( + kms_rest_client, + key_encryption_algorithm.into(), + key_id, + encapsulation, + None, + ) + .await?; + // determine the DEM parameters + let dem_cryptographic_parameters: CryptographicParameters = + data_encryption_algorithm.into(); + 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(), + )?; + //read the nonce + let mut nonce = vec![0; cipher.nonce_size()]; + input_file.read_exact(&mut nonce)?; + // decrypt the file + let mut stream_cipher = cipher.stream_cipher(Mode::Decrypt, &dek, &nonce, &aad)?; + let tag_size = cipher.tag_size(); + // read the file by chunks + let mut chunk = vec![0; 2 ^ 16]; //64K + 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(); + // keep at least the tag size in the local buffer + if available_bytes.len() > tag_size { + // process all bytes except the tag length last bytes + 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)?; + // keep the remaining bytes in the read buffer + read_buffer = available_bytes[num_bytes_to_process..].to_vec(); + } else { + // put everything in the read buffer + read_buffer = available_bytes; + }; + } + // recover the tag from the read_buffer + if read_buffer.len() < tag_size { + cli_bail!("The tag is missing from the encrypted file") + } + // write the remaining bytes before the tag + 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(()) } } diff --git a/crate/cli/src/actions/symmetric/encrypt.rs b/crate/cli/src/actions/symmetric/encrypt.rs index 15a9874e..a7fc6810 100644 --- a/crate/cli/src/actions/symmetric/encrypt.rs +++ b/crate/cli/src/actions/symmetric/encrypt.rs @@ -1,25 +1,53 @@ -use std::{fs::File, io::prelude::*, path::PathBuf}; +use std::{ + fs::File, + io::prelude::*, + path::{Path, PathBuf}, +}; use clap::Parser; use cosmian_kms_client::{ - cosmian_kmip::crypto::generic::kmip_requests::build_encryption_request, read_bytes_from_file, - KmsClient, + cosmian_kmip::crypto::{ + generic::kmip_requests::build_encryption_request, + symmetric::symmetric_ciphers::{random_key, random_nonce, Mode, SymCipher}, + }, + kmip::kmip_types::CryptographicParameters, + read_bytes_from_file, KmsClient, }; +use zeroize::Zeroizing; use crate::{ - actions::console, + actions::{ + console, + symmetric::{DataEncryptionAlgorithm, KeyEncryptionAlgorithm}, + }, cli_bail, - error::result::{CliResult, CliResultHelper}, + error::{ + result::{CliResult, CliResultHelper}, + CliError, + }, }; -/// Encrypt a file using AES GCM +/// Encrypt a file using a symmetric cipher /// -/// The resulting bytes are the concatenation of -/// - the nonce (12 bytes) +/// Encryption can happen in two ways: +/// - server side: the data is sent to the server and encrypted server side. +/// - client side: the data is encrypted client side using a randomly generated ephemeral key +/// called the data encryption key (DEK). The DEK is then wrapped (i.e., encrypted) server side +/// using the key encryption algorithm and the key encryption key (KEK) identified by the key id. +/// The ephemeral DEK key has a size of 256 bits (512 bits for XTS). +/// +/// To encrypt the data server side, do not specify the key encryption algorithm. +/// +/// The bytes written to the output file are the concatenation of +/// - if client side encryption is used: +/// - the length of the encapsulated DEK as an unsigned LEB128 integer +/// - the encapsulated DEK +/// - the nonce used for data encryption (or tweak for XTS) /// - the encrypted data (same size as the plaintext) -/// - the authentication tag (16 bytes) +/// - the authentication tag generated by the data encryption algorithm (none, for XTS) /// -/// Note: this is not a streaming call: the file is entirely loaded in memory before being sent for encryption. +/// Note: server side encryption is not a streaming call: +/// the data is entirely loaded in memory before being encrypted. #[derive(Parser, Debug)] #[clap(verbatim_doc_comment)] pub struct EncryptAction { @@ -32,6 +60,32 @@ pub struct EncryptAction { #[clap(long = "key-id", short = 'k', group = "key-tags")] key_id: Option, + /// The data encryption algorithm. + /// If not specified, aes-gcm is used. + /// + /// If no key encryption algorithm is specified, the data will be sent to the server + /// and will be encrypted server side. + #[clap( + long = "data-encryption-algorithm", + short = 'd', + default_value = "aes-gcm" + )] + data_encryption_algorithm: DataEncryptionAlgorithm, + + /// The optional key encryption algorithm used to encrypt the data encryption key. + /// + /// If not specified: + /// - the encryption of the data is performed server side. + /// - the key id is that of the data encryption key. + /// + /// If specified: + /// - the data is encrypted client side with the data encryption algorithm, and using + /// a randomly generated ephemeral key called the data encryption key (DEK). + /// - the DEK is wrapped server side using the key encryption algorithm and the key + /// identified by the key id. + #[clap(long = "key-encryption-algorithm", short = 'e', verbatim_doc_comment)] + key_encryption_algorithm: Option, + /// Tag to use to retrieve the key when no key id is specified. /// To specify multiple tags, use the option multiple times. #[clap(long = "tag", short = 't', value_name = "TAG", group = "key-tags")] @@ -41,18 +95,20 @@ pub struct EncryptAction { #[clap(required = false, long, short = 'o')] output_file: Option, - /// Optional authentication data. + /// Optional nonce/IV (or tweak for XTS) as a hex string. + /// If not provided, a random value is generated. + #[clap(required = false, long, short = 'n')] + nonce: Option, + + /// Optional additional authentication data as a hex string. /// This data needs to be provided back for decryption. + /// This data is ignored with XTS. #[clap(required = false, long, short = 'a')] authentication_data: Option, } impl EncryptAction { pub async fn run(&self, kms_rest_client: &KmsClient) -> CliResult<()> { - // Read the file to encrypt - let data = read_bytes_from_file(&self.input_file) - .with_context(|| "Cannot read bytes from the file to encrypt")?; - // Recover the unique identifier or set of tags let id = if let Some(key_id) = &self.key_id { key_id.clone() @@ -62,16 +118,90 @@ impl EncryptAction { cli_bail!("Either `--key-id` or one or more `--tag` must be specified") }; + 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( + kms_rest_client, + &id, + key_encryption_algorithm, + self.data_encryption_algorithm, + nonce, + &self.input_file, + &mut output_file, + authentication_data, + ) + .await?; + } else { + // Read the file to encrypt + 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?; + output_file + .write_all(&nonce) + .with_context(|| "failed to write the nonce")?; + output_file + .write_all(&data) + .context("failed to write the ciphertext")?; + output_file + .write_all(&tag) + .context("failed to write the authentication tag")?; + } + + let stdout = format!("The encrypted file is available at {output_file_name:?}"); + console::Stdout::new(&stdout).write()?; + + Ok(()) + } + + /// Encrypt the data using the specified key server side + /// Returns the nonce, the encrypted data, and the authentication tag + async fn server_side_encrypt( + &self, + kms_rest_client: &KmsClient, + key_id: &str, + cryptographic_parameters: CryptographicParameters, + nonce: Option>, + plaintext: Vec, + authenticated_data: Option>, + ) -> Result<(Vec, Vec, Vec), CliError> { // Create the kmip query let encrypt_request = build_encryption_request( - &id, + key_id, None, - data, - None, - self.authentication_data - .as_deref() - .map(|s| s.as_bytes().to_vec()), + plaintext, None, + nonce, + authenticated_data, + Some(cryptographic_parameters), )?; // Query the KMS with your kmip data and get the key pair ids @@ -80,42 +210,118 @@ impl EncryptAction { .await .with_context(|| "Can't execute the query on the kms server")?; - // Write the encrypted file - let output_file = self - .output_file - .clone() - .unwrap_or_else(|| self.input_file.with_extension("enc")); - let mut buffer = - File::create(&output_file).with_context(|| "failed to write the encrypted file")?; - // extract the nonce and write it let nonce = encrypt_response .iv_counter_nonce .context("the nonce is empty")?; - buffer - .write_all(&nonce) - .with_context(|| "failed to write the nonce")?; // extract the ciphertext and write it let data = encrypt_response .data .context("The encrypted data is empty")?; - buffer - .write_all(&data) - .context("failed to write the ciphertext")?; // extract the authentication tag and write it let authentication_tag = encrypt_response .authenticated_encryption_tag .context("the authentication tag is empty")?; + Ok((nonce, data, authentication_tag)) + } - buffer - .write_all(&authentication_tag) - .context("failed to write the authentication tag")?; + /// Encrypt a file using a symmetric stream cipher + /// and return the ephemeral key + #[allow(clippy::too_many_arguments)] + async fn client_side_encrypt( + &self, + kms_rest_client: &KmsClient, + key_id: &str, + key_encryption_algorithm: KeyEncryptionAlgorithm, + data_encryption_algorithm: DataEncryptionAlgorithm, + nonce: Option>, + input_file_name: &Path, + output_file: &mut File, + aad: Option>, + ) -> CliResult>> { + // Generate the ephemeral key (DEK) + let dek = match data_encryption_algorithm { + DataEncryptionAlgorithm::AesGcm => random_key(SymCipher::Aes256Gcm)?, + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::Chacha20Poly1305 => random_key(SymCipher::Chacha20Poly1305)?, + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::AesGcmSiv => random_key(SymCipher::Aes256Gcm)?, + DataEncryptionAlgorithm::AesXts => random_key(SymCipher::Aes256Xts)?, + }; - let stdout = format!("The encrypted file is available at {output_file:?}"); - console::Stdout::new(&stdout).write()?; + // Additional authenticated data (AAD) for AEAD ciphers + // (empty for XTS) + let aad = match data_encryption_algorithm { + DataEncryptionAlgorithm::AesXts => vec![], + DataEncryptionAlgorithm::AesGcm => aad.unwrap_or_default(), + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::Chacha20Poly1305 | DataEncryptionAlgorithm::AesGcmSiv => { + aad.unwrap_or_default() + } + }; - Ok(()) + // Wrap the DEK with the KEK + let (kem_nonce, kem_ciphertext, kem_tag) = self + .server_side_encrypt( + kms_rest_client, + key_id, + key_encryption_algorithm.into(), + None, + dek.to_vec(), + None, + ) + .await?; + #[allow(clippy::tuple_array_conversions)] + let encapsulation: Vec = [kem_nonce, kem_ciphertext, kem_tag].concat(); + + // write the encapsulation to the output file, starting with the length of the encapsulation + // as an unsigned LEB128 integer + leb128::write::unsigned(output_file, encapsulation.len() as u64)?; + output_file.write_all(&encapsulation)?; + + // Determine the DEM parameters + let cryptographic_parameters: CryptographicParameters = data_encryption_algorithm.into(); + let cipher = SymCipher::from_algorithm_and_key_size( + cryptographic_parameters + .cryptographic_algorithm + .ok_or_else(|| { + CliError::Default( + "No data encryption cryptographic algorithm specified".to_owned(), + ) + })?, + cryptographic_parameters.block_cipher_mode, + dek.len(), + )?; + + // we need a nonce (or tweak) + let nonce = match nonce { + Some(n) => n, + None => random_nonce(cipher)?, + }; + output_file.write_all(&nonce)?; + + // instantiate the stream cipher + let mut stream_cipher = cipher.stream_cipher(Mode::Encrypt, &dek, &nonce, &aad)?; + // process the data read from the file by 4096 chunks and write the encrypted data + let mut file = File::open(input_file_name)?; + let mut chunk = vec![0; 2 ^ 16]; //64K + 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)?; + } + // finalize the encryption and write the remaining bytes + let (remaining, tag) = stream_cipher.finalize_encryption()?; + output_file.write_all(&remaining)?; + // write the tag + output_file.write_all(&tag)?; + output_file.flush()?; + Ok(dek) } } diff --git a/crate/cli/src/actions/symmetric/keys/create_key.rs b/crate/cli/src/actions/symmetric/keys/create_key.rs index f1bd1c26..341d9a28 100644 --- a/crate/cli/src/actions/symmetric/keys/create_key.rs +++ b/crate/cli/src/actions/symmetric/keys/create_key.rs @@ -87,7 +87,6 @@ impl CreateKeyAction { 512 => CryptographicAlgorithm::SHA3512, _ => cli_bail!("invalid number of bits for sha3 {}", number_of_bits), }, - SymmetricAlgorithm::Shake => match number_of_bits { 128 => CryptographicAlgorithm::SHAKE128, 256 => CryptographicAlgorithm::SHAKE256, diff --git a/crate/cli/src/actions/symmetric/mod.rs b/crate/cli/src/actions/symmetric/mod.rs index 8655fad5..b96bb850 100644 --- a/crate/cli/src/actions/symmetric/mod.rs +++ b/crate/cli/src/actions/symmetric/mod.rs @@ -1,5 +1,8 @@ -use clap::Parser; -use cosmian_kms_client::KmsClient; +use clap::{Parser, ValueEnum}; +use cosmian_kms_client::{ + kmip::kmip_types::{BlockCipherMode, CryptographicAlgorithm, CryptographicParameters}, + KmsClient, +}; use self::{decrypt::DecryptAction, encrypt::EncryptAction, keys::KeysCommands}; use crate::error::result::CliResult; @@ -37,3 +40,85 @@ impl SymmetricCommands { Ok(()) } } + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub(crate) enum DataEncryptionAlgorithm { + #[cfg(not(feature = "fips"))] + Chacha20Poly1305, + AesGcm, + AesXts, + #[cfg(not(feature = "fips"))] + AesGcmSiv, +} + +impl From for CryptographicParameters { + fn from(value: DataEncryptionAlgorithm) -> Self { + match value { + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::Chacha20Poly1305 => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::ChaCha20Poly1305), + ..Self::default() + }, + DataEncryptionAlgorithm::AesGcm => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::GCM), + ..Self::default() + }, + DataEncryptionAlgorithm::AesXts => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::XTS), + ..Self::default() + }, + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::AesGcmSiv => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::GCMSIV), + ..Self::default() + }, + } + } +} + +#[derive(ValueEnum, Debug, Clone, Copy)] +pub(crate) enum KeyEncryptionAlgorithm { + #[cfg(not(feature = "fips"))] + Chacha20Poly1305, + AesGcm, + AesXts, + #[cfg(not(feature = "fips"))] + AesGcmSiv, + RFC5649, +} + +impl From for CryptographicParameters { + fn from(value: KeyEncryptionAlgorithm) -> Self { + match value { + #[cfg(not(feature = "fips"))] + KeyEncryptionAlgorithm::Chacha20Poly1305 => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::ChaCha20Poly1305), + ..Self::default() + }, + KeyEncryptionAlgorithm::AesGcm => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::GCM), + ..Self::default() + }, + KeyEncryptionAlgorithm::AesXts => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::XTS), + ..Self::default() + }, + #[cfg(not(feature = "fips"))] + KeyEncryptionAlgorithm::AesGcmSiv => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::GCMSIV), + ..Self::default() + }, + KeyEncryptionAlgorithm::RFC5649 => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::NISTKeyWrap), + ..Self::default() + }, + } + } +} diff --git a/crate/cli/src/tests/access.rs b/crate/cli/src/tests/access.rs index ba9cf71b..52aa26ab 100644 --- a/crate/cli/src/tests/access.rs +++ b/crate/cli/src/tests/access.rs @@ -7,6 +7,7 @@ use tracing::trace; use super::{symmetric::create_key::create_symmetric_key, utils::recover_cmd_logs}; use crate::{ + actions::symmetric::DataEncryptionAlgorithm, error::{result::CliResult, CliError}, tests::{ shared::{destroy, export_key, revoke, ExportKeyParams}, @@ -137,7 +138,13 @@ pub(crate) async fn test_ownership_and_grant() -> CliResult<()> { })?; // the owner can encrypt and decrypt - run_encrypt_decrypt_test(&ctx.owner_client_conf_path, &key_id)?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &key_id, + DataEncryptionAlgorithm::AesGcm, + None, + 0, + )?; // the user should not be able to export assert!( @@ -151,7 +158,16 @@ pub(crate) async fn test_ownership_and_grant() -> CliResult<()> { .is_err() ); // the user should not be able to encrypt or decrypt - assert!(run_encrypt_decrypt_test(&ctx.user_client_conf_path, &key_id).is_err()); + assert!( + run_encrypt_decrypt_test( + &ctx.user_client_conf_path, + &key_id, + DataEncryptionAlgorithm::AesGcm, + None, + 0 + ) + .is_err() + ); // the user should not be able to revoke the key assert!(revoke(&ctx.user_client_conf_path, "sym", &key_id, "failed revoke").is_err()); // the user should not be able to destroy the key @@ -186,7 +202,13 @@ pub(crate) async fn test_ownership_and_grant() -> CliResult<()> { ); // the user should now be able to encrypt or decrypt - run_encrypt_decrypt_test(&ctx.user_client_conf_path, &key_id)?; + run_encrypt_decrypt_test( + &ctx.user_client_conf_path, + &key_id, + DataEncryptionAlgorithm::AesGcm, + None, + 0, + )?; // the user should still not be able to revoke the key assert!(revoke(&ctx.user_client_conf_path, "sym", &key_id, "failed revoke").is_err()); // the user should still not be able to destroy the key @@ -490,7 +512,13 @@ pub(crate) async fn test_ownership_and_grant_wildcard_user() -> CliResult<()> { })?; // the owner can encrypt and decrypt - run_encrypt_decrypt_test(&ctx.owner_client_conf_path, &key_id)?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &key_id, + DataEncryptionAlgorithm::AesGcm, + None, + 0, + )?; // the user should not be able to export assert!( @@ -504,7 +532,16 @@ pub(crate) async fn test_ownership_and_grant_wildcard_user() -> CliResult<()> { .is_err() ); // the user should not be able to encrypt or decrypt - assert!(run_encrypt_decrypt_test(&ctx.user_client_conf_path, &key_id).is_err()); + assert!( + run_encrypt_decrypt_test( + &ctx.user_client_conf_path, + &key_id, + DataEncryptionAlgorithm::AesGcm, + None, + 0 + ) + .is_err() + ); // the user should not be able to revoke the key assert!(revoke(&ctx.user_client_conf_path, "sym", &key_id, "failed revoke").is_err()); // the user should not be able to destroy the key @@ -529,7 +566,13 @@ pub(crate) async fn test_ownership_and_grant_wildcard_user() -> CliResult<()> { ); // the user should now be able to encrypt or decrypt - run_encrypt_decrypt_test(&ctx.user_client_conf_path, &key_id)?; + run_encrypt_decrypt_test( + &ctx.user_client_conf_path, + &key_id, + DataEncryptionAlgorithm::AesGcm, + None, + 0, + )?; // the user should still not be able to revoke the key assert!(revoke(&ctx.user_client_conf_path, "sym", &key_id, "failed revoke").is_err()); // the user should still not be able to destroy the key diff --git a/crate/cli/src/tests/symmetric/encrypt_decrypt.rs b/crate/cli/src/tests/symmetric/encrypt_decrypt.rs index b91305f7..d3ecae68 100644 --- a/crate/cli/src/tests/symmetric/encrypt_decrypt.rs +++ b/crate/cli/src/tests/symmetric/encrypt_decrypt.rs @@ -7,22 +7,59 @@ use tempfile::TempDir; use super::SUB_COMMAND; use crate::{ + actions::symmetric::{DataEncryptionAlgorithm, KeyEncryptionAlgorithm}, error::{result::CliResult, CliError}, tests::{symmetric::create_key::create_symmetric_key, utils::recover_cmd_logs, PROG_NAME}, }; +const fn dek_algorithm_to_string(alg: DataEncryptionAlgorithm) -> &'static str { + match alg { + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::Chacha20Poly1305 => "chacha20-poly1305", + DataEncryptionAlgorithm::AesGcm => "aes-gcm", + DataEncryptionAlgorithm::AesXts => "aes-xts", + #[cfg(not(feature = "fips"))] + DataEncryptionAlgorithm::AesGcmSiv => "aes-gcm-siv", + } +} + +const fn kek_algorithm_to_string(alg: KeyEncryptionAlgorithm) -> &'static str { + match alg { + #[cfg(not(feature = "fips"))] + KeyEncryptionAlgorithm::Chacha20Poly1305 => "chacha20-poly1305", + KeyEncryptionAlgorithm::AesGcm => "aes-gcm", + KeyEncryptionAlgorithm::AesXts => "aes-xts", + #[cfg(not(feature = "fips"))] + KeyEncryptionAlgorithm::AesGcmSiv => "aes-gcm-siv", + KeyEncryptionAlgorithm::RFC5649 => "rfc5649", + } +} + /// Encrypts a file using the given symmetric key and access policy. pub(crate) fn encrypt( cli_conf_path: &str, input_file: &str, symmetric_key_id: &str, + data_encryption_algorithm: DataEncryptionAlgorithm, + key_encryption_algorithm: Option, output_file: Option<&str>, authentication_data: Option<&str>, ) -> CliResult<()> { let mut cmd = Command::cargo_bin(PROG_NAME)?; cmd.env(KMS_CLI_CONF_ENV, cli_conf_path); - let mut args = vec!["encrypt", input_file, "--key-id", symmetric_key_id]; + let mut args = vec![ + "encrypt", + input_file, + "--key-id", + symmetric_key_id, + "-d", + dek_algorithm_to_string(data_encryption_algorithm), + ]; + if let Some(key_encryption_algorithm) = key_encryption_algorithm { + args.push("-e"); + args.push(kek_algorithm_to_string(key_encryption_algorithm)); + } if let Some(output_file) = output_file { args.push("-o"); args.push(output_file); @@ -46,13 +83,26 @@ pub(crate) fn decrypt( cli_conf_path: &str, input_file: &str, symmetric_key_id: &str, + data_encryption_algorithm: DataEncryptionAlgorithm, + key_encryption_algorithm: Option, output_file: Option<&str>, authentication_data: Option<&str>, ) -> CliResult<()> { let mut cmd = Command::cargo_bin(PROG_NAME)?; cmd.env(KMS_CLI_CONF_ENV, cli_conf_path); - let mut args = vec!["decrypt", input_file, "--key-id", symmetric_key_id]; + let mut args = vec![ + "decrypt", + input_file, + "--key-id", + symmetric_key_id, + "-d", + dek_algorithm_to_string(data_encryption_algorithm), + ]; + if let Some(key_encryption_algorithm) = key_encryption_algorithm { + args.push("-e"); + args.push(kek_algorithm_to_string(key_encryption_algorithm)); + } if let Some(output_file) = output_file { args.push("-o"); args.push(output_file); @@ -71,14 +121,13 @@ pub(crate) fn decrypt( )) } -#[tokio::test] -async fn test_encrypt_decrypt_with_ids() -> CliResult<()> { - let ctx = start_default_test_kms_server().await; - let key_id = create_symmetric_key(&ctx.owner_client_conf_path, None, None, None, &[])?; - run_encrypt_decrypt_test(&ctx.owner_client_conf_path, &key_id) -} - -pub(crate) fn run_encrypt_decrypt_test(cli_conf_path: &str, key_id: &str) -> CliResult<()> { +pub(crate) fn run_encrypt_decrypt_test( + cli_conf_path: &str, + key_id: &str, + data_encryption_algorithm: DataEncryptionAlgorithm, + key_encryption_algorithm: Option, + encryption_overhead: u64, +) -> CliResult<()> { // create a temp dir let tmp_dir = TempDir::new()?; let tmp_path = tmp_dir.path(); @@ -99,17 +148,28 @@ pub(crate) fn run_encrypt_decrypt_test(cli_conf_path: &str, key_id: &str) -> Cli cli_conf_path, input_file.to_str().unwrap(), key_id, + data_encryption_algorithm, + key_encryption_algorithm, Some(output_file.to_str().unwrap()), - Some("myid"), + Some(&hex::encode(b"myid")), )?; + if encryption_overhead != 0 { + assert_eq!( + fs::metadata(output_file.clone())?.len(), + fs::metadata(input_file.clone())?.len() + encryption_overhead + ); + } + // the user key should be able to decrypt the file decrypt( cli_conf_path, output_file.to_str().unwrap(), key_id, + data_encryption_algorithm, + key_encryption_algorithm, Some(recovered_file.to_str().unwrap()), - Some("myid"), + Some(&hex::encode(b"myid")), )?; if !recovered_file.exists() { return Err(CliError::Default(format!( @@ -131,6 +191,85 @@ pub(crate) fn run_encrypt_decrypt_test(cli_conf_path: &str, key_id: &str) -> Cli Ok(()) } +#[tokio::test] +async fn test_aes_gcm_server_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let dek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(256), + None, + Some("aes"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &dek, + DataEncryptionAlgorithm::AesGcm, + None, + 12 /* nonce */ + 16, /* tag */ + ) +} + +#[tokio::test] +async fn test_aes_xts_server_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let dek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(512), + None, + Some("aes"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &dek, + DataEncryptionAlgorithm::AesXts, + None, + 16, /* tweak */ + ) +} + +#[cfg(not(feature = "fips"))] +#[tokio::test] +async fn test_aes_gcm_siv_server_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let dek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(256), + None, + Some("aes"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &dek, + DataEncryptionAlgorithm::AesGcmSiv, + None, + 12 /* nonce */ + 16, /* ag */ + ) +} + +#[cfg(not(feature = "fips"))] +#[tokio::test] +async fn test_chacha20_poly1305_server_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let dek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(256), + None, + Some("chacha20"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &dek, + DataEncryptionAlgorithm::Chacha20Poly1305, + None, + 12 /* nonce */ + 16, /* ag */ + ) +} + +#[cfg(not(feature = "fips"))] #[tokio::test] async fn test_encrypt_decrypt_with_tags() -> CliResult<()> { // create a temp dir @@ -157,8 +296,10 @@ async fn test_encrypt_decrypt_with_tags() -> CliResult<()> { &ctx.owner_client_conf_path, input_file.to_str().unwrap(), "[\"tag_sym\"]", + DataEncryptionAlgorithm::Chacha20Poly1305, + None, Some(output_file.to_str().unwrap()), - Some("myid"), + Some(&hex::encode(b"myid")), )?; // the user key should be able to decrypt the file @@ -166,8 +307,10 @@ async fn test_encrypt_decrypt_with_tags() -> CliResult<()> { &ctx.owner_client_conf_path, output_file.to_str().unwrap(), "[\"tag_sym\"]", + DataEncryptionAlgorithm::Chacha20Poly1305, + None, Some(recovered_file.to_str().unwrap()), - Some("myid"), + Some(&hex::encode(b"myid")), )?; if !recovered_file.exists() { return Err(CliError::Default(format!( @@ -188,3 +331,88 @@ async fn test_encrypt_decrypt_with_tags() -> CliResult<()> { Ok(()) } + +#[tokio::test] +async fn test_aes_gcm_aes_gcm_client_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let kek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(256), + None, + Some("aes"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &kek, + DataEncryptionAlgorithm::AesGcm, + Some(KeyEncryptionAlgorithm::AesGcm), + 12 + 32 + 16 /* encapsulation size */ + + 1 /* encapsulation len leb128 */ + + 12 /* nonce */ + 16, /* ag */ + ) +} + +#[tokio::test] +async fn test_aes_gcm_aes_xts_client_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let kek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(256), + None, + Some("aes"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &kek, + DataEncryptionAlgorithm::AesXts, + Some(KeyEncryptionAlgorithm::AesGcm), + 12 + 64 + 16 /* encapsulation size */ + + 1 /* encapsulation len leb128 */ + + 16, /* tweak */ + ) +} + +#[cfg(not(feature = "fips"))] +#[tokio::test] +async fn test_aes_gcm_chacha20_client_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let kek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(256), + None, + Some("aes"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &kek, + DataEncryptionAlgorithm::Chacha20Poly1305, + Some(KeyEncryptionAlgorithm::AesGcm), + 12 + 32 + 16 /* encapsulation size */ + + 1 /* encapsulation len leb128 */ + + 12 /* nonce */ + 16, /* ag */ + ) +} + +#[tokio::test] +async fn test_rfc5649_aes_gcm_client_side() -> CliResult<()> { + let ctx = start_default_test_kms_server().await; + let kek = create_symmetric_key( + &ctx.owner_client_conf_path, + Some(256), + None, + Some("aes"), + &[], + )?; + run_encrypt_decrypt_test( + &ctx.owner_client_conf_path, + &kek, + DataEncryptionAlgorithm::AesGcm, + Some(KeyEncryptionAlgorithm::RFC5649), + 8 + 32 /* encapsulation size */ + + 1 /* encapsulation len leb128 */ + + 12 /* nonce */ + 16, /* tag */ + ) +} diff --git a/crate/kmip/Cargo.toml b/crate/kmip/Cargo.toml index e7445cc8..a4394ec0 100644 --- a/crate/kmip/Cargo.toml +++ b/crate/kmip/Cargo.toml @@ -18,6 +18,7 @@ pyo3 = ["dep:pyo3"] fips = [] [dependencies] +aes-gcm-siv = "0.11.1" argon2 = "0.5" base64 = { workspace = true } bitflags = "2.6" diff --git a/crate/kmip/src/crypto/elliptic_curves/ecies/standard_curves.rs b/crate/kmip/src/crypto/elliptic_curves/ecies/standard_curves.rs index 6c830c6e..3d9f506f 100644 --- a/crate/kmip/src/crypto/elliptic_curves/ecies/standard_curves.rs +++ b/crate/kmip/src/crypto/elliptic_curves/ecies/standard_curves.rs @@ -9,7 +9,7 @@ use openssl::{ use zeroize::Zeroizing; use crate::{ - crypto::symmetric::aead::{aead_decrypt, aead_encrypt, AeadCipher}, + crypto::symmetric::symmetric_ciphers::{decrypt, encrypt, SymCipher}, error::{result::KmipResultHelper, KmipError}, kmip_bail, }; @@ -91,7 +91,7 @@ pub(crate) fn ecies_encrypt(pubkey: &PKey, plaintext: &[u8]) -> Result Result<(AeadCipher, MessageDigest), KmipError> { +fn aead_and_digest(curve: &EcGroupRef) -> Result<(SymCipher, MessageDigest), KmipError> { let (aead, md) = match curve.curve_name().context("Unsupported curve")? { - Nid::SECP384R1 | Nid::SECP521R1 => (AeadCipher::Aes256Gcm, MessageDigest::shake_256()), + Nid::SECP384R1 | Nid::SECP521R1 => (SymCipher::Aes256Gcm, MessageDigest::shake_256()), Nid::X9_62_PRIME256V1 | Nid::SECP224R1 | Nid::X9_62_PRIME192V1 => { - (AeadCipher::Aes128Gcm, MessageDigest::shake_128()) + (SymCipher::Aes128Gcm, MessageDigest::shake_128()) } other => kmip_bail!("Unsupported curve: {:?}", other), }; diff --git a/crate/kmip/src/crypto/generic/kmip_requests.rs b/crate/kmip/src/crypto/generic/kmip_requests.rs index 370d6bf9..80b48020 100644 --- a/crate/kmip/src/crypto/generic/kmip_requests.rs +++ b/crate/kmip/src/crypto/generic/kmip_requests.rs @@ -78,6 +78,7 @@ pub fn build_encryption_request( encryption_policy: Option, plaintext: Vec, header_metadata: Option>, + nonce: Option>, authentication_data: Option>, cryptographic_parameters: Option, ) -> Result { @@ -99,7 +100,7 @@ pub fn build_encryption_request( )), cryptographic_parameters, data: Some(data_to_encrypt), - iv_counter_nonce: None, + iv_counter_nonce: nonce, correlation_value: None, init_indicator: None, final_indicator: None, diff --git a/crate/kmip/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs b/crate/kmip/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs index 979f86d1..8aed140b 100644 --- a/crate/kmip/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs +++ b/crate/kmip/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs @@ -9,16 +9,16 @@ use super::FIPS_MIN_RSA_MODULUS_LENGTH; use crate::{ crypto::{ rsa::ckm_rsa_pkcs_oaep::{ckm_rsa_pkcs_oaep_key_unwrap, ckm_rsa_pkcs_oaep_key_wrap}, - symmetric::{ - rfc5649::{rfc5649_unwrap, rfc5649_wrap}, - AES_KWP_KEY_LENGTH, - }, + symmetric::rfc5649::{rfc5649_unwrap, rfc5649_wrap}, }, error::KmipError, kmip::kmip_types::HashingAlgorithm, kmip_bail, }; +/// AES KEY WRAP with padding key length in bytes. +pub const AES_KWP_KEY_LENGTH: usize = 0x20; + /// Asymmetrically wrap keys referring to PKCS#11 `CKM_RSA_AES_KEY_WRAP` available at /// #_Toc408226908 /// diff --git a/crate/kmip/src/crypto/symmetric/aead.rs b/crate/kmip/src/crypto/symmetric/aead.rs deleted file mode 100644 index 8111b9f3..00000000 --- a/crate/kmip/src/crypto/symmetric/aead.rs +++ /dev/null @@ -1,277 +0,0 @@ -use openssl::{ - rand::rand_bytes, - symm::{decrypt_aead, encrypt_aead, Cipher}, -}; -use zeroize::Zeroizing; - -use super::{ - AES_128_GCM_IV_LENGTH, AES_128_GCM_KEY_LENGTH, AES_128_GCM_MAC_LENGTH, AES_256_GCM_IV_LENGTH, - AES_256_GCM_KEY_LENGTH, AES_256_GCM_MAC_LENGTH, -}; -use crate::{ - error::KmipError, - kmip::kmip_types::{BlockCipherMode, CryptographicAlgorithm}, - kmip_bail, -}; - -#[cfg(not(feature = "fips"))] -/// Chacha20-Poly1305 key length in bytes. -pub const CHACHA20_POLY1305_KEY_LENGTH: usize = 32; -#[cfg(not(feature = "fips"))] -/// Chacha20-Poly1305 iv length in bytes. -pub const CHACHA20_POLY1305_IV_LENGTH: usize = 12; -#[cfg(not(feature = "fips"))] -/// Chacha20-Poly1305 tag/mac length in bytes. -pub const CHACHA20_POLY1305_MAC_LENGTH: usize = 16; - -/// The supported AEAD ciphers. -#[derive(Debug, Clone, Copy)] -pub enum AeadCipher { - Aes256Gcm, - Aes128Gcm, - #[cfg(not(feature = "fips"))] - Chacha20Poly1305, -} - -impl AeadCipher { - /// Convert to the corresponding OpenSSL cipher. - fn to_cipher(self) -> Cipher { - match self { - Self::Aes128Gcm => Cipher::aes_128_gcm(), - Self::Aes256Gcm => Cipher::aes_256_gcm(), - #[cfg(not(feature = "fips"))] - Self::Chacha20Poly1305 => Cipher::chacha20_poly1305(), - } - } - - /// Get the tag size in bytes. - #[must_use] - pub const fn tag_size(&self) -> usize { - match self { - Self::Aes128Gcm => AES_128_GCM_MAC_LENGTH, - Self::Aes256Gcm => AES_256_GCM_MAC_LENGTH, - #[cfg(not(feature = "fips"))] - Self::Chacha20Poly1305 => CHACHA20_POLY1305_MAC_LENGTH, - } - } - - /// Get the nonce size in bytes. - #[must_use] - pub const fn nonce_size(&self) -> usize { - match self { - Self::Aes128Gcm => AES_128_GCM_IV_LENGTH, - Self::Aes256Gcm => AES_256_GCM_IV_LENGTH, - #[cfg(not(feature = "fips"))] - Self::Chacha20Poly1305 => CHACHA20_POLY1305_IV_LENGTH, - } - } - - /// Get the key size in bytes. - #[must_use] - pub const fn key_size(&self) -> usize { - match self { - Self::Aes128Gcm => AES_128_GCM_KEY_LENGTH, - Self::Aes256Gcm => AES_256_GCM_KEY_LENGTH, - #[cfg(not(feature = "fips"))] - Self::Chacha20Poly1305 => CHACHA20_POLY1305_KEY_LENGTH, - } - } - - pub fn from_algorithm_and_key_size( - algorithm: CryptographicAlgorithm, - block_cipher_mode: Option, - key_size: usize, - ) -> Result { - match algorithm { - CryptographicAlgorithm::AES => { - if let Some(mode) = block_cipher_mode { - if BlockCipherMode::GCM != mode && BlockCipherMode::AEAD != mode { - kmip_bail!(KmipError::NotSupported(format!( - "AES is only supported with GCM mode. Got: {mode:?}" - ))); - } - } - match key_size { - AES_128_GCM_KEY_LENGTH => Ok(Self::Aes128Gcm), - AES_256_GCM_KEY_LENGTH => Ok(Self::Aes256Gcm), - _ => kmip_bail!(KmipError::NotSupported( - "AES key must be 16 or 32 bytes long".to_owned() - )), - } - } - #[cfg(not(feature = "fips"))] - CryptographicAlgorithm::ChaCha20 => { - if block_cipher_mode.is_some() { - kmip_bail!(KmipError::NotSupported( - "ChaCha20 is only supported with Poly1305. Do not specify the Block \ - Cipher Mode" - .to_owned() - )); - } - match key_size { - 32 => Ok(Self::Chacha20Poly1305), - _ => kmip_bail!(KmipError::NotSupported( - "ChaCha20 key must be 32 bytes long".to_owned() - )), - } - } - other => kmip_bail!(KmipError::NotSupported(format!( - "unsupported cryptographic algorithm: {other} for a symmetric key" - ))), - } - } -} - -/// Generate a random nonce for the given AEAD cipher. -pub fn random_nonce(aead_cipher: AeadCipher) -> Result, KmipError> { - let mut nonce = vec![0; aead_cipher.nonce_size()]; - rand_bytes(&mut nonce)?; - Ok(nonce) -} - -/// Generate a random key for the given AEAD cipher. -pub fn random_key(aead_cipher: AeadCipher) -> Result>, KmipError> { - let mut key = Zeroizing::from(vec![0; aead_cipher.key_size()]); - rand_bytes(&mut key)?; - Ok(key) -} - -/// Encrypt the plaintext using the given AEAD cipher, key, nonce and additional -/// authenticated data. -/// Return the ciphertext and the tag. -pub fn aead_encrypt( - aead_cipher: AeadCipher, - key: &[u8], - nonce: &[u8], - aad: &[u8], - plaintext: &[u8], -) -> Result<(Vec, Vec), KmipError> { - // Create buffer for the tag - let mut tag = vec![0; aead_cipher.tag_size()]; - // Encryption. - let ciphertext = encrypt_aead( - aead_cipher.to_cipher(), - key, - Some(nonce), - aad, - plaintext, - tag.as_mut(), - )?; - Ok((ciphertext, tag)) -} - -/// Decrypt the ciphertext using the given AEAD cipher, key, nonce and -/// additional authenticated data. -/// Return the plaintext. -pub fn aead_decrypt( - aead_cipher: AeadCipher, - key: &[u8], - nonce: &[u8], - aad: &[u8], - ciphertext: &[u8], - tag: &[u8], -) -> Result>, KmipError> { - let plaintext = Zeroizing::from(decrypt_aead( - aead_cipher.to_cipher(), - key, - Some(nonce), - aad, - ciphertext, - tag, - )?); - Ok(plaintext) -} - -#[allow(clippy::unwrap_used)] -#[cfg(test)] -mod tests { - #[cfg(feature = "fips")] - use openssl::provider::Provider; - use openssl::rand::rand_bytes; - - use crate::crypto::symmetric::aead::{ - aead_decrypt, aead_encrypt, random_key, random_nonce, AeadCipher, - }; - - #[test] - fn test_encrypt_decrypt_aes_gcm_128() { - #[cfg(feature = "fips")] - // Load FIPS provider module from OpenSSL. - Provider::load(None, "fips").unwrap(); - - let mut message = vec![0_u8; 42]; - rand_bytes(&mut message).unwrap(); - - let key = random_key(AeadCipher::Aes128Gcm).unwrap(); - - let nonce = random_nonce(AeadCipher::Aes128Gcm).unwrap(); - - let mut aad = vec![0_u8; 24]; - rand_bytes(&mut aad).unwrap(); - - let (ciphertext, tag) = - aead_encrypt(AeadCipher::Aes128Gcm, &key, &nonce, &aad, &message).unwrap(); - - let decrypted_data = - aead_decrypt(AeadCipher::Aes128Gcm, &key, &nonce, &aad, &ciphertext, &tag).unwrap(); - - // `to_vec()` conversion because of Zeroizing<>. - assert_eq!(decrypted_data.to_vec(), message); - } - - #[test] - fn test_encrypt_decrypt_aes_gcm_256() { - #[cfg(feature = "fips")] - // Load FIPS provider module from OpenSSL. - Provider::load(None, "fips").unwrap(); - - let mut message = vec![0_u8; 42]; - rand_bytes(&mut message).unwrap(); - - let key = random_key(AeadCipher::Aes256Gcm).unwrap(); - - let nonce = random_nonce(AeadCipher::Aes256Gcm).unwrap(); - - let mut aad = vec![0_u8; 24]; - rand_bytes(&mut aad).unwrap(); - - let (ciphertext, tag) = - aead_encrypt(AeadCipher::Aes256Gcm, &key, &nonce, &aad, &message).unwrap(); - - let decrypted_data = - aead_decrypt(AeadCipher::Aes256Gcm, &key, &nonce, &aad, &ciphertext, &tag).unwrap(); - - // `to_vec()` conversion because of Zeroizing<>. - assert_eq!(decrypted_data.to_vec(), message); - } - - #[cfg(not(feature = "fips"))] - #[test] - fn test_encrypt_decrypt_chacha20_poly1305() { - let mut message = vec![0_u8; 42]; - rand_bytes(&mut message).unwrap(); - - let key = random_key(AeadCipher::Chacha20Poly1305).unwrap(); - - let nonce = random_nonce(AeadCipher::Chacha20Poly1305).unwrap(); - - let mut aad = vec![0_u8; 24]; - rand_bytes(&mut aad).unwrap(); - - let (ciphertext, tag) = - aead_encrypt(AeadCipher::Chacha20Poly1305, &key, &nonce, &aad, &message).unwrap(); - - let decrypted_data = aead_decrypt( - AeadCipher::Chacha20Poly1305, - key.as_ref(), - &nonce, - &aad, - &ciphertext, - &tag, - ) - .unwrap(); - - // `to_vec()` conversion because of Zeroizing<>. - assert_eq!(decrypted_data.to_vec(), message); - } -} diff --git a/crate/kmip/src/crypto/symmetric/aes_256_gcm.rs b/crate/kmip/src/crypto/symmetric/aes_256_gcm.rs deleted file mode 100644 index 93035ef7..00000000 --- a/crate/kmip/src/crypto/symmetric/aes_256_gcm.rs +++ /dev/null @@ -1,189 +0,0 @@ -use openssl::{ - rand::rand_bytes, - symm::{decrypt_aead, encrypt_aead, Cipher}, -}; -use zeroize::Zeroizing; - -use super::{AES_256_GCM_IV_LENGTH, AES_256_GCM_KEY_LENGTH, AES_256_GCM_MAC_LENGTH}; -use crate::{ - crypto::{secret::Secret, DecryptionSystem, EncryptionSystem}, - error::KmipError, - kmip::{ - kmip_objects::Object, - kmip_operations::{Decrypt, DecryptResponse, Encrypt, EncryptResponse}, - kmip_types::UniqueIdentifier, - }, - kmip_bail, -}; - -pub struct AesGcmSystem { - key_uid: String, - symmetric_key: Secret, -} - -impl AesGcmSystem { - pub fn instantiate(uid: &str, symmetric_key: &Object) -> Result { - let Object::SymmetricKey { key_block } = symmetric_key else { - return Err(KmipError::NotSupported( - "Expected a KMIP Symmetric Key".to_owned(), - )) - }; - let mut symmetric_key: [u8; AES_256_GCM_KEY_LENGTH] = - key_block.key_bytes()?.to_vec().try_into()?; - - if symmetric_key.len() != AES_256_GCM_KEY_LENGTH { - kmip_bail!( - "Expected a KMIP Symmetric Key of length {}", - AES_256_GCM_KEY_LENGTH - ) - } - - Ok(Self { - key_uid: uid.into(), - symmetric_key: Secret::from_unprotected_bytes(&mut symmetric_key), - }) - } -} - -impl EncryptionSystem for AesGcmSystem { - fn encrypt(&self, request: &Encrypt) -> Result { - let uid = request - .authenticated_encryption_additional_data - .clone() - .unwrap_or_default(); - - let correlation_value = request.correlation_value.clone().or_else(|| { - if uid.is_empty() { - None - } else { - Some(uid.clone()) - } - }); - - let Some(plaintext) = &request.data else { - return Ok(EncryptResponse { - unique_identifier: UniqueIdentifier::TextString(self.key_uid.clone()), - data: None, - iv_counter_nonce: None, - correlation_value, - authenticated_encryption_tag: None, - }) - }; - - // Supplied Nonce or new one. - let nonce: [u8; AES_256_GCM_IV_LENGTH] = if let Some(iv) = request.iv_counter_nonce.as_ref() - { - iv.as_slice().try_into()? - } else { - let mut iv = [0; AES_256_GCM_IV_LENGTH]; - rand_bytes(&mut iv)?; - iv - }; - - // Additional data. - let mut aad = uid; - // For some unknown reason the block number is appended in little-endian mode - // see `Block` in crypto_base. - if let Some(cp) = &request.cryptographic_parameters { - if let Some(block_number) = cp.initial_counter_value { - aad.extend(usize::try_from(block_number)?.to_le_bytes()); - } - } - - // Create buffer for GCM tag (MAC). - let mut tag = vec![0; AES_256_GCM_MAC_LENGTH]; - - if self.symmetric_key.len() != AES_256_GCM_KEY_LENGTH { - kmip_bail!( - "Encrypt: Expected a KMIP Symmetric Key of length {}", - AES_256_GCM_KEY_LENGTH - ) - } - - // Encryption. - let ciphertext = encrypt_aead( - Cipher::aes_256_gcm(), - &self.symmetric_key, - Some(&nonce), - &aad, - plaintext, - tag.as_mut(), - )?; - - Ok(EncryptResponse { - unique_identifier: UniqueIdentifier::TextString(self.key_uid.clone()), - data: Some(ciphertext), - iv_counter_nonce: Some(nonce.to_vec()), - correlation_value, - authenticated_encryption_tag: Some(tag), - }) - } -} - -impl DecryptionSystem for AesGcmSystem { - fn decrypt(&self, request: &Decrypt) -> Result { - let uid = request - .authenticated_encryption_additional_data - .clone() - .unwrap_or_default(); - - let correlation_value = if uid.is_empty() { - None - } else { - Some(uid.clone()) - }; - - let Some(ciphertext) = &request.data else { - return Ok(DecryptResponse { - unique_identifier: UniqueIdentifier::TextString(self.key_uid.clone()), - data: None, - correlation_value, - }) - }; - - // Recover tag. Ensure it is of correct size. - let tag: [u8; AES_256_GCM_MAC_LENGTH] = request - .authenticated_encryption_tag - .clone() - .unwrap_or_else(|| vec![0_u8; AES_256_GCM_MAC_LENGTH]) - .try_into()?; - - // Recover nonce. - let request_nonce_bytes = request.iv_counter_nonce.as_ref().ok_or_else(|| { - KmipError::NotSupported("The nonce is mandatory for AES GCM.".to_owned()) - })?; - let nonce: [u8; AES_256_GCM_IV_LENGTH] = request_nonce_bytes.as_slice().try_into()?; - - // Additional data. - let mut aad = uid; - // For some unknown reason the block number is appended in little-endian mode - // see `Block` in crypto_base. - if let Some(cp) = &request.cryptographic_parameters { - if let Some(block_number) = cp.initial_counter_value { - aad.extend(usize::try_from(block_number)?.to_le_bytes()); - } - } - - if self.symmetric_key.len() != AES_256_GCM_KEY_LENGTH { - kmip_bail!( - "Decrypt: Expected a KMIP Symmetric Key of length {}", - AES_256_GCM_KEY_LENGTH - ) - } - - let plaintext = Zeroizing::from(decrypt_aead( - Cipher::aes_256_gcm(), - &self.symmetric_key, - Some(&nonce), - &aad, - ciphertext, - &tag, - )?); - - Ok(DecryptResponse { - unique_identifier: UniqueIdentifier::TextString(self.key_uid.clone()), - data: Some(plaintext), - correlation_value, - }) - } -} diff --git a/crate/kmip/src/crypto/symmetric/aes_gcm_siv_not_openssl.rs b/crate/kmip/src/crypto/symmetric/aes_gcm_siv_not_openssl.rs new file mode 100644 index 00000000..78bf4989 --- /dev/null +++ b/crate/kmip/src/crypto/symmetric/aes_gcm_siv_not_openssl.rs @@ -0,0 +1,95 @@ +//! AES GCM SIV implementation using aes-gcm-siv crate. +//! Openssl does implement AES GCM SIV, but it is not available in the openssl crate. + +use aes_gcm_siv::{AeadInPlace, Aes128GcmSiv, Aes256GcmSiv, Key, KeyInit, Nonce, Tag}; +use zeroize::Zeroizing; + +use crate::{ + crypto::symmetric::symmetric_ciphers::{ + AES_128_GCM_SIV_KEY_LENGTH, AES_256_GCM_SIV_KEY_LENGTH, + }, + KmipError, +}; + +/// Encrypt data using AES GCM SIV. +/// # Arguments +/// * `key` - The key to use for encryption. +/// * `nonce` - The nonce to use for encryption. +/// * `aad` - The additional authenticated data. +/// * `plaintext` - The data to encrypt. +/// # Returns +/// * The encrypted data and the tag. +/// # Errors +/// * If the key is not the correct size. +/// * If there is an error encrypting the data. +pub(crate) fn encrypt( + key: &[u8], + nonce: &[u8], + aad: &[u8], + plaintext: &[u8], +) -> Result<(Vec, Vec), KmipError> { + let nonce = Nonce::from_slice(nonce); + let mut buffer = plaintext.to_vec(); + let tag = if key.len() == AES_128_GCM_SIV_KEY_LENGTH { + Aes128GcmSiv::new(Key::::from_slice(key)) + .encrypt_in_place_detached(nonce, aad, &mut buffer) + .map_err(|e| { + KmipError::Default(format!("Error encrypting data with AES GCM SIV: {e}")) + })? + } else if key.len() == AES_256_GCM_SIV_KEY_LENGTH { + Aes256GcmSiv::new(Key::::from_slice(key)) + .encrypt_in_place_detached(nonce, aad, &mut buffer) + .map_err(|e| { + KmipError::Default(format!("Error encrypting data with AES GCM SIV: {e}")) + })? + } else { + return Err(KmipError::InvalidSize(format!( + "Invalid key size: {} for AES GCM SIV", + key.len() + ))); + }; + Ok((buffer.clone(), tag.to_vec())) +} + +/// Decrypt data using AES GCM SIV. +/// # Arguments +/// * `key` - The key to use for decryption. +/// * `nonce` - The nonce to use for decryption. +/// * `aad` - The additional authenticated data. +/// * `ciphertext` - The data to decrypt. +/// * `tag` - The tag to use for decryption. +/// # Returns +/// * The decrypted data. +/// # Errors +/// * If the key is not the correct size. +/// * If there is an error decrypting the data. +pub(crate) fn decrypt( + key: &[u8], + nonce: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], +) -> Result>, KmipError> { + let nonce = Nonce::from_slice(nonce); + let tag = Tag::from_slice(tag); + let mut buffer = ciphertext.to_vec(); + if key.len() == AES_128_GCM_SIV_KEY_LENGTH { + Aes128GcmSiv::new(Key::::from_slice(key)) + .decrypt_in_place_detached(nonce, aad, &mut buffer, tag) + .map_err(|e| { + KmipError::Default(format!("Error decrypting data with AES GCM SIV: {e}")) + })?; + } else if key.len() == AES_256_GCM_SIV_KEY_LENGTH { + Aes256GcmSiv::new(Key::::from_slice(key)) + .decrypt_in_place_detached(nonce, aad, &mut buffer, tag) + .map_err(|e| { + KmipError::Default(format!("Error decrypting data with AES GCM SIV: {e}")) + })?; + } else { + return Err(KmipError::InvalidSize(format!( + "Invalid key size: {} for AES GCM SIV", + key.len() + ))); + } + Ok(Zeroizing::new(buffer.clone())) +} diff --git a/crate/kmip/src/crypto/symmetric/mod.rs b/crate/kmip/src/crypto/symmetric/mod.rs index ab24c975..0f184892 100644 --- a/crate/kmip/src/crypto/symmetric/mod.rs +++ b/crate/kmip/src/crypto/symmetric/mod.rs @@ -1,30 +1,11 @@ mod symmetric_key; pub use symmetric_key::{create_symmetric_key_kmip_object, symmetric_key_create_request}; -mod aes_256_gcm; -pub use aes_256_gcm::AesGcmSystem; - -/// AES 128 GCM key length in bytes. -pub const AES_128_GCM_KEY_LENGTH: usize = 16; -/// AES 128 GCM nonce length in bytes. -pub const AES_128_GCM_IV_LENGTH: usize = 12; -/// AES 128 GCM tag/mac length in bytes. -pub const AES_128_GCM_MAC_LENGTH: usize = 16; - -/// AES 256 GCM key length in bytes. -pub const AES_256_GCM_KEY_LENGTH: usize = 32; -/// AES 256 GCM nonce length in bytes. -pub const AES_256_GCM_IV_LENGTH: usize = 12; -/// AES 256 GCM tag/mac length in bytes. -pub const AES_256_GCM_MAC_LENGTH: usize = 16; - -/// AES KEY WRAP with padding key length in bytes. -pub const AES_KWP_KEY_LENGTH: usize = 0x20; - -pub mod aead; +pub mod symmetric_ciphers; pub mod rfc5649; +#[cfg(not(feature = "fips"))] +mod aes_gcm_siv_not_openssl; #[cfg(test)] -#[allow(clippy::unwrap_used, clippy::panic_in_result_fn)] mod tests; diff --git a/crate/kmip/src/crypto/symmetric/rfc5649.rs b/crate/kmip/src/crypto/symmetric/rfc5649.rs index aaa7be87..bddd5248 100644 --- a/crate/kmip/src/crypto/symmetric/rfc5649.rs +++ b/crate/kmip/src/crypto/symmetric/rfc5649.rs @@ -449,4 +449,28 @@ mod tests { rfc5649_unwrap(&wrapped_key, kek).unwrap_err(); } + + #[test] + fn test_sizes() { + let dek_16 = [1_u8; 16]; + let kek_16 = [2_u8; 16]; + let dek_32 = [1_u8; 32]; + let kek_32 = [2_u8; 32]; + assert_eq!( + rfc5649_wrap(&dek_16, &kek_16).unwrap().len(), + dek_16.len() + 8 + ); + assert_eq!( + rfc5649_wrap(&dek_16, &kek_32).unwrap().len(), + dek_16.len() + 8 + ); + assert_eq!( + rfc5649_wrap(&dek_32, &kek_16).unwrap().len(), + dek_32.len() + 8 + ); + assert_eq!( + rfc5649_wrap(&dek_32, &kek_32).unwrap().len(), + dek_32.len() + 8 + ); + } } diff --git a/crate/kmip/src/crypto/symmetric/symmetric_ciphers.rs b/crate/kmip/src/crypto/symmetric/symmetric_ciphers.rs new file mode 100644 index 00000000..5b9ba495 --- /dev/null +++ b/crate/kmip/src/crypto/symmetric/symmetric_ciphers.rs @@ -0,0 +1,533 @@ +use std::cmp::PartialEq; + +use openssl::{ + rand::rand_bytes, + symm::{ + decrypt as openssl_decrypt, decrypt_aead as openssl_decrypt_aead, + encrypt as openssl_encrypt, encrypt_aead as openssl_encrypt_aead, Cipher, Crypter, + Mode as OpenSslMode, + }, +}; +use zeroize::Zeroizing; + +#[cfg(not(feature = "fips"))] +use super::aes_gcm_siv_not_openssl; +use crate::{ + crypto::symmetric::rfc5649::{rfc5649_unwrap, rfc5649_wrap}, + error::KmipError, + kmip::kmip_types::{BlockCipherMode, CryptographicAlgorithm}, + kmip_bail, +}; + +/// AES 128 GCM key length in bytes. +pub const AES_128_GCM_KEY_LENGTH: usize = 16; +/// AES 128 GCM nonce length in bytes. +pub const AES_128_GCM_IV_LENGTH: usize = 12; +/// AES 128 GCM tag/mac length in bytes. +pub const AES_128_GCM_MAC_LENGTH: usize = 16; + +/// AES 256 GCM key length in bytes. +pub const AES_256_GCM_KEY_LENGTH: usize = 32; +/// AES 256 GCM nonce length in bytes. +pub const AES_256_GCM_IV_LENGTH: usize = 12; +/// AES 256 GCM tag/mac length in bytes. +pub const AES_256_GCM_MAC_LENGTH: usize = 16; + +/// AES 128 XTS key length in bytes. +pub const AES_128_XTS_KEY_LENGTH: usize = 32; +/// AES 128 XTS nonce, actually called a tweak, length in bytes. +pub const AES_128_XTS_TWEAK_LENGTH: usize = 16; +/// AES 128 XTS has no authentication. +pub const AES_128_XTS_MAC_LENGTH: usize = 0; +/// AES 256 XTS key length in bytes. +pub const AES_256_XTS_KEY_LENGTH: usize = 64; +/// AES 256 XTS nonce actually called a tweak,length in bytes. +pub const AES_256_XTS_TWEAK_LENGTH: usize = 16; +/// AES 256 XTS has no authentication. +pub const AES_256_XTS_MAC_LENGTH: usize = 0; +/// AES 128 `GCM_SIV` key length in bytes. +#[cfg(not(feature = "fips"))] +pub const AES_128_GCM_SIV_KEY_LENGTH: usize = 16; +/// AES 128 `GCM_SIV` nonce length in bytes. +#[cfg(not(feature = "fips"))] +pub const AES_128_GCM_SIV_IV_LENGTH: usize = 12; +/// AES 128 `GCM_SIV` mac length in bytes. +#[cfg(not(feature = "fips"))] +pub const AES_128_GCM_SIV_MAC_LENGTH: usize = 16; +/// AES 256 `GCM_SIV` key length in bytes. +#[cfg(not(feature = "fips"))] +pub const AES_256_GCM_SIV_KEY_LENGTH: usize = 32; +/// AES 256 `GCM_SIV` nonce length in bytes. +#[cfg(not(feature = "fips"))] +pub const AES_256_GCM_SIV_IV_LENGTH: usize = 12; +/// AES 256 `GCM_SIV` mac length in bytes. +#[cfg(not(feature = "fips"))] +pub const AES_256_GCM_SIV_MAC_LENGTH: usize = 16; + +/// RFC 5649 with a 16-byte KEK. +pub const RFC5649_16_KEY_LENGTH: usize = 16; +// RFC 5649 IV is actually a fixed overhead +pub const RFC5649_16_IV_LENGTH: usize = 0; +/// RFC5649 has no authentication. +pub const RFC5649_16_MAC_LENGTH: usize = 0; +/// RFC 5649 with a 32-byte KEK. +pub const RFC5649_32_KEY_LENGTH: usize = 32; +// RFC 5649 IV is actually a fixed overhead +pub const RFC5649_32_IV_LENGTH: usize = 0; +/// RFC5649 has no authentication. +pub const RFC5649_32_MAC_LENGTH: usize = 0; + +#[cfg(not(feature = "fips"))] +/// Chacha20-Poly1305 key length in bytes. +pub const CHACHA20_POLY1305_KEY_LENGTH: usize = 32; +#[cfg(not(feature = "fips"))] +/// Chacha20-Poly1305 iv length in bytes. +pub const CHACHA20_POLY1305_IV_LENGTH: usize = 12; +#[cfg(not(feature = "fips"))] +/// Chacha20-Poly1305 tag/mac length in bytes. +pub const CHACHA20_POLY1305_MAC_LENGTH: usize = 16; + +/// The mode of operation for the symmetric stream cipher. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Mode { + Encrypt, + Decrypt, +} + +impl From for OpenSslMode { + fn from(mode: Mode) -> Self { + match mode { + Mode::Encrypt => Self::Encrypt, + Mode::Decrypt => Self::Decrypt, + } + } +} + +/// The supported AEAD ciphers. +#[derive(Debug, Clone, Copy)] +pub enum SymCipher { + Aes256Gcm, + Aes128Gcm, + Aes128Xts, + Aes256Xts, + Rfc5649_16, + Rfc5649_32, + #[cfg(not(feature = "fips"))] + Aes128GcmSiv, + #[cfg(not(feature = "fips"))] + Aes256GcmSiv, + #[cfg(not(feature = "fips"))] + Chacha20Poly1305, +} + +impl SymCipher { + /// Convert to the corresponding OpenSSL cipher. + fn to_openssl_cipher(self) -> Result { + match self { + Self::Aes128Gcm => Ok(Cipher::aes_128_gcm()), + Self::Aes256Gcm => Ok(Cipher::aes_256_gcm()), + Self::Aes128Xts => Ok(Cipher::aes_128_xts()), + Self::Aes256Xts => Ok(Cipher::aes_256_xts()), + Self::Rfc5649_16 | Self::Rfc5649_32 => { + kmip_bail!(KmipError::NotSupported( + "RFC5649 is not supported in this version of openssl".to_owned() + )) + } + #[cfg(not(feature = "fips"))] + Self::Chacha20Poly1305 => Ok(Cipher::chacha20_poly1305()), + #[cfg(not(feature = "fips"))] + Self::Aes128GcmSiv | Self::Aes256GcmSiv => { + //TODO: openssl supports AES GCM SIV but the rust openssl crate does not expose it + kmip_bail!(KmipError::NotSupported( + "AES GCM SIV is not supported in this version of openssl".to_owned() + )) + } + } + } + + /// Get the tag size in bytes. + #[must_use] + pub const fn tag_size(&self) -> usize { + match self { + Self::Aes128Gcm => AES_128_GCM_MAC_LENGTH, + Self::Aes256Gcm => AES_256_GCM_MAC_LENGTH, + Self::Aes128Xts => AES_128_XTS_MAC_LENGTH, + Self::Aes256Xts => AES_256_XTS_MAC_LENGTH, + Self::Rfc5649_16 => RFC5649_16_MAC_LENGTH, + Self::Rfc5649_32 => RFC5649_32_MAC_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Chacha20Poly1305 => CHACHA20_POLY1305_MAC_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Aes128GcmSiv => AES_128_GCM_SIV_MAC_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Aes256GcmSiv => AES_256_GCM_SIV_MAC_LENGTH, + } + } + + /// Get the nonce size in bytes. + #[must_use] + pub const fn nonce_size(&self) -> usize { + match self { + Self::Aes128Gcm => AES_128_GCM_IV_LENGTH, + Self::Aes256Gcm => AES_256_GCM_IV_LENGTH, + Self::Aes128Xts => AES_128_XTS_TWEAK_LENGTH, + Self::Aes256Xts => AES_256_XTS_TWEAK_LENGTH, + Self::Rfc5649_16 => RFC5649_16_IV_LENGTH, + Self::Rfc5649_32 => RFC5649_32_IV_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Chacha20Poly1305 => CHACHA20_POLY1305_IV_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Aes128GcmSiv => AES_128_GCM_SIV_IV_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Aes256GcmSiv => AES_256_GCM_SIV_IV_LENGTH, + } + } + + /// Get the key size in bytes. + #[must_use] + pub const fn key_size(&self) -> usize { + match self { + Self::Aes128Gcm => AES_128_GCM_KEY_LENGTH, + Self::Aes256Gcm => AES_256_GCM_KEY_LENGTH, + Self::Aes128Xts => AES_128_XTS_KEY_LENGTH, + Self::Aes256Xts => AES_256_XTS_KEY_LENGTH, + Self::Rfc5649_16 => RFC5649_16_KEY_LENGTH, + Self::Rfc5649_32 => RFC5649_32_KEY_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Chacha20Poly1305 => CHACHA20_POLY1305_KEY_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Aes128GcmSiv => AES_128_GCM_SIV_KEY_LENGTH, + #[cfg(not(feature = "fips"))] + Self::Aes256GcmSiv => AES_256_GCM_SIV_KEY_LENGTH, + } + } + + pub fn from_algorithm_and_key_size( + algorithm: CryptographicAlgorithm, + block_cipher_mode: Option, + key_size: usize, + ) -> Result { + match algorithm { + CryptographicAlgorithm::AES => { + let block_cipher_mode = block_cipher_mode.unwrap_or(BlockCipherMode::GCM); + match block_cipher_mode { + BlockCipherMode::AEAD | BlockCipherMode::GCM => match key_size { + AES_128_GCM_KEY_LENGTH => Ok(Self::Aes128Gcm), + AES_256_GCM_KEY_LENGTH => Ok(Self::Aes256Gcm), + _ => kmip_bail!(KmipError::NotSupported( + "AES key must be 16 or 32 bytes long for AES GCM ".to_owned() + )), + }, + BlockCipherMode::XTS => match key_size { + AES_128_XTS_KEY_LENGTH => Ok(Self::Aes128Xts), + AES_256_XTS_KEY_LENGTH => Ok(Self::Aes256Xts), + _ => kmip_bail!(KmipError::NotSupported( + "AES key must be 32 or 64 bytes long for AES XTS".to_owned() + )), + }, + #[cfg(not(feature = "fips"))] + BlockCipherMode::GCMSIV => match key_size { + AES_128_GCM_SIV_KEY_LENGTH => Ok(Self::Aes128GcmSiv), + AES_256_GCM_SIV_KEY_LENGTH => Ok(Self::Aes256GcmSiv), + _ => kmip_bail!(KmipError::NotSupported( + "AES key must be 16 or 32 bytes long for AES GCM SIV".to_owned() + )), + }, + BlockCipherMode::NISTKeyWrap => match key_size { + RFC5649_16_KEY_LENGTH => Ok(Self::Rfc5649_16), + RFC5649_32_KEY_LENGTH => Ok(Self::Rfc5649_32), + _ => kmip_bail!(KmipError::NotSupported( + "RFC5649 key must be 16 or 32 bytes long".to_owned() + )), + }, + mode => { + kmip_bail!(KmipError::NotSupported(format!( + "AES is not supported with mode: {mode:?}" + ))); + } + } + } + #[cfg(not(feature = "fips"))] + CryptographicAlgorithm::ChaCha20 | CryptographicAlgorithm::ChaCha20Poly1305 => { + match key_size { + 32 => Ok(Self::Chacha20Poly1305), + _ => kmip_bail!(KmipError::NotSupported( + "ChaCha20 key must be 32 bytes long".to_owned() + )), + } + } + other => kmip_bail!(KmipError::NotSupported(format!( + "unsupported cryptographic algorithm: {other} for a symmetric key" + ))), + } + } + + pub fn stream_cipher( + &self, + mode: Mode, + key: &[u8], + nonce: &[u8], + aad: &[u8], + ) -> Result { + StreamCipher::new(*self, mode, key, nonce, aad) + } +} + +/// Generate a random nonce for the given symmetric cipher. +pub fn random_nonce(sym_cipher: SymCipher) -> Result, KmipError> { + let mut nonce = vec![0; sym_cipher.nonce_size()]; + rand_bytes(&mut nonce)?; + Ok(nonce) +} + +/// Generate a random key for the given symmetric cipher. +pub fn random_key(sym_cipher: SymCipher) -> Result>, KmipError> { + let mut key = Zeroizing::from(vec![0; sym_cipher.key_size()]); + rand_bytes(&mut key)?; + Ok(key) +} + +/// Encrypt the plaintext using the given symmetric cipher. +/// Return the ciphertext and the tag. +/// For XTS mode, the nonce is the tweak, the aad is ignored, and the tag is empty. +pub fn encrypt( + sym_cipher: SymCipher, + key: &[u8], + nonce: &[u8], + aad: &[u8], + plaintext: &[u8], +) -> Result<(Vec, Vec), KmipError> { + match sym_cipher { + SymCipher::Aes128Xts | SymCipher::Aes256Xts => { + // XTS mode does not require a tag. + let ciphertext = + openssl_encrypt(sym_cipher.to_openssl_cipher()?, key, Some(nonce), plaintext)?; + Ok((ciphertext, vec![])) + } + #[cfg(not(feature = "fips"))] + SymCipher::Aes128GcmSiv | SymCipher::Aes256GcmSiv => { + aes_gcm_siv_not_openssl::encrypt(key, nonce, aad, plaintext) + } + SymCipher::Rfc5649_16 | SymCipher::Rfc5649_32 => { + Ok((rfc5649_wrap(plaintext, key)?, vec![])) + } + _ => { + // Create buffer for the tag + let mut tag = vec![0; sym_cipher.tag_size()]; + // Encryption. + let ciphertext = openssl_encrypt_aead( + sym_cipher.to_openssl_cipher()?, + key, + Some(nonce), + aad, + plaintext, + tag.as_mut(), + )?; + Ok((ciphertext, tag)) + } + } +} + +/// Decrypt the ciphertext using the given symmetric cipher. +/// Return the decrypted plaintext. +/// The tag is required for AEAD ciphers (`AES GCN`, `ChaCha20 Poly1305`, ...). +/// For XTS mode, the nonce is the tweak, the aad and the tag are ignored. +pub fn decrypt( + sym_cipher: SymCipher, + key: &[u8], + nonce: &[u8], + aad: &[u8], + ciphertext: &[u8], + tag: &[u8], +) -> Result>, KmipError> { + Ok(match sym_cipher { + SymCipher::Aes128Xts | SymCipher::Aes256Xts => { + // XTS mode does not require a tag. + Zeroizing::from(openssl_decrypt( + sym_cipher.to_openssl_cipher()?, + key, + Some(nonce), + ciphertext, + )?) + } + #[cfg(not(feature = "fips"))] + SymCipher::Aes128GcmSiv | SymCipher::Aes256GcmSiv => { + aes_gcm_siv_not_openssl::decrypt(key, nonce, aad, ciphertext, tag)? + } + SymCipher::Rfc5649_16 | SymCipher::Rfc5649_32 => rfc5649_unwrap(ciphertext, key)?, + _ => { + // Decryption. + Zeroizing::from(openssl_decrypt_aead( + sym_cipher.to_openssl_cipher()?, + key, + Some(nonce), + aad, + ciphertext, + tag, + )?) + } + }) +} + +pub enum UnderlyingCipher { + Openssl(Crypter), + AesGcmSiv, +} + +/// A stream cipher for encryption or decryption. / +pub struct StreamCipher { + underlying_cipher: UnderlyingCipher, + mode: Mode, + block_size: usize, + tag_size: usize, + buffer: Vec, +} + +impl StreamCipher { + fn new( + sym_cipher: SymCipher, + mode: Mode, + key: &[u8], + nonce: &[u8], + aad: &[u8], + ) -> Result { + match sym_cipher { + #[cfg(not(feature = "fips"))] + SymCipher::Aes128GcmSiv | SymCipher::Aes256GcmSiv => { + //TODO: the pure Rust crate does not support streaming. When openssl id exposed, this should be fixed + Err(KmipError::NotSupported( + "AES GCM SIV is not supported as a stream cipher for now".to_owned(), + )) + } + _ => { + let cipher = sym_cipher.to_openssl_cipher()?; + let block_size = match sym_cipher { + // This seems to be a bug in the openssl crate. The block size for AES is 16 bytes. + SymCipher::Aes128Xts => 16, + SymCipher::Aes256Xts => 32, + _ => cipher.block_size(), + }; + let mut crypter = Crypter::new(cipher, mode.into(), key, Some(nonce))?; + if !aad.is_empty() { + crypter.aad_update(aad)?; + } + Ok(Self { + underlying_cipher: UnderlyingCipher::Openssl(crypter), + mode, + block_size, + tag_size: sym_cipher.tag_size(), + buffer: vec![], + }) + } + } + } + + pub fn update(&mut self, bytes: &[u8]) -> Result, KmipError> { + match self.underlying_cipher { + UnderlyingCipher::Openssl(ref mut c) => { + // prepend the remaining bytes from the buffer + let available_bytes = [self.buffer.clone(), bytes.to_vec()].concat(); + // we only encrypt or decrypt in block sizes because XTS requires it (not GCM) + // but we always want to keep at least one block in the buffer + let len_to_park = available_bytes.len() % self.block_size + self.block_size; + if available_bytes.len() <= len_to_park { + // all bytes are pushed to the buffer + self.buffer = available_bytes; + return Ok(vec![]); + } + let len_to_update = available_bytes.len() - len_to_park; + let mut buffer = vec![0; len_to_update + self.block_size]; + let update_len = c.update(&available_bytes[..len_to_update], &mut buffer)?; + buffer.truncate(update_len); + // store the remaining bytes in the cipher buffer + self.buffer = available_bytes[len_to_update..].to_vec(); + Ok(buffer) + } + UnderlyingCipher::AesGcmSiv => Err(KmipError::NotSupported( + "AES GCM SIV is not supported as a stream cipher for now".to_owned(), + )), + } + } + + /// Finalize the encryption and return the ciphertext and the tag. + pub fn finalize_encryption(&mut self) -> Result<(Vec, Vec), KmipError> { + if self.mode != Mode::Encrypt { + kmip_bail!(KmipError::Default( + "finalize_encryption can only be called in encryption mode".to_owned() + )); + } + match self.underlying_cipher { + UnderlyingCipher::Openssl(ref mut c) => { + // if there are remaining bytes in the buffer, we need to update once more + // for XTS this may not be a multiple of the block size, but it must be greater than + // the block size + let mut final_bytes = if self.buffer.is_empty() { + vec![] + } else { + let mut final_bytes = vec![0; 2 * self.block_size]; + let len = c.update(&self.buffer, &mut final_bytes)?; + final_bytes.truncate(len); + final_bytes + }; + // finalize + let mut buffer = vec![0; self.block_size]; + let len = c.finalize(&mut buffer)?; + buffer.truncate(len); + final_bytes.extend(buffer); + // Append the tag if it exists and we are encrypting. + let tag = if self.tag_size > 0 { + let mut tag = vec![0; self.tag_size]; + c.get_tag(&mut tag)?; + tag + } else { + vec![] + }; + Ok((final_bytes, tag)) + } + UnderlyingCipher::AesGcmSiv => Err(KmipError::NotSupported( + "AES GCM SIV is not supported as a stream cipher for now".to_owned(), + )), + } + } + + pub fn finalize_decryption(&mut self, tag: &[u8]) -> Result, KmipError> { + if self.mode != Mode::Decrypt { + kmip_bail!(KmipError::Default( + "finalize_decryption can only be called in decryption mode".to_owned() + )); + } + match self.underlying_cipher { + UnderlyingCipher::Openssl(ref mut c) => { + // if there are remaining bytes in the buffer, we need to update once more + let mut final_bytes = if self.buffer.is_empty() { + vec![] + } else { + let mut final_bytes = vec![0; 2 * self.block_size]; + let len = c.update(&self.buffer, &mut final_bytes)?; + final_bytes.truncate(len); + final_bytes + }; + // Set the tag if it exists and we are decrypting. + if self.tag_size > 0 { + if tag.len() != self.tag_size { + kmip_bail!(KmipError::Default(format!( + "tag length mismatch. Expected: {}, got: {}", + self.tag_size, + tag.len() + ))); + } + c.set_tag(tag)?; + } + // finalize + let mut buffer = vec![0; self.block_size]; + let len = c.finalize(&mut buffer)?; + buffer.truncate(len); + final_bytes.extend(buffer); + Ok(final_bytes) + } + UnderlyingCipher::AesGcmSiv => Err(KmipError::NotSupported( + "AES GCM SIV is not supported as a stream cipher for now".to_owned(), + )), + } + } +} diff --git a/crate/kmip/src/crypto/symmetric/tests.rs b/crate/kmip/src/crypto/symmetric/tests.rs index 87a6e54e..ef6eeda6 100644 --- a/crate/kmip/src/crypto/symmetric/tests.rs +++ b/crate/kmip/src/crypto/symmetric/tests.rs @@ -1,74 +1,361 @@ +#![allow(clippy::unwrap_used)] + +#[cfg(feature = "fips")] +use openssl::provider::Provider; use openssl::rand::rand_bytes; -use crate::{ - crypto::{ - symmetric::{ - create_symmetric_key_kmip_object, AesGcmSystem, AES_256_GCM_IV_LENGTH, - AES_256_GCM_KEY_LENGTH, - }, - DecryptionSystem, EncryptionSystem, - }, - error::result::KmipResult, - kmip::{ - kmip_operations::{Decrypt, Encrypt}, - kmip_types::{CryptographicAlgorithm, CryptographicParameters, UniqueIdentifier}, - }, +#[cfg(not(feature = "fips"))] +use crate::crypto::symmetric::symmetric_ciphers::AES_128_GCM_SIV_MAC_LENGTH; +use crate::crypto::symmetric::symmetric_ciphers::{ + decrypt, encrypt, random_key, random_nonce, Mode, SymCipher, AES_128_GCM_MAC_LENGTH, + AES_128_XTS_MAC_LENGTH, AES_256_GCM_MAC_LENGTH, AES_256_XTS_MAC_LENGTH, }; #[test] -pub(crate) fn test_aes() -> KmipResult<()> { +fn test_encrypt_decrypt_aes_gcm_128() { #[cfg(feature = "fips")] // Load FIPS provider module from OpenSSL. - openssl::provider::Provider::load(None, "fips").unwrap(); + Provider::load(None, "fips").unwrap(); - let mut symmetric_key = vec![0; AES_256_GCM_KEY_LENGTH]; - rand_bytes(&mut symmetric_key).unwrap(); - let key = create_symmetric_key_kmip_object(&symmetric_key, CryptographicAlgorithm::AES)?; - let aes = AesGcmSystem::instantiate("blah", &key).unwrap(); - let mut data = zeroize::Zeroizing::from(vec![0_u8; 42]); - rand_bytes(&mut data).unwrap(); - let mut uid = vec![0_u8; 32]; - rand_bytes(&mut uid).unwrap(); + let mut message = vec![0_u8; 42]; + rand_bytes(&mut message).unwrap(); - let mut nonce = vec![0_u8; AES_256_GCM_IV_LENGTH]; - rand_bytes(&mut nonce).unwrap(); + let key = random_key(SymCipher::Aes128Gcm).unwrap(); + + let nonce = random_nonce(SymCipher::Aes128Gcm).unwrap(); + + let mut aad = vec![0_u8; 24]; + rand_bytes(&mut aad).unwrap(); + + let (ciphertext, tag) = encrypt(SymCipher::Aes128Gcm, &key, &nonce, &aad, &message).unwrap(); + assert_eq!(ciphertext.len(), message.len()); + assert_eq!(tag.len(), AES_128_GCM_MAC_LENGTH); + + let decrypted_data = + decrypt(SymCipher::Aes128Gcm, &key, &nonce, &aad, &ciphertext, &tag).unwrap(); + + // `to_vec()` conversion because of Zeroizing<>. + assert_eq!(decrypted_data.to_vec(), message); +} + +#[test] +fn test_encrypt_decrypt_aes_gcm_256() { + #[cfg(feature = "fips")] + // Load FIPS provider module from OpenSSL. + Provider::load(None, "fips").unwrap(); + + let mut message = vec![0_u8; 42]; + rand_bytes(&mut message).unwrap(); + + let key = random_key(SymCipher::Aes256Gcm).unwrap(); + + let nonce = random_nonce(SymCipher::Aes256Gcm).unwrap(); + + let mut aad = vec![0_u8; 24]; + rand_bytes(&mut aad).unwrap(); + + let (ciphertext, tag) = encrypt(SymCipher::Aes256Gcm, &key, &nonce, &aad, &message).unwrap(); + assert_eq!(ciphertext.len(), message.len()); + assert_eq!(tag.len(), AES_128_GCM_MAC_LENGTH); + + let decrypted_data = + decrypt(SymCipher::Aes256Gcm, &key, &nonce, &aad, &ciphertext, &tag).unwrap(); + + // `to_vec()` conversion because of Zeroizing<>. + assert_eq!(decrypted_data.to_vec(), message); +} + +#[test] +fn test_encrypt_decrypt_aes_xts_128() { + #[cfg(feature = "fips")] + // Load FIPS provider module from OpenSSL. + Provider::load(None, "fips").unwrap(); + + let mut message = vec![0_u8; 42]; + rand_bytes(&mut message).unwrap(); + + let key = random_key(SymCipher::Aes128Xts).unwrap(); + + let tweak = random_nonce(SymCipher::Aes128Xts).unwrap(); + + let (ciphertext, tag) = encrypt(SymCipher::Aes128Xts, &key, &tweak, &[], &message).unwrap(); + assert_eq!(ciphertext.len(), message.len()); + assert_eq!(tag.len(), AES_128_XTS_MAC_LENGTH); // always 0 + + let decrypted_data = + decrypt(SymCipher::Aes128Xts, &key, &tweak, &[], &ciphertext, &tag).unwrap(); + + // `to_vec()` conversion because of Zeroizing<>. + assert_eq!(decrypted_data.to_vec(), message); +} + +#[test] +fn test_encrypt_decrypt_aes_xts_256() { + #[cfg(feature = "fips")] + // Load FIPS provider module from OpenSSL. + Provider::load(None, "fips").unwrap(); + + let mut message = vec![0_u8; 42]; + rand_bytes(&mut message).unwrap(); + + let key = random_key(SymCipher::Aes256Xts).unwrap(); + + let tweak = random_nonce(SymCipher::Aes256Xts).unwrap(); + + let (ciphertext, tag) = encrypt(SymCipher::Aes256Xts, &key, &tweak, &[], &message).unwrap(); + assert_eq!(ciphertext.len(), message.len()); + assert_eq!(tag.len(), AES_256_XTS_MAC_LENGTH); // always 0 + + let decrypted_data = + decrypt(SymCipher::Aes256Xts, &key, &tweak, &[], &ciphertext, &tag).unwrap(); + + // `to_vec()` conversion because of Zeroizing<>. + assert_eq!(decrypted_data.to_vec(), message); +} + +#[cfg(not(feature = "fips"))] +#[test] +fn test_encrypt_decrypt_chacha20_poly1305() { + let mut message = vec![0_u8; 42]; + rand_bytes(&mut message).unwrap(); + + let key = random_key(SymCipher::Chacha20Poly1305).unwrap(); + + let nonce = random_nonce(SymCipher::Chacha20Poly1305).unwrap(); + + let mut aad = vec![0_u8; 24]; + rand_bytes(&mut aad).unwrap(); + + let (ciphertext, tag) = + encrypt(SymCipher::Chacha20Poly1305, &key, &nonce, &aad, &message).unwrap(); + + let decrypted_data = decrypt( + SymCipher::Chacha20Poly1305, + key.as_ref(), + &nonce, + &aad, + &ciphertext, + &tag, + ) + .unwrap(); + + // `to_vec()` conversion because of Zeroizing<>. + assert_eq!(decrypted_data.to_vec(), message); +} + +#[cfg(not(feature = "fips"))] +#[test] +fn test_encrypt_decrypt_aes_gcm_siv_128() { + #[cfg(feature = "fips")] + // Load FIPS provider module from OpenSSL. + Provider::load(None, "fips").unwrap(); + + let mut message = vec![0_u8; 42]; + rand_bytes(&mut message).unwrap(); + + let key = random_key(SymCipher::Aes128GcmSiv).unwrap(); + + let nonce = random_nonce(SymCipher::Aes128GcmSiv).unwrap(); + + let mut aad = vec![0_u8; 24]; + rand_bytes(&mut aad).unwrap(); + + let (ciphertext, tag) = encrypt(SymCipher::Aes128GcmSiv, &key, &nonce, &aad, &message).unwrap(); + assert_eq!(ciphertext.len(), message.len()); + assert_eq!(tag.len(), AES_128_GCM_SIV_MAC_LENGTH); + + let decrypted_data = decrypt( + SymCipher::Aes128GcmSiv, + &key, + &nonce, + &aad, + &ciphertext, + &tag, + ) + .unwrap(); + + // `to_vec()` conversion because of Zeroizing<>. + assert_eq!(decrypted_data.to_vec(), message); +} + +#[cfg(not(feature = "fips"))] +#[test] +fn test_encrypt_decrypt_aes_gcm_siv_256() { + #[cfg(feature = "fips")] + // Load FIPS provider module from OpenSSL. + Provider::load(None, "fips").unwrap(); + + let mut message = vec![0_u8; 42]; + rand_bytes(&mut message).unwrap(); + + let key = random_key(SymCipher::Aes256GcmSiv).unwrap(); + + let nonce = random_nonce(SymCipher::Aes256GcmSiv).unwrap(); + + let mut aad = vec![0_u8; 24]; + rand_bytes(&mut aad).unwrap(); + + let (ciphertext, tag) = encrypt(SymCipher::Aes256GcmSiv, &key, &nonce, &aad, &message).unwrap(); + assert_eq!(ciphertext.len(), message.len()); + assert_eq!(tag.len(), AES_128_GCM_SIV_MAC_LENGTH); + + let decrypted_data = decrypt( + SymCipher::Aes256GcmSiv, + &key, + &nonce, + &aad, + &ciphertext, + &tag, + ) + .unwrap(); + + // `to_vec()` conversion because of Zeroizing<>. + assert_eq!(decrypted_data.to_vec(), message); +} + +#[test] +fn aes_gcm_streaming_test() { + #[cfg(feature = "fips")] + // Load FIPS provider module from OpenSSL. + Provider::load(None, "fips").unwrap(); + + let mut message1 = vec![0_u8; 42]; + rand_bytes(&mut message1).unwrap(); + let mut message2 = vec![0_u8; 29]; + rand_bytes(&mut message2).unwrap(); + let mut message3 = vec![0_u8; 17]; + rand_bytes(&mut message3).unwrap(); + + let key = random_key(SymCipher::Aes256Gcm).unwrap(); + + let nonce = random_nonce(SymCipher::Aes256Gcm).unwrap(); + + let mut aad = vec![0_u8; 24]; + rand_bytes(&mut aad).unwrap(); // encrypt - let enc_res = aes - .encrypt(&Encrypt { - unique_identifier: Some(UniqueIdentifier::TextString("blah".to_owned())), - cryptographic_parameters: Some(CryptographicParameters { - cryptographic_algorithm: Some(CryptographicAlgorithm::AES), - initial_counter_value: Some(42), - ..Default::default() - }), - data: Some(data.clone()), - iv_counter_nonce: Some(nonce), - correlation_value: None, - init_indicator: None, - final_indicator: None, - authenticated_encryption_additional_data: Some(uid.clone()), - }) + let mut encryption_cipher = SymCipher::Aes256Gcm + .stream_cipher(Mode::Encrypt, &key, &nonce, &aad) .unwrap(); + let mut result = Vec::::new(); + result.extend(encryption_cipher.update(&message1).unwrap()); + result.extend(encryption_cipher.update(&message2).unwrap()); + result.extend(encryption_cipher.update(&message3).unwrap()); + let (remainder, tag) = encryption_cipher.finalize_encryption().unwrap(); + result.extend(remainder); + assert_eq!( + result.len(), + message1.len() + message2.len() + message3.len() + ); + assert_eq!(tag.len(), AES_256_GCM_MAC_LENGTH); // decrypt - let dec_res = aes - .decrypt(&Decrypt { - unique_identifier: Some(UniqueIdentifier::TextString("blah".to_owned())), - cryptographic_parameters: Some(CryptographicParameters { - cryptographic_algorithm: Some(CryptographicAlgorithm::AES), - initial_counter_value: Some(42), - ..Default::default() - }), - data: Some(enc_res.data.unwrap()), - iv_counter_nonce: Some(enc_res.iv_counter_nonce.unwrap()), - correlation_value: None, - init_indicator: None, - final_indicator: None, - authenticated_encryption_additional_data: Some(uid.clone()), - authenticated_encryption_tag: Some(enc_res.authenticated_encryption_tag.unwrap()), - }) + let mut decryption_cipher = SymCipher::Aes256Gcm + .stream_cipher(Mode::Decrypt, &key, &nonce, &aad) .unwrap(); - - assert_eq!(&data.clone(), &dec_res.data.unwrap()); - Ok(()) + let mut decrypted_data = decryption_cipher.update(&result).unwrap(); + decrypted_data.extend(decryption_cipher.finalize_decryption(&tag).unwrap()); + assert_eq!( + decrypted_data.len(), + message1.len() + message2.len() + message3.len() + ); + assert_eq!( + decrypted_data, + [&message1[..], &message2[..], &message3[..]].concat() + ); +} + +#[cfg(not(feature = "fips"))] +#[test] +fn chacha_streaming_test() { + let mut message1 = vec![0_u8; 42]; + rand_bytes(&mut message1).unwrap(); + let mut message2 = vec![0_u8; 29]; + rand_bytes(&mut message2).unwrap(); + let mut message3 = vec![0_u8; 17]; + rand_bytes(&mut message3).unwrap(); + + let key = random_key(SymCipher::Chacha20Poly1305).unwrap(); + + let nonce = random_nonce(SymCipher::Chacha20Poly1305).unwrap(); + + let mut aad = vec![0_u8; 24]; + rand_bytes(&mut aad).unwrap(); + + // encrypt + let mut encryption_cipher = SymCipher::Chacha20Poly1305 + .stream_cipher(Mode::Encrypt, &key, &nonce, &aad) + .unwrap(); + let mut result = Vec::::new(); + result.extend(encryption_cipher.update(&message1).unwrap()); + result.extend(encryption_cipher.update(&message2).unwrap()); + result.extend(encryption_cipher.update(&message3).unwrap()); + let (remainder, tag) = encryption_cipher.finalize_encryption().unwrap(); + result.extend(remainder); + assert_eq!( + result.len(), + message1.len() + message2.len() + message3.len() + ); + assert_eq!(tag.len(), AES_256_GCM_MAC_LENGTH); + // decrypt + let mut decryption_cipher = SymCipher::Chacha20Poly1305 + .stream_cipher(Mode::Decrypt, &key, &nonce, &aad) + .unwrap(); + let mut decrypted_data = decryption_cipher.update(&result).unwrap(); + decrypted_data.extend(decryption_cipher.finalize_decryption(&tag).unwrap()); + assert_eq!( + decrypted_data.len(), + message1.len() + message2.len() + message3.len() + ); + assert_eq!( + decrypted_data, + [&message1[..], &message2[..], &message3[..]].concat() + ); +} + +#[test] +fn aes_xts_streaming_test() { + #[cfg(feature = "fips")] + // Load FIPS provider module from OpenSSL. + Provider::load(None, "fips").unwrap(); + + let mut message1 = vec![0_u8; 42]; + rand_bytes(&mut message1).unwrap(); + let mut message2 = vec![0_u8; 27]; + rand_bytes(&mut message2).unwrap(); + let mut message3 = vec![0_u8; 17]; + rand_bytes(&mut message3).unwrap(); + + let key = random_key(SymCipher::Aes256Xts).unwrap(); + + let tweak = random_nonce(SymCipher::Aes256Xts).unwrap(); + + // encrypt + let mut encryption_cipher = SymCipher::Aes256Xts + .stream_cipher(Mode::Encrypt, &key, &tweak, &[]) + .unwrap(); + let mut result = Vec::::new(); + result.extend(encryption_cipher.update(&message1).unwrap()); + result.extend(encryption_cipher.update(&message2).unwrap()); + result.extend(encryption_cipher.update(&message3).unwrap()); + let (remainder, tag) = encryption_cipher.finalize_encryption().unwrap(); + result.extend(remainder); + assert_eq!( + result.len(), + message1.len() + message2.len() + message3.len() + ); + assert_eq!(tag.len(), AES_256_XTS_MAC_LENGTH); //0 + // decrypt + let mut decryption_cipher = SymCipher::Aes256Xts + .stream_cipher(Mode::Decrypt, &key, &tweak, &[]) + .unwrap(); + let mut decrypted_data = decryption_cipher.update(&result).unwrap(); + decrypted_data.extend(decryption_cipher.finalize_decryption(&tag).unwrap()); + assert_eq!( + decrypted_data.len(), + message1.len() + message2.len() + message3.len() + ); + assert_eq!( + decrypted_data, + [&message1[..], &message2[..], &message3[..]].concat() + ); } diff --git a/crate/kmip/src/crypto/wrap/unwrap_key.rs b/crate/kmip/src/crypto/wrap/unwrap_key.rs index 9763c8eb..894567b0 100644 --- a/crate/kmip/src/crypto/wrap/unwrap_key.rs +++ b/crate/kmip/src/crypto/wrap/unwrap_key.rs @@ -13,8 +13,8 @@ use crate::{ ckm_rsa_pkcs_oaep::ckm_rsa_pkcs_oaep_key_unwrap, }, symmetric::{ - aead::{aead_decrypt, AeadCipher}, rfc5649::rfc5649_unwrap, + symmetric_ciphers::{decrypt, SymCipher}, }, wrap::common::rsa_parameters, FIPS_MIN_SALT_SIZE, @@ -160,12 +160,12 @@ pub(crate) fn unwrap( if block_cipher_mode == Some(BlockCipherMode::GCM) { // unwrap using aes Gcm let len = ciphertext.len(); - let aead = AeadCipher::Aes256Gcm; + let aead = SymCipher::Aes256Gcm; let nonce = &ciphertext[..NONCE_LENGTH]; let wrapped_key_bytes = &ciphertext[NONCE_LENGTH..len - TAG_LENGTH]; let tag = &ciphertext[len - TAG_LENGTH..]; let authenticated_data = aad.unwrap_or_default(); - let plaintext = aead_decrypt( + let plaintext = decrypt( aead, &unwrap_secret, nonce, diff --git a/crate/kmip/src/crypto/wrap/wrap_key.rs b/crate/kmip/src/crypto/wrap/wrap_key.rs index b11d176c..1fecbcca 100644 --- a/crate/kmip/src/crypto/wrap/wrap_key.rs +++ b/crate/kmip/src/crypto/wrap/wrap_key.rs @@ -17,8 +17,8 @@ use crate::{ ckm_rsa_pkcs_oaep::ckm_rsa_pkcs_oaep_key_wrap, }, symmetric::{ - aead::{aead_encrypt, random_nonce, AeadCipher}, rfc5649::rfc5649_wrap, + symmetric_ciphers::{encrypt, random_nonce, SymCipher}, }, wrap::common::rsa_parameters, FIPS_MIN_SALT_SIZE, @@ -235,7 +235,7 @@ pub(crate) fn wrap( let aad = additional_data_encryption.unwrap_or_default(); if block_cipher_mode == Some(BlockCipherMode::GCM) { // wrap using aes GCM - let aead = AeadCipher::from_algorithm_and_key_size( + let aead = SymCipher::from_algorithm_and_key_size( cryptographic_algorithm, block_cipher_mode, key_bytes.len(), @@ -244,7 +244,7 @@ pub(crate) fn wrap( let nonce = random_nonce(aead)?; let (ct, authenticated_encryption_tag) = - aead_encrypt(aead, &key_bytes, &nonce, aad, key_to_wrap)?; + encrypt(aead, &key_bytes, &nonce, aad, key_to_wrap)?; let mut ciphertext = Vec::with_capacity( nonce.len() + ct.len() + authenticated_encryption_tag.len(), ); diff --git a/crate/kmip/src/kmip/kmip_types.rs b/crate/kmip/src/kmip/kmip_types.rs index c0b22ce8..e2034202 100644 --- a/crate/kmip/src/kmip/kmip_types.rs +++ b/crate/kmip/src/kmip/kmip_types.rs @@ -2415,6 +2415,9 @@ pub enum BlockCipherMode { #[value(name = "NISTKeyWrap")] // NISTKeyWrap refers to rfc5649 NISTKeyWrap = 0x8000_0001, + #[value(name = "GCMSIV")] + // AES GCM SIV + GCMSIV = 0x8000_0002, } #[allow(non_camel_case_types)] diff --git a/crate/pyo3/src/py_kms_client.rs b/crate/pyo3/src/py_kms_client.rs index 2c600bdf..4faa9fd0 100644 --- a/crate/pyo3/src/py_kms_client.rs +++ b/crate/pyo3/src/py_kms_client.rs @@ -548,6 +548,7 @@ impl KmsClient { Some(access_policy), data, header_metadata, + None, authentication_data, None, ) @@ -757,8 +758,9 @@ impl KmsClient { key_identifier: ToUniqueIdentifier, py: Python<'p>, ) -> PyResult<&PyAny> { - let request = build_encryption_request(&key_identifier.0, None, data, None, None, None) - .map_err(|e| PyException::new_err(e.to_string()))?; + let request = + build_encryption_request(&key_identifier.0, None, data, None, None, None, None) + .map_err(|e| PyException::new_err(e.to_string()))?; let client = self.0.clone(); pyo3_asyncio::tokio::future_into_py(py, async move { diff --git a/crate/server/Cargo.toml b/crate/server/Cargo.toml index 217913b8..a5fc98da 100644 --- a/crate/server/Cargo.toml +++ b/crate/server/Cargo.toml @@ -39,13 +39,13 @@ async-trait = "0.1" base64 = { workspace = true } chrono = { workspace = true } clap = { workspace = true, features = [ - "help", - "env", - "std", - "usage", - "error-context", - "derive", - "cargo", + "help", + "env", + "std", + "usage", + "error-context", + "derive", + "cargo", ] } cloudproof = { workspace = true } cloudproof_findex = { version = "5.0", features = ["findex-redis"] } @@ -57,10 +57,10 @@ hex = { workspace = true, features = ["serde"] } lazy_static = "1.5" log = { workspace = true } num-bigint-dig = { workspace = true, features = [ - "std", - "rand", - "serde", - "zeroize", + "std", + "rand", + "serde", + "zeroize", ] } num_cpus = { workspace = true } openssl = { workspace = true } @@ -70,11 +70,11 @@ opentelemetry-semantic-conventions = { version = "0.15.0" } opentelemetry_sdk = { version = "0.23.0", features = ["rt-tokio"] } rawsql = "0.1" redis = { version = "0.23", features = [ - "aio", - "ahash", - "script", - "connection-manager", - "tokio-comp", + "aio", + "ahash", + "script", + "connection-manager", + "tokio-comp", ] } # Important: align the rustls version with reqwest rustls dependency # When using client certificate authentication, reqwest will use the @@ -86,11 +86,11 @@ reqwest = { workspace = true, features = ["default", "json"] } serde = { workspace = true } serde_json = { workspace = true } sqlx = { version = "0.8.1", default-features = false, features = [ - "json", - "runtime-tokio-native-tls", - "mysql", - "postgres", - "sqlite", + "json", + "runtime-tokio-native-tls", + "mysql", + "postgres", + "sqlite", ] } strum = { workspace = true, features = ["std", "derive", "strum_macros"] } thiserror = { workspace = true } @@ -127,64 +127,64 @@ changelog = "../../CHANGELOG.md" section = "security" priority = "optional" assets = [ - [ - "target/release/cosmian_kms_server", - "usr/sbin/cosmian_kms", - "500", - ], - [ - "../../README.md", - "usr/share/doc/cosmian_kms/README", - "644", - ], - [ - "../../pkg/kms.toml", - "etc/cosmian_kms/", - "400", - ], - [ - "/usr/local/openssl/lib64/ossl-modules/legacy.so", - "usr/local/openssl/lib64/ossl-modules/legacy.so", - "400", - ], + [ + "target/release/cosmian_kms_server", + "usr/sbin/cosmian_kms", + "500", + ], + [ + "../../README.md", + "usr/share/doc/cosmian_kms/README", + "644", + ], + [ + "../../pkg/kms.toml", + "etc/cosmian_kms/", + "400", + ], + [ + "/usr/local/openssl/lib64/ossl-modules/legacy.so", + "usr/local/openssl/lib64/ossl-modules/legacy.so", + "400", + ], ] systemd-units = [ - { unit-name = "cosmian_kms", unit-scripts = "../../pkg", enable = true, start = false, restart-after-upgrade = false }, + { unit-name = "cosmian_kms", unit-scripts = "../../pkg", enable = true, start = false, restart-after-upgrade = false }, ] [package.metadata.deb.variants.fips] features = ["fips"] assets = [ - [ - "target/release/cosmian_kms_server", - "usr/sbin/cosmian_kms", - "500", - ], - [ - "../../README.md", - "usr/share/doc/cosmian_kms/README", - "644", - ], - [ - "../../pkg/kms.toml", - "etc/cosmian_kms/", - "400", - ], - [ - "/usr/local/openssl/lib64/ossl-modules/fips.so", - "usr/local/openssl/lib64/ossl-modules/fips.so", - "400", - ], - [ - "/usr/local/openssl/ssl/openssl.cnf", - "usr/local/openssl/ssl/openssl.cnf", - "400", - ], - [ - "/usr/local/openssl/ssl/fipsmodule.cnf", - "usr/local/openssl/ssl/fipsmodule.cnf", - "400", - ], + [ + "target/release/cosmian_kms_server", + "usr/sbin/cosmian_kms", + "500", + ], + [ + "../../README.md", + "usr/share/doc/cosmian_kms/README", + "644", + ], + [ + "../../pkg/kms.toml", + "etc/cosmian_kms/", + "400", + ], + [ + "/usr/local/openssl/lib64/ossl-modules/fips.so", + "usr/local/openssl/lib64/ossl-modules/fips.so", + "400", + ], + [ + "/usr/local/openssl/ssl/openssl.cnf", + "usr/local/openssl/ssl/openssl.cnf", + "400", + ], + [ + "/usr/local/openssl/ssl/fipsmodule.cnf", + "usr/local/openssl/ssl/fipsmodule.cnf", + "400", + ], ] # END DEBIAN PACKAGING @@ -195,11 +195,11 @@ assets = [ [package.metadata.generate-rpm] license = "BUSL-1.1" assets = [ - { source = "target/release/cosmian_kms_server", dest = "/usr/sbin/cosmian_kms", mode = "500" }, - { source = "/usr/local/openssl/lib64/ossl-modules/legacy.so", dest = "/usr/local/openssl/lib64/ossl-modules/legacy.so", mode = "500" }, - { source = "../../README.md", dest = "/usr/share/doc/cosmian_kms/README", mode = "644", doc = true }, - { source = "../../pkg/kms.toml", dest = "/etc/cosmian_kms/kms.toml", mode = "400" }, - { source = "../../pkg/cosmian_kms.service", dest = "/lib/systemd/system/cosmian_kms.service", mode = "644" }, + { source = "target/release/cosmian_kms_server", dest = "/usr/sbin/cosmian_kms", mode = "500" }, + { source = "/usr/local/openssl/lib64/ossl-modules/legacy.so", dest = "/usr/local/openssl/lib64/ossl-modules/legacy.so", mode = "500" }, + { source = "../../README.md", dest = "/usr/share/doc/cosmian_kms/README", mode = "644", doc = true }, + { source = "../../pkg/kms.toml", dest = "/etc/cosmian_kms/kms.toml", mode = "400" }, + { source = "../../pkg/cosmian_kms.service", dest = "/lib/systemd/system/cosmian_kms.service", mode = "644" }, ] auto-req = "no" # do not try to discover .so dependencies require-sh = true diff --git a/crate/server/src/core/extra_database_params.rs b/crate/server/src/core/extra_database_params.rs index d6011286..ffe22219 100644 --- a/crate/server/src/core/extra_database_params.rs +++ b/crate/server/src/core/extra_database_params.rs @@ -1,4 +1,4 @@ -use cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}; +use cosmian_kmip::crypto::{secret::Secret, symmetric::symmetric_ciphers::AES_256_GCM_KEY_LENGTH}; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; diff --git a/crate/server/src/core/implementation.rs b/crate/server/src/core/implementation.rs index ff4ae812..a40d1d5c 100644 --- a/crate/server/src/core/implementation.rs +++ b/crate/server/src/core/implementation.rs @@ -4,7 +4,7 @@ use cloudproof::reexport::{cover_crypt::Covercrypt, crypto_core::FixedSizeCBytes use cosmian_kmip::{ crypto::{ secret::Secret, - symmetric::{create_symmetric_key_kmip_object, AES_256_GCM_KEY_LENGTH}, + symmetric::{create_symmetric_key_kmip_object, symmetric_ciphers::AES_256_GCM_KEY_LENGTH}, }, kmip::{ kmip_objects::Object, diff --git a/crate/server/src/core/operations/decrypt.rs b/crate/server/src/core/operations/decrypt.rs index 4dbd60dc..9321548d 100644 --- a/crate/server/src/core/operations/decrypt.rs +++ b/crate/server/src/core/operations/decrypt.rs @@ -10,7 +10,7 @@ use cosmian_kmip::{ ckm_rsa_aes_key_wrap::ckm_rsa_aes_key_unwrap, ckm_rsa_pkcs_oaep::ckm_rsa_pkcs_oaep_key_decrypt, default_cryptographic_parameters, }, - symmetric::aead::{aead_decrypt, AeadCipher}, + symmetric::symmetric_ciphers::{decrypt as sym_decrypt, SymCipher}, DecryptionSystem, }, kmip::{ @@ -196,7 +196,7 @@ fn decrypt_bulk( let ciphertext = &nonce_ciphertext_tag [aead.nonce_size()..nonce_ciphertext_tag.len() - aead.tag_size()]; let tag = &nonce_ciphertext_tag[nonce_ciphertext_tag.len() - aead.tag_size()..]; - let plaintext = aead_decrypt(aead, &key_bytes, nonce, &[], ciphertext, tag)?; + let plaintext = sym_decrypt(aead, &key_bytes, nonce, &[], ciphertext, tag)?; plaintexts.push(plaintext); } } @@ -280,7 +280,7 @@ fn decrypt_single_with_symmetric_key( .authenticated_encryption_tag .as_deref() .unwrap_or(EMPTY_SLICE); - let plaintext = aead_decrypt(aead, &key_bytes, nonce, aad, ciphertext, tag)?; + let plaintext = sym_decrypt(aead, &key_bytes, nonce, aad, ciphertext, tag)?; Ok(Ok(DecryptResponse { unique_identifier: UniqueIdentifier::TextString(owm.id.clone()), data: Some(plaintext), @@ -291,7 +291,7 @@ fn decrypt_single_with_symmetric_key( fn get_aead_and_key( owm: &ObjectWithMetadata, request: &Decrypt, -) -> Result<(Zeroizing>, AeadCipher), KmsError> { +) -> Result<(Zeroizing>, SymCipher), KmsError> { let key_block = owm.object.key_block()?; // recover the cryptographic algorithm from the request or the key block or default to AES let cryptographic_algorithm = request @@ -309,7 +309,7 @@ fn get_aead_and_key( .as_ref() .and_then(|cp| cp.block_cipher_mode); let key_bytes = key_block.key_bytes()?; - let aead = AeadCipher::from_algorithm_and_key_size( + let aead = SymCipher::from_algorithm_and_key_size( cryptographic_algorithm, block_cipher_mode, key_bytes.len(), diff --git a/crate/server/src/core/operations/encrypt.rs b/crate/server/src/core/operations/encrypt.rs index 2fc18e64..7ee6e0bb 100644 --- a/crate/server/src/core/operations/encrypt.rs +++ b/crate/server/src/core/operations/encrypt.rs @@ -10,7 +10,7 @@ use cosmian_kmip::{ ckm_rsa_aes_key_wrap::ckm_rsa_aes_key_wrap, ckm_rsa_pkcs_oaep::ckm_rsa_pkcs_oaep_encrypt, default_cryptographic_parameters, }, - symmetric::aead::{aead_encrypt, random_nonce, AeadCipher}, + symmetric::symmetric_ciphers::{encrypt as sym_encrypt, random_nonce, SymCipher}, EncryptionSystem, }, kmip::{ @@ -73,7 +73,7 @@ pub(crate) async fn encrypt( fn encrypt_single(owm: &ObjectWithMetadata, request: &Encrypt) -> KResult { match &owm.object { - Object::SymmetricKey { .. } => encrypt_with_aead(request, owm), + Object::SymmetricKey { .. } => encrypt_with_symmetric_key(request, owm), Object::PublicKey { .. } => encrypt_with_public_key(request, owm), Object::Certificate { certificate_value, .. @@ -103,12 +103,18 @@ pub(crate) fn encrypt_bulk( match &owm.object { Object::SymmetricKey { .. } => { + let aad = request + .authenticated_encryption_additional_data + .as_deref() + .unwrap_or(EMPTY_SLICE); for plaintext in >>>>::into(bulk_data) { request.data = Some(plaintext.clone()); - let (key_bytes, aead) = get_aead_and_key(&request, owm)?; - let nonce = random_nonce(aead)?; - let (ciphertext, tag) = - aead_encrypt(aead, &key_bytes, &nonce, EMPTY_SLICE, &plaintext)?; + let (key_bytes, cipher) = get_cipher_and_key(&request, owm)?; + let nonce = request + .iv_counter_nonce + .clone() + .unwrap_or(random_nonce(cipher)?); + let (ciphertext, tag) = sym_encrypt(cipher, &key_bytes, &nonce, aad, &plaintext)?; // concatenate nonce || ciphertext || tag let nct = [nonce.as_slice(), ciphertext.as_slice(), tag.as_slice()].concat(); ciphertexts.push(Zeroizing::new(nct)); @@ -212,8 +218,11 @@ async fn get_key( Ok(owm) } -fn encrypt_with_aead(request: &Encrypt, owm: &ObjectWithMetadata) -> KResult { - let (key_bytes, aead) = get_aead_and_key(request, owm)?; +fn encrypt_with_symmetric_key( + request: &Encrypt, + owm: &ObjectWithMetadata, +) -> KResult { + let (key_bytes, aead) = get_cipher_and_key(request, owm)?; let plaintext = request.data.as_ref().ok_or_else(|| { KmsError::InvalidRequest("Encrypt: data to encrypt must be provided".to_owned()) })?; @@ -225,7 +234,7 @@ fn encrypt_with_aead(request: &Encrypt, owm: &ObjectWithMetadata) -> KResult KResult KResult<(Zeroizing>, AeadCipher)> { +) -> KResult<(Zeroizing>, SymCipher)> { // Make sure that the key used to encrypt can be used to encrypt. if !owm .object @@ -269,7 +278,7 @@ fn get_aead_and_key( .cryptographic_parameters .as_ref() .and_then(|cp| cp.block_cipher_mode); - AeadCipher::from_algorithm_and_key_size( + SymCipher::from_algorithm_and_key_size( cryptographic_algorithm, block_cipher_mode, key_bytes.len(), diff --git a/crate/server/src/database/cached_sqlcipher.rs b/crate/server/src/database/cached_sqlcipher.rs index 7b19f39a..f0475eed 100644 --- a/crate/server/src/database/cached_sqlcipher.rs +++ b/crate/server/src/database/cached_sqlcipher.rs @@ -8,7 +8,7 @@ use std::{ use async_trait::async_trait; use clap::crate_version; use cosmian_kmip::{ - crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}, + crypto::{secret::Secret, symmetric::symmetric_ciphers::AES_256_GCM_KEY_LENGTH}, kmip::{ kmip_objects::Object, kmip_types::{Attributes, StateEnumeration}, diff --git a/crate/server/src/database/cached_sqlite_struct.rs b/crate/server/src/database/cached_sqlite_struct.rs index ca313463..5644fd35 100644 --- a/crate/server/src/database/cached_sqlite_struct.rs +++ b/crate/server/src/database/cached_sqlite_struct.rs @@ -9,7 +9,7 @@ use std::{ }; use cloudproof::reexport::crypto_core::reexport::tiny_keccak; -use cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}; +use cosmian_kmip::crypto::{secret::Secret, symmetric::symmetric_ciphers::AES_256_GCM_KEY_LENGTH}; use sqlx::{Pool, Sqlite}; use tracing::{debug, info, trace}; @@ -508,7 +508,9 @@ mod tests { #![allow(clippy::unwrap_used, clippy::expect_used)] use std::{str::FromStr, sync::atomic::Ordering, time::Duration}; - use cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}; + use cosmian_kmip::crypto::{ + secret::Secret, symmetric::symmetric_ciphers::AES_256_GCM_KEY_LENGTH, + }; use sqlx::{ sqlite::{SqliteConnectOptions, SqlitePoolOptions}, ConnectOptions, diff --git a/crate/server/src/database/tests/mod.rs b/crate/server/src/database/tests/mod.rs index a9e6f7c1..055a9dc0 100644 --- a/crate/server/src/database/tests/mod.rs +++ b/crate/server/src/database/tests/mod.rs @@ -2,7 +2,7 @@ use std::path::Path; -use cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}; +use cosmian_kmip::crypto::{secret::Secret, symmetric::symmetric_ciphers::AES_256_GCM_KEY_LENGTH}; use cosmian_logger::log_utils::log_init; use tempfile::TempDir; diff --git a/crate/server/src/tests/cover_crypt_tests/integration_tests.rs b/crate/server/src/tests/cover_crypt_tests/integration_tests.rs index 46e0ff6a..5fc059b9 100644 --- a/crate/server/src/tests/cover_crypt_tests/integration_tests.rs +++ b/crate/server/src/tests/cover_crypt_tests/integration_tests.rs @@ -79,6 +79,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), Some(header_metadata.clone()), + None, Some(authentication_data.clone()), Some(CryptographicParameters { cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt), @@ -138,6 +139,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), None, )?; @@ -265,6 +267,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), Some(CryptographicParameters { cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt), @@ -359,6 +362,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), Some(CryptographicParameters { cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt), @@ -391,6 +395,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), Some(CryptographicParameters { cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt), @@ -421,6 +426,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), Some(CryptographicParameters { cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt), @@ -450,6 +456,7 @@ async fn integration_tests_use_ids_no_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), Some(CryptographicParameters { cryptographic_algorithm: Some(CryptographicAlgorithm::CoverCrypt), diff --git a/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs b/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs index fabfdfd9..b9168324 100644 --- a/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs +++ b/crate/server/src/tests/cover_crypt_tests/integration_tests_tags.rs @@ -70,6 +70,7 @@ async fn test_re_key_with_tags() -> KResult<()> { None, Some(authentication_data.clone()), None, + None, )?; let encrypt_response: EncryptResponse = test_utils::post(&app, &request).await?; let _encrypted_data = encrypt_response @@ -130,6 +131,7 @@ async fn integration_tests_with_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), Some(header_metadata.clone()), + None, Some(authentication_data.clone()), None, )?; @@ -181,6 +183,7 @@ async fn integration_tests_with_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), None, )?; @@ -285,6 +288,7 @@ async fn integration_tests_with_tags() -> KResult<()> { Some(encryption_policy.to_owned()), data.to_vec(), None, + None, Some(authentication_data.clone()), None, )?; @@ -319,8 +323,7 @@ async fn integration_tests_with_tags() -> KResult<()> { .data .context("There should be decrypted data")? .as_slice() - .try_into() - .unwrap(); + .try_into()?; assert_eq!(&data, &decrypted_data.plaintext.to_vec()); assert!(decrypted_data.metadata.is_empty()); diff --git a/crate/server/src/tests/cover_crypt_tests/unit_tests.rs b/crate/server/src/tests/cover_crypt_tests/unit_tests.rs index 39314b93..453bcc00 100644 --- a/crate/server/src/tests/cover_crypt_tests/unit_tests.rs +++ b/crate/server/src/tests/cover_crypt_tests/unit_tests.rs @@ -282,6 +282,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { Some(confidential_mkg_policy_attributes.to_owned()), confidential_mkg_data.to_vec(), None, + None, Some(confidential_authentication_data.clone()), None, )?, @@ -305,6 +306,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { Some(confidential_mkg_policy_attributes.to_owned()), confidential_mkg_data.to_vec(), None, + None, Some(confidential_authentication_data.clone()), None, )?, @@ -325,6 +327,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { Some(secret_fin_policy_attributes.to_owned()), secret_fin_data.to_vec(), None, + None, Some(secret_authentication_data.clone()), None, )?, @@ -348,6 +351,7 @@ async fn test_abe_encrypt_decrypt() -> KResult<()> { Some(secret_fin_policy_attributes.to_owned()), secret_fin_data.to_vec(), None, + None, Some(secret_authentication_data.clone()), None, )?, @@ -611,6 +615,7 @@ async fn test_import_decrypt() -> KResult<()> { Some(confidential_mkg_policy_attributes.to_owned()), confidential_mkg_data.to_vec(), None, + None, Some(confidential_authentication_data.clone()), None, )?, diff --git a/crate/test_server/Cargo.toml b/crate/test_server/Cargo.toml index d32bd656..b405b260 100644 --- a/crate/test_server/Cargo.toml +++ b/crate/test_server/Cargo.toml @@ -22,7 +22,7 @@ base64 = { workspace = true } cosmian_kmip = { path = "../kmip" } cosmian_kms_client = { path = "../client", default-features = false } cosmian_kms_server = { path = "../server", features = [ - "insecure", + "insecure", ], default-features = false } cosmian_logger = { path = "../logger" } serde_json = { workspace = true } @@ -32,7 +32,7 @@ tracing = { workspace = true } [dev-dependencies] criterion = { version = "0.5", features = [ - "html_reports", - "async_tokio", + "html_reports", + "async_tokio", ], default-features = false } zeroize = { workspace = true } diff --git a/crate/test_server/benches/rsa_benches.rs b/crate/test_server/benches/rsa_benches.rs index 8ad2db98..9404fcfd 100644 --- a/crate/test_server/benches/rsa_benches.rs +++ b/crate/test_server/benches/rsa_benches.rs @@ -292,6 +292,7 @@ pub(crate) async fn encrypt( cleartext, None, None, + None, Some(cryptographic_parameters.to_owned()), ) .unwrap(); diff --git a/crate/test_server/src/test_server.rs b/crate/test_server/src/test_server.rs index 7a085249..5032536e 100644 --- a/crate/test_server/src/test_server.rs +++ b/crate/test_server/src/test_server.rs @@ -10,7 +10,7 @@ use actix_server::ServerHandle; use base64::{engine::general_purpose::STANDARD as b64, Engine as _}; use cosmian_kms_client::{ client_bail, client_error, - cosmian_kmip::crypto::{secret::Secret, symmetric::AES_256_GCM_KEY_LENGTH}, + cosmian_kmip::crypto::{secret::Secret, symmetric::symmetric_ciphers::AES_256_GCM_KEY_LENGTH}, write_json_object_to_file, ClientConf, ClientError, GmailApiConf, KmsClient, }; use cosmian_kms_server::{ diff --git a/documentation/docs/algorithms.md b/documentation/docs/algorithms.md index 98a5f4d5..67b5c63a 100644 --- a/documentation/docs/algorithms.md +++ b/documentation/docs/algorithms.md @@ -31,6 +31,8 @@ The supported key-wrapping algorithms are: | Salsa Sealed Box | X25519, Ed25519 and Salsa20 Poly1305 | No | ECIES compatible with libsodium [Sealed Boxes](https://doc.libsodium.org/public-key_cryptography/sealed_boxes). | | ECIES | P-192, P-224, P-256, P-384, P-521 | No | ECIES with a NIST curve and using SHAKE 128 and AES 128 GCM (P-192, P-224, P-256) AES 256 GCM otherwise. | +Any encryption scheme below can be used for key-wrapping as well. + ## Encryption schemes Encryption is supported via the `Encrypt` and `Decrypt` kmip operations. @@ -42,15 +44,17 @@ Encryption can be performed using a key or a certificate. Decryption can be perf The supported encryption algorithms are: -| Algorithm | Encryption Key Type | FIPS mode | Description | -|------------------------------|---------------------------------------------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------| -| Covercrypt | Covercrypt | No | A fast post-quantum attribute based scheme: [Covercrypt](https://github.com/Cosmian/cover_crypt). | -| AES-128-GCM
AES-256-GCM | Symmetric authenticated encryption with additional data | NIST FIPS 197 | The NIST standardized symmetric encryption in [FIPS 197](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf). | -| ChaCha20-Poly1305 | Symmetric authenticated encryption with additional data | No | A popular symmetric encryption algorithm standardised in [RFC-8439](https://www.rfc-editor.org/rfc/rfc8439) | -| CKM_RSA_PKCS | RSA PKCS#1 v1.5 | Not anymore | RSA WITH PKCS#1 v1.5 padding - removed by NIST approved algorithms for encryption in FIPS 140-3 | -| CKM_RSA_PKCS_OAEP | RSA encryption with OAEP paddding | NIST 800-56B rev. 2 | RSA OAEP with NIST approved hashing functions for RSA key size 2048, 3072 or 4096 bits. | -| Salsa Sealed Box | X25519, Ed25519 and Salsa20 Poly1305 | No | ECIES compatible with libsodium [Sealed Boxes](https://doc.libsodium.org/public-key_cryptography/sealed_boxes). | -| ECIES | P-192, P-224, P-256, P-384, P-521 | No | ECIES with a NIST curve and using SHAKE 128 and AES-128-GCM. | +| Algorithm | Encryption Key Type | FIPS mode | Description | +|-------------------|---------------------------------------------------------|---------------------|--------------------------------------------------------------------------------------------------------------------------| +| Covercrypt | Covercrypt | No | A fast post-quantum attribute based scheme: [Covercrypt](https://github.com/Cosmian/cover_crypt). | +| AES GCM | Symmetric authenticated encryption with additional data | NIST FIPS 197 | The NIST standardized symmetric encryption in [FIPS 197](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf). | +| AES XTS | Symmetric, not authenticated | NIST SP 800-38E | Used in disk encryption. Requires 2 keys (e.g. a dopuble-sixed key) | +| AES GCM-SIV | Symmetric, authenticated, synthetic IV | No | Used for deterministic encryption and encryption of very large data sets. | +| ChaCha20-Poly1305 | Symmetric authenticated encryption with additional data | No | A popular symmetric encryption algorithm standardised in [RFC-8439](https://www.rfc-editor.org/rfc/rfc8439) | +| CKM_RSA_PKCS | RSA PKCS#1 v1.5 | Not anymore | RSA WITH PKCS#1 v1.5 padding - removed by NIST approved algorithms for encryption in FIPS 140-3 | +| CKM_RSA_PKCS_OAEP | RSA encryption with OAEP paddding | NIST 800-56B rev. 2 | RSA OAEP with NIST approved hashing functions for RSA key size 2048, 3072 or 4096 bits. | +| Salsa Sealed Box | X25519, Ed25519 and Salsa20 Poly1305 | No | ECIES compatible with libsodium [Sealed Boxes](https://doc.libsodium.org/public-key_cryptography/sealed_boxes). | +| ECIES | P-192, P-224, P-256, P-384, P-521 | No | ECIES with a NIST curve and using SHAKE 128 and AES-128-GCM. | ## Algorithms Details diff --git a/documentation/docs/cli/main_commands.md b/documentation/docs/cli/main_commands.md index 50086761..7bcfa5d1 100644 --- a/documentation/docs/cli/main_commands.md +++ b/documentation/docs/cli/main_commands.md @@ -280,7 +280,7 @@ Possible values: `"true", "false"` [default: `"false"`] `--block-cipher-mode [-m] ` Block cipher mode -Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap"` +Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap", "GCMSIV"` `--authenticated-additional-data [-d] ` Authenticated encryption additional data @@ -1027,7 +1027,7 @@ Possible values: `"true", "false"` [default: `"false"`] `--block-cipher-mode [-m] ` Block cipher mode -Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap"` +Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap", "GCMSIV"` `--authenticated-additional-data [-d] ` Authenticated encryption additional data @@ -1487,7 +1487,7 @@ Possible values: `"true", "false"` [default: `"false"`] `--block-cipher-mode [-m] ` Block cipher mode -Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap"` +Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap", "GCMSIV"` `--authenticated-additional-data [-d] ` Authenticated encryption additional data @@ -1706,9 +1706,9 @@ Manage symmetric keys. Encrypt and decrypt data **`keys`** [[12.1]](#121-ckms-sym-keys) Create, destroy, import, and export symmetric keys -**`encrypt`** [[12.2]](#122-ckms-sym-encrypt) Encrypt a file using AES GCM +**`encrypt`** [[12.2]](#122-ckms-sym-encrypt) Encrypt a file using a symmetric cipher -**`decrypt`** [[12.3]](#123-ckms-sym-decrypt) Decrypts a file using AES GCM +**`decrypt`** [[12.3]](#123-ckms-sym-decrypt) Decrypt a file using a symmetric key. --- @@ -1815,7 +1815,7 @@ Possible values: `"true", "false"` [default: `"false"`] `--block-cipher-mode [-m] ` Block cipher mode -Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap"` +Possible values: `"CBC", "ECB", "PCBC", "CFB", "OFB", "CTR", "CMAC", "CCM", "GCM", "CBCMAC", "XTS", "X9102AESKW", "X9102TDKW", "X9102AKW1", "X9102AKW2", "AEAD", "NISTKeyWrap", "GCMSIV"` `--authenticated-additional-data [-d] ` Authenticated encryption additional data @@ -1950,7 +1950,7 @@ Destroy a symmetric key ## 12.2 ckms sym encrypt -Encrypt a file using AES GCM +Encrypt a file using a symmetric cipher ### Usage `ckms sym encrypt [options] @@ -1960,11 +1960,21 @@ Encrypt a file using AES GCM `--key-id [-k] ` The symmetric key unique identifier. If not specified, tags should be specified +`--data-encryption-algorithm [-d] ` The data encryption algorithm. If not specified, aes-gcm is used + +Possible values: `"chacha20-poly1305", "aes-gcm", "aes-xts", "aes-gcm-siv"` [default: `"aes-gcm"`] + +`--key-encryption-algorithm [-e] ` The optional key encryption algorithm used to encrypt the data encryption key. + +Possible values: `"chacha20-poly1305", "aes-gcm", "aes-xts", "aes-gcm-siv", "rfc5649"` + `--tag [-t] ` Tag to use to retrieve the key when no key id is specified. To specify multiple tags, use the option multiple times `--output-file [-o] ` The encrypted output file path -`--authentication-data [-a] ` Optional authentication data. This data needs to be provided back for decryption +`--nonce [-n] ` Optional nonce/IV (or tweak for XTS) as a hex string. If not provided, a random value is generated + +`--authentication-data [-a] ` Optional additional authentication data as a hex string. This data needs to be provided back for decryption. This data is ignored with XTS @@ -1972,7 +1982,7 @@ Encrypt a file using AES GCM ## 12.3 ckms sym decrypt -Decrypts a file using AES GCM +Decrypt a file using a symmetric key. ### Usage `ckms sym decrypt [options] @@ -1982,11 +1992,19 @@ Decrypts a file using AES GCM `--key-id [-k] ` The private key unique identifier If not specified, tags should be specified +`--data-encryption-algorithm [-d] ` The data encryption algorithm. If not specified, aes-gcm is used + +Possible values: `"chacha20-poly1305", "aes-gcm", "aes-xts", "aes-gcm-siv"` [default: `"aes-gcm"`] + +`--key-encryption-algorithm [-e] ` The optional key encryption algorithm used to decrypt the data encryption key. + +Possible values: `"chacha20-poly1305", "aes-gcm", "aes-xts", "aes-gcm-siv", "rfc5649"` + `--tag [-t] ` Tag to use to retrieve the key when no key id is specified. To specify multiple tags, use the option multiple times `--output-file [-o] ` The encrypted output file path -`--authentication-data [-a] ` Optional authentication data that was supplied during encryption +`--authentication-data [-a] ` Optional authentication data that was supplied during encryption as a hex string diff --git a/documentation/docs/encrypting_and_decrypting_at_scale.md b/documentation/docs/encrypting_and_decrypting_at_scale.md new file mode 100644 index 00000000..81840445 --- /dev/null +++ b/documentation/docs/encrypting_and_decrypting_at_scale.md @@ -0,0 +1,289 @@ +# Encrypting and decrypting at scale + +The Cosmian KMS is particularly suited for client-side encryption scenarios which may require +high-performance encryption and decryption. + +The KMS offers two mechanisms for encrypting and decrypting data: + +- by calling the `Encrypt` and `Decrypt` operations on the KMS KMIP API and benefiting from its + parallelization, concurrency, and optimized batching capabilities. +- by using the `ckms` CLI client to encrypt and decrypt data locally, including large files. + +## Calling the KMS API + +The KMS provides a high-performance encryption and +decryption API that can be used to encrypt and decrypt data at scale. + +### Parallelization, concurrency, and batching + +Due to its stateless user session model, the Cosmian KMS is designed to take advantage of modern +multi-core processors and can parallelize encryption and decryption operations across multiple +cores. Parallelization can be achieved by scaling vertically (increasing the number of cores on a +single machine) or horizontally (increasing the number of machines in a cluster). + +The Cosmian KMS can also handle multiple concurrent encryption and decryption requests on a +single core using (async) concurrency primitives. +The asynchronous model optimizes the use of CPU resources by allowing the CPU to perform other tasks +while waiting for I/O operations to complete. + +Finally, batching can be used to further optimize the performance of encryption and decryption +operations. Batching allows multiple encryption or decryption operations to be performed in a +single request, reducing the overhead of making multiple requests to the KMS. + +### Efficient batching + +#### The KMIP way + +Batching in KMIP is achieved by sending multiple `Operation`s in a single KMIP `Message` operation. +The protocol is extremely flexible and allows for a wide range of operations to be batched together. + +The Cosmian KMS supports batching using the KMIP protocol. + +#### Optimized batching + +However, the overhead of the KMIP protocol can be significant, especially for small data sizes. +Each `Operation` in a KMIP message carries a significant amount of metadata, which can be +prohibitively expensive for small data sizes. + +When batching encryption or decryption requests, it is likely that the metadata is identical for +each request, and the overhead of unnecessarily sending the metadata multiple times can be +significant. + +To address this issue, the Cosmian KMS provides an optimized batching API that allows multiple +encryption or decryption requests to be batched together in a single request, without the overhead +of the KMIP protocol. The optimized batching API is designed to be lightweight and efficient, +allowing multiple encryption or decryption requests to be batched together with minimal overhead. + +The method is to use a single `Encrypt` or `Decrypt` operation with multiple data items encoded in +the `data` field of the request. + +#### Encoding scheme + +The encoding scheme is called `BulkData` and encodes an array of items, each item being an array of +bytes. It works as follows + +- the encoded data starts with the 2-byte fixed sequence `0x87 0x87` +- followed by the unsigned leb128 encoded number of items in the array +- followed, for each item, by + - the unsigned leb128 encoded byte length of the item + - the item itself + +```text +BulkData = 0x87 0x87 ... + +number of items = leb128 encoded number of items +item 1 length = leb128 encoded length of item 1 +``` + +When the server receives an `Encrypt` or `Decrypt` operation and detects the header `0x87 0x87`, it +attempts to decode the data as a `BulkData` array. If the decoding is unsuccessful, the server +falls back to the standard KMIP protocol (i.e., single item encryption or decryption). + +If the decoding is successful, the server processes each item in the array as a separate encryption +or decryption request and re-encodes the results. + +When processing symmetric encryption results, the server will first, for each encrypted item, +concatenate the IV, the ciphertext, and the MAC, and then encode the result as a single item in the +`BulkData` array. + +For AES-GCM encryption, the concatenation is as follows: + +- the IV (12 bytes) +- the ciphertext (same size as the plaintext) +- the MAC (16 bytes) + +### Performance heuristics + +The Cosmian KMS uses heuristics to determine the optimal batch size for encryption and decryption +requests. The heuristics take into account the size of the data, the number of cores available, and +the expected latency of the KMS. + +Typically, for 64-byte data items, the optimal batch size is around 100,000 items. With these +kinds of batch sizes, each CPU core should be sent an average of five batches to maximize +concurrency. + +Hence, to encrypt 5 million messages on 10 core machines, 50 requests of 100,000 items each +should be sent in parallel. On a standard server CPU, the total processing time should be around 8 +seconds, excluding network latency. + +## Using the `ckms` CLI client + +The `ckms` CLI client can be used to encrypt and decrypt data locally, including large files. + +Encryption can be performed in two modes: + +- server side: the file data is sent server side and encrypted there. This mode is well suited + for small or medium files and where a direct encryption scheme is required. +- client side: the file data is encrypted locally using a hybrid encryption scheme with key + wrapping. This mode is well suited for any type of files, including large ones, and where + high performance is required. + +### Server side encryption and decryption + +When using server side encryption or decryption, the file content is sent to the server. To use +this method use the `encrypt` or `decrypt` command of the `ckms` CLI client WITHOUT specifying a +`--key-encryption-algorithm` option. + +Say, the KMS holds a 256-bit AES key with the ID `43d28ec7-7438-4d2c-a1a0-00379fa4fe5d` +and you want to encrypt a file `image.png` with AES 256-bit GCM encryption: + +```bash +ckms sym encrypt \ +--data-encryption-algorithm aes-gcm \ +--key-id 43d28ec7-7438-4d2c-a1a0-00379fa4fe5d \ +--output-file image.enc \ +image.png +``` + +To decrypt the file, use the `decrypt` command: + +```bash +ckms sym decrypt \ +--data-encryption-algorithm aes-gcm \ +--key-id 43d28ec7-7438-4d2c-a1a0-00379fa4fe5d \ +--output-file decrypted-image.png \ +image.enc +``` + +#### Available ciphers + +The following ciphers are available for server-side encryption and decryption: + +| Cipher | Description | NIST Certified? | +| ----------------- | -------------------------- | --------------- | +| aes-gcm | AES in Galois Counter Mode | yes | +| aes-xts | AES XTS | yes | +| aes-gcm-siv | AES GCM SIV | no | +| chacha20-poly1305 | ChaCha20 Poly1305 | no | + +When in doubt, use AES GCM with a 256-bit key. AES GCM is NIST-certified (as NIST SP +800–38D) and well suited for arbitrary encryption of data with length of up to 2^39–256 bits ~ 64 +GB. + +Please note that for AES XTS, that + +- the key size must be doubled to achieve the same security level: 256 bits for AES 128 and 512 bits + for AES 256. +- there is no authentication + +#### Format of the encrypted file + +The encrypted file is the concatenation of the IV (or Tweak for XTS), the ciphertext, and the +MAC (None for XTS). + +```bash +IV || Ciphertext || MAC +``` + +With these symmetric block ciphers, the size of the ciphertext is equal to the size of the +plaintext. + +The table below shows the size of the IV (tweak for XTS) and the MAC in bytes. + +| Cipher | IV size | MAC size | +| ----------------- | ------- | -------- | +| aes-gcm | 12 | 16 | +| aes-xts | 16 | 0 | +| aes-gcm-siv | 12 | 16 | +| chacha20-poly1305 | 12 | 16 | + +### Client side encryption and decryption + +When using client side encryption or decryption, the file content is encrypted locally using a +hybrid encryption scheme with key wrapping: + +- a random data encryption key (DEK) is generated. The key size is 256 bits for all schemes except + for AES, where the key size is 512 bits to provide 256 bits of classic security, 128 bits + post-quantum. +- the DEK is used to locally encrypt the file content using the specified + `--data-encryption-algorithm` for the data encryption mechanism (DEM). +- the DEK is server side encrypted (i.e., wrapped) using the specified + `--key-encryption-algorithm` for the key encryption mechanism (KEM) and the KMS key encryption + key (KEK) identified by `--key-id`. + +To use this method, use the `encrypt` or `decrypt` command and specify BOTH the +`--key-encryption-algorithm` and `--data-encryption-algorithm`. + +Say, the KMS holds a 256-bit AES KEK (key encryption key) with the ID +`43d28ec7-7438-4d2c-a1a0-00379fa4fe5d` and you want to client-side encrypt a file `image.png` +with AES-GCM encryption, the ephemeral KEK key being wrapped with RFC5649 (a.k.a. NIST key wrap): + +```bash +ckms sym encrypt \ +--data-encryption-algorithm aes-gcm \ +--key-encryption-algorithm rfc5649 \ +--key-id 43d28ec7-7438-4d2c-a1a0-00379fa4fe5d \ +--output-file image.enc \ +image.png +``` + +To decrypt the file, use the `decrypt` command: + +```bash +ckms sym decrypt \ +--data-encryption-algorithm aes-gcm \ +--key-encryption-algorithm rfc5649 \ +--key-id 43d28ec7-7438-4d2c-a1a0-00379fa4fe5d \ +--output-file decrypted-image.png \ +image.enc +``` + +#### Available ciphers + +The following ciphers are available for client-side encryption and decryption: + +* Data Encryption * + +| Cipher | Description | NIST Certified? | +| ----------------- | -------------------------- | --------------- | +| aes-gcm | AES in Galois Counter Mode | yes | +| aes-xts | AES XTS | yes | +| chacha20-poly1305 | ChaCha20 Poly1305 | no | + +* Key Wrapping (Encryption) * + +| Cipher | Description | NIST Certified? | +| ----------------- | -------------------------- | --------------- | +| rfc5649 | NIST Key Wrap | yes | +| aes-gcm | AES in Galois Counter Mode | yes | +| aes-xts | AES XTS | yes | +| aes-gcm-siv | AES GCM SIV | no | +| chacha20-poly1305 | ChaCha20 Poly1305 | no | + +When in doubt, use the AES GCM data encryption scheme with the AES GCM key encryption scheme (or +RFC5649) with a 256-bit key. These are the most widely used schemes, and they are NIST-certified. + +#### Format of the encrypted file + +The encrypted file is the concatenation of + +- the length of the key wrapping (a.k.a. encapsulation) in unsigned LEB 128 format +- the key encapsulation +- the data encryption mechanism (DEM) IV (or tweak for XTS) +- the ciphertext (same size as the plaintext) +- the data encryption mechanism (DEM) MAC + +```bash +encapsulation length || encapsulation || DEM IV || Ciphertext || DEM MAC +``` + +The key `encapsulation` is the concatenation of + +- the key encryption mechanism (KEM) IV (or tweak for XTS, none for RFC5649) +- the encrypted DEK (same length as the DEK, +8 bytes for RFC5649) +- the key encryption mechanism (KEM) MAC (none for XTS and RFC5649) + +```bash +KEM IV || Encrypted DEK || KEM MAC +``` + +Using AES GCM as a KEM and a DEM, the details will be as follows: + +- 1 unsigned LEB 128 byte holding the length of the encapsulation (60) +- 60 bytes of encapsulation decomposed in : + - 12 byte KEM IV + - 32 bytes encrypted DEK + - 16 byte KEM MAC +- 12 bytes of DEM IV +- x bytes of ciphertext (same size as plaintext) +- 16 bytes of DEM MAC diff --git a/documentation/docs/high_performance_encryption_decryption.md b/documentation/docs/high_performance_encryption_decryption.md deleted file mode 100644 index 267e0286..00000000 --- a/documentation/docs/high_performance_encryption_decryption.md +++ /dev/null @@ -1,91 +0,0 @@ -

High performance encryption and decryption

- -The Cosmian KMS is particularly suited for client-side encryption scenarios which may require high -performance encryption and decryption. The KMS provides a high performance encryption and decryption -API that can be used to encrypt and decrypt data at scale. - -## Parallelization, concurrency, and batching - -Dur to its stateless user sesison model, the Cosmian KMS is designed to take advantage of modern -multicore processors and can parallelize encryption and decryption operations across multiple -cores. Parallelization can be achieved by scaling vertically (increasing the number of cores on a -single machine) or horizontally (increasing the number of machines in a cluster). - -The Cosmian KMS can also handle multiple concurrent encryption and decryption requests on a -single core using (async) concurrency primitivies. The asynchronous model optimizes the use of -CPU resources by allowing the CPU to perform other tasks while waiting for I/O operations to -complete. - -FInally, batching can be used to further optimize the performance of encryption and decryption -operations. Batching allows multiple encryption or decryption operations to be performed in a -single request, reducing the overhead of making multiple requests to the KMS. - -## Batching, the KMIP way - -Batching in KMIP is achieved by sending multiple `Operation`s in a single KMIP `Message` operation. -The protocol is extremely flexible and allows for a wide range of operations to be batched together. - -The Cosmian KMS supports batching using the KMIP protocol. - -## Optimized batching - -However, the overhead of the KMIP protocol can be significant, especially for small data sizes. -Each `Operation` in a KMIP message carries a significant amount of metadata, which can be -prohibitively expensive for small data sizes. - -When batching encryption or decryption requests, it is likely that the metadata is identical for -each request, and the overhead of unnecessarily sending the metadata multiple times can be -significant. - -To address this issue, the Cosmian KMS provides an optimized batching API that allows multiple -encryption or decryption requests to be batched together in a single request, without the overhead -of the KMIP protocol. The optimized batching API is designed to be lightweight and efficient, -allowing multiple encryption or decryption requests to be batched together with minimal overhead. - -The method is to use a single `Encrypt` or `Decrypt` operation with multiple data items encoded in -the `data` field of the request. - -### Encoding scheme - -The encoding scheme is called `BulkData` and encodes an array of items, each item being an array of -bytes. It works as follows - -- the encoded data starts with the 2-byte fixed sequence `0x87 0x87` -- followed by the unsigned leb128 encoded number of items in the array -- followed, for each item, by - - the unsigned leb128 encoded byte length of the item - - the item itself - -When the server receives an `Encrypt` or `Decrypt` operation and detects the header `0x87 0x87`, it -attempts to decode the data as a `BulkData` array. If the decoding is unsuccessful, the server -falls back to the standard KMIP protocol (i.e., single item encryption or decryption). - -If the decoding is successful, the server processes each item in the array as a separate encryption -or decryption request and re-encodes the results. - -When processing symmetric encryption results, the server will first, for each encrypted item, -concatenate the IV, the ciphertext and the MAC, and then encode the result as a single item in the -`BulkData` array. - -For AES-GCM encryption, the concatenation is as follows: - -- the IV (12 bytes) -- the ciphertext (same size as the plaintext) -- the MAC (16 bytes) - -## Performance heuristics - -The Cosmian KMS uses heuristics to determine the optimal batch size for encryption and decryption -requests. The heuristics take into account the size of the data, the number of cores available, and -the expected latency of the KMS. - -Typically, for 64-byte data items, the optimal batch size is around 100,000 items. With these -kinds of batch sizes, each CPU core should be sent an average of five batches to maximize -concurrency. - -Hence, to encrypt 5 million messages on 10 core machines, 50 requests of 100,000 items each -should be sent in parallel. On a standard server CPU, the total processing time should be around 8 -seconds, excluding network latency. - - - diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml index 06726de9..4a3ccb31 100644 --- a/documentation/mkdocs.yml +++ b/documentation/mkdocs.yml @@ -52,7 +52,7 @@ nav: - FIPS 140-3: fips.md - Zeroization: zeroization.md - Cryptographic algorithms: algorithms.md - - High performance encryption and decryption: high_performance_encryption_decryption.md + - Encrypting and decrypting at scale: encrypting_and_decrypting_at_scale.md - Enabling TLS: tls.md - Logging and telemetry: logging.md - Configuring database: database.md