mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-12 07:46:08 +00:00
craft-pdf: crypt ports of two open PdfCraft changes (revision 2 reserved bits, attachments-only encryption) (#952)
* craft-pdf: the standard security handler from PdfCraft (`crypt`) Adds the `crypt` feature: PdfCraft's pdfcraft-crypt crate (revision 2b65ab000bfe0997dec5fbed2769d3da3799355b) as craft_pdf::crypt, with its unit, integration and property tests. The code is unchanged; doc comments and test imports name the new module. It decrypts and encrypts strings and streams for revisions 2 to 6 and creates encryption dictionaries. The reader keeps its own decryptor. * craft-pdf: the crypt unit tests build for wasm The property test in src/crypt/tests.rs needs proptest, which is a dev-dependency of the non-wasm targets only. It is now compiled for those targets only; the other unit tests still build for wasm. * craft-pdf: crypt create sets every bit revision 2 reserves Revision 2 reserves bits 7 to 32 of /P, later revisions bits 7-8 and 13-32. create left bits 9 to 12 as the caller gave them for RC4-40. Ported from storytold/pdfcraft#2789 (the create half; the accessors already read /P by revision). * craft-pdf: crypt can encrypt only embedded files create_attachments_only builds AES encryption whose /StmF and /StrF are /Identity and whose /EFF is the standard filter (ISO 32000-2 §7.6.6). RC4-40 and RC4-128 have no crypt filters and are refused. Ported from storytold/pdfcraft#5071 (the crypt half). The test is new: the source change tests it through PdfCraft's object layer. --------- Co-authored-by: Ahmad Mujahid <me@ahmadmu.com> Co-authored-by: Harrison Booth <73434116+Harrison-Booth@users.noreply.github.com>
This commit is contained in:
co-authored by
Ahmad Mujahid
Harrison Booth
parent
9e78e96c16
commit
8b7f64e2e0
+19
-2
@@ -726,8 +726,10 @@ pub fn file_id(seed: &[u8; 32]) -> Vec<u8> {
|
||||
|
||||
/// Build the `/Encrypt` values and an authenticated (owner) handler for a new encryption.
|
||||
pub fn create(params: &NewEncryption, id0: &[u8]) -> Result<SecurityHandler, CryptError> {
|
||||
// Bits 7–8 and 13–32 must be 1; bits 1–2 must be 0 (§7.6.4.2).
|
||||
let p = (params.permissions | !0xFFF | 0xC0) & !0x3;
|
||||
// R2 reserves bits 7-32; later revisions reserve bits 7-8 and 13-32.
|
||||
// Reserved bits must be 1, and bits 1-2 must be 0 (§7.6.4.2).
|
||||
let reserved = if params.algorithm == Algorithm::Rc4_40 { !0x3F } else { !0xFFF | 0xC0 };
|
||||
let p = (params.permissions | reserved) & !0x3;
|
||||
let mut d = EncryptDict { filter: b"Standard".to_vec(), p, encrypt_metadata: params.encrypt_metadata, ..Default::default() };
|
||||
let user = prepared_password(params.algorithm, params.user_password);
|
||||
let owner = prepared_password(params.algorithm, params.owner_password);
|
||||
@@ -792,5 +794,20 @@ pub fn create(params: &NewEncryption, id0: &[u8]) -> Result<SecurityHandler, Cry
|
||||
}
|
||||
}
|
||||
|
||||
/// Like [`create`], but only embedded file streams are encrypted (`/EFF` names the standard
|
||||
/// filter; `/StmF` and `/StrF` are `/Identity`, §7.6.6). Needs crypt filters, so RC4-40 and
|
||||
/// RC4-128 (V < 4) are refused rather than silently encrypting everything.
|
||||
pub fn create_attachments_only(params: &NewEncryption, id0: &[u8]) -> Result<SecurityHandler, CryptError> {
|
||||
if matches!(params.algorithm, Algorithm::Rc4_40 | Algorithm::Rc4_128) {
|
||||
return Err(CryptError::Unsupported("encrypting only attachments needs AES (crypt filters)".into()));
|
||||
}
|
||||
let h = create(params, id0)?;
|
||||
let mut dict = h.dict.clone();
|
||||
dict.stm_f = b"Identity".to_vec();
|
||||
dict.str_f = b"Identity".to_vec();
|
||||
dict.ef_f = b"StdCF".to_vec();
|
||||
Ok(SecurityHandler::new(dict, h.key, h.auth, h.recovered_user))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -447,3 +447,49 @@ fn pdfdoc_encoding_round_trips() {
|
||||
assert_eq!(legacy_candidates("Çok"), [Zeroizing::new(vec![0xC7, b'o', b'k'])]);
|
||||
assert_eq!(legacy_candidates("şifre"), [Zeroizing::new(b"ifre".to_vec()), Zeroizing::new("şifre".as_bytes().to_vec())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn r2_create_sets_every_bit_revision_2_reserves() {
|
||||
// ISO 32000-1 section 7.6.3.2: -44 permits printing and copying only.
|
||||
let d = new(Algorithm::Rc4_40, "user", "owner", 0x14).dict().clone();
|
||||
assert_eq!(d.p, -44);
|
||||
assert_eq!(new(Algorithm::Rc4_40, "user", "owner", 0).dict().p, -64, "R2 reserves bits 7 through 32");
|
||||
let user = SecurityHandler::open(d, ID, Some("user")).unwrap();
|
||||
assert_eq!(user.dict().p, -44);
|
||||
let p = user.permissions();
|
||||
assert_eq!((p.print(), p.print_high_quality(), p.copy(), p.extract_for_accessibility()), (true, true, true, true));
|
||||
assert_eq!((p.modify(), p.annotate(), p.fill_forms(), p.assemble()), (false, false, false, false));
|
||||
for alg in [Algorithm::Rc4_128, Algorithm::Aes128, Algorithm::Aes256] {
|
||||
assert_eq!(new(alg, "user", "owner", 0x14).dict().p, -3884, "{alg:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attachments_only_encrypts_embedded_files_and_nothing_else() {
|
||||
let params = |algorithm| NewEncryption {
|
||||
algorithm,
|
||||
user_password: "user",
|
||||
owner_password: "owner",
|
||||
permissions: -4,
|
||||
encrypt_metadata: true,
|
||||
seed: [7; 32],
|
||||
};
|
||||
for alg in [Algorithm::Aes128, Algorithm::Aes256] {
|
||||
let h = create_attachments_only(¶ms(alg), ID).unwrap();
|
||||
let d = h.dict().clone();
|
||||
assert_eq!((d.stm_f.as_slice(), d.str_f.as_slice(), d.ef_f.as_slice()), (&b"Identity"[..], &b"Identity"[..], &b"StdCF"[..]), "{alg:?}");
|
||||
assert!(!h.encrypts_strings(), "{alg:?}");
|
||||
assert!(!h.decrypts(StreamKind::Normal) && h.decrypts(StreamKind::EmbeddedFile), "{alg:?}");
|
||||
assert_eq!(h.encrypt_string(3, 0, b"plain"), b"plain", "{alg:?}");
|
||||
assert_eq!(h.encrypt_stream(3, 0, b"page content", StreamKind::Normal), b"page content", "{alg:?}");
|
||||
let stored = h.encrypt_stream(4, 0, b"attachment", StreamKind::EmbeddedFile);
|
||||
assert_ne!(stored, b"attachment", "{alg:?}");
|
||||
// The file a reader opens: the same dictionary, the user password.
|
||||
let reader = SecurityHandler::open(d, ID, Some("user")).unwrap();
|
||||
assert_eq!(reader.decrypt_stream(4, 0, &stored, StreamKind::EmbeddedFile), b"attachment", "{alg:?}");
|
||||
assert_eq!(reader.decrypt_stream(3, 0, b"page content", StreamKind::Normal), b"page content", "{alg:?}");
|
||||
}
|
||||
for alg in [Algorithm::Rc4_40, Algorithm::Rc4_128] {
|
||||
assert!(matches!(create_attachments_only(¶ms(alg), ID), Err(CryptError::Unsupported(_))), "{alg:?}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user