Skip to content

Commit 69afde7

Browse files
authored
pkcs8: encryption support (#302)
Following up on #293 which added PKCS#8 decryption support, this adds the corresponding support for encrypting `PrivateKeyInfo` as `EncryptedPrivateKeyInfo`. It provides a simple API which generates a random salt and IV using a provided `CryptoRng`, then uses PBES2 with PBKDF2-SHA256 and AES-256-CBC. It also provides a paramaterized `encrypt_with_params` which allows for supplying a `pbes2::Parameters` structure.
1 parent 92371fd commit 69afde7

File tree

12 files changed

+238
-52
lines changed

12 files changed

+238
-52
lines changed

.github/workflows/pkcs5.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ jobs:
3939
target: ${{ matrix.target }}
4040
override: true
4141
- run: cargo build --target ${{ matrix.target }} --release
42+
- run: cargo build --target ${{ matrix.target }} --release --features alloc
4243
- run: cargo build --target ${{ matrix.target }} --release --features pbes2
44+
- run: cargo build --target ${{ matrix.target }} --release --features alloc,pbes2
4345

4446
test:
4547
runs-on: ubuntu-latest
@@ -56,4 +58,6 @@ jobs:
5658
toolchain: ${{ matrix.rust }}
5759
override: true
5860
- run: cargo test --release
61+
- run: cargo test --release --features alloc
62+
- run: cargo test --release --features pbes2
5963
- run: cargo test --release --all-features

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkcs5/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ sha2 = { version = "0.9", optional = true, default-features = false }
2727
hex-literal = "0.3"
2828

2929
[features]
30+
alloc = []
3031
pbes2 = ["aes", "block-modes", "hmac", "pbkdf2", "sha2"]
3132

3233
[package.metadata.docs.rs]

pkcs5/src/lib.rs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,34 @@
2525
#![forbid(unsafe_code)]
2626
#![warn(missing_docs, rust_2018_idioms, unused_qualifications)]
2727

28+
#[cfg(all(feature = "alloc", feature = "pbes2"))]
29+
extern crate alloc;
30+
31+
pub mod pbes1;
32+
pub mod pbes2;
33+
2834
pub use der::{self, Error, ObjectIdentifier};
2935
pub use spki::AlgorithmIdentifier;
3036

31-
use core::convert::{TryFrom, TryInto};
37+
use core::{
38+
convert::{TryFrom, TryInto},
39+
fmt,
40+
};
3241
use der::{sequence, Any, Encodable, Encoder, Length};
3342

34-
pub mod pbes1;
35-
pub mod pbes2;
43+
#[cfg(all(feature = "alloc", feature = "pbes2"))]
44+
use alloc::vec::Vec;
3645

3746
/// Cryptographic errors
3847
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
3948
pub struct CryptoError;
4049

50+
impl fmt::Display for CryptoError {
51+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52+
f.write_str("PKCS#5 cryptographic error")
53+
}
54+
}
55+
4156
/// Supported PKCS#5 password-based encryption schemes.
4257
#[derive(Clone, Debug, Eq, PartialEq)]
4358
#[non_exhaustive]
@@ -55,18 +70,18 @@ pub enum EncryptionScheme<'a> {
5570
}
5671

5772
impl<'a> EncryptionScheme<'a> {
58-
/// Encrypt the given ciphertext in-place using a key derived from the
59-
/// provided password and this scheme's parameters.
60-
#[cfg(feature = "pbes2")]
73+
/// Attempt to decrypt the given ciphertext, allocating and returning a
74+
/// byte vector containing the plaintext.
75+
#[cfg(all(feature = "alloc", feature = "pbes2"))]
76+
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
6177
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
62-
pub fn encrypt_in_place<'b>(
78+
pub fn decrypt(
6379
&self,
6480
password: impl AsRef<[u8]>,
65-
buffer: &'b mut [u8],
66-
pos: usize,
67-
) -> Result<&'b [u8], CryptoError> {
81+
ciphertext: &[u8],
82+
) -> Result<Vec<u8>, CryptoError> {
6883
match self {
69-
Self::Pbes2(params) => params.encrypt_in_place(password, buffer, pos),
84+
Self::Pbes2(params) => params.decrypt(password, ciphertext),
7085
_ => Err(CryptoError),
7186
}
7287
}
@@ -90,6 +105,38 @@ impl<'a> EncryptionScheme<'a> {
90105
}
91106
}
92107

108+
/// Encrypt the given plaintext, allocating and returning a vector
109+
/// containing the ciphertext.
110+
#[cfg(all(feature = "alloc", feature = "pbes2"))]
111+
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
112+
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
113+
pub fn encrypt(
114+
&self,
115+
password: impl AsRef<[u8]>,
116+
plaintext: &[u8],
117+
) -> Result<Vec<u8>, CryptoError> {
118+
match self {
119+
Self::Pbes2(params) => params.encrypt(password, plaintext),
120+
_ => Err(CryptoError),
121+
}
122+
}
123+
124+
/// Encrypt the given ciphertext in-place using a key derived from the
125+
/// provided password and this scheme's parameters.
126+
#[cfg(feature = "pbes2")]
127+
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
128+
pub fn encrypt_in_place<'b>(
129+
&self,
130+
password: impl AsRef<[u8]>,
131+
buffer: &'b mut [u8],
132+
pos: usize,
133+
) -> Result<&'b [u8], CryptoError> {
134+
match self {
135+
Self::Pbes2(params) => params.encrypt_in_place(password, buffer, pos),
136+
_ => Err(CryptoError),
137+
}
138+
}
139+
93140
/// Get the [`ObjectIdentifier`] (a.k.a OID) for this algorithm.
94141
pub fn oid(&self) -> ObjectIdentifier {
95142
match self {

pkcs5/src/pbes2.rs

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ use core::convert::{TryFrom, TryInto};
1010
use der::{Any, Decodable, Encodable, Encoder, Error, ErrorKind, Length, Message, OctetString};
1111
use spki::AlgorithmParameters;
1212

13+
#[cfg(all(feature = "alloc", feature = "pbes2"))]
14+
use alloc::vec::Vec;
15+
1316
/// Password-Based Encryption Scheme 2 (PBES2) OID.
1417
///
1518
/// <https://tools.ietf.org/html/rfc8018#section-6.2>
@@ -81,17 +84,20 @@ impl<'a> Parameters<'a> {
8184
Ok(Self { kdf, encryption })
8285
}
8386

84-
/// Encrypt the given ciphertext in-place using a key derived from the
85-
/// provided password and this scheme's parameters.
86-
#[cfg(feature = "pbes2")]
87+
/// Attempt to decrypt the given ciphertext, allocating and returning a
88+
/// byte vector containing the plaintext.
89+
#[cfg(all(feature = "alloc", feature = "pbes2"))]
90+
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
8791
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
88-
pub fn encrypt_in_place<'b>(
92+
pub fn decrypt(
8993
&self,
9094
password: impl AsRef<[u8]>,
91-
buffer: &'b mut [u8],
92-
pos: usize,
93-
) -> Result<&'b [u8], CryptoError> {
94-
encryption::encrypt_in_place(self, password, buffer, pos)
95+
ciphertext: &[u8],
96+
) -> Result<Vec<u8>, CryptoError> {
97+
let mut buffer = ciphertext.to_vec();
98+
let pt_len = self.decrypt_in_place(password, &mut buffer)?.len();
99+
buffer.truncate(pt_len);
100+
Ok(buffer)
95101
}
96102

97103
/// Attempt to decrypt the given ciphertext in-place using a key derived
@@ -109,6 +115,43 @@ impl<'a> Parameters<'a> {
109115
) -> Result<&'b [u8], CryptoError> {
110116
encryption::decrypt_in_place(self, password, buffer)
111117
}
118+
119+
/// Encrypt the given plaintext, allocating and returning a vector
120+
/// containing the ciphertext.
121+
#[cfg(all(feature = "alloc", feature = "pbes2"))]
122+
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
123+
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
124+
pub fn encrypt(
125+
&self,
126+
password: impl AsRef<[u8]>,
127+
plaintext: &[u8],
128+
) -> Result<Vec<u8>, CryptoError> {
129+
// TODO(tarcieri): support non-AES ciphers?
130+
let mut buffer = Vec::with_capacity(plaintext.len() + AES_BLOCK_SIZE);
131+
buffer.extend_from_slice(plaintext);
132+
buffer.extend_from_slice(&[0u8; AES_BLOCK_SIZE]);
133+
134+
let ct_len = self
135+
.encrypt_in_place(password, &mut buffer, plaintext.len())?
136+
.len();
137+
138+
buffer.truncate(ct_len);
139+
Ok(buffer)
140+
}
141+
142+
/// Encrypt the given plaintext in-place using a key derived from the
143+
/// provided password and this scheme's parameters, writing the ciphertext
144+
/// into the same buffer.
145+
#[cfg(feature = "pbes2")]
146+
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
147+
pub fn encrypt_in_place<'b>(
148+
&self,
149+
password: impl AsRef<[u8]>,
150+
buffer: &'b mut [u8],
151+
pos: usize,
152+
) -> Result<&'b [u8], CryptoError> {
153+
encryption::encrypt_in_place(self, password, buffer, pos)
154+
}
112155
}
113156

114157
impl<'a> TryFrom<Any<'a>> for Parameters<'a> {

pkcs8/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@ der = { version = "0.2", features = ["oid"], path = "../der" }
1818
spki = { version = "0.2", path = "../spki" }
1919

2020
base64ct = { version = "0.2", optional = true, features = ["alloc"], path = "../base64ct" }
21+
rand_core = { version = "0.6", optional = true, default-features = false }
2122
pkcs5 = { version = "0.1", optional = true, path = "../pkcs5" }
2223
zeroize = { version = "1", optional = true, default-features = false, features = ["alloc"] }
2324

2425
[dev-dependencies]
2526
hex-literal = "0.3"
2627

2728
[features]
28-
encryption = ["alloc", "pkcs5/pbes2"]
29+
encryption = ["alloc", "pkcs5/alloc", "pkcs5/pbes2", "rand_core"]
2930
std = ["alloc", "der/std"]
3031
alloc = ["der/alloc", "zeroize"]
3132
pem = ["alloc", "base64ct"]

pkcs8/src/document/encrypted_private_key.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,20 @@ use {
3333
pub struct EncryptedPrivateKeyDocument(Zeroizing<Vec<u8>>);
3434

3535
impl EncryptedPrivateKeyDocument {
36+
/// Attempt to decrypt this encrypted private key using the provided
37+
/// password to derive an encryption key.
38+
#[cfg(feature = "encryption")]
39+
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
40+
pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result<PrivateKeyDocument> {
41+
self.encrypted_private_key_info().decrypt(password)
42+
}
43+
44+
/// Parse the [`EncryptedPrivateKeyInfo`] contained in this [`EncryptedPrivateKeyDocument`].
45+
pub fn encrypted_private_key_info(&self) -> EncryptedPrivateKeyInfo<'_> {
46+
EncryptedPrivateKeyInfo::try_from(self.0.as_ref())
47+
.expect("malformed EncryptedPrivateKeyDocument")
48+
}
49+
3650
/// Parse [`EncryptedPrivateKeyDocument`] from ASN.1 DER-encoded PKCS#8.
3751
pub fn from_der(bytes: &[u8]) -> Result<Self> {
3852
bytes.try_into()
@@ -92,20 +106,6 @@ impl EncryptedPrivateKeyDocument {
92106
pub fn write_pem_file(&self, path: impl AsRef<Path>) -> Result<()> {
93107
write_secret_file(path, self.to_pem().as_bytes())
94108
}
95-
96-
/// Parse the [`EncryptedPrivateKeyInfo`] contained in this [`EncryptedPrivateKeyDocument`].
97-
pub fn encrypted_private_key_info(&self) -> EncryptedPrivateKeyInfo<'_> {
98-
EncryptedPrivateKeyInfo::try_from(self.0.as_ref())
99-
.expect("malformed EncryptedPrivateKeyDocument")
100-
}
101-
102-
/// Attempt to decrypt this encrypted private key using the provided
103-
/// password to derive an encryption key.
104-
#[cfg(feature = "encryption")]
105-
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
106-
pub fn decrypt(&self, password: impl AsRef<[u8]>) -> Result<PrivateKeyDocument> {
107-
self.encrypted_private_key_info().decrypt(password)
108-
}
109109
}
110110

111111
impl AsRef<[u8]> for EncryptedPrivateKeyDocument {

pkcs8/src/document/private_key.rs

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ use core::{
99
use der::Encodable;
1010
use zeroize::{Zeroize, Zeroizing};
1111

12+
#[cfg(feature = "encryption")]
13+
use {
14+
crate::{EncryptedPrivateKeyDocument, EncryptedPrivateKeyInfo},
15+
pkcs5::pbes2,
16+
rand_core::{CryptoRng, RngCore},
17+
};
18+
1219
#[cfg(feature = "pem")]
1320
use {crate::pem, alloc::string::String, core::str::FromStr};
1421

@@ -25,6 +32,11 @@ use std::{fs, path::Path, str};
2532
pub struct PrivateKeyDocument(Zeroizing<Vec<u8>>);
2633

2734
impl PrivateKeyDocument {
35+
/// Parse the [`PrivateKeyInfo`] contained in this [`PrivateKeyDocument`]
36+
pub fn private_key_info(&self) -> PrivateKeyInfo<'_> {
37+
PrivateKeyInfo::try_from(self.0.as_ref()).expect("malformed PrivateKeyDocument")
38+
}
39+
2840
/// Parse [`PrivateKeyDocument`] from ASN.1 DER-encoded PKCS#8
2941
pub fn from_der(bytes: &[u8]) -> Result<Self> {
3042
bytes.try_into()
@@ -82,9 +94,48 @@ impl PrivateKeyDocument {
8294
write_secret_file(path, self.to_pem().as_bytes())
8395
}
8496

85-
/// Parse the [`PrivateKeyInfo`] contained in this [`PrivateKeyDocument`]
86-
pub fn private_key_info(&self) -> PrivateKeyInfo<'_> {
87-
PrivateKeyInfo::try_from(self.0.as_ref()).expect("malformed PrivateKeyDocument")
97+
/// Encrypt this private key using a symmetric encryption key derived
98+
/// from the provided password.
99+
#[cfg(feature = "encryption")]
100+
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
101+
pub fn encrypt(
102+
&self,
103+
mut rng: impl CryptoRng + RngCore,
104+
password: impl AsRef<[u8]>,
105+
) -> Result<EncryptedPrivateKeyDocument> {
106+
let mut salt = [0u8; 16];
107+
rng.fill_bytes(&mut salt);
108+
109+
let mut iv = [0u8; 16];
110+
rng.fill_bytes(&mut iv);
111+
112+
let pbkdf2_iterations = 10_000;
113+
let pbes2_params =
114+
pbes2::Parameters::pbkdf2_sha256_aes256cbc(pbkdf2_iterations, &salt, &iv)
115+
.map_err(|_| Error::Encode)?; // TODO(tarcieri): add `pkcs8::Error::Crypto`
116+
117+
self.encrypt_with_params(pbes2_params, password)
118+
}
119+
120+
/// Encrypt this private key using a symmetric encryption key derived
121+
/// from the provided password and [`pbes2::Parameters`].
122+
#[cfg(feature = "encryption")]
123+
#[cfg_attr(docsrs, doc(cfg(feature = "encryption")))]
124+
pub fn encrypt_with_params(
125+
&self,
126+
pbes2_params: pbes2::Parameters<'_>,
127+
password: impl AsRef<[u8]>,
128+
) -> Result<EncryptedPrivateKeyDocument> {
129+
pbes2_params
130+
.encrypt(password, self.as_ref())
131+
.map(|encrypted_data| {
132+
EncryptedPrivateKeyInfo {
133+
encryption_algorithm: pbes2_params.into(),
134+
encrypted_data: &encrypted_data,
135+
}
136+
.into()
137+
})
138+
.map_err(|_| Error::Encode)
88139
}
89140
}
90141

pkcs8/src/document/public_key.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ use {crate::pem, alloc::string::String, core::str::FromStr};
2424
pub struct PublicKeyDocument(Vec<u8>);
2525

2626
impl PublicKeyDocument {
27+
/// Parse the [`SubjectPublicKeyInfo`] contained in this [`PublicKeyDocument`]
28+
pub fn spki(&self) -> SubjectPublicKeyInfo<'_> {
29+
SubjectPublicKeyInfo::try_from(self.0.as_slice()).expect("malformed PublicKeyDocument")
30+
}
31+
2732
/// Parse [`PublicKeyDocument`] from ASN.1 DER
2833
pub fn from_der(bytes: &[u8]) -> Result<Self> {
2934
bytes.try_into()
@@ -82,11 +87,6 @@ impl PublicKeyDocument {
8287
fs::write(path, self.to_pem().as_bytes())?;
8388
Ok(())
8489
}
85-
86-
/// Parse the [`SubjectPublicKeyInfo`] contained in this [`PublicKeyDocument`]
87-
pub fn spki(&self) -> SubjectPublicKeyInfo<'_> {
88-
SubjectPublicKeyInfo::try_from(self.0.as_slice()).expect("malformed PublicKeyDocument")
89-
}
9090
}
9191

9292
impl AsRef<[u8]> for PublicKeyDocument {

0 commit comments

Comments
 (0)