Skip to content

Commit 3250d10

Browse files
committed
PEM decryption
1 parent d9bf81e commit 3250d10

File tree

4 files changed

+108
-119
lines changed

4 files changed

+108
-119
lines changed

src/rust/cryptography-crypto/src/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
// 2.0, and the BSD License. See the LICENSE file in the root of this repository
33
// for complete details.
44

5+
pub mod pbkdf1;
56
pub mod pkcs12;

src/rust/src/backend/keys.rs

+74-35
Original file line numberDiff line numberDiff line change
@@ -64,50 +64,89 @@ fn load_pem_private_key<'p>(
6464
backend: Option<pyo3::Bound<'_, pyo3::PyAny>>,
6565
unsafe_skip_rsa_key_validation: bool,
6666
) -> CryptographyResult<pyo3::Bound<'p, pyo3::PyAny>> {
67+
let _ = backend;
68+
6769
let p = x509::find_in_pem(
6870
data.as_bytes(),
6971
|p| ["PRIVATE KEY", "ENCRYPTED PRIVATE KEY", "RSA PRIVATE KEY", "EC PRIVATE KEY", "DSA PRIVATE KEY"].contains(&p.tag()),
7072
"Valid PEM but no BEGIN/END delimiters for a private key found. Are you sure this is a private key?"
7173
)?;
72-
// TODO: if proc-type is present, decrypt PEM layer.
73-
if p.headers().get("Proc-Type").is_none() {
74-
let pkey = match p.tag() {
75-
"PRIVATE KEY" => cryptography_key_parsing::pkcs8::parse_private_key(p.contents())?,
76-
"ENCRYPTED PRIVATE KEY" => {
77-
cryptography_key_parsing::pkcs8::parse_encrypted_private_key(
78-
p.contents(),
79-
password.as_ref().map(|v| v.as_bytes()),
80-
)?
81-
}
82-
"RSA PRIVATE KEY" => {
83-
cryptography_key_parsing::rsa::parse_pkcs1_private_key(p.contents())?
84-
}
85-
"EC PRIVATE KEY" => {
86-
cryptography_key_parsing::ec::parse_pkcs1_private_key(p.contents(), None)?
87-
}
88-
"DSA PRIVATE KEY" => {
89-
cryptography_key_parsing::dsa::parse_pkcs1_private_key(p.contents())?
90-
}
91-
_ => unreachable!(),
92-
};
93-
if password.is_some() && p.tag() != "ENCRYPTED PRIVATE KEY" {
74+
let password = password.as_ref().map(|v| v.as_bytes());
75+
let mut password_used = false;
76+
// TODO: Surely we can avoid this clone?
77+
let tag = p.tag().to_string();
78+
let data = match p.headers().get("Proc-Type") {
79+
Some("4,ENCRYPTED") => {
80+
password_used = true;
81+
let Some(dek_info) = p.headers().get("DEK-Info") else {
82+
todo!()
83+
};
84+
let Some((cipher_algorithm, iv)) = dek_info.split_once(',') else {
85+
todo!()
86+
};
87+
88+
let password = match password {
89+
None | Some(b"") => {
90+
return Err(CryptographyError::from(
91+
pyo3::exceptions::PyTypeError::new_err(
92+
"Password was not given but private key is encrypted",
93+
),
94+
))
95+
}
96+
Some(p) => p,
97+
};
98+
99+
let cipher = match cipher_algorithm {
100+
"AES-128-CBC" => openssl::symm::Cipher::aes_128_cbc(),
101+
"AES-256-CBC" => openssl::symm::Cipher::aes_256_cbc(),
102+
"DES-EDE3-CBC" => openssl::symm::Cipher::des_ede3_cbc(),
103+
_ => {
104+
return Err(CryptographyError::from(
105+
pyo3::exceptions::PyValueError::new_err(
106+
"Key encrypted with unknown cipher.",
107+
),
108+
))
109+
}
110+
};
111+
let iv = utils::hex_decode(iv)?;
112+
let key = cryptography_crypto::pbkdf1::openssl_kdf(
113+
openssl::hash::MessageDigest::md5(),
114+
password,
115+
&iv,
116+
cipher.key_len(),
117+
)?;
118+
openssl::symm::decrypt(cipher, &key, Some(&iv), p.contents()).map_err(|_| {
119+
pyo3::exceptions::PyValueError::new_err("Incorrect password, could not decrypt key")
120+
})?
121+
}
122+
Some(_) => {
94123
return Err(CryptographyError::from(
95-
pyo3::exceptions::PyTypeError::new_err(
96-
"Password was given but private key is not encrypted.",
124+
pyo3::exceptions::PyValueError::new_err(
125+
"Proc-Type PEM header is not valid, key could not be decrypted.",
97126
),
98-
));
127+
))
99128
}
100-
return private_key_from_pkey(py, &pkey, unsafe_skip_rsa_key_validation);
101-
}
129+
None => p.into_contents(),
130+
};
102131

