diff --git a/Cargo.lock b/Cargo.lock index dbc86b850..e5ff657e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aead" +version = "0.6.0-rc.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b657e772794c6b04730ea897b66a058ccd866c16d1967da05eeeecec39043fe" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "aes" version = "0.9.0" @@ -13,6 +23,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.11.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22c0c90bbe8d4f77c3ca9ddabe41a1f8382d6fc1f7cea89459d0f320371f972" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aes-kw" version = "0.3.0" @@ -439,6 +463,15 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "ctr" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17469f8eb9bdbfad10f71f4cfddfd38b01143520c0e717d8796ccb4d44d44e42" +dependencies = [ + "cipher", +] + [[package]] name = "ctutils" version = "0.4.2" @@ -680,6 +713,15 @@ dependencies = [ "wasip3", ] +[[package]] +name = "ghash" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" +dependencies = [ + "polyval", +] + [[package]] name = "glob" version = "0.3.3" @@ -1033,6 +1075,7 @@ name = "pkcs5" version = "0.8.0" dependencies = [ "aes", + "aes-gcm", "cbc", "der", "des", @@ -1060,6 +1103,17 @@ dependencies = [ "tempfile", ] +[[package]] +name = "polyval" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" +dependencies = [ + "cpubits", + "cpufeatures", + "universal-hash", +] + [[package]] name = "postcard" version = "1.1.3" @@ -1895,6 +1949,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" +dependencies = [ + "crypto-common", + "ctutils", +] + [[package]] name = "wait-timeout" version = "0.2.1" diff --git a/pkcs5/Cargo.toml b/pkcs5/Cargo.toml index 7231258c4..14a458c8d 100644 --- a/pkcs5/Cargo.toml +++ b/pkcs5/Cargo.toml @@ -21,6 +21,7 @@ spki = "0.8" # optional dependencies aes = { version = "0.9", optional = true, default-features = false } +aes-gcm = { version = "0.11.0-rc.3", optional = true, default-features = false, features = ["aes"] } cbc = { version = "0.2", optional = true } des = { version = "0.9", optional = true, default-features = false } pbkdf2 = { version = "0.13", optional = true, default-features = false, features = ["hmac"] } @@ -39,7 +40,7 @@ alloc = [] 3des = ["dep:des", "pbes2"] des-insecure = ["dep:des", "pbes2"] getrandom = ["dep:getrandom", "rand_core"] -pbes2 = ["dep:aes", "dep:cbc", "dep:pbkdf2", "dep:scrypt", "dep:sha2"] +pbes2 = ["dep:aes", "dep:aes-gcm", "dep:cbc", "dep:pbkdf2", "dep:scrypt", "dep:sha2"] rand_core = ["dep:rand_core"] sha1-insecure = ["dep:sha1", "pbes2"] diff --git a/pkcs5/src/pbes2.rs b/pkcs5/src/pbes2.rs index aed1cb4ef..3bf322994 100644 --- a/pkcs5/src/pbes2.rs +++ b/pkcs5/src/pbes2.rs @@ -39,6 +39,14 @@ pub const AES_192_CBC_OID: ObjectIdentifier = pub const AES_256_CBC_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.42"); +/// 128-bit Advanced Encryption Standard (AES) algorithm with Galois Counter Mode +pub const AES_128_GCM_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.6"); + +/// 256-bit Advanced Encryption Standard (AES) algorithm with Galois Counter Mode +pub const AES_256_GCM_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.46"); + /// DES operating in CBC mode #[cfg(feature = "des-insecure")] pub const DES_CBC_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.3.14.3.2.7"); @@ -55,6 +63,12 @@ pub const PBES2_OID: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.11 /// AES cipher block size const AES_BLOCK_SIZE: usize = 16; +/// GCM nonce size +/// +/// We could use any value here but GCM is most efficient +/// with 96 bit nonces +const GCM_NONCE_SIZE: usize = 12; + /// DES / Triple DES block size #[cfg(any(feature = "3des", feature = "des-insecure"))] const DES_BLOCK_SIZE: usize = 8; @@ -255,6 +269,46 @@ impl Parameters { Ok(Self { kdf, encryption }) } + /// Initialize PBES2 parameters using scrypt as the password-based + /// key derivation function and AES-128-GCM as the symmetric cipher. + /// + /// For more information on scrypt parameters, see documentation for the + /// [`scrypt::Params`] struct. + /// + /// # Errors + /// Propagates errors from [`ScryptParams::from_params_and_salt`]. + // TODO(tarcieri): encapsulate `scrypt::Params`? + #[cfg(feature = "pbes2")] + pub fn scrypt_aes128gcm( + params: scrypt::Params, + salt: &[u8], + gcm_nonce: [u8; GCM_NONCE_SIZE], + ) -> Result { + let kdf = ScryptParams::from_params_and_salt(params, salt)?.into(); + let encryption = EncryptionScheme::Aes128Gcm { nonce: gcm_nonce }; + Ok(Self { kdf, encryption }) + } + + /// Initialize PBES2 parameters using scrypt as the password-based + /// key derivation function and AES-256-GCM as the symmetric cipher. + /// + /// For more information on scrypt parameters, see documentation for the + /// [`scrypt::Params`] struct. + /// + /// # Errors + /// Propagates errors from [`ScryptParams::from_params_and_salt`]. + // TODO(tarcieri): encapsulate `scrypt::Params`? + #[cfg(feature = "pbes2")] + pub fn scrypt_aes256gcm( + params: scrypt::Params, + salt: &[u8], + gcm_nonce: [u8; GCM_NONCE_SIZE], + ) -> Result { + let kdf = ScryptParams::from_params_and_salt(params, salt)?.into(); + let encryption = EncryptionScheme::Aes256Gcm { nonce: gcm_nonce }; + Ok(Self { kdf, encryption }) + } + /// Attempt to decrypt the given ciphertext, allocating and returning a /// byte vector containing the plaintext. /// @@ -387,6 +441,18 @@ pub enum EncryptionScheme { iv: [u8; AES_BLOCK_SIZE], }, + /// AES-128 in CBC mode + Aes128Gcm { + /// GCM nonce + nonce: [u8; GCM_NONCE_SIZE], + }, + + /// AES-256 in GCM mode + Aes256Gcm { + /// GCM nonce + nonce: [u8; GCM_NONCE_SIZE], + }, + /// 3-Key Triple DES in CBC mode #[cfg(feature = "3des")] DesEde3Cbc { @@ -410,6 +476,8 @@ impl EncryptionScheme { Self::Aes128Cbc { .. } => 16, Self::Aes192Cbc { .. } => 24, Self::Aes256Cbc { .. } => 32, + Self::Aes128Gcm { .. } => 16, + Self::Aes256Gcm { .. } => 32, #[cfg(feature = "des-insecure")] Self::DesCbc { .. } => 8, #[cfg(feature = "3des")] @@ -424,6 +492,8 @@ impl EncryptionScheme { Self::Aes128Cbc { .. } => AES_128_CBC_OID, Self::Aes192Cbc { .. } => AES_192_CBC_OID, Self::Aes256Cbc { .. } => AES_256_CBC_OID, + Self::Aes128Gcm { .. } => AES_128_GCM_OID, + Self::Aes256Gcm { .. } => AES_256_GCM_OID, #[cfg(feature = "des-insecure")] Self::DesCbc { .. } => DES_CBC_OID, #[cfg(feature = "3des")] @@ -468,6 +538,12 @@ impl TryFrom> for EncryptionScheme { AES_256_CBC_OID => Ok(Self::Aes256Cbc { iv: iv.try_into().map_err(|_| Tag::OctetString.value_error())?, }), + AES_128_GCM_OID => Ok(Self::Aes128Gcm { + nonce: iv.try_into().map_err(|_| Tag::OctetString.value_error())?, + }), + AES_256_GCM_OID => Ok(Self::Aes256Gcm { + nonce: iv.try_into().map_err(|_| Tag::OctetString.value_error())?, + }), #[cfg(feature = "des-insecure")] DES_CBC_OID => Ok(Self::DesCbc { iv: iv[0..DES_BLOCK_SIZE] @@ -493,6 +569,8 @@ impl<'a> TryFrom<&'a EncryptionScheme> for AlgorithmIdentifierRef<'a> { EncryptionScheme::Aes128Cbc { iv } => iv.as_slice(), EncryptionScheme::Aes192Cbc { iv } => iv.as_slice(), EncryptionScheme::Aes256Cbc { iv } => iv.as_slice(), + EncryptionScheme::Aes128Gcm { nonce } => nonce.as_slice(), + EncryptionScheme::Aes256Gcm { nonce } => nonce.as_slice(), #[cfg(feature = "des-insecure")] EncryptionScheme::DesCbc { iv } => iv.as_slice(), #[cfg(feature = "3des")] diff --git a/pkcs5/src/pbes2/encryption.rs b/pkcs5/src/pbes2/encryption.rs index f0291fee2..426f3d62f 100644 --- a/pkcs5/src/pbes2/encryption.rs +++ b/pkcs5/src/pbes2/encryption.rs @@ -2,6 +2,7 @@ use super::{EncryptionScheme, Kdf, Parameters, Pbkdf2Params, Pbkdf2Prf, ScryptParams}; use crate::{Error, Result}; +use aes_gcm::{KeyInit as GcmKeyInit, Nonce, Tag, aead::AeadInOut}; use cbc::cipher::{ BlockCipherDecrypt, BlockCipherEncrypt, BlockModeDecrypt, BlockModeEncrypt, KeyInit, KeyIvInit, block_padding::Pkcs7, @@ -12,7 +13,7 @@ use pbkdf2::{ digest::{ FixedOutput, HashMarker, Update, block_api::BlockSizeUser, - typenum::{IsLess, NonZero, True, U256}, + typenum::{IsLess, NonZero, True, U12, U16, U256}, }, }, pbkdf2_hmac, @@ -47,6 +48,65 @@ fn cbc_decrypt<'a, C: BlockCipherDecrypt + KeyInit>( .map_err(|_| Error::DecryptFailed) } +fn gcm_encrypt( + es: EncryptionScheme, + key: EncryptionKey, + nonce: Nonce, + buffer: &mut [u8], + pos: usize, +) -> Result<&[u8]> +where + C: BlockSizeUser + GcmKeyInit + BlockCipherEncrypt, + aes_gcm::AesGcm: GcmKeyInit, + TagSize: aes_gcm::TagSize, + NonceSize: aes::cipher::array::ArraySize, +{ + if buffer.len() < TagSize::USIZE + pos { + return Err(Error::EncryptFailed); + } + let gcm = + as GcmKeyInit>::new_from_slice(key.as_slice()) + .map_err(|_| es.to_alg_params_invalid())?; + let tag = gcm + .encrypt_inout_detached(&nonce, &[], (&mut buffer[..pos]).into()) + .map_err(|_| Error::EncryptFailed)?; + buffer[pos..].copy_from_slice(tag.as_ref()); + Ok(&buffer[0..pos + TagSize::USIZE]) +} + +fn gcm_decrypt( + es: EncryptionScheme, + key: EncryptionKey, + nonce: Nonce, + buffer: &mut [u8], +) -> Result<&[u8]> +where + C: BlockSizeUser + GcmKeyInit + BlockCipherEncrypt, + aes_gcm::AesGcm: GcmKeyInit, + TagSize: aes_gcm::TagSize, + NonceSize: aes::cipher::array::ArraySize, +{ + let msg_len = buffer + .len() + .checked_sub(TagSize::USIZE) + .ok_or(Error::DecryptFailed)?; + + let gcm = + as GcmKeyInit>::new_from_slice(key.as_slice()) + .map_err(|_| es.to_alg_params_invalid())?; + + let tag = Tag::try_from(&buffer[msg_len..]).map_err(|_| Error::DecryptFailed)?; + + if gcm + .decrypt_inout_detached(&nonce, &[], (&mut buffer[..msg_len]).into(), &tag) + .is_err() + { + return Err(Error::DecryptFailed); + } + + Ok(&buffer[..msg_len]) +} + pub fn encrypt_in_place<'b>( params: &Parameters, password: impl AsRef<[u8]>, @@ -64,6 +124,12 @@ pub fn encrypt_in_place<'b>( EncryptionScheme::Aes128Cbc { iv } => cbc_encrypt::(es, key, &iv, buf, pos), EncryptionScheme::Aes192Cbc { iv } => cbc_encrypt::(es, key, &iv, buf, pos), EncryptionScheme::Aes256Cbc { iv } => cbc_encrypt::(es, key, &iv, buf, pos), + EncryptionScheme::Aes128Gcm { nonce } => { + gcm_encrypt::(es, key, Nonce::from(nonce), buf, pos) + } + EncryptionScheme::Aes256Gcm { nonce } => { + gcm_encrypt::(es, key, Nonce::from(nonce), buf, pos) + } #[cfg(feature = "3des")] EncryptionScheme::DesEde3Cbc { iv } => cbc_encrypt::(es, key, &iv, buf, pos), #[cfg(feature = "des-insecure")] @@ -86,6 +152,12 @@ pub fn decrypt_in_place<'a>( EncryptionScheme::Aes128Cbc { iv } => cbc_decrypt::(es, key, &iv, buf), EncryptionScheme::Aes192Cbc { iv } => cbc_decrypt::(es, key, &iv, buf), EncryptionScheme::Aes256Cbc { iv } => cbc_decrypt::(es, key, &iv, buf), + EncryptionScheme::Aes128Gcm { nonce } => { + gcm_decrypt::(es, key, Nonce::from(nonce), buf) + } + EncryptionScheme::Aes256Gcm { nonce } => { + gcm_decrypt::(es, key, Nonce::from(nonce), buf) + } #[cfg(feature = "3des")] EncryptionScheme::DesEde3Cbc { iv } => cbc_decrypt::(es, key, &iv, buf), #[cfg(feature = "des-insecure")] diff --git a/pkcs8/tests/encrypted_private_key.rs b/pkcs8/tests/encrypted_private_key.rs index acd639c85..6989dde7d 100644 --- a/pkcs8/tests/encrypted_private_key.rs +++ b/pkcs8/tests/encrypted_private_key.rs @@ -13,7 +13,7 @@ use der::Encode; use der::EncodePem; #[cfg(feature = "encryption")] -use pkcs8::PrivateKeyInfoRef; +use pkcs8::{EncryptedPrivateKeyInfoOwned, PrivateKeyInfoRef}; /// Ed25519 PKCS#8 private key plaintext encoded as ASN.1 DER #[cfg(feature = "encryption")] @@ -50,6 +50,28 @@ const ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE: &[u8] = const ED25519_DER_AES256_SCRYPT_EXAMPLE: &[u8] = include_bytes!("examples/ed25519-encpriv-aes256-scrypt.der"); +/// Ed25519 PKCS#8 encrypted private key (PBES2 + AES-128-GCM + scrypt) encoded as ASN.1 DER. +/// +/// Generated using: +/// +/// ``` +/// $ botan pkcs8 ed25519-priv-pkcs8v1.der --der-out '--pbe=PBES2(AES-128/GCM,Scrypt)' --pass-out=hunter42 +/// ``` +#[cfg(feature = "encryption")] +const ED25519_DER_AES128_GCM_SCRYPT_EXAMPLE: &[u8] = + include_bytes!("examples/ed25519-encpriv-aes128-gcm-scrypt.der"); + +/// Ed25519 PKCS#8 encrypted private key (PBES2 + AES-256-GCM + scrypt) encoded as ASN.1 DER. +/// +/// Generated using: +/// +/// ``` +/// $ botan pkcs8 ed25519-priv-pkcs8v1.der --der-out '--pbe=PBES2(AES-256/GCM,Scrypt)' --pass-out=hunter42 +/// ``` +#[cfg(feature = "encryption")] +const ED25519_DER_AES256_GCM_SCRYPT_EXAMPLE: &[u8] = + include_bytes!("examples/ed25519-encpriv-aes256-gcm-scrypt.der"); + /// Ed25519 PKCS#8 encrypted private key encoded as PEM #[cfg(feature = "pem")] const ED25519_PEM_AES256_PBKDF2_SHA256_EXAMPLE: &str = @@ -167,6 +189,66 @@ fn decrypt_ed25519_der_encpriv_aes256_scrypt() { assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE); } +#[cfg(feature = "encryption")] +#[test] +fn decrypt_ed25519_der_encpriv_aes128_gcm_scrypt() { + let enc_pk = + EncryptedPrivateKeyInfoOwned::try_from(ED25519_DER_AES128_GCM_SCRYPT_EXAMPLE).unwrap(); + let pk = enc_pk.decrypt(PASSWORD).unwrap(); + assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE); +} + +#[cfg(feature = "encryption")] +#[test] +fn encrypt_ed25519_der_encpriv_aes128_gcm_scrypt() { + let scrypt_params = pbes2::Parameters::scrypt_aes128gcm( + pkcs5::scrypt::Params::new(14, 8, 1).unwrap(), + &hex!("05BE17663E551D120F81308E"), + hex!("D7E967A5DF6189471BCC1F49"), + ) + .unwrap(); + + let pk_plaintext = PrivateKeyInfoRef::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap(); + let pk_encrypted = pk_plaintext + .encrypt_with_params(scrypt_params, PASSWORD) + .unwrap(); + + assert_eq!( + pk_encrypted.as_bytes(), + ED25519_DER_AES128_GCM_SCRYPT_EXAMPLE + ); +} + +#[cfg(feature = "encryption")] +#[test] +fn decrypt_ed25519_der_encpriv_aes256_gcm_scrypt() { + let enc_pk = + EncryptedPrivateKeyInfoOwned::try_from(ED25519_DER_AES256_GCM_SCRYPT_EXAMPLE).unwrap(); + let pk = enc_pk.decrypt(PASSWORD).unwrap(); + assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE); +} + +#[cfg(feature = "encryption")] +#[test] +fn encrypt_ed25519_der_encpriv_aes256_gcm_scrypt() { + let scrypt_params = pbes2::Parameters::scrypt_aes256gcm( + pkcs5::scrypt::Params::new(15, 8, 1).unwrap(), + &hex!("F67F4005A8393BD41F5B4981"), + hex!("98B118A950D39E2ECB5B125C"), + ) + .unwrap(); + + let pk_plaintext = PrivateKeyInfoRef::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap(); + let pk_encrypted = pk_plaintext + .encrypt_with_params(scrypt_params, PASSWORD) + .unwrap(); + + assert_eq!( + pk_encrypted.as_bytes(), + ED25519_DER_AES256_GCM_SCRYPT_EXAMPLE + ); +} + #[cfg(feature = "encryption")] #[test] fn encrypt_ed25519_der_encpriv_aes256_pbkdf2_sha256() { diff --git a/pkcs8/tests/examples/ed25519-encpriv-aes128-gcm-scrypt.der b/pkcs8/tests/examples/ed25519-encpriv-aes128-gcm-scrypt.der new file mode 100644 index 000000000..6d76fb764 Binary files /dev/null and b/pkcs8/tests/examples/ed25519-encpriv-aes128-gcm-scrypt.der differ diff --git a/pkcs8/tests/examples/ed25519-encpriv-aes256-gcm-scrypt.der b/pkcs8/tests/examples/ed25519-encpriv-aes256-gcm-scrypt.der new file mode 100644 index 000000000..c95e45c71 Binary files /dev/null and b/pkcs8/tests/examples/ed25519-encpriv-aes256-gcm-scrypt.der differ