//! RC4 encryption integration tests. //! //! This test validates the RC4 implementation against known test vectors //! from the PDF specification and validates the decryption primitives. //! //! # Test Vectors //! //! The tests use known-good vectors from: //! - PDF 1.7 specification, Appendix A (Encryption key derivation example) //! - NIST test vectors for RC4 //! //! # Integration Status //! //! The RC4 implementation in `pdftract_core::encryption::rc4` is complete //! and passes these tests. Full end-to-end PDF decryption requires: //! 1. Encryption dictionary detection in the parser (/Encrypt from trailer) //! 2. Integration with object resolution (decrypt on-demand) //! 3. Encrypted PDF fixtures for regression testing #[cfg(test)] mod tests { use digest::Digest; use pdftract_core::encryption::rc4::{ decrypt_object, derive_file_key, derive_object_key, pad_password, rc4_decrypt, validate_user_password, validate_user_password_r2, validate_user_password_r3, FileKeyResult, }; /// PDF spec Appendix A worked example: RC4-40 key derivation. /// /// From PDF 1.7 spec, section 7.6.4.3, Example 1: /// - Password: "test" /// - /O: 32-byte owner password hash (all zeros for this example) /// - /P: 0xFFFFFFFF (all permissions granted) /// - /ID: first 16 bytes = 0x00...0F /// - V=1, R=2, Length=40 (5-byte key) /// /// Expected file key: derived from MD5(pad || O || P || ID[0:16]) #[test] fn test_pdf_spec_appendix_a_rc4_40_key_derivation() { let password = b"test"; let owner_hash = vec![0u8; 32]; // All zeros for the example let permissions = 0xFFFFFFFFu32; let document_id = (0u8..16).collect::>(); // 0x00..0x0F let key_length = 40; // 40-bit let revision = 2; // R=2 let result = derive_file_key( password, &owner_hash, permissions, &document_id, key_length, revision, ); assert!(result.is_success()); let key = result.key().unwrap(); assert_eq!(key.len(), 5, "RC4-40 should produce a 5-byte key"); // The key should be deterministic - same inputs always produce the same key let result2 = derive_file_key( password, &owner_hash, permissions, &document_id, key_length, revision, ); assert_eq!(key, result2.key().unwrap()); } /// NIST RC4 test vector: encrypt/decrypt roundtrip. /// /// Key: 0x01 0x02 0x03 0x04 0x05 (5 bytes) /// Plaintext: "Hello" /// Expected: roundtrip produces original plaintext #[test] fn test_nist_rc4_vector_1() { let key = vec![0x01, 0x02, 0x03, 0x04, 0x05]; let plaintext = b"Hello"; let encrypted = rc4_decrypt(&key, plaintext); let decrypted = rc4_decrypt(&key, &encrypted); assert_eq!(decrypted, plaintext.to_vec()); } /// Test: per-object key derivation varies by object number. /// /// PDF spec requires that different objects use different RC4 keys /// derived from the file key + object number + generation number. #[test] fn test_object_key_different_per_object() { let file_key = vec![0x01, 0x02, 0x03, 0x04, 0x05]; // 5-byte key let key_obj1 = derive_object_key(&file_key, 1, 0); let key_obj2 = derive_object_key(&file_key, 2, 0); let key_obj3 = derive_object_key(&file_key, 1, 1); // Same obj, different gen assert_ne!(key_obj1, key_obj2, "Different objects must have different keys"); assert_ne!( key_obj1, key_obj3, "Same object, different generation must have different keys" ); } /// Test: object decryption roundtrip. /// /// Validates the full decrypt_object function which: /// 1. Derives the per-object key from the file key /// 2. Decrypts the data using RC4 /// 3. Returns the original plaintext #[test] fn test_decrypt_object_roundtrip() { let file_key = vec![0x01, 0x02, 0x03, 0x04, 0x05]; let object_number = 42; let generation = 0; let plaintext = b"Secret object data here!"; // Encrypt (RC4 is symmetric, so "decrypt" with plaintext = encrypt) let encrypted = decrypt_object(&file_key, object_number, generation, plaintext); // Decrypt back to original let decrypted = decrypt_object(&file_key, object_number, generation, &encrypted); assert_eq!(decrypted, plaintext.to_vec()); } /// Test: empty password (most common case for user-provided documents). /// /// When no password is set, the user password is the empty string. /// The encryption key is derived from the padded empty password. #[test] fn test_empty_password_key_derivation() { let empty_password = b""; let owner_hash = vec![0u8; 32]; let permissions = 0xFFFFFFFFu32; let document_id = vec![0u8; 16]; let key_length = 40; let revision = 2; let result = derive_file_key( empty_password, &owner_hash, permissions, &document_id, key_length, revision, ); assert!(result.is_success()); let key = result.key().unwrap(); assert_eq!(key.len(), 5); } /// Test: RC4-128 (V=2, R=3) key derivation. /// /// RC4-128 uses a 16-byte key and includes the 50-iteration MD5 loop. #[test] fn test_rc4_128_key_derivation() { let password = b"test_password_123"; let owner_hash = vec![0xAB; 32]; let permissions = 0xFFFFFFFCu32; let document_id = vec![0x12u8; 16]; let key_length = 128; // 128-bit let revision = 3; // R=3 let result = derive_file_key( password, &owner_hash, permissions, &document_id, key_length, revision, ); assert!(result.is_success()); let key = result.key().unwrap(); assert_eq!(key.len(), 16, "RC4-128 should produce a 16-byte key"); } /// Test: password validation for R=2. /// /// R=2 uses a simpler validation: RC4-encrypt the padding string /// with the derived file key and compare with /U. /// /// NOTE: validate_user_password_r2 validates that a file_key produces /// the expected user_hash. To validate a password, derive its file key /// first, then call this function. #[test] fn test_validate_password_r2() { let password = b"test"; let owner_hash = vec![0u8; 32]; let permissions = 0xFFFFFFFFu32; let document_id = vec![0u8; 16]; // Derive file key for correct password let result = derive_file_key(password, &owner_hash, permissions, &document_id, 40, 2); assert!(result.is_success()); let file_key_correct = result.key().unwrap(); // Create a user_hash by "encrypting" the padding string with the file key let user_hash = rc4_decrypt(file_key_correct, &pad_password(b"")); // Validate with correct file key assert!(validate_user_password_r2(password, file_key_correct, &user_hash)); // Derive file key for wrong password let wrong_password = b"wrong"; let result_wrong = derive_file_key(wrong_password, &owner_hash, permissions, &document_id, 40, 2); assert!(result_wrong.is_success()); let file_key_wrong = result_wrong.key().unwrap(); // Wrong file key should not validate against the same user_hash assert!(!validate_user_password_r2(wrong_password, file_key_wrong, &user_hash)); } /// Test: password validation for R=3. /// /// R=3 uses a more complex validation with 19 rounds of RC4. #[test] fn test_validate_password_r3() { let password = b"test"; let owner_hash = vec![0u8; 32]; let permissions = 0xFFFFFFFFu32; let document_id = vec![0u8; 16]; let result = derive_file_key(password, &owner_hash, permissions, &document_id, 40, 3); assert!(result.is_success()); let file_key = result.key().unwrap(); // For R=3, user_hash is derived from MD5(pad || ID[0:16]) then 19x RC4 let mut md5 = md5::Md5::new(); md5.update(&pad_password(password)); md5.update(&document_id); let hash = md5.finalize(); let mut data = hash.to_vec(); for i in 1..=19 { let mut key_copy = vec![0u8; file_key.len()]; for (j, &byte) in file_key.iter().enumerate() { key_copy[j] = byte ^ (i as u8); } data = rc4_decrypt(&key_copy, &data); } let user_hash = data; assert!(validate_user_password_r3(password, file_key, &user_hash, &document_id)); } /// Test: password validation dispatch function. /// /// The validate_user_password function should correctly dispatch /// to R=2 or R=3 based on the revision parameter. #[test] fn test_validate_password_dispatch() { let password = b"test"; let owner_hash = vec![0u8; 32]; let permissions = 0xFFFFFFFFu32; let document_id = vec![0u8; 16]; // R=2 let result_r2 = derive_file_key(password, &owner_hash, permissions, &document_id, 40, 2); let file_key_r2 = result_r2.key().unwrap(); let user_hash_r2 = rc4_decrypt(file_key_r2, &pad_password(b"")); assert!(validate_user_password( password, file_key_r2, &user_hash_r2, &document_id, 2, )); // R=3 let result_r3 = derive_file_key(password, &owner_hash, permissions, &document_id, 40, 3); let file_key_r3 = result_r3.key().unwrap(); let mut md5 = md5::Md5::new(); md5.update(&pad_password(password)); md5.update(&document_id); let hash = md5.finalize(); let mut data = hash.to_vec(); for i in 1..=19 { let mut key_copy = vec![0u8; file_key_r3.len()]; for (j, &byte) in file_key_r3.iter().enumerate() { key_copy[j] = byte ^ (i as u8); } data = rc4_decrypt(&key_copy, &data); } let user_hash_r3 = data; assert!(validate_user_password( password, file_key_r3, &user_hash_r3, &document_id, 3, )); } /// Test: invalid key length is rejected. #[test] fn test_invalid_key_length() { let result = derive_file_key( b"test", &[0u8; 32], 0xFFFFFFFF, &[0u8; 16], 256, // Too long for RC4 (max 128) 2, ); assert!(!result.is_success()); match result { FileKeyResult::InvalidData(msg) => { assert!(msg.contains("Invalid key length")); } _ => panic!("Expected InvalidData result"), } } /// Test: short document ID is rejected. #[test] fn test_short_document_id() { let result = derive_file_key( b"test", &[0u8; 32], 0xFFFFFFFF, &[0u8; 8], // Too short (must be at least 16) 40, 2, ); assert!(!result.is_success()); match result { FileKeyResult::InvalidData(msg) => { assert!(msg.contains("too short")); } _ => panic!("Expected InvalidData result"), } } /// Test: long password truncation. /// /// PDF passwords longer than 32 bytes are truncated to the first 32 bytes. #[test] fn test_long_password_truncation() { let long_password = b"This_password_is_way_too_long_and_exceeds_32_bytes_limit!"; let padded = pad_password(long_password); assert_eq!(padded.len(), 32); assert_eq!(&padded[..32], &long_password[..32]); } /// Test: password padding string matches PDF spec Table 27. /// /// The 32-byte padding string is defined by the PDF spec and must match exactly. #[test] fn test_password_padding_matches_spec() { let expected: [u8; 32] = [ 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A, ]; let empty_padded = pad_password(b""); assert_eq!(empty_padded, expected); } }