feat: AES GCM SIV + AES XTS + Client-side encryption with ckms (#328)

* stream cipher alpha

* need to fix file read ahead of tag

* need to fix file read ahead of tag

* encrypt decrypt stream beta

* almost, alsmost there on client side

* added nonce, but still....

* fix client streaming error

* fixed RFC5649

* all tests symmetric

* lints

* fixed covercrypt tests

* python test

* documentation

* rebased on develop

* chore: PR review

---------

Co-authored-by: Manuthor <manu.coste@gmail.com>
This commit is contained in:
Bruno Grieder 2024-10-08 20:38:00 +02:00 committed by GitHub
parent fc93250539
commit e788a2c982
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 2410 additions and 903 deletions

View file

@ -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:

View file

@ -4,3 +4,4 @@ MinAlertLevel = suggestion
[*.md]
BasedOnStyles = Vale
Vale.Spelling = NO

View file

@ -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](<https://github>.
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

17
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -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 }

View file

@ -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()),

View file

@ -62,6 +62,7 @@ impl EncryptAction {
None,
data,
None,
None,
self.authentication_data
.as_deref()
.map(|s| s.as_bytes().to_vec()),

View file

@ -96,6 +96,7 @@ impl EncryptAction {
data,
None,
None,
None,
Some(to_cryptographic_parameters(
self.encryption_algorithm,
self.hash_fn,

View file

@ -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<String>,
/// 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<KeyEncryptionAlgorithm>,
/// 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<PathBuf>,
/// 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<String>,
}
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::<Vec<_>>();
let tag = data.drain(data.len() - 16..).collect::<Vec<_>>();
// 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<u8>,
aad: Option<Vec<u8>>,
) -> CliResult<Zeroizing<Vec<u8>>> {
// 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::<Vec<_>>();
let tag = ciphertext
.drain(ciphertext.len() - tag_size..)
.collect::<Vec<_>>();
// 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<Vec<u8>>,
) -> 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(())
}
}

View file

@ -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<String>,
/// 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<KeyEncryptionAlgorithm>,
/// 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<PathBuf>,
/// 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<String>,
/// 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<String>,
}
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<Vec<u8>>,
plaintext: Vec<u8>,
authenticated_data: Option<Vec<u8>>,
) -> Result<(Vec<u8>, Vec<u8>, Vec<u8>), 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<Vec<u8>>,
input_file_name: &Path,
output_file: &mut File,
aad: Option<Vec<u8>>,
) -> CliResult<Zeroizing<Vec<u8>>> {
// 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<u8> = [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)
}
}

View file

@ -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,

View file

@ -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<DataEncryptionAlgorithm> 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<KeyEncryptionAlgorithm> 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()
},
}
}
}

View file

@ -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

View file

@ -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<KeyEncryptionAlgorithm>,
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<KeyEncryptionAlgorithm>,
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<KeyEncryptionAlgorithm>,
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 */
)
}

View file

@ -18,6 +18,7 @@ pyo3 = ["dep:pyo3"]
fips = []
[dependencies]
aes-gcm-siv = "0.11.1"
argon2 = "0.5"
base64 = { workspace = true }
bitflags = "2.6"

View file

@ -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<Public>, plaintext: &[u8]) -> Result<V
let iv = ecies_get_iv(Q.public_key(), R.public_key(), curve, aead.nonce_size(), md)?;
// Encrypt data using the provided.
let (ciphertext, tag) = aead_encrypt(aead, &key, &iv, &[], plaintext)?;
let (ciphertext, tag) = encrypt(aead, &key, &iv, &[], plaintext)?;
let R_bytes = R
.public_key()
@ -145,16 +145,16 @@ pub(crate) fn ecies_decrypt(
let key = ecies_get_key(&S, curve, aead.key_size(), md)?;
// We could use ou own aead to offer more DEM options.
let plaintext = aead_decrypt(aead, &key, &iv, &[], ct, tag)?;
let plaintext = decrypt(aead, &key, &iv, &[], ct, tag)?;
Ok(plaintext)
}
fn aead_and_digest(curve: &EcGroupRef) -> 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),
};

View file

@ -78,6 +78,7 @@ pub fn build_encryption_request(
encryption_policy: Option<String>,
plaintext: Vec<u8>,
header_metadata: Option<Vec<u8>>,
nonce: Option<Vec<u8>>,
authentication_data: Option<Vec<u8>>,
cryptographic_parameters: Option<CryptographicParameters>,
) -> Result<Encrypt, KmipError> {
@ -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,

View file

@ -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
/// <http://docs.oasis-open.org/pkcs11/pkcs11-curr/v2.40/cos01/pkcs11-curr-v2.40-cos01.html>#_Toc408226908
///

View file

@ -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<BlockCipherMode>,
key_size: usize,
) -> Result<Self, KmipError> {
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<Vec<u8>, 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<Zeroizing<Vec<u8>>, 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<u8>, Vec<u8>), 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<Zeroizing<Vec<u8>>, 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);
}
}

View file

@ -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<AES_256_GCM_KEY_LENGTH>,
}
impl AesGcmSystem {
pub fn instantiate(uid: &str, symmetric_key: &Object) -> Result<Self, KmipError> {
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<EncryptResponse, KmipError> {
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<DecryptResponse, KmipError> {
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,
})
}
}

View file

@ -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<u8>, Vec<u8>), 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::<Aes128GcmSiv>::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::<Aes256GcmSiv>::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<Zeroizing<Vec<u8>>, 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::<Aes128GcmSiv>::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::<Aes256GcmSiv>::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()))
}

View file

