# pdftract-2t1an: Encryption dictionary detection + EncryptionInfo struct ## Summary Implemented `detect_encryption(trailer, resolver)` function that parses the PDF trailer's /Encrypt dictionary into a structured `EncryptionInfo`. The implementation recognizes: - /Filter (Standard handler only) - /V (1/2/4/5) - /R (2/3/4/5/6) - /KeyLength (with defaults per version) - /O, /U (owner/user password hashes with length validation) - /P (permissions bitfield) - /Perms (V=5 encrypted permissions) - /CF, /StmF, /StrF (crypt filters for V>=4) - /ID[0] (file ID from trailer) ## Implementation Details ### Public Types - `EncryptionInfo`: Main encryption metadata struct - version, revision, key_length - owner_hash, user_hash, perms, file_id - Optional crypt_filters for V>=4 - `CryptFiltersV4`: Crypt filter metadata for V=4/5 - stream_filter, string_filter - filters: BTreeMap - `CryptFilterDef`: Individual crypt filter definition - cfm: CryptFilterMethod (Identity/V2/AesV2/AesV3) - length: Option - auth_event: AuthEvent ### Detection Function `detect_encryption(trailer, resolver, diagnostics) -> Option` 1. Looks up /Encrypt in trailer (handles both ObjRef and inline dict) 2. Resolves ObjRef via XrefResolver 3. Validates /Filter == /Standard; emits ENCRYPTION_UNSUPPORTED if not 4. Parses /V, /R, /KeyLength (with version-based defaults) 5. Parses /O, /U with length validation (32 bytes for R<=4, 48 for R>=5) 6. Parses /P permissions bitfield 7. For V>=4, parses /CF, /StmF, /StrF 8. For V=5, parses /Perms encrypted permissions 9. Extracts /ID[0] from trailer (or empty if missing) 10. Returns Some(EncryptionInfo) or None with diagnostics ## Diagnostics - `ENCRYPTION_UNSUPPORTED`: Emitted for non-Standard filters (e.g., Adobe Public Key, custom) - `ENCRYPTION_INVALID_DICT`: Emitted for invalid /O or /U lengths, missing required fields ## Test Results All 10 unit tests pass: - test_v1_r2_rc4_40: ✅ V=1 R=2 RC4-40 (version=1, revision=2, key_length=40) - test_v5_r6_aes_256: ✅ V=5 R=6 AES-256 (version=5, revision=6, key_length=256) - test_non_standard_filter_emits_diagnostic: ✅ Returns None + ENCRYPTION_UNSUPPORTED - test_invalid_o_length_emits_diagnostic: ✅ Returns None + ENCRYPTION_INVALID_DICT - test_invalid_u_length_emits_diagnostic: ✅ Returns None + ENCRYPTION_INVALID_DICT - test_v5_invalid_hash_length_emits_diagnostic: ✅ 48-byte validation for R>=5 - test_no_encrypt_key: ✅ Returns None cleanly when no /Encrypt - test_missing_id: ✅ Empty file_id when /ID missing - test_v4_crypt_filters: ✅ Parses /CF, /StmF, /StrF correctly - test_all_v_r_combinations: ✅ Covers V=1/R=2, V=2/R=3, V=4/R=4 ## Acceptance Criteria Status | Criterion | Status | Evidence | |-----------|--------|----------| | V=1 R=2 RC4-40 detection | PASS | test_v1_r2_rc4_40 | | V=5 R=6 AES-256 detection | PASS | test_v5_r6_aes_256 | | Non-Standard filter rejection | PASS | test_non_standard_filter_emits_diagnostic | | Invalid /O length handling | PASS | test_invalid_o_length_emits_diagnostic | | No /Encrypt key handling | PASS | test_no_encrypt_key | | All V/R combinations tested | PASS | test_all_v_r_combinations | ## Module Location `crates/pdftract-core/src/encryption/detection.rs` (feature-gated on decrypt) ## Dependencies - `pdftract-core::parser::object::{PdfDict, PdfObject, ObjRef}` - `pdftract-core::diagnostics::{emit, Diagnostic, DiagCode}` - `std::collections::BTreeMap`