103-
let _ = backend;
104-
let password = password.as_ref().map(CffiBuf::as_bytes);
105-
let mut status = utils::PasswordCallbackStatus::Unused;
106-
let pkey = openssl::pkey::PKey::private_key_from_pem_callback(
107-
data.as_bytes(),
108-
utils::password_callback(&mut status, password),
109-
);
110-
let pkey = utils::handle_key_load_result(py, pkey, status, password)?;
132+
let pkey = match tag.as_str() {
133+
"PRIVATE KEY" => cryptography_key_parsing::pkcs8::parse_private_key(&data)?,
134+
"ENCRYPTED PRIVATE KEY" => {
135+
password_used = true;
136+
cryptography_key_parsing::pkcs8::parse_encrypted_private_key(&data, password)?
137+
}
138+
"RSA PRIVATE KEY" => cryptography_key_parsing::rsa::parse_pkcs1_private_key(&data)?,
139+
"EC PRIVATE KEY" => cryptography_key_parsing::ec::parse_pkcs1_private_key(&data, None)?,
140+
"DSA PRIVATE KEY" => cryptography_key_parsing::dsa::parse_pkcs1_private_key(&data)?,
141+
_ => unreachable!(),
142+
};
143+
if password.is_some() && !password_used {
144+
return Err(CryptographyError::from(
145+
pyo3::exceptions::PyTypeError::new_err(
146+
"Password was given but private key is not encrypted.",
147+
),
148+
));
149+
}
111150
private_key_from_pkey(py, &pkey, unsafe_skip_rsa_key_validation)
112151
}
113152

src/rust/src/backend/utils.rs

+32-58
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
use crate::backend::hashes::Hash;
66
use crate::error::{CryptographyError, CryptographyResult};
7-
use crate::{error, types};
7+
use crate::types;
88
use pyo3::types::{PyAnyMethods, PyBytesMethods};
99

1010
pub(crate) fn py_int_to_bn(
@@ -403,66 +403,40 @@ pub(crate) fn calculate_digest_and_algorithm<'p>(
403403
Ok((data, algorithm))
404404
}
405405

