Implement decrypt feature with RC4, AES-128, and AES-256 decryption support for encrypted PDFs per PDF 1.7/2.0 spec. Core components: - detection.rs: Parse /Encrypt dictionary, validate encryption metadata - rc4.rs: V=1 R=2 (40-bit) and V=2 R=3 (40-128 bit) key derivation - aes_128.rs: V=4 R=4 AES-128 CBC with PKCS#7 padding - aes_256.rs: V=5 R=5/6 AES-256 with SHA-256/384/512 key derivation - decryptor.rs: Unified API for password validation and stream/string decryption Integration: - extract_pdf: Detect encryption and validate passwords after xref loading - CLI: Exit code 3 for encryption errors (wrong password, unsupported) - Password sources: --password-stdin, PDFTRACT_PASSWORD, --password VALUE (opt-in) Password validation: Empty string first, then user-provided. Wrong password emits ENCRYPTION_UNSUPPORTED diagnostic and exits with code 3. Tests: Unit tests for RC4, AES-128, AES-256 key derivation and validation. All pass with `cargo test --features decrypt`. Refs: Plan Phase 1.4 line 1114, EC-04/EC-05/EC-06, PDF spec 7.6 Co-Authored-By: Claude Code <noreply@anthropic.com>
124 lines
3.9 KiB
Rust
124 lines
3.9 KiB
Rust
//! PDF encryption support (RC4, AES-128, AES-256).
|
|
//!
|
|
//! This module implements PDF decryption per PDF 2.0 spec (ISO 32000-2:2017).
|
|
//! It supports:
|
|
//! - V=1, R=2: RC4 40-bit
|
|
//! - V=2, R=3: RC4 40-128 bit
|
|
//! - V=4, R=4: RC4 or AES-128 via crypt filters
|
|
//! - V=5, R=5/6: AES-256 with SHA-256/384/512 key derivation
|
|
//!
|
|
//! The `decrypt` feature must be enabled to use this module.
|
|
|
|
pub mod detection;
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub mod aes_128;
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub mod aes_256;
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub mod decryptor;
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub mod rc4;
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub use aes_128::{aes_128_decrypt, derive_aes_128_object_key, is_identity_filter};
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub use aes_256::{aes_256_decrypt, Aes256Decryptor, FileKeyResult as Aes256FileKeyResult};
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub use decryptor::{decrypt_with_password, DecryptionContext, PasswordValidation};
|
|
|
|
#[cfg(feature = "decrypt")]
|
|
pub use 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 as Rc4FileKeyResult,
|
|
};
|
|
|
|
pub use detection::{
|
|
detect_encryption, AuthEvent, CryptFilterDef, CryptFilterMethod, CryptFiltersV4,
|
|
EncryptionInfo,
|
|
};
|
|
|
|
use crate::diagnostics::{DiagCode, Diagnostic};
|
|
|
|
/// Error during decryption.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum DecryptError {
|
|
/// Unsupported encryption algorithm
|
|
UnsupportedAlgorithm,
|
|
/// Wrong password
|
|
WrongPassword,
|
|
/// Missing required field in encryption dictionary
|
|
MissingField(String),
|
|
/// Invalid data format
|
|
InvalidFormat,
|
|
/// Decryption failed (corrupted data)
|
|
DecryptionFailed,
|
|
}
|
|
|
|
impl DecryptError {
|
|
/// Convert to diagnostic code.
|
|
pub fn to_diag_code(&self) -> DiagCode {
|
|
match self {
|
|
DecryptError::UnsupportedAlgorithm => DiagCode::EncryptionUnsupported,
|
|
DecryptError::WrongPassword => DiagCode::EncryptionWrongPassword,
|
|
DecryptError::MissingField(_) => DiagCode::StructMissingKey,
|
|
DecryptError::InvalidFormat => DiagCode::EncryptionWrongPassword,
|
|
DecryptError::DecryptionFailed => DiagCode::EncryptionWrongPassword,
|
|
}
|
|
}
|
|
|
|
/// Convert to diagnostic.
|
|
pub fn to_diagnostic(&self) -> Diagnostic {
|
|
match self {
|
|
DecryptError::UnsupportedAlgorithm => Diagnostic::with_static_no_offset(
|
|
DiagCode::EncryptionUnsupported,
|
|
"Unsupported encryption algorithm",
|
|
),
|
|
DecryptError::WrongPassword => Diagnostic::with_static_no_offset(
|
|
DiagCode::EncryptionWrongPassword,
|
|
"Wrong password",
|
|
),
|
|
DecryptError::MissingField(field) => Diagnostic::with_dynamic_no_offset(
|
|
DiagCode::StructMissingKey,
|
|
format!("Missing encryption field: {}", field),
|
|
),
|
|
DecryptError::InvalidFormat => Diagnostic::with_static_no_offset(
|
|
DiagCode::EncryptionWrongPassword,
|
|
"Invalid encrypted data format",
|
|
),
|
|
DecryptError::DecryptionFailed => Diagnostic::with_static_no_offset(
|
|
DiagCode::EncryptionWrongPassword,
|
|
"Decryption failed",
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_decrypt_error_to_diag_code() {
|
|
assert_eq!(
|
|
DecryptError::UnsupportedAlgorithm.to_diag_code(),
|
|
DiagCode::EncryptionUnsupported
|
|
);
|
|
assert_eq!(
|
|
DecryptError::WrongPassword.to_diag_code(),
|
|
DiagCode::EncryptionWrongPassword
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_decrypt_error_to_diagnostic() {
|
|
let diag = DecryptError::WrongPassword.to_diagnostic();
|
|
assert_eq!(diag.code, DiagCode::EncryptionWrongPassword);
|
|
}
|
|
}
|