diff --git a/CHANGELOG.md b/CHANGELOG.md index 09dae87994..52dc218c89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,16 @@ All notable changes to this project will be documented in this file. +## [5.15.0] - 2026-01-09 + +### ⚠️ WARNING + +Any previously **manually** exported keys in **JSON** format must be manually updated if they have been previously wrapped with AES. This can be done using the following command: + +```bash +sed -i 's/NISTKeyWrap/AESKeyWrapPadding/g' your_exported_key.json +``` + ## [5.14.1] - 2025-12-26 ### 🚀 Features diff --git a/crate/cli/src/actions/kms/symmetric/decrypt.rs b/crate/cli/src/actions/kms/symmetric/decrypt.rs index 10392330ea..e9baf71a27 100644 --- a/crate/cli/src/actions/kms/symmetric/decrypt.rs +++ b/crate/cli/src/actions/kms/symmetric/decrypt.rs @@ -390,7 +390,6 @@ impl DecryptAction { // determine the DEM parameters let dem_cryptographic_parameters: CryptographicParameters = data_encryption_algorithm.into(); - trace!("dek length {}", dek.len()); let sym_cipher = SymCipher::from_algorithm_and_key_size( dem_cryptographic_parameters diff --git a/crate/cli/src/actions/kms/symmetric/mod.rs b/crate/cli/src/actions/kms/symmetric/mod.rs index 00b6b4079e..fdb2a68bd4 100644 --- a/crate/cli/src/actions/kms/symmetric/mod.rs +++ b/crate/cli/src/actions/kms/symmetric/mod.rs @@ -51,6 +51,7 @@ pub enum KeyEncryptionAlgorithm { AesXts, #[cfg(feature = "non-fips")] AesGcmSiv, + RFC3394, RFC5649, } @@ -79,6 +80,11 @@ impl From for CryptographicParameters { ..Self::default() }, KeyEncryptionAlgorithm::RFC5649 => Self { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::AESKeyWrapPadding), + ..Self::default() + }, + KeyEncryptionAlgorithm::RFC3394 => Self { cryptographic_algorithm: Some(CryptographicAlgorithm::AES), block_cipher_mode: Some(BlockCipherMode::NISTKeyWrap), ..Self::default() diff --git a/crate/cli/src/tests/kms/secret_data/create_secret.rs b/crate/cli/src/tests/kms/secret_data/create_secret.rs index dcf24a6203..e43919eada 100644 --- a/crate/cli/src/tests/kms/secret_data/create_secret.rs +++ b/crate/cli/src/tests/kms/secret_data/create_secret.rs @@ -145,7 +145,11 @@ pub(crate) async fn test_secret_data_export_with_different_wrapping_algorithms() .await?; // Test different wrapping algorithms - let wrapping_algorithms = [WrappingAlgorithm::NistKeyWrap, WrappingAlgorithm::AesGCM]; + let wrapping_algorithms = [ + WrappingAlgorithm::AESKeyWrapPadding, // RFC 5649 + WrappingAlgorithm::NistKeyWrap, // RFC 3394 + WrappingAlgorithm::AesGCM, + ]; for (i, algorithm) in wrapping_algorithms.iter().enumerate() { let wrapped_file = tmp_path.join(format!("wrapped_secret_{i}.json")); diff --git a/crate/cli/src/tests/kms/shared/export.rs b/crate/cli/src/tests/kms/shared/export.rs index 113e8547ea..ccf3d3bf5c 100644 --- a/crate/cli/src/tests/kms/shared/export.rs +++ b/crate/cli/src/tests/kms/shared/export.rs @@ -193,7 +193,7 @@ pub(crate) async fn test_export_wrapped() -> KmsCliResult<()> { key_id: Some(private_key_id.to_string()), key_file: tmp_path.join("output.export"), wrap_key_id: Some(sym_key_id.to_string()), - wrapping_algorithm: Some(WrappingAlgorithm::NistKeyWrap), + wrapping_algorithm: Some(WrappingAlgorithm::AESKeyWrapPadding), ..Default::default() } .run(ctx.get_owner_client()) diff --git a/crate/cli/src/tests/kms/shared/export_import.rs b/crate/cli/src/tests/kms/shared/export_import.rs index d54cdcd1b1..6451ef78c9 100644 --- a/crate/cli/src/tests/kms/shared/export_import.rs +++ b/crate/cli/src/tests/kms/shared/export_import.rs @@ -48,7 +48,11 @@ pub(crate) async fn test_wrap_on_export_unwrap_on_import() -> KmsCliResult<()> { .to_string(); // Export and import the key with different block cipher modes - for wrapping_algorithm in [WrappingAlgorithm::AesGCM, WrappingAlgorithm::NistKeyWrap] { + for wrapping_algorithm in [ + WrappingAlgorithm::AesGCM, + WrappingAlgorithm::NistKeyWrap, + WrappingAlgorithm::AESKeyWrapPadding, + ] { debug!("wrapping algorithm: {wrapping_algorithm:?}",); ExportSecretDataOrKeyAction { key_id: Some(dek_id.clone()), diff --git a/crate/client_utils/src/export_utils.rs b/crate/client_utils/src/export_utils.rs index 7f1fa3f878..6223f75c99 100644 --- a/crate/client_utils/src/export_utils.rs +++ b/crate/client_utils/src/export_utils.rs @@ -35,7 +35,8 @@ pub enum ExportKeyFormat { #[derive(Debug, Clone, PartialEq, Eq, EnumString, ValueEnum)] #[strum(serialize_all = "kebab-case")] pub enum WrappingAlgorithm { - NistKeyWrap, + AESKeyWrapPadding, // RFC 5649 + NistKeyWrap, // RFC 3394 AesGCM, RsaPkcsV15Sha1, RsaPkcsV15, @@ -155,6 +156,11 @@ pub fn prepare_key_export_elements( wrapping_algorithm .as_ref() .map(|wrapping_algorithm| match wrapping_algorithm { + WrappingAlgorithm::AESKeyWrapPadding => CryptographicParameters { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + block_cipher_mode: Some(BlockCipherMode::AESKeyWrapPadding), + ..CryptographicParameters::default() + }, WrappingAlgorithm::NistKeyWrap => CryptographicParameters { cryptographic_algorithm: Some(CryptographicAlgorithm::AES), block_cipher_mode: Some(BlockCipherMode::NISTKeyWrap), diff --git a/crate/client_utils/src/symmetric_utils.rs b/crate/client_utils/src/symmetric_utils.rs index a144cdd0ab..acdb879d40 100644 --- a/crate/client_utils/src/symmetric_utils.rs +++ b/crate/client_utils/src/symmetric_utils.rs @@ -114,18 +114,15 @@ pub const AES_256_GCM_SIV_IV_LENGTH: usize = 12; #[cfg(feature = "non-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 3394 IV is actually a fixed overhead +pub const RFC3394_IV_LENGTH: usize = 0; +/// RFC3394 has no authentication. +pub const RFC3394_MAC_LENGTH: usize = 0; + // RFC 5649 IV is actually a fixed overhead -pub const RFC5649_32_IV_LENGTH: usize = 0; +pub const RFC5649_IV_LENGTH: usize = 0; /// RFC5649 has no authentication. -pub const RFC5649_32_MAC_LENGTH: usize = 0; +pub const RFC5649_MAC_LENGTH: usize = 0; #[cfg(feature = "non-fips")] /// Chacha20-Poly1305 key length in bytes. @@ -156,7 +153,8 @@ pub fn parse_decrypt_elements( } BlockCipherMode::CBC => (AES_128_CBC_IV_LENGTH, AES_128_CBC_MAC_LENGTH), BlockCipherMode::XTS => (AES_128_XTS_TWEAK_LENGTH, AES_128_XTS_MAC_LENGTH), - BlockCipherMode::NISTKeyWrap => (RFC5649_16_IV_LENGTH, RFC5649_16_MAC_LENGTH), + BlockCipherMode::AESKeyWrapPadding => (RFC5649_IV_LENGTH, RFC5649_MAC_LENGTH), + BlockCipherMode::NISTKeyWrap => (RFC3394_IV_LENGTH, RFC3394_MAC_LENGTH), _ => { return Err(UtilsError::Default( "Unsupported block cipher mode".to_owned(), diff --git a/crate/crypto/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs b/crate/crypto/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs index c6235a971e..9d65beeceb 100644 --- a/crate/crypto/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs +++ b/crate/crypto/src/crypto/rsa/ckm_rsa_aes_key_wrap.rs @@ -582,7 +582,7 @@ FQIDAQAB crypto_bail!("test_for_byok: RFC5649 pkeyutl failed: {output:?}"); } let rfc5649_encapsulation = fs::read(&rfc5649_encapsulation_file)?; - // Check against our implementation of NistKeyWrap + // Check against our implementation of AESKeyWrapPadding let rec_secret_bytes = rfc5649_unwrap( &rfc5649_encapsulation, &hex::decode(ephemeral) diff --git a/crate/crypto/src/crypto/symmetric/mod.rs b/crate/crypto/src/crypto/symmetric/mod.rs index a346b5eb71..956189ca60 100644 --- a/crate/crypto/src/crypto/symmetric/mod.rs +++ b/crate/crypto/src/crypto/symmetric/mod.rs @@ -1,6 +1,6 @@ pub mod symmetric_ciphers; -#[expect(clippy::indexing_slicing)] +pub mod rfc3394; pub mod rfc5649; #[cfg(feature = "non-fips")] diff --git a/crate/crypto/src/crypto/symmetric/rfc3394.rs b/crate/crypto/src/crypto/symmetric/rfc3394.rs new file mode 100644 index 0000000000..f9e4a62bf6 --- /dev/null +++ b/crate/crypto/src/crypto/symmetric/rfc3394.rs @@ -0,0 +1,208 @@ +//! AES Key Wrap (RFC 3394) without padding (KW) via rust-openssl. +//! Please prefer using the RFC 5649, as it's the current standard. This implementation is only made available to comply with API that still support legacy encryption standards. +//! +//! Spec references: +//! - RFC 3394: +//! - NIST SP 800-38F: +//! +//! Notes: +//! - Input must be a multiple of 8 bytes and at least 16 bytes (n >= 2 blocks). +//! - No padding is performed; for non-8-byte input lengths, use RFC 5649 (KWP). +use openssl::cipher::{Cipher, CipherRef}; +use openssl::cipher_ctx::CipherCtx; +use zeroize::Zeroizing; + +use crate::error::CryptoError; +use crate::error::result::CryptoResult; + +const AES_BLOCK_SIZE: usize = 16; // 128-bit +const AES_WRAP_BLOCK_SIZE: usize = 8; // 64-bit + +fn select_cipher(kek: &[u8]) -> CryptoResult<&CipherRef> { + Ok(match kek.len() { + 16 => Cipher::aes_128_wrap(), + 24 => Cipher::aes_192_wrap(), + 32 => Cipher::aes_256_wrap(), + _ => { + return Err(CryptoError::InvalidSize( + "The KEK size should be 16, 24 or 32 bytes".to_owned(), + )); + } + }) +} + +pub fn rfc3394_wrap(plaintext: &[u8], kek: &[u8]) -> CryptoResult> { + let n_bytes = plaintext.len(); + + // RFC 3394 requires plaintext to be at least 16 bytes and a multiple of 8 bytes. + if !n_bytes.is_multiple_of(AES_WRAP_BLOCK_SIZE) || n_bytes < 2 * AES_WRAP_BLOCK_SIZE { + return Err(CryptoError::InvalidSize( + "The plaintext size should be >= 16 and a multiple of 8".to_owned(), + )); + } + + let cipher = select_cipher(kek)?; + + // Initialize cipher context for encryption + let mut ctx = CipherCtx::new()?; + ctx.encrypt_init(Some(cipher), Some(kek), None)?; + + // Allocate output buffer: wrapped size is plaintext + 8 bytes (IV) + 2 extra blocks for cipher_final. + // The extra blocks will not propagate to the result as it's truncated to the actual size. Due to how the openssl library is programmed, + // not adding at least 1 extra block results in a panic. We chose to add two because that's how the openssl library operates when using this cipher. + let mut ciphertext = vec![0_u8; n_bytes + AES_WRAP_BLOCK_SIZE + (AES_BLOCK_SIZE * 2)]; + + // Perform the key wrap operation + let mut written = ctx.cipher_update(plaintext, Some(&mut ciphertext))?; + written += ctx.cipher_final(ciphertext.get_mut(written..).ok_or_else(|| { + CryptoError::IndexingSlicing("Buffer too small for cipher_final".to_owned()) + })?)?; + + // Truncate to actual output size. + ciphertext.truncate(written); + + Ok(ciphertext) +} + +pub fn rfc3394_unwrap(ciphertext: &[u8], kek: &[u8]) -> CryptoResult>> { + let n_bytes = ciphertext.len(); + + // RFC 3394 requires ciphertext to be at least 24 bytes (16 bytes plaintext + 8 bytes IV) and a multiple of 8. + if !n_bytes.is_multiple_of(AES_WRAP_BLOCK_SIZE) || n_bytes < 3 * AES_WRAP_BLOCK_SIZE { + return Err(CryptoError::InvalidSize( + "The ciphertext size should be >= 24 and a multiple of 8".to_owned(), + )); + } + + let cipher = select_cipher(kek)?; + + // Initialize cipher context for decryption. + let mut ctx = CipherCtx::new()?; + ctx.decrypt_init(Some(cipher), Some(kek), None)?; + + // Allocate output buffer: unwrapped size is ciphertext - 8 bytes (IV) + extra blocks for cipher_final. Same comments as above. + let mut plaintext = Zeroizing::new(vec![ + 0_u8; + n_bytes - AES_WRAP_BLOCK_SIZE + (AES_BLOCK_SIZE * 2) + ]); + + // Perform the key unwrap operation + let mut written = ctx.cipher_update(ciphertext, Some(&mut plaintext))?; + written += ctx.cipher_final(plaintext.get_mut(written..).ok_or_else(|| { + CryptoError::IndexingSlicing("Buffer too small for cipher_final".to_owned()) + })?)?; + + // Truncate to actual output size. + plaintext.truncate(written); + + Ok(plaintext) +} + +#[allow(clippy::unwrap_used, clippy::expect_used)] +#[cfg(test)] +mod tests { + use super::*; + use zeroize::Zeroizing; + + /// Helper to run wrap/unwrap roundtrip test + fn test_wrap_unwrap(kek_hex: &str, plaintext_hex: &str, expected_ciphertext_hex: &str) { + #[cfg(not(feature = "non-fips"))] + openssl::provider::Provider::load(None, "fips").unwrap(); + let kek = hex::decode(kek_hex).unwrap(); + let p = hex::decode(plaintext_hex).unwrap(); + let c_expected = hex::decode(expected_ciphertext_hex).unwrap(); + + let c = rfc3394_wrap(&p, &kek).unwrap(); + assert_eq!(c, c_expected, "Wrap output mismatch"); + + let p_unwrapped = rfc3394_unwrap(&c, &kek).unwrap(); + assert_eq!(p_unwrapped, Zeroizing::from(p), "Unwrap output mismatch"); + } + + // RFC 3394 test vectors with AES-128 KEK + #[test] + fn test_rfc3394_aes128_kek() { + // Section 4.1: 128-bit plaintext + test_wrap_unwrap( + "000102030405060708090A0B0C0D0E0F", + "00112233445566778899AABBCCDDEEFF", + "1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5", + ); + } + + // RFC 3394 test vectors with AES-192 KEK + #[test] + fn test_rfc3394_aes192_kek() { + let kek = "000102030405060708090A0B0C0D0E0F1011121314151617"; + + // Section 4.2: 128-bit plaintext + test_wrap_unwrap( + kek, + "00112233445566778899AABBCCDDEEFF", + "96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D", + ); + + // Section 4.4: 192-bit plaintext + test_wrap_unwrap( + kek, + "00112233445566778899AABBCCDDEEFF0001020304050607", + "031D33264E15D33268F24EC260743EDCE1C6C7DDEE725A936BA814915C6762D2", + ); + } + + // RFC 3394 test vectors with AES-256 KEK + #[test] + fn test_rfc3394_aes256_kek() { + let kek = "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"; + + // Section 4.3: 128-bit plaintext + test_wrap_unwrap( + kek, + "00112233445566778899AABBCCDDEEFF", + "64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7", + ); + + // Section 4.5: 192-bit plaintext + test_wrap_unwrap( + kek, + "00112233445566778899AABBCCDDEEFF0001020304050607", + "A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1", + ); + + // Section 4.6: 256-bit plaintext + test_wrap_unwrap( + kek, + "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F", + "28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21", + ); + } + + // Additional sanity: error cases + #[test] + fn test_errors() { + #[cfg(not(feature = "non-fips"))] + openssl::provider::Provider::load(None, "fips").unwrap(); + + // KEK length invalid + let kek_bad = [0x00_u8; 1]; + let p16 = [0x11_u8; 16]; + rfc3394_wrap(&p16, &kek_bad).unwrap_err(); + let c24 = [0x22_u8; 24]; + rfc3394_unwrap(&c24, &kek_bad).unwrap_err(); + + // Plaintext not multiple of 8 or too small + let kek16 = [0x01_u8; 16]; + let p15 = [0x33_u8; 15]; + rfc3394_wrap(&p15, &kek16).unwrap_err(); + let p8 = [0x44_u8; 8]; + rfc3394_wrap(&p8, &kek16).unwrap_err(); + + // Ciphertext too small + let c16 = [0x55_u8; 16]; + rfc3394_unwrap(&c16, &kek16).unwrap_err(); + + // Ciphertext not multiple of 8 + let c23 = [0x66_u8; 23]; + rfc3394_unwrap(&c23, &kek16).unwrap_err(); + } +} diff --git a/crate/crypto/src/crypto/symmetric/rfc5649.rs b/crate/crypto/src/crypto/symmetric/rfc5649.rs index ab52754245..438bb67914 100644 --- a/crate/crypto/src/crypto/symmetric/rfc5649.rs +++ b/crate/crypto/src/crypto/symmetric/rfc5649.rs @@ -1,294 +1,120 @@ -//! Symmetrically wrap keys using RFC 5649 available at: -//! -> +//! AES Key Wrap (RFC 5649) with padding (KWP) via rust-openssl. +//! This is the current standard for AES key wrapping according to the NIST SP 800-38F. +//! +//! Spec references: +//! - RFC 5649: +//! - NIST SP 800-38F: //! //! This RFC is an improvement of RFC 3394 and allows to wrap keys of any size. //! This is done by introducing an Integrity Check Register (ICR) of 64 bits. The //! encryption algorithm is fed blocks of 64 bits concatenated to the ICR for a -//! total of 128 bits blocks. AES in ECB mode is used since padding and integrity -//! check are done manually following the RFC. -//! -//! OpenSSL unfortunately does not provide a way to use AES-KWP directly. -//! See: -//! Google provides a patch : -//! and so does AWS: - -use openssl::symm::{Cipher, Crypter, Mode, encrypt}; +//! total of 128 bits blocks. +use openssl::cipher::{Cipher, CipherRef}; +use openssl::cipher_ctx::CipherCtx; use zeroize::Zeroizing; use crate::error::{CryptoError, result::CryptoResult}; -const DEFAULT_RFC5649_CONST: u32 = 0xA659_59A6_u32; -const DEFAULT_IV: u64 = 0xA6A6_A6A6_A6A6_A6A6; -const AES_WRAP_PAD_BLOCK_SIZE: usize = 0x8; -const AES_BLOCK_SIZE: usize = 0x10; - -/// Build the iv according to the RFC 5649. -/// -/// `wrapped_key_size` is the size of the key to wrap. -fn build_iv(wrapped_key_size: usize) -> CryptoResult { - // RFC 5649 AIV: 0xA65959A6 || MLI (MLI is 32-bit big-endian in the low 32 bits) - let mli_u32 = u32::try_from(wrapped_key_size)?; - Ok((u64::from(DEFAULT_RFC5649_CONST) << 32) | u64::from(mli_u32)) -} - -/// Check if the `iv` value obtained after decryption is appropriate according -/// to the RFC 5649. -fn check_iv(iv: u64, data: &[u8]) -> CryptoResult { - let data_size: usize = data.len(); - // High 32 bits must match the RFC 5649 constant - if u32::try_from(iv >> 32)? != DEFAULT_RFC5649_CONST { - return Ok(false); - } - - // Low 32 bits contain MLI (big-endian value placed in low 32 bits) - let low32 = u32::try_from(iv & 0xFFFF_FFFF)?; - let real_data_size = usize::try_from(low32)?; - if real_data_size > data_size || real_data_size <= (data_size - 8) { - return Ok(false); - } - - Ok(data[real_data_size..].iter().all(|&x| x == 0)) -} - -/// Wrap a plain text of variable length following RFC 5649. -/// -/// KEK stands for `Key-Encryption Key` -/// The function name matches the one used in the RFC and has no link to the -/// unwrap function in Rust. -pub fn rfc5649_wrap(plain: &[u8], kek: &[u8]) -> Result, CryptoError> { - let n = plain.len(); - let n_bytes_key = n % 8; - - // Pad the plaintext with null bytes. - let padded_plain = if n_bytes_key != 0 { - let missing_bytes = 8 - n_bytes_key; - [plain.to_vec(), vec![0_u8; missing_bytes]].concat() - } else { - plain.to_vec() - }; - - if n <= 8 { - // (C[0] | C[1]) = ENC(K, A | P[1]). - let iv_and_key = [ - &build_iv(n)?.to_be_bytes(), - &padded_plain[0..AES_WRAP_PAD_BLOCK_SIZE], - ] - .concat(); - - // Encrypt block using AES with ECB mode i.e. raw AES as specified in - // RFC5649. - let ciphertext = match kek.len() { - 16 => encrypt(Cipher::aes_128_ecb(), kek, None, &iv_and_key)?, - 24 => encrypt(Cipher::aes_192_ecb(), kek, None, &iv_and_key)?, - 32 => encrypt(Cipher::aes_256_ecb(), kek, None, &iv_and_key)?, - _ => { - return Err(CryptoError::InvalidSize( - "The kek size should be 16, 24 or 32".to_owned(), - )); - } - }; - - Ok(ciphertext[..AES_BLOCK_SIZE].to_vec()) - } else { - wrap_64(&padded_plain, kek, Some(build_iv(n)?)) - } -} - -/// Unwrap to a plain text of variable length according to RFC 5649. -/// -/// The function name matches the one used in the RFC and has no link to the -/// unwrap function in Rust. -pub fn rfc5649_unwrap(ciphertext: &[u8], kek: &[u8]) -> Result>, CryptoError> { - let n = ciphertext.len(); - - if !n.is_multiple_of(AES_WRAP_PAD_BLOCK_SIZE) || n < AES_BLOCK_SIZE { - return Err(CryptoError::InvalidSize( - "The ciphertext size should be >= 16 and a multiple of 16.".to_owned(), - )); - } - - if n > 16 { - let (iv, padded_plain) = unwrap_64(ciphertext, kek)?; +const AES_BLOCK_SIZE: usize = 16; // 128-bit +const AES_WRAP_BLOCK_SIZE: usize = 8; // 64-bit - // Verify integrity check register as described in RFC 5649. - if !check_iv(iv, &padded_plain)? { - return Err(CryptoError::InvalidSize( - "The ciphertext is invalid. Decrypted IV is not appropriate".to_owned(), - )); - } - - // Extract MLI (low 32 bits, big-endian numeric value) - let unpadded_size = usize::try_from(u32::try_from(iv & 0xFFFF_FFFF)?)?; - Ok(Zeroizing::from(padded_plain[0..unpadded_size].to_vec())) - } else { - // Encrypt block using AES with ECB mode i.e. raw AES as specified in - // RFC5649. - // Make use of OpenSSL Crypter interface to decrypt blocks incrementally - // without padding since RFC5649 has special padding methods. - let mut decrypt_cipher = match kek.len() { - 16 => Crypter::new(Cipher::aes_128_ecb(), Mode::Decrypt, kek, None)?, - 24 => Crypter::new(Cipher::aes_192_ecb(), Mode::Decrypt, kek, None)?, - 32 => Crypter::new(Cipher::aes_256_ecb(), Mode::Decrypt, kek, None)?, - _ => { - return Err(CryptoError::InvalidSize( - "The kek size should be 16, 24 or 32 bytes".to_owned(), - )); - } - }; - decrypt_cipher.pad(false); - - //// A | P[1] = DEC(K, C[0] | C[1]) - let mut plaintext = Zeroizing::from(vec![0; ciphertext.len() + AES_BLOCK_SIZE]); - let mut dec_len = decrypt_cipher.update(ciphertext, &mut plaintext)?; - dec_len += decrypt_cipher.finalize(&mut plaintext)?; - plaintext.truncate(dec_len); - - // Verify integrity check register as described in RFC 5649. - if !check_iv( - u64::from_be_bytes(plaintext[0..AES_WRAP_PAD_BLOCK_SIZE].try_into()?), - &plaintext[AES_WRAP_PAD_BLOCK_SIZE..16], - )? { +fn select_cipher_pad(kek: &[u8]) -> CryptoResult<&CipherRef> { + Ok(match kek.len() { + 16 => Cipher::aes_128_wrap_pad(), + 24 => Cipher::aes_192_wrap_pad(), + 32 => Cipher::aes_256_wrap_pad(), + _ => { return Err(CryptoError::InvalidSize( - "The ciphertext is invalid. Decrypted IV is not appropriate".to_owned(), + "The KEK size should be 16, 24 or 32 bytes".to_owned(), )); } + }) +} - let unpadded_size = usize::try_from(u32::from_be_bytes(plaintext[4..8].try_into()?))?; +pub fn rfc5649_wrap(plaintext: &[u8], kek: &[u8]) -> CryptoResult> { + const MAX_PLAINTEXT_LEN: u64 = 1 << 32; + let n_bytes = plaintext.len(); - Ok(Zeroizing::from( - plaintext[AES_WRAP_PAD_BLOCK_SIZE..(AES_WRAP_PAD_BLOCK_SIZE + unpadded_size)].to_vec(), - )) + // RFC 5649 requires plaintext to be at least 1 byte and less than 2^32 bytes + if n_bytes == 0 { + return Err(CryptoError::InvalidSize( + "The plaintext size should be at least 1 byte".to_owned(), + )); } -} - -/// Wrap a plain text of a 64-bits modulo size according to RFC 3394. -/// -/// The function name matches the one used in the RFC and has no link to the -/// unwrap function in Rust. -fn wrap_64(plain: &[u8], kek: &[u8], iv: Option) -> Result, CryptoError> { - let n = plain.len(); - if !n.is_multiple_of(AES_WRAP_PAD_BLOCK_SIZE) { + // Check maximum length (2^32 - 1 bytes as per NIST SP 800-38F Section 5.3.1) + if u64::try_from(n_bytes).is_ok_and(|n_bytes| n_bytes >= MAX_PLAINTEXT_LEN) { return Err(CryptoError::InvalidSize( - "The plaintext size should be a multiple of 8".to_owned(), + "The plaintext size should be less than 2^32 bytes".to_owned(), )); } - // Number of 64-bit blocks (block size for RFC 5649). - let n = n / AES_WRAP_PAD_BLOCK_SIZE; + let cipher = select_cipher_pad(kek)?; - // ICR stands for Integrity Check Register initially containing the IV. - let mut icr = iv.unwrap_or(DEFAULT_IV); - let mut blocks = Vec::with_capacity(n); + let mut ctx = CipherCtx::new()?; + ctx.encrypt_init(Some(cipher), Some(kek), None)?; - for chunk in plain.chunks(AES_WRAP_PAD_BLOCK_SIZE) { - blocks.push(u64::from_be_bytes(chunk.try_into()?)); - } - let cipher = match kek.len() { - 16 => Cipher::aes_128_ecb(), - 24 => Cipher::aes_192_ecb(), - 32 => Cipher::aes_256_ecb(), - _ => { - return Err(CryptoError::InvalidSize( - "The kek size should be 16, 24 or 32".to_owned(), - )); - } + // Calculate output size: for KWP, output is always a multiple of 8 bytes + // Minimum output is 16 bytes (2 "semi-blocks") + // The wrapped size includes the AIV (8 bytes) plus padded plaintext + let padded_len = if n_bytes <= AES_WRAP_BLOCK_SIZE { + AES_WRAP_BLOCK_SIZE // Special case: single block encryption + } else { + // Calculate padding: round up to next multiple of 8, then add 8 for AIV + let padded_plaintext_len = n_bytes.div_ceil(8) * 8; + padded_plaintext_len + 8 }; - for j in 0..6 { - for (i, block) in blocks.iter_mut().enumerate().take(n) { - // B = AES(K, A | R[i]) - let plaintext_block = ((u128::from(icr) << 64) | u128::from(*block)).to_be_bytes(); + // Allocate output buffer with extra space for cipher_final + let mut ciphertext = vec![0_u8; padded_len + (AES_BLOCK_SIZE * 2)]; - // Encrypt block using AES with ECB mode i.e. raw AES as specified in - // RFC5649. - let ciphertext = encrypt(cipher, kek, None, &plaintext_block)?; + // Perform the key wrap operation + let mut written = ctx.cipher_update(plaintext, Some(&mut ciphertext))?; + written += ctx.cipher_final(ciphertext.get_mut(written..).ok_or_else(|| { + CryptoError::IndexingSlicing("Buffer too small for cipher_final".to_owned()) + })?)?; - // A = MSB(64, B) ^ t where t = (n*j)+i - let t = u64::try_from((n * j) + (i + 1))?; + // Truncate to actual output size + ciphertext.truncate(written); - icr = u64::from_be_bytes(ciphertext[0..8].try_into()?) ^ t; - *block = u64::from_be_bytes(ciphertext[8..16].try_into()?); - } - } - - let mut wrapped_key = Vec::with_capacity(8 * (blocks.len() + 1)); - wrapped_key.extend(icr.to_be_bytes()); - for block in blocks { - wrapped_key.extend(block.to_be_bytes()); - } - - Ok(wrapped_key) + Ok(ciphertext) } -fn unwrap_64(ciphertext: &[u8], kek: &[u8]) -> Result<(u64, Zeroizing>), CryptoError> { - let n = ciphertext.len(); +pub fn rfc5649_unwrap(ciphertext: &[u8], kek: &[u8]) -> CryptoResult>> { + let cipher = select_cipher_pad(kek)?; + let n_bytes = ciphertext.len(); - if !n.is_multiple_of(AES_WRAP_PAD_BLOCK_SIZE) || n < AES_BLOCK_SIZE { + // RFC 5649 requires ciphertext to be at least 2 semi-blocks (so 1 block) and a multiple of 8 bytes (complete blocks) + if !n_bytes.is_multiple_of(AES_WRAP_BLOCK_SIZE) || n_bytes < 2 * AES_WRAP_BLOCK_SIZE { return Err(CryptoError::InvalidSize( "The ciphertext size should be >= 16 and a multiple of 8".to_owned(), )); } - // Number of 64-bit blocks minus 1 - let n = n / 8 - 1; + // Initialize cipher context for decryption + let mut ctx = CipherCtx::new()?; + ctx.decrypt_init(Some(cipher), Some(kek), None)?; - let mut blocks = Zeroizing::from(Vec::with_capacity(n + 1)); - for chunk in ciphertext.chunks(AES_WRAP_PAD_BLOCK_SIZE) { - blocks.push(u64::from_be_bytes(chunk.try_into()?)); - } + // Allocate output buffer: maximum plaintext size is ciphertext - 8 bytes (AIV) + // Add extra space for cipher_final + let mut plaintext = Zeroizing::new(vec![ + 0_u8; + n_bytes - AES_WRAP_BLOCK_SIZE + (AES_BLOCK_SIZE * 2) + ]); - // ICR stands for Integrity Check Register initially containing the IV. - let mut icr = blocks[0]; - - // Encrypt block using AES with ECB mode i.e. raw AES as specified in - // RFC5649. - // Make use of OpenSSL Crypter interface to decrypt blocks incrementally - // without padding since RFC5649 has special padding methods. - let mut decrypt_cipher = match kek.len() { - 16 => Crypter::new(Cipher::aes_128_ecb(), Mode::Decrypt, kek, None)?, - 24 => Crypter::new(Cipher::aes_192_ecb(), Mode::Decrypt, kek, None)?, - 32 => Crypter::new(Cipher::aes_256_ecb(), Mode::Decrypt, kek, None)?, - _ => { - return Err(CryptoError::InvalidSize( - "The kek size should be 16, 24 or 32".to_owned(), - )); - } - }; - decrypt_cipher.pad(false); - - for j in (0..6).rev() { - for (i, block) in blocks[1..].iter_mut().rev().enumerate().take(n) { - let t = u64::try_from((n * j) + (n - i))?; + // Perform the key unwrap operation + let mut written = ctx.cipher_update(ciphertext, Some(&mut plaintext))?; + written += ctx.cipher_final(plaintext.get_mut(written..).ok_or_else(|| { + CryptoError::IndexingSlicing("Buffer too small for cipher_final".to_owned()) + })?)?; - // B = AES-1(K, (A ^ t) | R[i]) where t = n*j+i - let big_i = ((u128::from(icr ^ t) << 64) | u128::from(*block)).to_be_bytes(); - let big_b = big_i.as_slice(); + // Truncate to actual output size (OpenSSL's wrap_pad removes padding automatically) + plaintext.truncate(written); - let mut plaintext = Zeroizing::from(vec![0; big_b.len() + AES_BLOCK_SIZE]); - let mut dec_len = decrypt_cipher.update(big_b, &mut plaintext)?; - dec_len += decrypt_cipher.finalize(&mut plaintext)?; - plaintext.truncate(dec_len); - - // A = MSB(64, B) - icr = u64::from_be_bytes(plaintext[0..AES_WRAP_PAD_BLOCK_SIZE].try_into()?); - - // R[i] = LSB(64, B) - *block = u64::from_be_bytes( - plaintext[AES_WRAP_PAD_BLOCK_SIZE..AES_WRAP_PAD_BLOCK_SIZE * 2].try_into()?, - ); - } - } - - let mut unwrapped_key = Zeroizing::from(Vec::with_capacity((blocks.len() - 1) * 8)); - for block in &blocks[1..] { - unwrapped_key.extend(block.to_be_bytes()); - } - - Ok((icr, unwrapped_key)) + Ok(plaintext) } -#[expect(clippy::unwrap_used, clippy::expect_used)] +#[expect(clippy::unwrap_used, clippy::expect_used, clippy::indexing_slicing)] #[cfg(test)] mod tests { use aes_kw::KeyInit; @@ -296,31 +122,25 @@ mod tests { use crate::crypto::symmetric::rfc5649::{rfc5649_unwrap, rfc5649_wrap}; - #[test] - pub(super) fn test_wrap1() { - const TEST_SIZE_LIMIT: usize = 100; + // Helper to load FIPS provider before each test (if needed) + fn setup_openssl_fips_provider() { #[cfg(not(feature = "non-fips"))] - // Load FIPS provider module from OpenSSL. - openssl::provider::Provider::load(None, "fips").unwrap(); + { + openssl::provider::Provider::load(None, "fips").unwrap(); + } + } - let kek = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; - let key_to_wrap = - b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F"; - let wrapped_key = [ - 199, 131, 191, 63, 110, 233, 156, 72, 218, 187, 196, 16, 226, 132, 197, 44, 191, 117, - 133, 120, 152, 157, 225, 138, 50, 148, 201, 164, 209, 151, 200, 162, 98, 112, 72, 139, - 28, 233, 128, 22, - ]; + #[test] + pub(super) fn test_wrap_unwrap_minimal() { + const TEST_SIZE_LIMIT: usize = 100; + setup_openssl_fips_provider(); + let kek = b"\x58\x40\xdf\x6e\x29\xb0\x2a\xf1\xab\x49\x3b\x70\x5b\xf1\x6e\xa1\xae\x83\x38\xf4\xdc\xc1\x76\xa8"; - assert_eq!( - rfc5649_wrap(key_to_wrap, kek).expect("Fail to wrap"), - wrapped_key - ); - assert_eq!( - rfc5649_unwrap(&wrapped_key, kek).expect("Fail to unwrap"), - Zeroizing::from(key_to_wrap.to_vec()) - ); + // Empty plaintext should fail + let empty: &[u8] = &[]; + rfc5649_wrap(empty, kek).unwrap_err(); + // The rest of the round-trips should succeed, including the single byte one for size in 1..=TEST_SIZE_LIMIT { let key_to_wrap = &[0_u8; TEST_SIZE_LIMIT][..size]; let ciphertext = rfc5649_wrap(key_to_wrap, kek).expect("Fail to wrap"); @@ -331,19 +151,17 @@ mod tests { } } + // This test uses the vectors provided by the official RFC paper #[test] - pub(super) fn test_wrap_large_length() { - #[cfg(not(feature = "non-fips"))] - // Load FIPS provider module from OpenSSL. - openssl::provider::Provider::load(None, "fips").unwrap(); - + pub(super) fn test_rfc_test_vectors() { + setup_openssl_fips_provider(); let kek = b"\x58\x40\xdf\x6e\x29\xb0\x2a\xf1\xab\x49\x3b\x70\x5b\xf1\x6e\xa1\xae\x83\x38\xf4\xdc\xc1\x76\xa8"; - let key_to_wrap = - b"\xc3\x7b\x7e\x64\x92\x58\x43\x40\xbe\xd1\x22\x07\x80\x89\x41\x15\x50\x68\xf7\x38"; + + // 7 bytes of data + let key_to_wrap = b"\x46\x6f\x72\x50\x61\x73\x69"; let wrapped_key = [ - 0x13, 0x8b, 0xde, 0xaa, 0x9b, 0x8f, 0xa7, 0xfc, 0x61, 0xf9, 0x77, 0x42, 0xe7, 0x22, - 0x48, 0xee, 0x5a, 0xe6, 0xae, 0x53, 0x60, 0xd1, 0xae, 0x6a, 0x5f, 0x54, 0xf3, 0x73, - 0xfa, 0x54, 0x3b, 0x6a, + 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x0, 0xf2, 0xcc, 0xb5, 0xb, 0xb2, + 0x4f, ]; assert_eq!( @@ -354,19 +172,14 @@ mod tests { rfc5649_unwrap(&wrapped_key, kek).expect("Fail to unwrap"), Zeroizing::from(key_to_wrap.to_vec()) ); - } - - #[test] - pub(super) fn test_wrap_small_length() { - #[cfg(not(feature = "non-fips"))] - // Load FIPS provider module from OpenSSL. - openssl::provider::Provider::load(None, "fips").unwrap(); - let kek = b"\x58\x40\xdf\x6e\x29\xb0\x2a\xf1\xab\x49\x3b\x70\x5b\xf1\x6e\xa1\xae\x83\x38\xf4\xdc\xc1\x76\xa8"; - let key_to_wrap = b"\x46\x6f\x72\x50\x61\x73\x69"; + // 20 bytes of data + let key_to_wrap = + b"\xc3\x7b\x7e\x64\x92\x58\x43\x40\xbe\xd1\x22\x07\x80\x89\x41\x15\x50\x68\xf7\x38"; let wrapped_key = [ - 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x0, 0xf2, 0xcc, 0xb5, 0xb, 0xb2, - 0x4f, + 0x13, 0x8b, 0xde, 0xaa, 0x9b, 0x8f, 0xa7, 0xfc, 0x61, 0xf9, 0x77, 0x42, 0xe7, 0x22, + 0x48, 0xee, 0x5a, 0xe6, 0xae, 0x53, 0x60, 0xd1, 0xae, 0x6a, 0x5f, 0x54, 0xf3, 0x73, + 0xfa, 0x54, 0x3b, 0x6a, ]; assert_eq!( @@ -381,10 +194,7 @@ mod tests { #[test] pub(super) fn test_wrap_bad_key_size() { - #[cfg(not(feature = "non-fips"))] - // Load FIPS provider module from OpenSSL. - openssl::provider::Provider::load(None, "fips").unwrap(); - + setup_openssl_fips_provider(); // Small input let kek = b"\x00"; let key_to_wrap = b"\x46\x6f\x72\x50\x61\x73\x69"; @@ -412,10 +222,7 @@ mod tests { #[test] pub(super) fn test_wrap_bad_input_size() { - #[cfg(not(feature = "non-fips"))] - // Load FIPS provider module from OpenSSL. - openssl::provider::Provider::load(None, "fips").unwrap(); - + setup_openssl_fips_provider(); let kek = b"\x58\x40\xdf\x6e\x29\xb0\x2a\xf1\xab\x49\x3b\x70\x5b\xf1\x6e\xa1\xae\x83\x38\xf4\xdc\xc1\x76\xa8"; let wrapped_key = [ 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x0, 0xf2, 0xcc, 0xb5, 0xb, 0xb2, @@ -426,10 +233,7 @@ mod tests { #[test] pub(super) fn test_wrap_bad_input_content() { - #[cfg(not(feature = "non-fips"))] - // Load FIPS provider module from OpenSSL. - openssl::provider::Provider::load(None, "fips").unwrap(); - + setup_openssl_fips_provider(); let kek = b"\x58\x40\xdf\x6e\x29\xb0\x2a\xf1\xab\x49\x3b\x70\x5b\xf1\x6e\xa1\xae\x83\x38\xf4\xdc\xc1\x76\xa8"; let wrapped_key = [ 0xaf, 0xbe, 0xb0, 0xf0, 0x7d, 0xfb, 0xf5, 0x41, 0x92, 0x0, 0xf2, 0xcc, 0xb5, 0xb, 0xb2, @@ -449,6 +253,7 @@ mod tests { #[test] fn test_sizes() { + setup_openssl_fips_provider(); let dek_16 = [1_u8; 16]; let kek_16 = [2_u8; 16]; let dek_32 = [1_u8; 32]; @@ -473,6 +278,7 @@ mod tests { #[test] fn test_openssl_compat() { + setup_openssl_fips_provider(); let kek = "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a85840df6e29b02af1"; let dek = "afbeb0f07dfbf5419200f2ccb50bb24aafbeb0f07dfbf5419200f2ccb50bb24a"; @@ -506,8 +312,8 @@ mod tests { #[test] fn test_aes_kw_compat() { + setup_openssl_fips_provider(); // Test the compatibility with AES_KEY_WRAP_PAD (RFC 5649) implemented by the aes_kw crate. - let kek = "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a85840df6e29b02af1"; let dek = "afbeb0f07dfbf5419200f2ccb50bb24aafbeb0f07dfbf5419200f2ccb50bb24a"; diff --git a/crate/crypto/src/crypto/symmetric/symmetric_ciphers.rs b/crate/crypto/src/crypto/symmetric/symmetric_ciphers.rs index 82a7171515..a77ed7fba0 100644 --- a/crate/crypto/src/crypto/symmetric/symmetric_ciphers.rs +++ b/crate/crypto/src/crypto/symmetric/symmetric_ciphers.rs @@ -4,7 +4,7 @@ use cosmian_kmip::{ kmip_0::kmip_types::{BlockCipherMode, PaddingMethod}, kmip_2_1::kmip_types::CryptographicAlgorithm, }; -use cosmian_logger::trace; +use cosmian_logger::{info, trace}; use openssl::{ rand::rand_bytes, symm::{ @@ -18,6 +18,7 @@ use zeroize::Zeroizing; #[cfg(feature = "non-fips")] use super::aes_gcm_siv_not_openssl; use crate::{ + crypto::symmetric::rfc3394::{rfc3394_unwrap, rfc3394_wrap}, crypto::symmetric::rfc5649::{rfc5649_unwrap, rfc5649_wrap}, crypto_bail, error::{CryptoError, result::CryptoResult}, @@ -96,18 +97,27 @@ pub const AES_256_GCM_SIV_IV_LENGTH: usize = 12; #[cfg(feature = "non-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 3394 IV is actually a fixed overhead +pub const RFC3394_IV_LENGTH: usize = 0; +/// RFC3394 has no authentication. +pub const RFC3394_MAC_LENGTH: usize = 0; +/// RFC 3394 with a 16-byte KEK. +pub const RFC3394_16_KEY_LENGTH: usize = 16; +/// RFC 3394 with a 24-byte KEK. +pub const RFC3394_24_KEY_LENGTH: usize = 24; +/// RFC 3394 with a 32-byte KEK. +pub const RFC3394_32_KEY_LENGTH: usize = 32; + // RFC 5649 IV is actually a fixed overhead -pub const RFC5649_16_IV_LENGTH: usize = 0; +pub const RFC5649_IV_LENGTH: usize = 0; /// RFC5649 has no authentication. -pub const RFC5649_16_MAC_LENGTH: usize = 0; +pub const RFC5649_MAC_LENGTH: usize = 0; +/// RFC 5649 with a 16-byte KEK. +pub const RFC5649_16_KEY_LENGTH: usize = 16; +/// RFC 5649 with a 24-byte KEK. +pub const RFC5649_24_KEY_LENGTH: usize = 24; /// 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(feature = "non-fips")] /// Chacha20-Poly1305 key length in bytes. @@ -158,7 +168,11 @@ pub enum SymCipher { Aes128Gcm, Aes128Xts, Aes256Xts, + Rfc3394_16, + Rfc3394_24, + Rfc3394_32, Rfc5649_16, + Rfc5649_24, Rfc5649_32, #[cfg(feature = "non-fips")] Aes128GcmSiv, @@ -185,7 +199,12 @@ impl SymCipher { 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 => { + Self::Rfc3394_16 | Self::Rfc3394_24 | Self::Rfc3394_32 => { + crypto_bail!(CryptoError::NotSupported( + "RFC3394 is not supported in this version of openssl".to_owned() + )) + } + Self::Rfc5649_16 | Self::Rfc5649_24 | Self::Rfc5649_32 => { crypto_bail!(CryptoError::NotSupported( "RFC5649 is not supported in this version of openssl".to_owned() )) @@ -219,8 +238,8 @@ impl SymCipher { 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, + Self::Rfc3394_16 | Self::Rfc3394_24 | Self::Rfc3394_32 => RFC3394_MAC_LENGTH, + Self::Rfc5649_16 | Self::Rfc5649_24 | Self::Rfc5649_32 => RFC5649_MAC_LENGTH, #[cfg(feature = "non-fips")] Self::Chacha20Poly1305 => CHACHA20_POLY1305_MAC_LENGTH, #[cfg(feature = "non-fips")] @@ -247,8 +266,8 @@ impl SymCipher { 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, + Self::Rfc3394_16 | Self::Rfc3394_24 | Self::Rfc3394_32 => RFC3394_IV_LENGTH, + Self::Rfc5649_16 | Self::Rfc5649_24 | Self::Rfc5649_32 => RFC5649_IV_LENGTH, #[cfg(feature = "non-fips")] Self::Chacha20Poly1305 => CHACHA20_POLY1305_IV_LENGTH, #[cfg(feature = "non-fips")] @@ -275,7 +294,11 @@ impl SymCipher { Self::Aes256Gcm => AES_256_GCM_KEY_LENGTH, Self::Aes128Xts => AES_128_XTS_KEY_LENGTH, Self::Aes256Xts => AES_256_XTS_KEY_LENGTH, + Self::Rfc3394_16 => RFC3394_16_KEY_LENGTH, + Self::Rfc3394_24 => RFC3394_24_KEY_LENGTH, + Self::Rfc3394_32 => RFC3394_32_KEY_LENGTH, Self::Rfc5649_16 => RFC5649_16_KEY_LENGTH, + Self::Rfc5649_24 => RFC5649_24_KEY_LENGTH, Self::Rfc5649_32 => RFC5649_32_KEY_LENGTH, #[cfg(feature = "non-fips")] Self::Chacha20Poly1305 => CHACHA20_POLY1305_KEY_LENGTH, @@ -344,11 +367,21 @@ impl SymCipher { {key_size} bytes", ))), }, - BlockCipherMode::NISTKeyWrap => match key_size { + BlockCipherMode::AESKeyWrapPadding => match key_size { RFC5649_16_KEY_LENGTH => Ok(Self::Rfc5649_16), + RFC5649_24_KEY_LENGTH => Ok(Self::Rfc5649_24), RFC5649_32_KEY_LENGTH => Ok(Self::Rfc5649_32), _ => crypto_bail!(CryptoError::NotSupported(format!( - "RFC5649 key must be 16 or 32 bytes long. Found {key_size} bytes", + "RFC5649 key must be 16, 24 or 32 bytes long. Found {key_size} \ + bytes", + ))), + }, + BlockCipherMode::NISTKeyWrap => match key_size { + RFC3394_16_KEY_LENGTH => Ok(Self::Rfc3394_16), + RFC3394_24_KEY_LENGTH => Ok(Self::Rfc3394_24), + RFC3394_32_KEY_LENGTH => Ok(Self::Rfc3394_32), + _ => crypto_bail!(CryptoError::NotSupported(format!( + "RFC3394 key must be 16, 24 or 32 bytes long. Found {key_size} bytes", ))), }, mode => { @@ -511,9 +544,15 @@ pub fn encrypt( SymCipher::Aes128GcmSiv | SymCipher::Aes256GcmSiv => { aes_gcm_siv_not_openssl::encrypt(key, nonce, aad, plaintext) } - SymCipher::Rfc5649_16 | SymCipher::Rfc5649_32 => { + SymCipher::Rfc5649_16 | SymCipher::Rfc5649_24 | SymCipher::Rfc5649_32 => { Ok((rfc5649_wrap(plaintext, key)?, vec![])) } + SymCipher::Rfc3394_16 | SymCipher::Rfc3394_24 | SymCipher::Rfc3394_32 => { + info!( + "RFC 3394 is deprecated in favor of RFC 5649 and is supported only for legacy compatibility. Please consider using `BlockCipherMode::AESKeyWrapPadding` (RFC 5649) for new applications instead of `BlockCipherMode::NISTKeyWrap `." + ); + Ok((rfc3394_wrap(plaintext, key)?, vec![])) + } #[cfg(feature = "non-fips")] SymCipher::Chacha20 => { trace!( @@ -657,7 +696,15 @@ pub fn decrypt( 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)?, + SymCipher::Rfc3394_16 | SymCipher::Rfc3394_24 | SymCipher::Rfc3394_32 => { + info!( + "RFC 3394 is deprecated in favor of RFC 5649 and is supported only for legacy compatibility. Please consider using `BlockCipherMode::AESKeyWrapPadding` (RFC 5649) for new applications instead of `BlockCipherMode::NISTKeyWrap `." + ); + rfc3394_unwrap(ciphertext, key)? + } + SymCipher::Rfc5649_16 | SymCipher::Rfc5649_24 | SymCipher::Rfc5649_32 => { + rfc5649_unwrap(ciphertext, key)? + } #[cfg(feature = "non-fips")] SymCipher::Chacha20 => { if key.len() != CHACHA20_KEY_LENGTH { diff --git a/crate/crypto/src/crypto/wrap/wrap_key.rs b/crate/crypto/src/crypto/wrap/wrap_key.rs index 899e4d505f..351d5ca09c 100644 --- a/crate/crypto/src/crypto/wrap/wrap_key.rs +++ b/crate/crypto/src/crypto/wrap/wrap_key.rs @@ -262,7 +262,7 @@ pub(super) fn wrap( let block_cipher_mode = cryptographic_parameters .as_ref() .and_then(|params| params.block_cipher_mode) - .unwrap_or(BlockCipherMode::NISTKeyWrap); + .unwrap_or(BlockCipherMode::AESKeyWrapPadding); let padding_method = cryptographic_parameters .as_ref() .and_then(|params| params.padding_method) diff --git a/crate/kmip/src/kmip_0/kmip_types.rs b/crate/kmip/src/kmip_0/kmip_types.rs index 1061e33f0a..36aed000fb 100644 --- a/crate/kmip/src/kmip_0/kmip_types.rs +++ b/crate/kmip/src/kmip_0/kmip_types.rs @@ -765,7 +765,6 @@ pub enum ShreddingAlgorithm { Unsupervised = 0x3, } -// Block Cipher Mode Enumeration #[kmip_enum] pub enum BlockCipherMode { CBC = 0x0000_0001, @@ -779,8 +778,8 @@ pub enum BlockCipherMode { GCM = 0x0000_0009, CBCMAC = 0x0000_000A, XTS = 0x0000_000B, - AESKeyWrapPadding = 0x0000_000C, - NISTKeyWrap = 0x8000_000D, + AESKeyWrapPadding = 0x0000_000C, // RFC 5649 + NISTKeyWrap = 0x0000_000D, // RFC 3394 X9102AESKW = 0x0000_000E, X9102TDKW = 0x0000_000F, X9102AKW1 = 0x0000_0010, @@ -789,6 +788,10 @@ pub enum BlockCipherMode { // Extensions - 8XXXXXXX // AES GCM SIV GCMSIV = 0x8000_0002, + // This variant was introduced to support backward compatibility with versions prior to 5.15 + // In the database layer, right after deserialization, objects that have a saved BlockCipherMode (via their `KeyWrappingData`) are tested for this mode and + // converted to AESKeyWrapPadding if found. + LegacyNISTKeyWrap = 0x8000_000D, } /// Padding Method Enumeration diff --git a/crate/kmip/src/ttlv/xml/deserializer.rs b/crate/kmip/src/ttlv/xml/deserializer.rs index 3f1597aad1..1e087b63dd 100644 --- a/crate/kmip/src/ttlv/xml/deserializer.rs +++ b/crate/kmip/src/ttlv/xml/deserializer.rs @@ -540,8 +540,6 @@ impl TTLVXMLDeserializer { "DSA" => Some((0x0000_0005, "DSA")), "ECDSA" => Some((0x0000_0006, "ECDSA")), // Vendor / extension algorithms present in mandatory vectors - // NISTKeyWrap appears as an algorithm enumeration in interop vectors; assign vendor range code - "NISTKeyWrap" => Some((0x8000_000D, "NISTKeyWrap")), "HMAC_SHA1" => Some((0x0000_0007, "HMACSHA1")), "HMAC_SHA224" => Some((0x0000_0008, "HMACSHA224")), "HMAC_SHA256" => Some((0x0000_0009, "HMACSHA256")), @@ -697,6 +695,8 @@ impl TTLVXMLDeserializer { "CCM" => Some((0x0000_0008, "CCM")), "CMAC" => Some((0x0000_0007, "CMAC")), "AEAD" => Some((0x0000_0012, "AEAD")), + "AESKeyWrapPadding" => Some((0x0000_000C, "AESKeyWrapPadding")), + "NISTKeyWrap" => Some((0x0000_000D, "NISTKeyWrap")), // PaddingMethod "None" => Some((0x1, "None")), "OAEP" => Some((0x2, "OAEP")), diff --git a/crate/server_database/README.md b/crate/server_database/README.md index b22322daa7..d5ff0390c8 100644 --- a/crate/server_database/README.md +++ b/crate/server_database/README.md @@ -77,7 +77,7 @@ ENC_Findex v8(u:userid) → ENC_Findex v8(permission_triplet) ENC_Findex v8(object_uid) → ENC_Findex v8(metadata) ``` -A more colorful and clear description of how the Redis backend operates with Findex can be red on the its original PR description : [github.com/Cosmian/kms/pull/542](github.com/Cosmian/kms/pull/542). +A more colorful and clear description of how the Redis backend operates with Findex can be red on the its original PR description : [github.com/Cosmian/kms/pull/542](https://github.com/Cosmian/kms/pull/542). ### Environment Variables diff --git a/crate/server_database/src/lib.rs b/crate/server_database/src/lib.rs index ca58ad963e..81675cbcc8 100644 --- a/crate/server_database/src/lib.rs +++ b/crate/server_database/src/lib.rs @@ -55,3 +55,56 @@ pub mod reexport { #[cfg(feature = "non-fips")] pub use redis; } + +use cosmian_kmip::kmip_2_1::kmip_objects::Object; + +/// Upgrades wrongly serialized `BlockCipherMode::LegacyNISTKeyWrap` (`0x8000_000D`) to +/// `BlockCipherMode::AESKeyWrapPadding` (`0x0000_000C`) for backward compatibility +/// with versions prior to 5.15.x Calling this right after deserializing ensures that no faulty +/// data even gets read from a (previously) valid database. +/// +/// This function checks if the object has a `KeyWrappingData` with the legacy +/// `NISTKeyWrap` value and converts it to the correct `AESKeyWrapPadding` value. +pub(crate) fn migrate_block_cipher_mode_if_needed(mut object: Object) -> Object { + use cosmian_kmip::{ + kmip_0::kmip_types::BlockCipherMode, + kmip_2_1::kmip_objects::{ + Object, PGPKey, PrivateKey, PublicKey, SecretData, SplitKey, SymmetricKey, + }, + }; + // Only objects with key blocks can have key wrapping data + let key_block = match &mut object { + Object::SymmetricKey(SymmetricKey { key_block }) + | Object::PrivateKey(PrivateKey { key_block }) + | Object::PublicKey(PublicKey { key_block }) + | Object::SecretData(SecretData { key_block, .. }) + | Object::SplitKey(SplitKey { key_block, .. }) + | Object::PGPKey(PGPKey { key_block, .. }) => key_block, + // These object types don't have key blocks + Object::Certificate(_) | Object::CertificateRequest(_) | Object::OpaqueObject(_) => { + return object; + } + }; + + // Check if key_wrapping_data exists and has encryption_key_information + if let Some(key_wrapping_data) = &mut key_block.key_wrapping_data { + if let Some(encryption_key_info) = &mut key_wrapping_data.encryption_key_information { + if let Some(crypto_params) = &mut encryption_key_info.cryptographic_parameters { + if crypto_params.block_cipher_mode == Some(BlockCipherMode::LegacyNISTKeyWrap) { + crypto_params.block_cipher_mode = Some(BlockCipherMode::AESKeyWrapPadding); + } + } + } + + // Also check MAC/signature key information (if present) + if let Some(mac_sign_key_info) = &mut key_wrapping_data.mac_signature_key_information { + if let Some(crypto_params) = &mut mac_sign_key_info.cryptographic_parameters { + if crypto_params.block_cipher_mode == Some(BlockCipherMode::LegacyNISTKeyWrap) { + crypto_params.block_cipher_mode = Some(BlockCipherMode::AESKeyWrapPadding); + } + } + } + } + + object +} diff --git a/crate/server_database/src/stores/redis/objects_db.rs b/crate/server_database/src/stores/redis/objects_db.rs index a9f2159374..0f61f0719b 100644 --- a/crate/server_database/src/stores/redis/objects_db.rs +++ b/crate/server_database/src/stores/redis/objects_db.rs @@ -18,7 +18,10 @@ use cosmian_kms_crypto::reexport::cosmian_crypto_core::{ use redis::{AsyncCommands, aio::ConnectionManager, pipe}; use serde::{Deserialize, Serialize}; -use crate::{DbError, db_bail, error::DbResult, stores::redis::findex::Keyword}; +use crate::{ + DbError, db_bail, error::DbResult, migrate_block_cipher_mode_if_needed, + stores::redis::findex::Keyword, +}; /// Extract the keywords from the attributes pub(crate) fn keywords_from_attributes(attributes: &Attributes) -> HashSet { @@ -152,7 +155,7 @@ impl ObjectsDB { })?; Nonce::new(&mut *rng) }; - let ct = self.dem.encrypt( + let ct: Vec = self.dem.encrypt( &nonce, &serde_json::to_vec(redis_db_object)?, Some(uid.as_bytes()), @@ -182,8 +185,10 @@ impl ObjectsDB { Some(uid.as_bytes()), ) .with_context(|| format!("decrypt_object uid: {uid}"))?; - let redis_db_object: RedisDbObject = serde_json::from_slice(&plaintext) + // Mutability below is needed to Migrate legacy BlockCipherMode in-place - otherwise we should destructure and that's very verbose. + let mut redis_db_object: RedisDbObject = serde_json::from_slice(&plaintext) .with_context(|| format!("decrypt_object uid: {uid}"))?; + redis_db_object.object = migrate_block_cipher_mode_if_needed(redis_db_object.object); Ok(redis_db_object) } diff --git a/crate/server_database/src/stores/sql/mysql.rs b/crate/server_database/src/stores/sql/mysql.rs index 25f4c681bc..872a668a1d 100644 --- a/crate/server_database/src/stores/sql/mysql.rs +++ b/crate/server_database/src/stores/sql/mysql.rs @@ -30,6 +30,7 @@ const DEFAULT_LOCK_WAIT_TIMEOUT_SECS: u32 = 10; use crate::{ db_bail, db_error, error::{DbError, DbResult, DbResultHelper}, + migrate_block_cipher_mode_if_needed, stores::{ MYSQL_QUERIES, migrate::HasDatabase, @@ -70,6 +71,7 @@ fn my_sql_row_to_owm(row: &MySqlRow) -> Result { let id = row.get::(0); let object: Object = serde_json::from_str(&row.get::(1)) .context("failed deserializing the object")?; + let object = migrate_block_cipher_mode_if_needed(object); let attributes: Attributes = serde_json::from_value(row.get::(2)) .context("failed deserializing the Attributes")?; let owner = row.get::(3); diff --git a/crate/server_database/src/stores/sql/pgsql.rs b/crate/server_database/src/stores/sql/pgsql.rs index 46241a28f7..11f953bcb3 100644 --- a/crate/server_database/src/stores/sql/pgsql.rs +++ b/crate/server_database/src/stores/sql/pgsql.rs @@ -26,6 +26,7 @@ use uuid::Uuid; use crate::{ db_bail, db_error, error::{DbError, DbResult, DbResultHelper}, + migrate_block_cipher_mode_if_needed, stores::{ PGSQL_QUERIES, migrate::HasDatabase, @@ -58,6 +59,7 @@ fn pg_row_to_owm(row: &PgRow) -> Result { let id = row.get::(0); let object: Object = serde_json::from_str(&row.get::(1)) .context("failed deserializing the object")?; + let object = migrate_block_cipher_mode_if_needed(object); let attributes: Attributes = serde_json::from_value(row.get::(2)) .context("failed deserializing the Attributes")?; let owner = row.get::(3); diff --git a/crate/server_database/src/stores/sql/sqlite.rs b/crate/server_database/src/stores/sql/sqlite.rs index 904b5a8963..a62dd6d6bb 100644 --- a/crate/server_database/src/stores/sql/sqlite.rs +++ b/crate/server_database/src/stores/sql/sqlite.rs @@ -26,6 +26,7 @@ use uuid::Uuid; use crate::{ DbError, db_bail, db_error, error::{DbResult, DbResultHelper}, + migrate_block_cipher_mode_if_needed, stores::{ SQLITE_QUERIES, migrate::HasDatabase, @@ -62,6 +63,7 @@ fn sqlite_row_to_owm(row: &SqliteRow) -> Result { let id = row.get::(0); let object: Object = serde_json::from_str(&row.get::(1)) .context("failed deserializing the object")?; + let object = migrate_block_cipher_mode_if_needed(object); let raw_attributes = row.get::(2); let attributes = serde_json::from_value(raw_attributes)?; let owner = row.get::(3); diff --git a/crate/server_database/src/tests/database_tests.rs b/crate/server_database/src/tests/database_tests.rs index dc57c0d115..24195bffac 100644 --- a/crate/server_database/src/tests/database_tests.rs +++ b/crate/server_database/src/tests/database_tests.rs @@ -1,9 +1,10 @@ use std::{collections::HashSet, sync::Arc}; use cosmian_kmip::{ - kmip_0::kmip_types::State, + kmip_0::kmip_types::{BlockCipherMode, State}, kmip_2_1::{ kmip_attributes::Attributes, + kmip_objects::{Object, SymmetricKey}, kmip_types::{CryptographicAlgorithm, Link, LinkType, LinkedObjectIdentifier}, requests::create_symmetric_key_kmip_object, }, @@ -399,3 +400,92 @@ pub(super) async fn crud( Ok(()) } + +/// Test that any legacy value (`0x8000_000D`) is correctly migrated +/// to `BlockCipherMode::AESKeyWrapPadding` (`0x0000_000C`) when objects are retrieved from the database. +/// This ensures backward compatibility with some databases that might have been created by KMS versions prior to 5.15. +/// This test should should be deleted once `BlockCipherMode::LegacyNISTKeyWrap` is permanently removed from the codebase. +pub(super) async fn block_cipher_mode_migration_after_json_deserialization( + db: &DB, + db_params: Option>, +) -> DbResult<()> { + cosmian_logger::log_init(None); + + let owner = "test_owner"; + let uid = Uuid::new_v4().to_string(); + + // This is a sort of hack to trick the database into saving that deprecated value + let json = r#" + { + "SymmetricKey": { + "KeyBlock": { + "KeyFormatType": "TransparentSymmetricKey", + "CryptographicAlgorithm": "AES", + "CryptographicLength": 256, + "KeyWrappingData": { + "WrappingMethod": "Encrypt", + "EncryptionKeyInformation": { + "UniqueIdentifier": "aes_wrapper", + "CryptographicParameters": { + "BlockCipherMode": "LegacyNISTKeyWrap", + "CryptographicAlgorithm": "AES" + } + }, + "EncodingOption": "TTLVEncoding" + } + } + } + } + "#; + + let object: Object = serde_json::from_str(json).expect("Deserialization failed"); + let attributes = Attributes { + cryptographic_algorithm: Some(CryptographicAlgorithm::AES), + ..Default::default() + }; + + // Store the object in the database, it will encode to 0x8000_000D + db.create( + Some(uid.clone()), + owner, + &object, + &attributes, + &HashSet::new(), + db_params.clone(), + ) + .await?; + + let retrieved = db + .retrieve(&uid, db_params.clone()) + .await? + .ok_or_else(|| DbError::ItemNotFound("Object should exist".to_owned()))?; + + // Verify the BlockCipherMode was migrated + if let Object::SymmetricKey(SymmetricKey { key_block }) = retrieved.object() { + let block_cipher_mode = key_block + .key_wrapping_data + .as_ref() + .ok_or_else(|| DbError::ServerError("KeyWrappingData should exist".to_owned()))? + .encryption_key_information + .as_ref() + .ok_or_else(|| { + DbError::ServerError("EncryptionKeyInformation should exist".to_owned()) + })? + .cryptographic_parameters + .as_ref() + .ok_or_else(|| DbError::ServerError("CryptographicParameters should exist".to_owned()))? + .block_cipher_mode; + + if block_cipher_mode != Some(BlockCipherMode::AESKeyWrapPadding) { + return Err(DbError::ServerError(format!( + "Legacy BlockCipherMode should be migrated to AESKeyWrapPadding, found: {block_cipher_mode:?}" + ))); + } + } else { + return Err(DbError::ServerError( + "Expected SymmetricKey object".to_owned(), + )); + } + + Ok(()) +} diff --git a/crate/server_database/src/tests/mod.rs b/crate/server_database/src/tests/mod.rs index fc206e045d..a3b310773d 100644 --- a/crate/server_database/src/tests/mod.rs +++ b/crate/server_database/src/tests/mod.rs @@ -26,7 +26,10 @@ use crate::stores::additional_redis_findex_tests::{ use crate::{ error::DbResult, stores::{MySqlPool, PgPool, SqlitePool}, - tests::{database_tests::atomic, list_uids_for_tags_test::list_uids_for_tags_test}, + tests::{ + database_tests::{atomic, block_cipher_mode_migration_after_json_deserialization}, + list_uids_for_tags_test::list_uids_for_tags_test, + }, }; mod database_tests; @@ -105,6 +108,8 @@ pub(crate) async fn test_db_redis_with_findex() -> DbResult<()> { upsert(&get_redis_with_findex().await?, None).await?; crud(&get_redis_with_findex().await?, None).await?; list_uids_for_tags_test(&get_redis_with_findex().await?, None).await?; + block_cipher_mode_migration_after_json_deserialization(&get_redis_with_findex().await?, None) + .await?; Ok(()) } @@ -135,7 +140,8 @@ pub(crate) async fn test_db_sqlite() -> DbResult<()> { upsert(&get_sqlite(&db_file).await?, None).await?; crud(&get_sqlite(&db_file).await?, None).await?; list_uids_for_tags_test(&get_sqlite(&db_file).await?, None).await?; - + block_cipher_mode_migration_after_json_deserialization(&get_sqlite(&db_file).await?, None) + .await?; Ok(()) } @@ -154,6 +160,7 @@ pub(crate) async fn test_db_postgresql() -> DbResult<()> { upsert(&get_pgsql().await?, None).await?; crud(&get_pgsql().await?, None).await?; list_uids_for_tags_test(&get_pgsql().await?, None).await?; + block_cipher_mode_migration_after_json_deserialization(&get_pgsql().await?, None).await?; Ok(()) } @@ -171,5 +178,6 @@ pub(crate) async fn test_db_mysql() -> DbResult<()> { upsert(&get_mysql().await?, None).await?; crud(&get_mysql().await?, None).await?; list_uids_for_tags_test(&get_mysql().await?, None).await?; + block_cipher_mode_migration_after_json_deserialization(&get_mysql().await?, None).await?; Ok(()) } diff --git a/ui/src/KeysExport.tsx b/ui/src/KeysExport.tsx index 4c17908e69..0a80854e31 100644 --- a/ui/src/KeysExport.tsx +++ b/ui/src/KeysExport.tsx @@ -17,10 +17,11 @@ interface KeyExportFormData { type ExportKeyFormat = "json-ttlv" | "sec1-pem" | "sec1-der" | "pkcs1-pem" | "pkcs1-der" | "pkcs8-pem" | "pkcs8-der" | "base64" | "raw"; -type WrappingAlgorithm = "nist-key-wrap" | "aes-gcm" | "rsa-pkcs-v15" | "rsa-oaep" | "rsa-aes-key-wrap"; +type WrappingAlgorithm = "aes-key-wrap-padding" | "nist-key-wrap" | "aes-gcm" | "rsa-pkcs-v15" | "rsa-oaep" | "rsa-aes-key-wrap"; const WRAPPING_ALGORITHMS: { label: string; value: WrappingAlgorithm }[] = [ - { label: "NIST Key Wrap (RFC 5649)", value: "nist-key-wrap" }, + { label: "AES Key Wrap with Padding (RFC 5649)", value: "aes-key-wrap-padding" }, + { label: "AES Key Wrap with NO Padding (RFC 3394)", value: "nist-key-wrap" }, { label: "AES GCM", value: "aes-gcm" }, { label: "RSA PKCS v1.5", value: "rsa-pkcs-v15" }, { label: "RSA OAEP", value: "rsa-oaep" },