@ -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;

View file

@ -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
);
}
}

View file

@ -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<Mode> 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<Cipher, KmipError> {
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<BlockCipherMode>,
key_size: usize,
) -> Result<Self, KmipError> {
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, KmipError> {
StreamCipher::new(*self, mode, key, nonce, aad)
}
}
/// Generate a random nonce for the given symmetric cipher.
pub fn random_nonce(sym_cipher: SymCipher) -> Result<Vec<u8>, 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<Zeroizing<Vec<u8>>, 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<u8>, Vec<u8>), 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<Zeroizing<Vec<u8>>, 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<u8>,
}
impl StreamCipher {
fn new(
sym_cipher: SymCipher,
mode: Mode,
key: &[u8],
nonce: &[u8],
aad: &[u8],
) -> Result<Self, KmipError> {
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<Vec<u8>, 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<u8>, Vec<u8>), 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<Vec<u8>, 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(),
)),
}
}
}

View file

@ -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::<u8>::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::<u8>::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::<u8>::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()
);
}

View file

@ -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,

View file

@ -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(),
);

View file

@ -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)]

View file

@ -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 {

View file

@ -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

View file

@ -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;

View file

@ -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,

View file

@ -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<Vec<u8>>, AeadCipher), KmsError> {
) -> Result<(Zeroizing<Vec<u8>>, 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(),

View file

@ -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<EncryptResponse> {
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 <BulkData as Into<Vec<Zeroizing<Vec<u8>>>>>::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<EncryptResponse> {
let (key_bytes, aead) = get_aead_and_key(request, owm)?;
fn encrypt_with_symmetric_key(
request: &Encrypt,
owm: &ObjectWithMetadata,
) -> KResult<EncryptResponse> {
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<Enc
.authenticated_encryption_additional_data
.as_deref()
.unwrap_or(EMPTY_SLICE);
let (ciphertext, tag) = aead_encrypt(aead, &key_bytes, &nonce, aad, plaintext)?;
let (ciphertext, tag) = sym_encrypt(aead, &key_bytes, &nonce, aad, plaintext)?;
Ok(EncryptResponse {
unique_identifier: UniqueIdentifier::TextString(owm.id.clone()),
data: Some(ciphertext),
@ -235,10 +244,10 @@ fn encrypt_with_aead(request: &Encrypt, owm: &ObjectWithMetadata) -> KResult<Enc
})
}
fn get_aead_and_key(
fn get_cipher_and_key(
request: &Encrypt,
owm: &ObjectWithMetadata,
) -> KResult<(Zeroizing<Vec<u8>>, AeadCipher)> {
) -> KResult<(Zeroizing<Vec<u8>>, 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(),

View file

@ -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},

View file

@ -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,

View file

@ -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;

View file

@ -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),

View file

@ -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());

View file

@ -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,
)?,

View file

@ -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 }

View file

@ -292,6 +292,7 @@ pub(crate) async fn encrypt(
cleartext,
None,
None,
None,
Some(cryptographic_parameters.to_owned()),
)
.unwrap();

View file

@ -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::{

View file

@ -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<br />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

View file

@ -280,7 +280,7 @@ Possible values: `"true", "false"` [default: `"false"`]
`--block-cipher-mode [-m] <BLOCK_CIPHER_MODE>` 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_ADDITIONAL_DATA>` Authenticated encryption additional data
@ -1027,7 +1027,7 @@ Possible values: `"true", "false"` [default: `"false"`]
`--block-cipher-mode [-m] <BLOCK_CIPHER_MODE>` 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_ADDITIONAL_DATA>` Authenticated encryption additional data
@ -1487,7 +1487,7 @@ Possible values: `"true", "false"` [default: `"false"`]
`--block-cipher-mode [-m] <BLOCK_CIPHER_MODE>` 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_ADDITIONAL_DATA>` 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>` 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_ADDITIONAL_DATA>` 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] <FILE>
@ -1960,11 +1960,21 @@ Encrypt a file using AES GCM
`--key-id [-k] <KEY_ID>` The symmetric key unique identifier. If not specified, tags should be specified
`--data-encryption-algorithm [-d] <DATA_ENCRYPTION_ALGORITHM>` 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] <KEY_ENCRYPTION_ALGORITHM>` 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>` 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] <OUTPUT_FILE>` The encrypted output file path
`--authentication-data [-a] <AUTHENTICATION_DATA>` Optional authentication data. This data needs to be provided back for decryption
`--nonce [-n] <NONCE>` Optional nonce/IV (or tweak for XTS) as a hex string. If not provided, a random value is generated
`--authentication-data [-a] <AUTHENTICATION_DATA>` 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] <FILE>
@ -1982,11 +1992,19 @@ Decrypts a file using AES GCM
`--key-id [-k] <KEY_ID>` The private key unique identifier If not specified, tags should be specified
`--data-encryption-algorithm [-d] <DATA_ENCRYPTION_ALGORITHM>` 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] <KEY_ENCRYPTION_ALGORITHM>` 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>` 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] <OUTPUT_FILE>` The encrypted output file path
`--authentication-data [-a] <AUTHENTICATION_DATA>` Optional authentication data that was supplied during encryption
`--authentication-data [-a] <AUTHENTICATION_DATA>` Optional authentication data that was supplied during encryption as a hex string

View file

@ -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> <item 1 length> <item 1> ... <item n length> <item n>
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
80038D) and well suited for arbitrary encryption of data with length of up to 2^39256 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

View file

@ -1,91 +0,0 @@
<h1>High performance encryption and decryption</h1>
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.

View file

@ -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