Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkcs5: add support for using AES-GCM in PBES2 #1433

Merged
merged 9 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<'a, C, NonceSize, TagSize>(
es: EncryptionScheme,
key: EncryptionKey,
nonce: Nonce<NonceSize>,
buffer: &'a mut [u8],
pos: usize,
) -> Result<&'a [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<'a, C, NonceSize, TagSize>(
es: EncryptionScheme,
key: EncryptionKey,
nonce: Nonce<NonceSize>,
buffer: &'a mut [u8],
) -> Result<&'a [u8]>
where
C: BlockCipher + BlockSizeUser<BlockSize = U16> + GcmKeyInit + BlockCipherEncrypt,
aes_gcm::AesGcm<C, U12>: GcmKeyInit,
randombit marked this conversation as resolved.
Show resolved Hide resolved
TagSize: aes_gcm::TagSize,
NonceSize: aes::cipher::ArraySize,
{
let msg_len = if buffer.len() < TagSize::USIZE {
return Err(Error::DecryptFailed);
} else {
buffer.len() - TagSize::USIZE
};
randombit marked this conversation as resolved.
Show resolved Hide resolved

let gcm = <aes_gcm::AesGcm<C, NonceSize> as GcmKeyInit>::new_from_slice(key.as_slice())
randombit marked this conversation as resolved.
Show resolved Hide resolved
.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.
Loading