Skip to content

Commit 8ced406

Browse files
authored
pkcs5: add support for using AES-GCM in PBES2 (#1433)
Interoperable with pycryptodome and botan
1 parent 91ae4f9 commit 8ced406

File tree

7 files changed

+231
-2
lines changed

7 files changed

+231
-2
lines changed

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ whirlpool = { git = "https://github.com/RustCrypto/hashes.git" }
6868
cbc = { git = "https://github.com/RustCrypto/block-modes.git" }
6969
# Pending a release of 0.11.0-pre
7070
salsa20 = { git = "https://github.com/RustCrypto/stream-ciphers.git" }
71+
# Pending a release of 0.11.0-pre
72+
aes-gcm = { git = "https://github.com/RustCrypto/AEADs.git" }
73+
aead = { git = "https://github.com/RustCrypto/traits.git" }
74+
ctr = { git = "https://github.com/RustCrypto/block-modes.git" }
7175

7276
# https://github.com/RustCrypto/formats/pull/1055
7377
# https://github.com/RustCrypto/signatures/pull/809

pkcs5/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ spki = { version = "=0.8.0-pre.0" }
2222
# optional dependencies
2323
cbc = { version = "=0.2.0-pre", optional = true }
2424
aes = { version = "=0.9.0-pre", optional = true, default-features = false }
25+
aes-gcm = { version = "=0.11.0-pre", optional = true, default-features = false, features = ["aes"] }
2526
des = { version = "=0.9.0-pre.0", optional = true, default-features = false }
2627
pbkdf2 = { version = "=0.13.0-pre.0", optional = true, default-features = false, features = ["hmac"] }
2728
rand_core = { version = "0.6.4", optional = true, default-features = false }
@@ -37,7 +38,7 @@ alloc = []
3738
3des = ["dep:des", "pbes2"]
3839
des-insecure = ["dep:des", "pbes2"]
3940
getrandom = ["rand_core/getrandom"]
40-
pbes2 = ["dep:aes", "dep:cbc", "dep:pbkdf2", "dep:scrypt", "dep:sha2"]
41+
pbes2 = ["dep:aes", "dep:cbc", "dep:pbkdf2", "dep:scrypt", "dep:sha2", "dep:aes-gcm"]
4142
sha1-insecure = ["dep:sha1", "pbes2"]
4243

4344
[package.metadata.docs.rs]

pkcs5/src/pbes2.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ pub const AES_192_CBC_OID: ObjectIdentifier =
3939
pub const AES_256_CBC_OID: ObjectIdentifier =
4040
ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.42");
4141

42+
/// 128-bit Advanced Encryption Standard (AES) algorithm with Galois Counter Mode
43+
pub const AES_128_GCM_OID: ObjectIdentifier =
44+
ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.6");
45+
46+
/// 256-bit Advanced Encryption Standard (AES) algorithm with Galois Counter Mode
47+
pub const AES_256_GCM_OID: ObjectIdentifier =
48+
ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.46");
49+
4250
/// DES operating in CBC mode
4351
#[cfg(feature = "des-insecure")]
4452
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
5563
/// AES cipher block size
5664
const AES_BLOCK_SIZE: usize = 16;
5765

66+
/// GCM nonce size
67+
///
68+
/// We could use any value here but GCM is most efficient
69+
/// with 96 bit nonces
70+
const GCM_NONCE_SIZE: usize = 12;
71+
5872
/// DES / Triple DES block size
5973
#[cfg(any(feature = "3des", feature = "des-insecure"))]
6074
const DES_BLOCK_SIZE: usize = 8;
@@ -205,6 +219,40 @@ impl Parameters {
205219
Ok(Self { kdf, encryption })
206220
}
207221

222+
/// Initialize PBES2 parameters using scrypt as the password-based
223+
/// key derivation function and AES-128-GCM as the symmetric cipher.
224+
///
225+
/// For more information on scrypt parameters, see documentation for the
226+
/// [`scrypt::Params`] struct.
227+
// TODO(tarcieri): encapsulate `scrypt::Params`?
228+
#[cfg(feature = "pbes2")]
229+
pub fn scrypt_aes128gcm(
230+
params: scrypt::Params,
231+
salt: &[u8],
232+
gcm_nonce: [u8; GCM_NONCE_SIZE],
233+
) -> Result<Self> {
234+
let kdf = ScryptParams::from_params_and_salt(params, salt)?.into();
235+
let encryption = EncryptionScheme::Aes128Gcm { nonce: gcm_nonce };
236+
Ok(Self { kdf, encryption })
237+
}
238+
239+
/// Initialize PBES2 parameters using scrypt as the password-based
240+
/// key derivation function and AES-256-GCM as the symmetric cipher.
241+
///
242+
/// For more information on scrypt parameters, see documentation for the
243+
/// [`scrypt::Params`] struct.
244+
// TODO(tarcieri): encapsulate `scrypt::Params`?
245+
#[cfg(feature = "pbes2")]
246+
pub fn scrypt_aes256gcm(
247+
params: scrypt::Params,
248+
salt: &[u8],
249+
gcm_nonce: [u8; GCM_NONCE_SIZE],
250+
) -> Result<Self> {
251+
let kdf = ScryptParams::from_params_and_salt(params, salt)?.into();
252+
let encryption = EncryptionScheme::Aes256Gcm { nonce: gcm_nonce };
253+
Ok(Self { kdf, encryption })
254+
}
255+
208256
/// Attempt to decrypt the given ciphertext, allocating and returning a
209257
/// byte vector containing the plaintext.
210258
#[cfg(all(feature = "alloc", feature = "pbes2"))]
@@ -321,6 +369,18 @@ pub enum EncryptionScheme {
321369
iv: [u8; AES_BLOCK_SIZE],
322370
},
323371

372+
/// AES-128 in CBC mode
373+
Aes128Gcm {
374+
/// GCM nonce
375+
nonce: [u8; GCM_NONCE_SIZE],
376+
},
377+
378+
/// AES-256 in GCM mode
379+
Aes256Gcm {
380+
/// GCM nonce
381+
nonce: [u8; GCM_NONCE_SIZE],
382+
},
383+
324384
/// 3-Key Triple DES in CBC mode
325385
#[cfg(feature = "3des")]
326386
DesEde3Cbc {
@@ -343,6 +403,8 @@ impl EncryptionScheme {
343403
Self::Aes128Cbc { .. } => 16,
344404
Self::Aes192Cbc { .. } => 24,
345405
Self::Aes256Cbc { .. } => 32,
406+
Self::Aes128Gcm { .. } => 16,
407+
Self::Aes256Gcm { .. } => 32,
346408
#[cfg(feature = "des-insecure")]
347409
Self::DesCbc { .. } => 8,
348410
#[cfg(feature = "3des")]
@@ -356,6 +418,8 @@ impl EncryptionScheme {
356418
Self::Aes128Cbc { .. } => AES_128_CBC_OID,
357419
Self::Aes192Cbc { .. } => AES_192_CBC_OID,
358420
Self::Aes256Cbc { .. } => AES_256_CBC_OID,
421+
Self::Aes128Gcm { .. } => AES_128_GCM_OID,
422+
Self::Aes256Gcm { .. } => AES_256_GCM_OID,
359423
#[cfg(feature = "des-insecure")]
360424
Self::DesCbc { .. } => DES_CBC_OID,
361425
#[cfg(feature = "3des")]
@@ -399,6 +463,12 @@ impl TryFrom<AlgorithmIdentifierRef<'_>> for EncryptionScheme {
399463
AES_256_CBC_OID => Ok(Self::Aes256Cbc {
400464
iv: iv.try_into().map_err(|_| Tag::OctetString.value_error())?,
401465
}),
466+
AES_128_GCM_OID => Ok(Self::Aes128Gcm {
467+
nonce: iv.try_into().map_err(|_| Tag::OctetString.value_error())?,
468+
}),
469+
AES_256_GCM_OID => Ok(Self::Aes256Gcm {
470+
nonce: iv.try_into().map_err(|_| Tag::OctetString.value_error())?,
471+
}),
402472
#[cfg(feature = "des-insecure")]
403473
DES_CBC_OID => Ok(Self::DesCbc {
404474
iv: iv[0..DES_BLOCK_SIZE]
@@ -424,6 +494,8 @@ impl<'a> TryFrom<&'a EncryptionScheme> for AlgorithmIdentifierRef<'a> {
424494
EncryptionScheme::Aes128Cbc { iv } => iv.as_slice(),
425495
EncryptionScheme::Aes192Cbc { iv } => iv.as_slice(),
426496
EncryptionScheme::Aes256Cbc { iv } => iv.as_slice(),
497+
EncryptionScheme::Aes128Gcm { nonce } => nonce.as_slice(),
498+
EncryptionScheme::Aes256Gcm { nonce } => nonce.as_slice(),
427499
#[cfg(feature = "des-insecure")]
428500
EncryptionScheme::DesCbc { iv } => iv.as_slice(),
429501
#[cfg(feature = "3des")]

pkcs5/src/pbes2/encryption.rs

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
use super::{EncryptionScheme, Kdf, Parameters, Pbkdf2Params, Pbkdf2Prf, ScryptParams};
44
use crate::{Error, Result};
5+
use aes_gcm::{AeadInPlace, KeyInit as GcmKeyInit, Nonce, Tag};
56
use cbc::cipher::{
67
block_padding::Pkcs7, BlockCipher, BlockCipherDecrypt, BlockCipherEncrypt, BlockModeDecrypt,
78
BlockModeEncrypt, KeyInit, KeyIvInit,
@@ -11,7 +12,7 @@ use pbkdf2::{
1112
digest::{
1213
block_buffer::Eager,
1314
core_api::{BlockSizeUser, BufferKindUser, FixedOutputCore, UpdateCore},
14-
typenum::{IsLess, Le, NonZero, U256},
15+
typenum::{IsLess, Le, NonZero, U12, U16, U256},
1516
HashMarker,
1617
},
1718
EagerHash,
@@ -48,6 +49,65 @@ fn cbc_decrypt<'a, C: BlockCipherDecrypt + BlockCipher + KeyInit>(
4849
.map_err(|_| Error::EncryptFailed)
4950
}
5051

52+
fn gcm_encrypt<C, NonceSize, TagSize>(
53+
es: EncryptionScheme,
54+
key: EncryptionKey,
55+
nonce: Nonce<NonceSize>,
56+
buffer: &mut [u8],
57+
pos: usize,
58+
) -> Result<&[u8]>
59+
where
60+
C: BlockCipher + BlockSizeUser<BlockSize = U16> + GcmKeyInit + BlockCipherEncrypt,
61+
aes_gcm::AesGcm<C, NonceSize, TagSize>: GcmKeyInit,
62+
TagSize: aes_gcm::TagSize,
63+
NonceSize: aes::cipher::ArraySize,
64+
{
65+
if buffer.len() < TagSize::USIZE + pos {
66+
return Err(Error::EncryptFailed);
67+
}
68+
let gcm =
69+
<aes_gcm::AesGcm<C, NonceSize, TagSize> as GcmKeyInit>::new_from_slice(key.as_slice())
70+
.map_err(|_| es.to_alg_params_invalid())?;
71+
let tag = gcm
72+
.encrypt_in_place_detached(&nonce, &[], &mut buffer[..pos])
73+
.map_err(|_| Error::EncryptFailed)?;
74+
buffer[pos..].copy_from_slice(tag.as_ref());
75+
Ok(&buffer[0..pos + TagSize::USIZE])
76+
}
77+
78+
fn gcm_decrypt<C, NonceSize, TagSize>(
79+
es: EncryptionScheme,
80+
key: EncryptionKey,
81+
nonce: Nonce<NonceSize>,
82+
buffer: &mut [u8],
83+
) -> Result<&[u8]>
84+
where
85+
C: BlockCipher + BlockSizeUser<BlockSize = U16> + GcmKeyInit + BlockCipherEncrypt,
86+
aes_gcm::AesGcm<C, NonceSize, TagSize>: GcmKeyInit,
87+
TagSize: aes_gcm::TagSize,
88+
NonceSize: aes::cipher::ArraySize,
89+
{
90+
let msg_len = buffer
91+
.len()
92+
.checked_sub(TagSize::USIZE)
93+
.ok_or(Error::DecryptFailed)?;
94+
95+
let gcm =
96+
<aes_gcm::AesGcm<C, NonceSize, TagSize> as GcmKeyInit>::new_from_slice(key.as_slice())
97+
.map_err(|_| es.to_alg_params_invalid())?;
98+
99+
let tag = Tag::try_from(&buffer[msg_len..]).map_err(|_| Error::DecryptFailed)?;
100+
101+
if gcm
102+
.decrypt_in_place_detached(&nonce, &[], &mut buffer[..msg_len], &tag)
103+
.is_err()
104+
{
105+
return Err(Error::DecryptFailed);
106+
}
107+
108+
Ok(&buffer[..msg_len])
109+
}
110+
51111
pub fn encrypt_in_place<'b>(
52112
params: &Parameters,
53113
password: impl AsRef<[u8]>,
@@ -65,6 +125,12 @@ pub fn encrypt_in_place<'b>(
65125
EncryptionScheme::Aes128Cbc { iv } => cbc_encrypt::<aes::Aes128Enc>(es, key, &iv, buf, pos),
66126
EncryptionScheme::Aes192Cbc { iv } => cbc_encrypt::<aes::Aes192Enc>(es, key, &iv, buf, pos),
67127
EncryptionScheme::Aes256Cbc { iv } => cbc_encrypt::<aes::Aes256Enc>(es, key, &iv, buf, pos),
128+
EncryptionScheme::Aes128Gcm { nonce } => {
129+
gcm_encrypt::<aes::Aes128Enc, U12, U16>(es, key, Nonce::from(nonce), buf, pos)
130+
}
131+
EncryptionScheme::Aes256Gcm { nonce } => {
132+
gcm_encrypt::<aes::Aes256Enc, U12, U16>(es, key, Nonce::from(nonce), buf, pos)
133+
}
68134
#[cfg(feature = "3des")]
69135
EncryptionScheme::DesEde3Cbc { iv } => cbc_encrypt::<des::TdesEde3>(es, key, &iv, buf, pos),
70136
#[cfg(feature = "des-insecure")]
@@ -87,6 +153,12 @@ pub fn decrypt_in_place<'a>(
87153
EncryptionScheme::Aes128Cbc { iv } => cbc_decrypt::<aes::Aes128Dec>(es, key, &iv, buf),
88154
EncryptionScheme::Aes192Cbc { iv } => cbc_decrypt::<aes::Aes192Dec>(es, key, &iv, buf),
89155
EncryptionScheme::Aes256Cbc { iv } => cbc_decrypt::<aes::Aes256Dec>(es, key, &iv, buf),
156+
EncryptionScheme::Aes128Gcm { nonce } => {
157+
gcm_decrypt::<aes::Aes128Enc, U12, U16>(es, key, Nonce::from(nonce), buf)
158+
}
159+
EncryptionScheme::Aes256Gcm { nonce } => {
160+
gcm_decrypt::<aes::Aes256Enc, U12, U16>(es, key, Nonce::from(nonce), buf)
161+
}
90162
#[cfg(feature = "3des")]
91163
EncryptionScheme::DesEde3Cbc { iv } => cbc_decrypt::<des::TdesEde3>(es, key, &iv, buf),
92164
#[cfg(feature = "des-insecure")]

pkcs8/tests/encrypted_private_key.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,28 @@ const ED25519_DER_AES256_PBKDF2_SHA256_EXAMPLE: &[u8] =
4646
const ED25519_DER_AES256_SCRYPT_EXAMPLE: &[u8] =
4747
include_bytes!("examples/ed25519-encpriv-aes256-scrypt.der");
4848

49+
/// Ed25519 PKCS#8 encrypted private key (PBES2 + AES-128-GCM + scrypt) encoded as ASN.1 DER.
50+
///
51+
/// Generated using:
52+
///
53+
/// ```
54+
/// $ botan pkcs8 ed25519-priv-pkcs8v1.der --der-out '--pbe=PBES2(AES-128/GCM,Scrypt)' --pass-out=hunter42
55+
/// ```
56+
#[cfg(feature = "encryption")]
57+
const ED25519_DER_AES128_GCM_SCRYPT_EXAMPLE: &[u8] =
58+
include_bytes!("examples/ed25519-encpriv-aes128-gcm-scrypt.der");
59+
60+
/// Ed25519 PKCS#8 encrypted private key (PBES2 + AES-256-GCM + scrypt) encoded as ASN.1 DER.
61+
///
62+
/// Generated using:
63+
///
64+
/// ```
65+
/// $ botan pkcs8 ed25519-priv-pkcs8v1.der --der-out '--pbe=PBES2(AES-256/GCM,Scrypt)' --pass-out=hunter42
66+
/// ```
67+
#[cfg(feature = "encryption")]
68+
const ED25519_DER_AES256_GCM_SCRYPT_EXAMPLE: &[u8] =
69+
include_bytes!("examples/ed25519-encpriv-aes256-gcm-scrypt.der");
70+
4971
/// Ed25519 PKCS#8 encrypted private key encoded as PEM
5072
#[cfg(feature = "pem")]
5173
const ED25519_PEM_AES256_PBKDF2_SHA256_EXAMPLE: &str =
@@ -158,6 +180,64 @@ fn decrypt_ed25519_der_encpriv_aes256_scrypt() {
158180
assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE);
159181
}
160182

183+
#[cfg(feature = "encryption")]
184+
#[test]
185+
fn decrypt_ed25519_der_encpriv_aes128_gcm_scrypt() {
186+
let enc_pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_AES128_GCM_SCRYPT_EXAMPLE).unwrap();
187+
let pk = enc_pk.decrypt(PASSWORD).unwrap();
188+
assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE);
189+
}
190+
191+
#[cfg(feature = "encryption")]
192+
#[test]
193+
fn encrypt_ed25519_der_encpriv_aes128_gcm_scrypt() {
194+
let scrypt_params = pkcs5::pbes2::Parameters::scrypt_aes128gcm(
195+
pkcs5::scrypt::Params::new(14, 8, 1, 16).unwrap(),
196+
&hex!("05BE17663E551D120F81308E"),
197+
hex!("D7E967A5DF6189471BCC1F49"),
198+
)
199+
.unwrap();
200+
201+
let pk_plaintext = PrivateKeyInfo::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap();
202+
let pk_encrypted = pk_plaintext
203+
.encrypt_with_params(scrypt_params, PASSWORD)
204+
.unwrap();
205+
206+
assert_eq!(
207+
pk_encrypted.as_bytes(),
208+
ED25519_DER_AES128_GCM_SCRYPT_EXAMPLE
209+
);
210+
}
211+
212+
#[cfg(feature = "encryption")]
213+
#[test]
214+
fn decrypt_ed25519_der_encpriv_aes256_gcm_scrypt() {
215+
let enc_pk = EncryptedPrivateKeyInfo::try_from(ED25519_DER_AES256_GCM_SCRYPT_EXAMPLE).unwrap();
216+
let pk = enc_pk.decrypt(PASSWORD).unwrap();
217+
assert_eq!(pk.as_bytes(), ED25519_DER_PLAINTEXT_EXAMPLE);
218+
}
219+
220+
#[cfg(feature = "encryption")]
221+
#[test]
222+
fn encrypt_ed25519_der_encpriv_aes256_gcm_scrypt() {
223+
let scrypt_params = pkcs5::pbes2::Parameters::scrypt_aes256gcm(
224+
pkcs5::scrypt::Params::new(15, 8, 1, 32).unwrap(),
225+
&hex!("F67F4005A8393BD41F5B4981"),
226+
hex!("98B118A950D39E2ECB5B125C"),
227+
)
228+
.unwrap();
229+
230+
let pk_plaintext = PrivateKeyInfo::try_from(ED25519_DER_PLAINTEXT_EXAMPLE).unwrap();
231+
let pk_encrypted = pk_plaintext
232+
.encrypt_with_params(scrypt_params, PASSWORD)
233+
.unwrap();
234+
235+
assert_eq!(
236+
pk_encrypted.as_bytes(),
237+
ED25519_DER_AES256_GCM_SCRYPT_EXAMPLE
238+
);
239+
}
240+
161241
#[cfg(feature = "encryption")]
162242
#[test]
163243
fn encrypt_ed25519_der_encpriv_aes256_pbkdf2_sha256() {
Binary file not shown.
Binary file not shown.

0 commit comments

Comments
 (0)