406-
pub(crate) enum PasswordCallbackStatus {
407-
Unused,
408-
Used,
409-
BufferTooSmall(usize),
410-
}
411-
412-
pub(crate) fn password_callback<'a>(
413-
status: &'a mut PasswordCallbackStatus,
414-
password: Option<&'a [u8]>,
415-
) -> impl FnOnce(&mut [u8]) -> Result<usize, openssl::error::ErrorStack> + 'a {
416-
move |buf| {
417-
*status = PasswordCallbackStatus::Used;
418-
match password.as_ref() {
419-
Some(p) if p.len() <= buf.len() => {
420-
buf[..p.len()].copy_from_slice(p);
421-
Ok(p.len())
422-
}
423-
Some(_) => {
424-
*status = PasswordCallbackStatus::BufferTooSmall(buf.len());
425-
Ok(0)
426-
}
427-
None => Ok(0),
428-
}
406+
pub(crate) fn hex_decode(v: &str) -> CryptographyResult<Vec<u8>> {
407+
if v.len() % 2 != 0 {
408+
return Err(CryptographyError::from(
409+
pyo3::exceptions::PyValueError::new_err("Invalid hex value - odd length"),
410+
));
429411
}
430-
}
431412

432-
pub(crate) fn handle_key_load_result<T>(
433-
py: pyo3::Python<'_>,
434-
pkey: Result<openssl::pkey::PKey<T>, openssl::error::ErrorStack>,
435-
status: PasswordCallbackStatus,
436-
password: Option<&[u8]>,
437-
) -> CryptographyResult<openssl::pkey::PKey<T>> {
438-
match (pkey, status, password) {
439-
(Ok(k), PasswordCallbackStatus::Unused, None)
440-
| (Ok(k), PasswordCallbackStatus::Used, Some(_)) => Ok(k),
441-
442-
(Ok(_), PasswordCallbackStatus::Unused, Some(_)) => Err(CryptographyError::from(
443-
pyo3::exceptions::PyTypeError::new_err(
444-
"Password was given but private key is not encrypted.",
445-
),
446-
)),
413+
let mut b = Vec::with_capacity(v.len() / 2);
414+
let v = v.as_bytes();
415+
for i in (0..v.len()).step_by(2) {
416+
let high = match v[i] {
417+
b @ b'0'..=b'9' => b - b'0',
418+
b @ b'a'..=b'f' => b - b'a' + 10,
419+
b @ b'A'..=b'F' => b - b'A' + 10,
420+
_ => {
421+
return Err(CryptographyError::from(
422+
pyo3::exceptions::PyValueError::new_err("Invalid hex value"),
423+
))
424+
}
425+
};
447426

448-
(_, PasswordCallbackStatus::Used, None | Some(b"")) => Err(CryptographyError::from(
449-
pyo3::exceptions::PyTypeError::new_err(
450-
"Password was not given but private key is encrypted",
451-
),
452-
)),
453-
(_, PasswordCallbackStatus::BufferTooSmall(size), _) => Err(CryptographyError::from(
454-
pyo3::exceptions::PyValueError::new_err(format!(
455-
"Passwords longer than {size} bytes are not supported"
456-
)),
457-
)),
458-
(Err(e), _, _) => {
459-
let errors = error::list_from_openssl_error(py, &e);
460-
Err(CryptographyError::from(
461-
pyo3::exceptions::PyValueError::new_err((
462-
"Could not deserialize key data. The data may be in an incorrect format, the provided password may be incorrect, it may be encrypted with an unsupported algorithm, or it may be an unsupported key type (e.g. EC curves with explicit parameters).",
463-
errors.unbind(),
427+
let low = match v[i + 1] {
428+
b @ b'0'..=b'9' => b - b'0',
429+
b @ b'a'..=b'f' => b - b'a' + 10,
430+
b @ b'A'..=b'F' => b - b'A' + 10,
431+
_ => {
432+
return Err(CryptographyError::from(
433+
pyo3::exceptions::PyValueError::new_err("Invalid hex value"),
464434
))
465-
))
466-
}
435+
}
436+
};
437+
438+
b.push((high << 4) | low);
467439
}
440+
441+
Ok(b)
468442
}

tests/hazmat/backends/test_openssl.py

+1-26
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55

66
import itertools
7-
import os
87

98
import pytest
109

@@ -22,10 +21,7 @@
2221
DummyMode,
2322
)
2423
from ...hazmat.primitives.test_rsa import rsa_key_2048
25-
from ...utils import (
26-
load_vectors_from_file,
27-
raises_unsupported_algorithm,
28-
)
24+
from ...utils import raises_unsupported_algorithm
2925

3026
# Make ruff happy since we're importing fixtures that pytest patches in as
3127
# func args
@@ -200,27 +196,6 @@ def test_unsupported_mgf1_hash_algorithm_md5_decrypt(self, rsa_key_2048):
200196
)
201197

202198

203-
class TestOpenSSLSerializationWithOpenSSL:
204-
def test_very_long_pem_serialization_password(self):
205-
password = b"x" * 1025
206-
207-
with pytest.raises(ValueError, match="Passwords longer than"):
208-
load_vectors_from_file(
209-
os.path.join(
210-
"asymmetric",
211-
"Traditional_OpenSSL_Serialization",
212-
"key1.pem",
213-
),
214-
lambda pemfile: (
215-
serialization.load_pem_private_key(
216-
pemfile.read().encode(),
217-
password,
218-
unsafe_skip_rsa_key_validation=False,
219-
)
220-
),
221-
)
222-
223-
224199
class TestRSAPEMSerialization:
225200
def test_password_length_limit(self, rsa_key_2048):
226201
password = b"x" * 1024

0 commit comments

Comments
 (0)