mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
fix(crypt): R2–R4 passwords with non-ASCII characters open (PDFDocEncoding) (#905)
Revisions 2–4 of the standard security handler take the password in PDFDocEncoding (ISO 32000-2 §7.6.4.3.2, Algorithm 2). PdfCraft authenticates an encrypted file twice, and neither did that: - pdfcraft-crypt took Latin-1 and dropped every other character, so "ı" (0x9A) and "€" (0xA0) vanished and "ılık€" became "lk"; - the renderer (vendored hayro-syntax, "TODO: Convert to PDFDocEncoding") used the UTF-8 bytes, so any non-ASCII password failed, even "Çok güzel", which Latin-1 and PDFDocEncoding write alike; - the engine handed the renderer an owner-recovered user password as Latin-1 characters, which the renderer then read as UTF-8 again. Files that qpdf or Acrobat protected with such passwords could not be opened with the right password. - pdfcraft-crypt encodes R2–R4 passwords in PDFDocEncoding and, for a password with characters PDFDocEncoding lacks (ğ, ş, İ), also tries its UTF-8 bytes, which qpdf writes for those. The PDFDocEncoding table moves here from pdfcraft-cos (which depends on this crate) and cos uses it. - hayro-syntax (vendored patch 14) tries an R2–R4 password as given, then in PDFDocEncoding. - The engine passes a recovered user password to the renderer as UTF-8 when its bytes are UTF-8 and as PDFDocEncoding otherwise, so the renderer gets exactly those bytes back. Co-authored-by: Brandon Thomas <[email protected]>
This commit is contained in:
1 parent
9035ae3c82
commit
9c2d9c02bf
8 files changed
+259
-40
No files matched your search
@@ -2817,3 +2817,39 @@ fn oversized_exports_clamp_and_report_their_dpi_unless_strict() {
|
||||
let err = export::Exporter::from_source(strict).png(0, 600.0).unwrap_err();
|
||||
assert!(err.contains("exceeds renderer limits"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_passwords_with_turkish_characters_open() {
|
||||
// RC4-128 (R3): user "ılık€" in PDFDocEncoding (ı = 0x9A, € = 0xA0), owner "Çağlar"
|
||||
// (PDFDocEncoding has no ğ: written without it, as pdfcraft-crypt writes R2–R4 passwords).
|
||||
let mut cos = pdfcraft_cos::Document::open(Arc::new(fixture(1))).unwrap();
|
||||
let params = pdfcraft_cos::NewEncryption {
|
||||
algorithm: pdfcraft_cos::Algorithm::Rc4_128,
|
||||
user_password: "ılık€",
|
||||
owner_password: "Çağlar",
|
||||
permissions: -1,
|
||||
encrypt_metadata: true,
|
||||
seed: [7; 32],
|
||||
};
|
||||
cos.set_encryption(¶ms).unwrap();
|
||||
let bytes = Arc::new(pdfcraft_cos::write_full(&cos, &pdfcraft_cos::SaveOptions::default()).unwrap());
|
||||
let mut s = Session::new();
|
||||
// The renderer used to try only the UTF-8 bytes, and the owner fallback handed it the
|
||||
// recovered user password as Latin-1: both came back as a wrong password.
|
||||
for pw in ["ılık€", "Çağlar"] {
|
||||
let id = s.open("x.pdf", None, bytes.clone(), Some(pw)).unwrap_or_else(|e| panic!("{pw}: {e:?}"));
|
||||
assert_eq!(page_texts(&s, id), ["Page 1"], "{pw}");
|
||||
assert!(s.get(id).unwrap().info.encrypted, "{pw}");
|
||||
}
|
||||
assert!(matches!(s.open("x.pdf", None, bytes, Some("ilik€")), Err(OpenError::WrongPassword)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn recovered_user_passwords_reach_the_renderer_byte_for_byte() {
|
||||
assert_eq!(crate::renderer_password(b"pw"), "pw");
|
||||
// UTF-8 bytes (qpdf writes "şifre" so; its \xC5\x9F has no PDFDocEncoding reading).
|
||||
assert_eq!(crate::renderer_password("şifre".as_bytes()), "şifre");
|
||||
// PDFDocEncoding bytes: ı = 0x9A, € = 0xA0, Ç = 0xC7.
|
||||
assert_eq!(crate::renderer_password(&[0x9A, b'l', 0x9A, b'k', 0xA0]), "ılık€");
|
||||
assert_eq!(crate::renderer_password(&[0xC7, b'o', b'k']), "Çok");
|
||||
}
|
||||
Reference in new issue
Block a user