1use cms::content_info::ContentInfo;
26use cms::enveloped_data::{EnvelopedData, RecipientIdentifier, RecipientInfo};
27use const_oid::ObjectIdentifier;
28use der::{Decode, Encode};
29use rsa::{Oaep, Pkcs1v15Encrypt, RsaPrivateKey};
30use sha1::{Digest as Sha1Digest, Sha1};
31use sha2::Sha256;
32use x509_cert::Certificate;
33
34use crate::crypto::{CryptMethod, StandardSecurityHandler, resolve_v4_crypt_filters};
35use crate::error::{PdfError, PdfResult};
36use crate::types::{PdfDictionary, PdfValue};
37
38const OID_RSA_ENCRYPTION: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
40const OID_RSA_OAEP: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.7");
42const OID_ENVELOPED_DATA: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113549.1.7.3");
44
45pub struct PubSecCredential<'a> {
49 pub certificate_der: &'a [u8],
50 pub private_key_der: &'a [u8],
51}
52
53pub fn open_pubsec(
59 encrypt_dict: &PdfDictionary,
60 credential: &PubSecCredential,
61) -> PdfResult<StandardSecurityHandler> {
62 let v = encrypt_dict
63 .get("V")
64 .and_then(PdfValue::as_integer)
65 .unwrap_or(0);
66 let sub_filter = encrypt_dict
67 .get("SubFilter")
68 .and_then(PdfValue::as_name)
69 .unwrap_or("");
70
71 if !matches!(sub_filter, "adbe.pkcs7.s4" | "adbe.pkcs7.s5") {
72 return Err(PdfError::Unsupported(format!(
73 "Adobe.PubSec /SubFilter /{sub_filter} is not supported (only adbe.pkcs7.s4 and adbe.pkcs7.s5)"
74 )));
75 }
76
77 let recipient_blobs = collect_recipient_blobs(encrypt_dict, sub_filter, v)?;
81 if recipient_blobs.is_empty() {
82 return Err(PdfError::Corrupt(
83 "Adobe.PubSec /Encrypt has no /Recipients".to_string(),
84 ));
85 }
86
87 let recipient_cert = Certificate::from_der(credential.certificate_der).map_err(|err| {
90 PdfError::Corrupt(format!("recipient certificate is not valid DER: {err}"))
91 })?;
92 let private_key = load_rsa_private_key(credential.private_key_der)?;
95
96 let mut recipients_buffer: Vec<u8> = Vec::new();
100 for blob in &recipient_blobs {
101 recipients_buffer.extend_from_slice(blob);
102 }
103
104 let mut decrypted_seed_and_perms: Option<Vec<u8>> = None;
106 for blob in &recipient_blobs {
107 if let Some(plaintext) = try_unwrap_recipient(blob, &recipient_cert, &private_key)? {
108 decrypted_seed_and_perms = Some(plaintext);
109 break;
110 }
111 }
112
113 let plaintext = decrypted_seed_and_perms.ok_or(PdfError::InvalidPassword)?;
114 if plaintext.len() < 24 {
115 return Err(PdfError::Corrupt(
116 "decrypted PubSec seed must be at least 24 bytes (20-byte seed + 4-byte permissions)"
117 .to_string(),
118 ));
119 }
120 let seed = &plaintext[..20];
121 let permission_bytes = &plaintext[20..24];
122
123 let file_key: Vec<u8> = match sub_filter {
125 "adbe.pkcs7.s4" => {
126 let mut hasher = Sha1::new();
127 hasher.update(seed);
128 hasher.update(&recipients_buffer);
129 hasher.update(permission_bytes);
130 hasher.finalize().to_vec()
131 }
132 "adbe.pkcs7.s5" => {
133 let mut hasher = Sha256::new();
134 hasher.update(seed);
135 hasher.update(&recipients_buffer);
136 hasher.update(permission_bytes);
137 hasher.finalize().to_vec()
138 }
139 _ => unreachable!("sub_filter validated above"),
140 };
141
142 let (string_method, stream_method) = match v {
144 4 => resolve_v4_crypt_filters(encrypt_dict)?,
145 5 => (CryptMethod::AesV3, CryptMethod::AesV3),
146 other => {
147 return Err(PdfError::Unsupported(format!(
148 "Adobe.PubSec V={other} is not supported (only V=4 and V=5)"
149 )));
150 }
151 };
152
153 let key_length_bytes = match v {
155 4 => 16,
156 5 => 32,
157 _ => unreachable!("v validated above"),
158 };
159 let truncated_file_key = file_key[..key_length_bytes.min(file_key.len())].to_vec();
160
161 let encrypt_metadata = encrypt_dict
162 .get("EncryptMetadata")
163 .and_then(PdfValue::as_bool)
164 .unwrap_or(true);
165
166 Ok(StandardSecurityHandler::from_file_key(
167 truncated_file_key,
168 string_method,
169 stream_method,
170 encrypt_metadata,
171 ))
172}
173
174fn collect_recipient_blobs(
179 encrypt_dict: &PdfDictionary,
180 sub_filter: &str,
181 v: i64,
182) -> PdfResult<Vec<Vec<u8>>> {
183 let array = match (v, sub_filter) {
184 (4, "adbe.pkcs7.s4") => encrypt_dict.get("Recipients").and_then(PdfValue::as_array),
185 (5, "adbe.pkcs7.s5") => {
186 let cf = encrypt_dict
187 .get("CF")
188 .and_then(PdfValue::as_dictionary)
189 .ok_or_else(|| {
190 PdfError::Corrupt("Adobe.PubSec V=5 requires /CF dictionary".to_string())
191 })?;
192 let stmf = encrypt_dict
193 .get("StmF")
194 .and_then(PdfValue::as_name)
195 .unwrap_or("DefaultCryptFilter");
196 let filter_dict = cf
197 .get(stmf)
198 .and_then(PdfValue::as_dictionary)
199 .ok_or_else(|| {
200 PdfError::Corrupt(format!(
201 "Adobe.PubSec V=5 /CF entry /{stmf} is missing or not a dictionary"
202 ))
203 })?;
204 filter_dict.get("Recipients").and_then(PdfValue::as_array)
205 }
206 _ => {
207 return Err(PdfError::Unsupported(format!(
208 "Adobe.PubSec V={v} /SubFilter /{sub_filter} combination is not supported"
209 )));
210 }
211 };
212
213 let array = array.ok_or_else(|| {
214 PdfError::Corrupt(format!(
215 "Adobe.PubSec /Recipients array is missing for /SubFilter /{sub_filter}"
216 ))
217 })?;
218
219 let mut blobs = Vec::with_capacity(array.len());
220 for entry in array {
221 let bytes = match entry {
222 PdfValue::String(s) => s.0.clone(),
223 _ => {
224 return Err(PdfError::Corrupt(
225 "Adobe.PubSec /Recipients entry must be a byte string".to_string(),
226 ));
227 }
228 };
229 blobs.push(bytes);
230 }
231 Ok(blobs)
232}
233
234fn try_unwrap_recipient(
242 blob: &[u8],
243 recipient_cert: &Certificate,
244 private_key: &RsaPrivateKey,
245) -> PdfResult<Option<Vec<u8>>> {
246 let content_info = ContentInfo::from_der(blob).map_err(|err| {
247 PdfError::Corrupt(format!(
248 "Adobe.PubSec recipient blob is not a valid CMS ContentInfo: {err}"
249 ))
250 })?;
251 if content_info.content_type != OID_ENVELOPED_DATA {
252 return Err(PdfError::Corrupt(format!(
253 "Adobe.PubSec recipient blob has wrong CMS content type {:?}",
254 content_info.content_type
255 )));
256 }
257 let inner_der = content_info
258 .content
259 .to_der()
260 .map_err(|err| PdfError::Corrupt(format!("CMS inner re-encode failed: {err}")))?;
261 let enveloped = EnvelopedData::from_der(&inner_der)
262 .map_err(|err| PdfError::Corrupt(format!("CMS EnvelopedData decode failed: {err}")))?;
263
264 let mut content_encryption_key: Option<Vec<u8>> = None;
265 for ri in enveloped.recip_infos.0.iter() {
266 let ktri = match ri {
267 RecipientInfo::Ktri(ktri) => ktri,
268 RecipientInfo::Kari(_) => {
269 return Err(PdfError::Unsupported(
270 "Adobe.PubSec key-agreement recipients (KeyAgreeRecipientInfo) are not supported"
271 .to_string(),
272 ));
273 }
274 _ => continue, };
276
277 if !rid_matches(&ktri.rid, recipient_cert) {
278 continue;
279 }
280
281 let cek = rsa_decrypt(private_key, ktri.key_enc_alg.oid, ktri.enc_key.as_bytes())?;
282 content_encryption_key = Some(cek);
283 break;
284 }
285
286 let Some(cek) = content_encryption_key else {
287 return Ok(None);
288 };
289
290 let alg = &enveloped.encrypted_content.content_enc_alg;
292 let ciphertext = enveloped
293 .encrypted_content
294 .encrypted_content
295 .as_ref()
296 .ok_or_else(|| {
297 PdfError::Corrupt("CMS EnvelopedData encryptedContent is missing".to_string())
298 })?
299 .as_bytes();
300 let iv_param = alg.parameters.as_ref().ok_or_else(|| {
301 PdfError::Corrupt("CMS content encryption algorithm has no parameters".to_string())
302 })?;
303 let iv_bytes = iv_param
304 .decode_as::<der::asn1::OctetString>()
305 .map_err(|err| {
306 PdfError::Corrupt(format!(
307 "CMS content encryption IV is not an OCTET STRING: {err}"
308 ))
309 })?;
310 let iv = iv_bytes.as_bytes();
311
312 let plaintext = decrypt_cms_inner(alg.oid, &cek, iv, ciphertext)?;
313 Ok(Some(plaintext))
314}
315
316fn decrypt_cms_inner(
321 algorithm_oid: ObjectIdentifier,
322 cek: &[u8],
323 iv: &[u8],
324 ciphertext: &[u8],
325) -> PdfResult<Vec<u8>> {
326 use aes::cipher::{BlockDecrypt, KeyInit, generic_array::GenericArray};
327 use aes::{Aes128, Aes192, Aes256};
328
329 if iv.len() != 16 {
330 return Err(PdfError::Corrupt(format!(
331 "CMS AES-CBC IV must be 16 bytes, got {}",
332 iv.len()
333 )));
334 }
335 if ciphertext.is_empty() || ciphertext.len() % 16 != 0 {
336 return Err(PdfError::Corrupt(format!(
337 "CMS AES-CBC ciphertext length {} is not a positive multiple of 16",
338 ciphertext.len()
339 )));
340 }
341
342 const AES_128_CBC: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.2");
343 const AES_192_CBC: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.22");
344 const AES_256_CBC: ObjectIdentifier = ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.1.42");
345
346 let mut prev: [u8; 16] = iv.try_into().expect("iv length checked");
347 let mut output = Vec::with_capacity(ciphertext.len());
348
349 macro_rules! decrypt_with {
350 ($cipher:ty, $expected_key:expr) => {{
351 if cek.len() != $expected_key {
352 return Err(PdfError::Corrupt(format!(
353 "CEK length {} does not match algorithm key size {}",
354 cek.len(),
355 $expected_key
356 )));
357 }
358 let cipher = <$cipher>::new_from_slice(cek)
359 .map_err(|err| PdfError::Corrupt(format!("AES init failed: {err}")))?;
360 for chunk in ciphertext.chunks(16) {
361 let mut block = GenericArray::clone_from_slice(chunk);
362 cipher.decrypt_block(&mut block);
363 for (plain_byte, iv_byte) in block.iter_mut().zip(prev.iter()) {
364 *plain_byte ^= iv_byte;
365 }
366 output.extend_from_slice(block.as_slice());
367 prev.copy_from_slice(chunk);
368 }
369 }};
370 }
371
372 if algorithm_oid == AES_128_CBC {
373 decrypt_with!(Aes128, 16);
374 } else if algorithm_oid == AES_192_CBC {
375 decrypt_with!(Aes192, 24);
376 } else if algorithm_oid == AES_256_CBC {
377 decrypt_with!(Aes256, 32);
378 } else {
379 return Err(PdfError::Unsupported(format!(
380 "CMS content encryption algorithm {algorithm_oid} is not supported (only AES-CBC)"
381 )));
382 }
383
384 let pad = *output.last().ok_or_else(|| {
386 PdfError::Corrupt("CMS AES-CBC plaintext is empty after decrypt".to_string())
387 })?;
388 if pad == 0 || pad > 16 || pad as usize > output.len() {
389 return Err(PdfError::Corrupt(format!(
390 "invalid PKCS#7 padding length {pad} in CMS plaintext"
391 )));
392 }
393 let new_len = output.len() - pad as usize;
394 output.truncate(new_len);
395 Ok(output)
396}
397
398fn rid_matches(rid: &RecipientIdentifier, cert: &Certificate) -> bool {
402 match rid {
403 RecipientIdentifier::IssuerAndSerialNumber(iasn) => {
404 iasn.issuer == cert.tbs_certificate.issuer
405 && iasn.serial_number == cert.tbs_certificate.serial_number
406 }
407 RecipientIdentifier::SubjectKeyIdentifier(ski) => {
408 let Some(extensions) = cert.tbs_certificate.extensions.as_ref() else {
410 return false;
411 };
412 for ext in extensions {
413 if ext.extn_id == const_oid::db::rfc5912::ID_CE_SUBJECT_KEY_IDENTIFIER {
414 return ext.extn_value.as_bytes() == ski.0.as_bytes();
415 }
416 }
417 false
418 }
419 }
420}
421
422fn rsa_decrypt(
425 private_key: &RsaPrivateKey,
426 algorithm_oid: ObjectIdentifier,
427 ciphertext: &[u8],
428) -> PdfResult<Vec<u8>> {
429 if algorithm_oid == OID_RSA_ENCRYPTION {
430 private_key
431 .decrypt(Pkcs1v15Encrypt, ciphertext)
432 .map_err(|err| PdfError::Corrupt(format!("RSA-PKCS1v15 unwrap failed: {err}")))
433 } else if algorithm_oid == OID_RSA_OAEP {
434 private_key
435 .decrypt(Oaep::new::<Sha1>(), ciphertext)
436 .map_err(|err| PdfError::Corrupt(format!("RSA-OAEP unwrap failed: {err}")))
437 } else {
438 Err(PdfError::Unsupported(format!(
439 "Adobe.PubSec key-encryption OID {algorithm_oid} is not supported"
440 )))
441 }
442}
443
444fn load_rsa_private_key(der: &[u8]) -> PdfResult<RsaPrivateKey> {
448 use rsa::pkcs1::DecodeRsaPrivateKey;
449 use rsa::pkcs8::DecodePrivateKey;
450
451 if let Ok(key) = RsaPrivateKey::from_pkcs8_der(der) {
452 return Ok(key);
453 }
454 RsaPrivateKey::from_pkcs1_der(der).map_err(|err| {
455 PdfError::Corrupt(format!(
456 "private key is neither valid PKCS#8 nor PKCS#1 RSA DER: {err}"
457 ))
458 })
459}