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:
parent
fc93250539
commit
e788a2c982
49 changed files with 2410 additions and 903 deletions
|
@ -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:
|
||||
|
|
|
@ -4,3 +4,4 @@ MinAlertLevel = suggestion
|
|||
|
||||
[*.md]
|
||||
BasedOnStyles = Vale
|
||||
Vale.Spelling = NO
|
||||
|
|
11
CHANGELOG.md
11
CHANGELOG.md
|
@ -6,15 +6,20 @@ All notable changes to this project will be documented in this file.
|
|||
|
||||
### 🚀 Features
|
||||
|
||||
- Google Workspace Client-Side-Encryption (CSE) updates ([#319](https://github.com/Cosmian/kms/pull/319))
|
||||
- Generate Google S/MIME key-pairs and identities and upload them to Gmail API from ckms CLI ([#270](https://github.com/Cosmian/kms/issues/270))
|
||||
- Google Workspace Client-Side-Encryption (CSE)
|
||||
updates ([#319](https://github.com/Cosmian/kms/pull/319))
|
||||
- Generate Google S/MIME key-pairs and identities and upload them to Gmail API from ckms
|
||||
CLI ([#270](https://github.com/Cosmian/kms/issues/270))
|
||||
- Server-side, export cert at PKCS7 format
|
||||
- Implement missing CSE endpoints
|
||||
- Wrap/unwrap CSE elements with authenticated encryption
|
||||
- Export wrapped keys from KMS specifying the cipher mode
|
||||
- Handle auth for guest users ([#271](https://github.com/Cosmian/kms/issues/271))
|
||||
- Add SetAttribute/DeleteAttribute KMIP operations ([#303](https://github.com/Cosmian/kms/pull/303))
|
||||
- Reenable wrap/unwrap on ckms by linking statically on openssl ([#317](https://github.com/Cosmian/kms/pull/317))
|
||||
- Re-enable wrap/unwrap on ckms by linking statically on openssl ([#317](<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
17
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -62,6 +62,7 @@ impl EncryptAction {
|
|||
None,
|
||||
data,
|
||||
None,
|
||||
None,
|
||||
self.authentication_data
|
||||
.as_deref()
|
||||
.map(|s| s.as_bytes().to_vec()),
|
||||
|
|
|
@ -96,6 +96,7 @@ impl EncryptAction {
|
|||
data,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(to_cryptographic_parameters(
|
||||
self.encryption_algorithm,
|
||||
self.hash_fn,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 */
|
||||
)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ pyo3 = ["dep:pyo3"]
|
|||
fips = []
|
||||
|
||||
[dependencies]
|
||||
aes-gcm-siv = "0.11.1"
|
||||
argon2 = "0.5"
|
||||
base64 = { workspace = true }
|
||||
bitflags = "2.6"
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
///
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
95
crate/kmip/src/crypto/symmetric/aes_gcm_siv_not_openssl.rs
Normal file
95
crate/kmip/src/crypto/symmetric/aes_gcm_siv_not_openssl.rs
Normal 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()))
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
533
crate/kmip/src/crypto/symmetric/symmetric_ciphers.rs
Normal file
533
crate/kmip/src/crypto/symmetric/symmetric_ciphers.rs
Normal 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(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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,
|
||||
)?,
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -292,6 +292,7 @@ pub(crate) async fn encrypt(
|
|||
cleartext,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(cryptographic_parameters.to_owned()),
|
||||
)
|
||||
.unwrap();
|
||||
|
|
|
@ -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::{
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
289
documentation/docs/encrypting_and_decrypting_at_scale.md
Normal file
289
documentation/docs/encrypting_and_decrypting_at_scale.md
Normal 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
|
||||
800–38D) and well suited for arbitrary encryption of data with length of up to 2^39–256 bits ~ 64
|
||||
GB.
|
||||
|
||||
Please note that for AES XTS, that
|
||||
|
||||
- the key size must be doubled to achieve the same security level: 256 bits for AES 128 and 512 bits
|
||||
for AES 256.
|
||||
- there is no authentication
|
||||
|
||||
#### Format of the encrypted file
|
||||
|
||||
The encrypted file is the concatenation of the IV (or Tweak for XTS), the ciphertext, and the
|
||||
MAC (None for XTS).
|
||||
|
||||
```bash
|
||||
IV || Ciphertext || MAC
|
||||
```
|
||||
|
||||
With these symmetric block ciphers, the size of the ciphertext is equal to the size of the
|
||||
plaintext.
|
||||
|
||||
The table below shows the size of the IV (tweak for XTS) and the MAC in bytes.
|
||||
|
||||
| Cipher | IV size | MAC size |
|
||||
| ----------------- | ------- | -------- |
|
||||
| aes-gcm | 12 | 16 |
|
||||
| aes-xts | 16 | 0 |
|
||||
| aes-gcm-siv | 12 | 16 |
|
||||
| chacha20-poly1305 | 12 | 16 |
|
||||
|
||||
### Client side encryption and decryption
|
||||
|
||||
When using client side encryption or decryption, the file content is encrypted locally using a
|
||||
hybrid encryption scheme with key wrapping:
|
||||
|
||||
- a random data encryption key (DEK) is generated. The key size is 256 bits for all schemes except
|
||||
for AES, where the key size is 512 bits to provide 256 bits of classic security, 128 bits
|
||||
post-quantum.
|
||||
- the DEK is used to locally encrypt the file content using the specified
|
||||
`--data-encryption-algorithm` for the data encryption mechanism (DEM).
|
||||
- the DEK is server side encrypted (i.e., wrapped) using the specified
|
||||
`--key-encryption-algorithm` for the key encryption mechanism (KEM) and the KMS key encryption
|
||||
key (KEK) identified by `--key-id`.
|
||||
|
||||
To use this method, use the `encrypt` or `decrypt` command and specify BOTH the
|
||||
`--key-encryption-algorithm` and `--data-encryption-algorithm`.
|
||||
|
||||
Say, the KMS holds a 256-bit AES KEK (key encryption key) with the ID
|
||||
`43d28ec7-7438-4d2c-a1a0-00379fa4fe5d` and you want to client-side encrypt a file `image.png`
|
||||
with AES-GCM encryption, the ephemeral KEK key being wrapped with RFC5649 (a.k.a. NIST key wrap):
|
||||
|
||||
```bash
|
||||
ckms sym encrypt \
|
||||
--data-encryption-algorithm aes-gcm \
|
||||
--key-encryption-algorithm rfc5649 \
|
||||
--key-id 43d28ec7-7438-4d2c-a1a0-00379fa4fe5d \
|
||||
--output-file image.enc \
|
||||
image.png
|
||||
```
|
||||
|
||||
To decrypt the file, use the `decrypt` command:
|
||||
|
||||
```bash
|
||||
ckms sym decrypt \
|
||||
--data-encryption-algorithm aes-gcm \
|
||||
--key-encryption-algorithm rfc5649 \
|
||||
--key-id 43d28ec7-7438-4d2c-a1a0-00379fa4fe5d \
|
||||
--output-file decrypted-image.png \
|
||||
image.enc
|
||||
```
|
||||
|
||||
#### Available ciphers
|
||||
|
||||
The following ciphers are available for client-side encryption and decryption:
|
||||
|
||||
* Data Encryption *
|
||||
|
||||
| Cipher | Description | NIST Certified? |
|
||||
| ----------------- | -------------------------- | --------------- |
|
||||
| aes-gcm | AES in Galois Counter Mode | yes |
|
||||
| aes-xts | AES XTS | yes |
|
||||
| chacha20-poly1305 | ChaCha20 Poly1305 | no |
|
||||
|
||||
* Key Wrapping (Encryption) *
|
||||
|
||||
| Cipher | Description | NIST Certified? |
|
||||
| ----------------- | -------------------------- | --------------- |
|
||||
| rfc5649 | NIST Key Wrap | yes |
|
||||
| aes-gcm | AES in Galois Counter Mode | yes |
|
||||
| aes-xts | AES XTS | yes |
|
||||
| aes-gcm-siv | AES GCM SIV | no |
|
||||
| chacha20-poly1305 | ChaCha20 Poly1305 | no |
|
||||
|
||||
When in doubt, use the AES GCM data encryption scheme with the AES GCM key encryption scheme (or
|
||||
RFC5649) with a 256-bit key. These are the most widely used schemes, and they are NIST-certified.
|
||||
|
||||
#### Format of the encrypted file
|
||||
|
||||
The encrypted file is the concatenation of
|
||||
|
||||
- the length of the key wrapping (a.k.a. encapsulation) in unsigned LEB 128 format
|
||||
- the key encapsulation
|
||||
- the data encryption mechanism (DEM) IV (or tweak for XTS)
|
||||
- the ciphertext (same size as the plaintext)
|
||||
- the data encryption mechanism (DEM) MAC
|
||||
|
||||
```bash
|
||||
encapsulation length || encapsulation || DEM IV || Ciphertext || DEM MAC
|
||||
```
|
||||
|
||||
The key `encapsulation` is the concatenation of
|
||||
|
||||
- the key encryption mechanism (KEM) IV (or tweak for XTS, none for RFC5649)
|
||||
- the encrypted DEK (same length as the DEK, +8 bytes for RFC5649)
|
||||
- the key encryption mechanism (KEM) MAC (none for XTS and RFC5649)
|
||||
|
||||
```bash
|
||||
KEM IV || Encrypted DEK || KEM MAC
|
||||
```
|
||||
|
||||
Using AES GCM as a KEM and a DEM, the details will be as follows:
|
||||
|
||||
- 1 unsigned LEB 128 byte holding the length of the encapsulation (60)
|
||||
- 60 bytes of encapsulation decomposed in :
|
||||
- 12 byte KEM IV
|
||||
- 32 bytes encrypted DEK
|
||||
- 16 byte KEM MAC
|
||||
- 12 bytes of DEM IV
|
||||
- x bytes of ciphertext (same size as plaintext)
|
||||
- 16 bytes of DEM MAC
|
|
@ -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.
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue