Skip to content

Commit c10dbef

Browse files
authored
pkcs5 encryption + pkcs8 decryption (#293)
Implements PKCS#5 encryption support, presently targeting only support for PBES2 with PBKDF2-SHA-256 and AES-CBC (with 128 or 256-bit key size) Note that these are presently the best options supported by PKCS#5 v2.1. Support for legacy algorithms like DES, 3DES, MD2, and SHA-1 is deliberately omitted. We can revisit potentially adding these upon request if there is demand, however since these algorithms are insecure we don't support them in this initial implementation. Additionally adds an initial `EncryptedPrivateKeyInfo::decrypt` function which is able to decrypt an OpenSSL-generated test vector contained in the `tests/examples/` directory (DER-only). Finally, adds a heap-backed `EncryptedPrivateKeyDocument` type similar to the other "document" types (which are now factored into their own respective modules). No support for PEM-encoded `EncryptedPrivateKeyInfo` has been added yet, nor support for encrypting `PrivateKeyInfo`.
1 parent 80194ea commit c10dbef

File tree

19 files changed

+958
-360
lines changed

19 files changed

+958
-360
lines changed

.github/workflows/pkcs5.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ jobs:
3838
toolchain: ${{ matrix.rust }}
3939
target: ${{ matrix.target }}
4040
override: true
41-
- run: cargo build --release --target ${{ matrix.target }}
41+
- run: cargo build --target ${{ matrix.target }} --release
42+
- run: cargo build --target ${{ matrix.target }} --release --features pbes2
4243

4344
test:
4445
runs-on: ubuntu-latest
@@ -55,3 +56,4 @@ jobs:
5556
toolchain: ${{ matrix.rust }}
5657
override: true
5758
- run: cargo test --release
59+
- run: cargo test --release --all-features

.github/workflows/pkcs8.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@ jobs:
4141
override: true
4242
- run: cargo build --release --target ${{ matrix.target }} --no-default-features
4343
- run: cargo build --release --target ${{ matrix.target }} --no-default-features --features alloc
44+
- run: cargo build --release --target ${{ matrix.target }} --no-default-features --features encryption
4445
- run: cargo build --release --target ${{ matrix.target }} --no-default-features --features pem
46+
- run: cargo build --release --target ${{ matrix.target }} --no-default-features --features pkcs5
4547

4648
test:
4749
runs-on: ubuntu-latest
@@ -60,5 +62,7 @@ jobs:
6062
- run: cargo test --release --no-default-features
6163
- run: cargo test --release
6264
- run: cargo test --release --features alloc
65+
- run: cargo test --release --features encryption
6366
- run: cargo test --release --features pem
67+
- run: cargo test --release --features pkcs5
6468
- run: cargo test --release --all-features

.github/workflows/workspace.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
- uses: actions/checkout@v1
1717
- uses: actions-rs/toolchain@v1
1818
with:
19-
toolchain: 1.46.0 # MSRV
19+
toolchain: 1.47.0 # Highest MSRV in repo
2020
components: clippy
2121
override: true
2222
profile: minimal

Cargo.lock

Lines changed: 146 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkcs5/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,18 @@ readme = "README.md"
1717
der = { version = "0.2.7", features = ["oid"], path = "../der" }
1818
spki = { version = "0.2", path = "../spki" }
1919

20+
aes = { version = "0.6", optional = true }
21+
block-modes = { version = "0.7", optional = true, default-features = false }
22+
hmac = { version = "0.10", optional = true, default-features = false }
23+
pbkdf2 = { version = "0.7", optional = true, default-features = false }
24+
sha2 = { version = "0.9", optional = true, default-features = false }
25+
2026
[dev-dependencies]
2127
hex-literal = "0.3"
2228

29+
[features]
30+
pbes2 = ["aes", "block-modes", "hmac", "pbkdf2", "sha2"]
31+
2332
[package.metadata.docs.rs]
2433
all-features = true
2534
rustdoc-args = ["--cfg", "docsrs"]

pkcs5/src/lib.rs

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

28-
pub use der::{self, Error, ObjectIdentifier, Result};
28+
pub use der::{self, Error, ObjectIdentifier};
2929
pub use spki::AlgorithmIdentifier;
3030

3131
use core::convert::{TryFrom, TryInto};
@@ -34,6 +34,10 @@ use der::{sequence, Any, Encodable, Encoder, Length};
3434
pub mod pbes1;
3535
pub mod pbes2;
3636

37+
/// Cryptographic errors
38+
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
39+
pub struct CryptoError;
40+
3741
/// Supported PKCS#5 password-based encryption schemes.
3842
#[derive(Clone, Debug, Eq, PartialEq)]
3943
#[non_exhaustive]
@@ -51,6 +55,41 @@ pub enum EncryptionScheme<'a> {
5155
}
5256

5357
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")]
61+
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
62+
pub fn encrypt_in_place<'b>(
63+
&self,
64+
password: impl AsRef<[u8]>,
65+
buffer: &'b mut [u8],
66+
pos: usize,
67+
) -> Result<&'b [u8], CryptoError> {
68+
match self {
69+
Self::Pbes2(params) => params.encrypt_in_place(password, buffer, pos),
70+
_ => Err(CryptoError),
71+
}
72+
}
73+
74+
/// Attempt to decrypt the given ciphertext in-place using a key derived
75+
/// from the provided password and this scheme's parameters.
76+
///
77+
/// Returns an error if the algorithm specified in this scheme's parameters
78+
/// is unsupported, or if the ciphertext is malformed (e.g. not a multiple
79+
/// of a block mode's padding)
80+
#[cfg(feature = "pbes2")]
81+
#[cfg_attr(docsrs, doc(cfg(feature = "pbes2")))]
82+
pub fn decrypt_in_place<'b>(
83+
&self,
84+
password: impl AsRef<[u8]>,
85+
buffer: &'b mut [u8],
86+
) -> Result<&'b [u8], CryptoError> {
87+
match self {
88+
Self::Pbes2(params) => params.decrypt_in_place(password, buffer),
89+
_ => Err(CryptoError),
90+
}
91+
}
92+
5493
/// Get the [`ObjectIdentifier`] (a.k.a OID) for this algorithm.
5594
pub fn oid(&self) -> ObjectIdentifier {
5695
match self {
@@ -79,23 +118,23 @@ impl<'a> EncryptionScheme<'a> {
79118
impl<'a> TryFrom<&'a [u8]> for EncryptionScheme<'a> {
80119
type Error = Error;
81120

82-
fn try_from(bytes: &'a [u8]) -> Result<EncryptionScheme<'a>> {
121+
fn try_from(bytes: &'a [u8]) -> der::Result<EncryptionScheme<'a>> {
83122
AlgorithmIdentifier::try_from(bytes).and_then(TryInto::try_into)
84123
}
85124
}
86125

87126
impl<'a> TryFrom<Any<'a>> for EncryptionScheme<'a> {
88127
type Error = Error;
89128

90-
fn try_from(any: Any<'a>) -> Result<EncryptionScheme<'a>> {
129+
fn try_from(any: Any<'a>) -> der::Result<EncryptionScheme<'a>> {
91130
AlgorithmIdentifier::try_from(any).and_then(TryInto::try_into)
92131
}
93132
}
94133

95134
impl<'a> TryFrom<AlgorithmIdentifier<'a>> for EncryptionScheme<'a> {
96135
type Error = Error;
97136

98-
fn try_from(alg: AlgorithmIdentifier<'a>) -> Result<EncryptionScheme<'_>> {
137+
fn try_from(alg: AlgorithmIdentifier<'a>) -> der::Result<EncryptionScheme<'_>> {
99138
match alg.oid {
100139
pbes2::PBES2_OID => pbes2::Parameters::try_from(alg.parameters_any()?).map(Into::into),
101140
_ => pbes1::Parameters::try_from(alg).map(Into::into),
@@ -116,14 +155,14 @@ impl<'a> From<pbes2::Parameters<'a>> for EncryptionScheme<'a> {
116155
}
117156

118157
impl<'a> Encodable for EncryptionScheme<'a> {
119-
fn encoded_len(&self) -> Result<Length> {
158+
fn encoded_len(&self) -> der::Result<Length> {
120159
match self {
121160
Self::Pbes1(pbes1) => pbes1.encoded_len(),
122161
Self::Pbes2(pbes2) => sequence::encoded_len(&[&pbes2::PBES2_OID, pbes2]),
123162
}
124163
}
125164

126-
fn encode(&self, encoder: &mut Encoder<'_>) -> Result<()> {
165+
fn encode(&self, encoder: &mut Encoder<'_>) -> der::Result<()> {
127166
match self {
128167
Self::Pbes1(pbes1) => pbes1.encode(encoder),
129168
Self::Pbes2(pbes2) => encoder.sequence(&[&pbes2::PBES2_OID, pbes2]),

0 commit comments

Comments
 (0)