diff --git a/Cargo.toml b/Cargo.toml index 1bc56ca67..f8879bd7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -68,6 +68,10 @@ whirlpool = { git = "https://github.com/RustCrypto/hashes.git" } cbc = { git = "https://github.com/RustCrypto/block-modes.git" } # Pending a release of 0.11.0-pre salsa20 = { git = "https://github.com/RustCrypto/stream-ciphers.git" } +# Pending a release of 0.11.0-pre +aes-gcm = { git = "https://github.com/RustCrypto/AEADs.git" } +aead = { git = "https://github.com/RustCrypto/traits.git" } +ctr = { git = "https://github.com/RustCrypto/block-modes.git" } # https://github.com/RustCrypto/formats/pull/1055 # https://github.com/RustCrypto/signatures/pull/809 diff --git a/pkcs5/Cargo.toml b/pkcs5/Cargo.toml index 94fb4d9fc..7d988f334 100644 --- a/pkcs5/Cargo.toml +++ b/pkcs5/Cargo.toml @@ -22,6 +22,7 @@ spki = { version = "=0.8.0-pre.0" } # optional dependencies cbc = { version = "=0.2.0-pre", optional = true } aes = { version = "=0.9.0-pre", optional = true, default-features = false } +aes-gcm = { version = "=0.11.0-pre", optional = true, default-features = false, features = ["aes"] } des = { version = "=0.9.0-pre.0", optional = true, default-features = false } pbkdf2 = { version = "=0.13.0-pre.0", optional = true, default-features = false, features = ["hmac"] } rand_core = { version = "0.6.4", optional = true, default-features = false } @@ -37,7 +38,7 @@ alloc = [] 3des = ["dep:des", "pbes2"] des-insecure = ["dep:des", "pbes2"] getrandom = ["rand_core/getrandom"] -pbes2 = ["dep:aes", "dep:cbc", "dep:pbkdf2", "dep:scrypt", "dep:sha2"] +pbes2 = ["dep:aes", "dep:cbc", "dep:pbkdf2", "dep:scrypt", "dep:sha2", "dep:aes-gcm"] sha1-insecure = ["dep:sha1", "pbes2"] [package.metadata.docs.rs] diff --git a/pkcs5/src/pbes2.rs b/pkcs5/src/pbes2.rs index d7b63b8f4..80f7485e5 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; @@ -205,6 +219,40 @@ 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. + // 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. + // 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. #[cfg(all(feature = "alloc", feature = "pbes2"))] @@ -321,6 +369,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 { @@ -343,6 +403,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")] @@ -356,6 +418,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")] @@ -399,6 +463,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] @@ -424,6 +494,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 4cf4924cb..7a749158a 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::{AeadInPlace, KeyInit as GcmKeyInit, Nonce, Tag}; use cbc::cipher::{ block_padding::Pkcs7, BlockCipher, BlockCipherDecrypt, BlockCipherEncrypt, BlockModeDecrypt, BlockModeEncrypt, KeyInit, KeyIvInit, @@ -11,7 +12,7 @@ use pbkdf2::{ digest::{ block_buffer::Eager, core_api::{BlockSizeUser, BufferKindUser, FixedOutputCore, UpdateCore}, - typenum::{IsLess, Le, NonZero, U256}, + typenum::{IsLess, Le, NonZero, U12, U16, U256}, HashMarker, }, EagerHash, @@ -48,6 +49,65 @@ fn cbc_decrypt<'a, C: BlockCipherDecrypt + BlockCipher + KeyInit>( .map_err(|_| Error::EncryptFailed) } +fn gcm_encrypt( + es: EncryptionScheme, + key: EncryptionKey, + nonce: Nonce, + buffer: &mut [u8], + pos: usize, +) -> Result<&[u8]> +where + C: BlockCipher + BlockSizeUser + GcmKeyInit + BlockCipherEncrypt, + aes_gcm::AesGcm: GcmKeyInit, + TagSize: aes_gcm::TagSize, + NonceSize: aes::cipher::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_in_place_detached(&nonce, &[], &mut buffer[..pos]) + .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: BlockCipher + BlockSizeUser + GcmKeyInit + BlockCipherEncrypt, + aes_gcm::AesGcm: GcmKeyInit, + TagSize: aes_gcm::TagSize, + NonceSize: aes::cipher::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_in_place_detached(&nonce, &[], &mut buffer[..msg_len], &tag) + .is_err() + { + return Err(Error::DecryptFailed); + } + + Ok(&buffer[..msg_len]) +} + pub fn encrypt_in_place<'b>( params: &Parameters, password: impl AsRef<[u8]>, @@ -65,6 +125,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")] @@ -87,6 +153,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 9d601c749..cd7ad6b0a 100644 --- a/pkcs8/tests/encrypted_private_key.rs +++ b/pkcs8/tests/encrypted_private_key.rs @@ -46,6 +46,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 = @@ -158,6 +180,64 @@ 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 = EncryptedPrivateKeyInfo::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 = pkcs5::pbes2::Parameters::scrypt_aes128gcm( + pkcs5::scrypt::Params::new(14, 8, 1, 16).unwrap(), + &hex!("05BE17663E551D120F81308E"), + hex!("D7E967A5DF6189471BCC1F49"), + ) + .unwrap(); + + let pk_plaintext = PrivateKeyInfo::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 = EncryptedPrivateKeyInfo::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 = pkcs5::pbes2::Parameters::scrypt_aes256gcm( + pkcs5::scrypt::Params::new(15, 8, 1, 32).unwrap(), + &hex!("F67F4005A8393BD41F5B4981"), + hex!("98B118A950D39E2ECB5B125C"), + ) + .unwrap(); + + let pk_plaintext = PrivateKeyInfo::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