Skip to content

Commit

Permalink
pkcs5: add support for using AES-GCM in PBES2 (#1433)
Browse files Browse the repository at this point in the history
Interoperable with pycryptodome and botan
  • Loading branch information
randombit committed Jun 15, 2024
1 parent 91ae4f9 commit 8ced406
Show file tree
Hide file tree
Showing 7 changed files with 231 additions and 2 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion pkcs5/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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]
Expand Down
72 changes: 72 additions & 0 deletions pkcs5/src/pbes2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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;
Expand Down Expand Up @@ -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<Self> {
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<Self> {
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"))]
Expand Down Expand Up @@ -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 {
Expand All @@ -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")]
Expand All @@ -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")]
Expand Down Expand Up @@ -399,6 +463,12 @@ impl TryFrom<AlgorithmIdentifierRef<'_>> 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]
Expand All @@ -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")]
Expand Down
74 changes: 73 additions & 1 deletion pkcs5/src/pbes2/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -48,6 +49,65 @@ fn cbc_decrypt<'a, C: BlockCipherDecrypt + BlockCipher + KeyInit>(
.map_err(|_| Error::EncryptFailed)
}

fn gcm_encrypt<C, NonceSize, TagSize>(
es: EncryptionScheme,
key: EncryptionKey,
nonce: Nonce<NonceSize>,
buffer: &mut [u8],
pos: usize,
) -> Result<&[u8]>
where
C: BlockCipher + BlockSizeUser<BlockSize = U16> + GcmKeyInit + BlockCipherEncrypt,
aes_gcm::AesGcm<C, NonceSize, TagSize>: GcmKeyInit,
TagSize: aes_gcm::TagSize,
NonceSize: aes::cipher::ArraySize,
{
if buffer.len() < TagSize::USIZE + pos {
return Err(Error::EncryptFailed);
}
let gcm =
<aes_gcm::AesGcm<C, NonceSize, TagSize> 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<C, NonceSize, TagSize>(
es: EncryptionScheme,
key: EncryptionKey,
nonce: Nonce<NonceSize>,
buffer: &mut [u8],
) -> Result<&[u8]>
where
C: BlockCipher + BlockSizeUser<BlockSize = U16> + GcmKeyInit + BlockCipherEncrypt,
aes_gcm::AesGcm<C, NonceSize, TagSize>: GcmKeyInit,
TagSize: aes_gcm::TagSize,
NonceSize: aes::cipher::ArraySize,
{
let msg_len = buffer
.len()
.checked_sub(TagSize::USIZE)
.ok_or(Error::DecryptFailed)?;

let gcm =
<aes_gcm::AesGcm<C, NonceSize, TagSize> 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]>,
Expand All @@ -65,6 +125,12 @@ pub fn encrypt_in_place<'b>(
EncryptionScheme::Aes128Cbc { iv } => cbc_encrypt::<aes::Aes128Enc>(es, key, &iv, buf, pos),
EncryptionScheme::Aes192Cbc { iv } => cbc_encrypt::<aes::Aes192Enc>(es, key, &iv, buf, pos),
EncryptionScheme::Aes256Cbc { iv } => cbc_encrypt::<aes::Aes256Enc>(es, key, &iv, buf, pos),
EncryptionScheme::Aes128Gcm { nonce } => {
gcm_encrypt::<aes::Aes128Enc, U12, U16>(es, key, Nonce::from(nonce), buf, pos)
}
EncryptionScheme::Aes256Gcm { nonce } => {
gcm_encrypt::<aes::Aes256Enc, U12, U16>(es, key, Nonce::from(nonce), buf, pos)
}
#[cfg(feature = "3des")]
EncryptionScheme::DesEde3Cbc { iv } => cbc_encrypt::<des::TdesEde3>(es, key, &iv, buf, pos),
#[cfg(feature = "des-insecure")]
Expand All @@ -87,6 +153,12 @@ pub fn decrypt_in_place<'a>(
EncryptionScheme::Aes128Cbc { iv } => cbc_decrypt::<aes::Aes128Dec>(es, key, &iv, buf),
EncryptionScheme::Aes192Cbc { iv } => cbc_decrypt::<aes::Aes192Dec>(es, key, &iv, buf),
EncryptionScheme::Aes256Cbc { iv } => cbc_decrypt::<aes::Aes256Dec>(es, key, &iv, buf),
EncryptionScheme::Aes128Gcm { nonce } => {
gcm_decrypt::<aes::Aes128Enc, U12, U16>(es, key, Nonce::from(nonce), buf)
}
EncryptionScheme::Aes256Gcm { nonce } => {
gcm_decrypt::<aes::Aes256Enc, U12, U16>(es, key, Nonce::from(nonce), buf)
}
#[cfg(feature = "3des")]
EncryptionScheme::DesEde3Cbc { iv } => cbc_decrypt::<des::TdesEde3>(es, key, &iv, buf),
#[cfg(feature = "des-insecure")]
Expand Down
80 changes: 80 additions & 0 deletions pkcs8/tests/encrypted_private_key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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() {
Expand Down
Binary file not shown.
Binary file not shown.

0 comments on commit 8ced406

Please sign in to comment.