use crate::error::{Error, Result};
use crate::object::Object;
mod aes;
mod algorithms;
mod certificate;
mod handler;
pub mod permissions;
pub(crate) mod rc4;
mod write_handler;
pub use algorithms::{compute_encryption_key, compute_owner_password_hash, compute_user_password_hash};
pub use certificate::{
CertEncryptDict, CertSubFilter, CertificateEncryption, CertificateEncryptionHandler, KeyTransportAlgorithm,
RecipientInfo, RecipientPermissions,
};
pub use handler::EncryptionHandler;
pub use permissions::PdfPermissions;
pub use write_handler::EncryptionWriteHandler;
pub(crate) fn md5_kdf_hasher() -> Result<Box<dyn crate::crypto::Hasher>> {
crate::crypto::active()
.hasher(crate::crypto::HashAlgorithm::Md5)
.map_err(|e| {
Error::InvalidPdf(format!(
"MD5 key derivation is not permitted by the active crypto \
policy/provider (ISO 32000-1 §7.6.3 Standard Security \
Handler, R≤4): {e}"
))
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Algorithm {
None,
RC4_40,
Rc4_128,
Aes128,
Aes256,
}
impl Algorithm {
pub fn key_length(&self) -> usize {
match self {
Algorithm::None => 0,
Algorithm::RC4_40 => 5,
Algorithm::Rc4_128 => 16,
Algorithm::Aes128 => 16,
Algorithm::Aes256 => 32,
}
}
pub fn is_aes(&self) -> bool {
matches!(self, Algorithm::Aes128 | Algorithm::Aes256)
}
pub fn is_rc4(&self) -> bool {
matches!(self, Algorithm::RC4_40 | Algorithm::Rc4_128)
}
}
#[derive(Debug, Clone)]
pub struct EncryptDict {
pub filter: String,
pub sub_filter: Option<String>,
pub version: u32,
pub length: Option<u32>,
pub revision: u32,
pub owner_password: Vec<u8>,
pub user_password: Vec<u8>,
pub permissions: i32,
pub encrypt_metadata: bool,
pub owner_encryption: Option<Vec<u8>>,
pub user_encryption: Option<Vec<u8>>,
pub perms: Option<Vec<u8>>,
pub stream_crypt_method: Option<String>,
}
impl EncryptDict {
pub fn from_object(obj: &Object) -> Result<Self> {
let dict = obj
.as_dict()
.ok_or_else(|| Error::InvalidPdf("Encrypt entry is not a dictionary".to_string()))?;
let filter = dict
.get("Filter")
.and_then(|o| o.as_name())
.ok_or_else(|| Error::InvalidPdf("Encrypt dictionary missing /Filter".to_string()))?
.to_string();
let version = dict
.get("V")
.and_then(|o| match o {
Object::Integer(i) => Some(*i as u32),
_ => None,
})
.ok_or_else(|| Error::InvalidPdf("Encrypt dictionary missing /V".to_string()))?;
let revision = dict
.get("R")
.and_then(|o| match o {
Object::Integer(i) => Some(*i as u32),
_ => None,
})
.ok_or_else(|| Error::InvalidPdf("Encrypt dictionary missing /R".to_string()))?;
let owner_password = dict
.get("O")
.and_then(|o| o.as_string())
.ok_or_else(|| Error::InvalidPdf("Encrypt dictionary missing /O".to_string()))?
.to_vec();
let user_password = dict
.get("U")
.and_then(|o| o.as_string())
.ok_or_else(|| Error::InvalidPdf("Encrypt dictionary missing /U".to_string()))?
.to_vec();
let permissions = dict
.get("P")
.and_then(|o| match o {
Object::Integer(i) => Some(*i as i32),
_ => None,
})
.ok_or_else(|| Error::InvalidPdf("Encrypt dictionary missing /P".to_string()))?;
let sub_filter = dict.get("SubFilter").and_then(|o| o.as_name()).map(|s| s.to_string());
let length = dict.get("Length").and_then(|o| match o {
Object::Integer(i) => Some(*i as u32),
_ => None,
});
let encrypt_metadata = dict
.get("EncryptMetadata")
.and_then(|o| match o {
Object::Boolean(b) => Some(*b),
_ => None,
})
.unwrap_or(true);
let owner_encryption = dict.get("OE").and_then(|o| o.as_string()).map(|s| s.to_vec());
let user_encryption = dict.get("UE").and_then(|o| o.as_string()).map(|s| s.to_vec());
let perms = dict.get("Perms").and_then(|o| o.as_string()).map(|s| s.to_vec());
let stream_crypt_method = if version == 4 {
let stm_filter_name = dict.get("StmF").and_then(|o| o.as_name()).unwrap_or("Identity");
dict.get("CF")
.and_then(|cf| cf.as_dict())
.and_then(|cf_dict| cf_dict.get(stm_filter_name))
.and_then(|filter_obj| filter_obj.as_dict())
.and_then(|filter_dict| filter_dict.get("CFM"))
.and_then(|cfm| cfm.as_name())
.map(|s| s.to_string())
} else {
None
};
Ok(EncryptDict {
filter,
sub_filter,
version,
length,
revision,
owner_password,
user_password,
permissions,
encrypt_metadata,
owner_encryption,
user_encryption,
perms,
stream_crypt_method,
})
}
pub fn algorithm(&self) -> Result<Algorithm> {
match (self.version, self.revision) {
(1, 2) => Ok(Algorithm::RC4_40),
(2, 3) => Ok(Algorithm::Rc4_128),
(4, _) => {
match self.stream_crypt_method.as_deref() {
Some("V2") => {
tracing::debug!(
revision = self.revision,
cfm = "V2",
"resolved crypt filter method: RC4-128"
);
Ok(Algorithm::Rc4_128)
}
Some("AESV2") | None => {
Ok(Algorithm::Aes128)
}
Some(other) => {
tracing::warn!(
revision = self.revision,
cfm = other,
"unknown crypt filter method; falling back to AES-128"
);
Ok(Algorithm::Aes128)
}
}
}
(5, 5) | (5, 6) => Ok(Algorithm::Aes256),
(1, r) => {
tracing::warn!(revision = r, "non-standard encryption V=1; using RC4-40");
Ok(Algorithm::RC4_40)
}
(2, r) => {
tracing::warn!(revision = r, "non-standard encryption V=2; using RC4-128");
Ok(Algorithm::Rc4_128)
}
_ => Err(Error::Unsupported(format!(
"Unsupported encryption version V={}, R={}",
self.version, self.revision
))),
}
}
pub fn key_length_bytes(&self) -> usize {
if let Some(length) = self.length {
(length / 8) as usize
} else {
match self.version {
1 => 5,
2 => 16,
4 => 16,
5 => 32,
_ => 16,
}
}
}
pub fn to_object(&self) -> Object {
use std::collections::HashMap;
let mut dict: HashMap<String, Object> = HashMap::new();
dict.insert("Filter".to_string(), Object::Name(self.filter.clone()));
dict.insert("V".to_string(), Object::Integer(self.version as i64));
dict.insert("R".to_string(), Object::Integer(self.revision as i64));
dict.insert("O".to_string(), Object::String(self.owner_password.clone()));
dict.insert("U".to_string(), Object::String(self.user_password.clone()));
dict.insert("P".to_string(), Object::Integer(self.permissions as i64));
if let Some(ref sub_filter) = self.sub_filter {
dict.insert("SubFilter".to_string(), Object::Name(sub_filter.clone()));
}
if let Some(length) = self.length {
dict.insert("Length".to_string(), Object::Integer(length as i64));
}
if !self.encrypt_metadata {
dict.insert("EncryptMetadata".to_string(), Object::Boolean(false));
}
if let Some(ref oe) = self.owner_encryption {
dict.insert("OE".to_string(), Object::String(oe.clone()));
}
if let Some(ref ue) = self.user_encryption {
dict.insert("UE".to_string(), Object::String(ue.clone()));
}
if let Some(ref perms) = self.perms {
dict.insert("Perms".to_string(), Object::String(perms.clone()));
}
if self.version == 4 {
let cfm = self.stream_crypt_method.as_deref().unwrap_or("AESV2").to_string();
let mut cf_dict: HashMap<String, Object> = HashMap::new();
let mut std_cf: HashMap<String, Object> = HashMap::new();
std_cf.insert("CFM".to_string(), Object::Name(cfm));
std_cf.insert("AuthEvent".to_string(), Object::Name("DocOpen".to_string()));
std_cf.insert("Length".to_string(), Object::Integer(16));
cf_dict.insert("StdCF".to_string(), Object::Dictionary(std_cf));
dict.insert("CF".to_string(), Object::Dictionary(cf_dict));
dict.insert("StmF".to_string(), Object::Name("StdCF".to_string()));
dict.insert("StrF".to_string(), Object::Name("StdCF".to_string()));
}
if self.version == 5 {
let mut cf_dict: HashMap<String, Object> = HashMap::new();
let mut std_cf: HashMap<String, Object> = HashMap::new();
std_cf.insert("CFM".to_string(), Object::Name("AESV3".to_string()));
std_cf.insert("AuthEvent".to_string(), Object::Name("DocOpen".to_string()));
std_cf.insert("Length".to_string(), Object::Integer(32));
cf_dict.insert("StdCF".to_string(), Object::Dictionary(std_cf));
dict.insert("CF".to_string(), Object::Dictionary(cf_dict));
dict.insert("StmF".to_string(), Object::Name("StdCF".to_string()));
dict.insert("StrF".to_string(), Object::Name("StdCF".to_string()));
}
Object::Dictionary(dict)
}
}
pub struct EncryptDictBuilder {
algorithm: Algorithm,
user_password: Vec<u8>,
owner_password: Vec<u8>,
permissions: i32,
encrypt_metadata: bool,
}
impl EncryptDictBuilder {
pub fn new(algorithm: Algorithm) -> Self {
Self {
algorithm,
user_password: Vec::new(),
owner_password: Vec::new(),
permissions: -1,
encrypt_metadata: true,
}
}
pub fn user_password(mut self, password: &[u8]) -> Self {
self.user_password = password.to_vec();
self
}
pub fn owner_password(mut self, password: &[u8]) -> Self {
self.owner_password = password.to_vec();
self
}
pub fn permissions(mut self, permissions: i32) -> Self {
self.permissions = permissions;
self
}
pub fn encrypt_metadata(mut self, encrypt: bool) -> Self {
self.encrypt_metadata = encrypt;
self
}
pub fn build(self, file_id: &[u8]) -> Result<EncryptDict> {
let (version, revision) = match self.algorithm {
Algorithm::None => (0, 0),
Algorithm::RC4_40 => (1, 2),
Algorithm::Rc4_128 => (2, 3),
Algorithm::Aes128 => (4, 4),
Algorithm::Aes256 => (5, 6),
};
if revision > 0 && revision <= 4 && !crate::crypto::active().is_legacy_allowed() {
return Err(crate::Error::InvalidPdf(format!(
"active CryptoProvider '{}' rejects PDF Standard Security R={} \
(R≤4 requires MD5 + RC4; FIPS 140-3 forbids both). \
Use Algorithm::Aes256 (R=6) or build xberg-native-pdf \
without the 'fips' feature.",
crate::crypto::active().name(),
revision
)));
}
if revision > 0 && revision <= 4 {
crate::crypto::record_algorithm_use(crate::crypto::AlgorithmId::HashMd5);
}
if revision > 0
&& revision <= 4
&& !crate::crypto::active_policy()
.allows(crate::crypto::AlgorithmId::HashMd5, crate::crypto::AlgorithmUse::Write)
{
return Err(crate::Error::InvalidPdf(format!(
"active crypto SecurityPolicy (mode={}) forbids writing PDF \
Standard Security R={} (R≤4 requires MD5; denied for write). \
Use Algorithm::Aes256 (R=6) or set a 'compat' crypto policy.",
crate::crypto::active_policy().mode().token(),
revision
)));
}
let key_length = self.algorithm.key_length();
let owner_pass = if self.owner_password.is_empty() {
self.user_password.clone()
} else {
self.owner_password.clone()
};
if revision >= 5 {
let (user_hash, user_encryption, file_key) =
algorithms::compute_u_and_ue(&self.user_password, key_length, revision)?;
let (owner_hash, owner_encryption) =
algorithms::compute_o_and_oe(&owner_pass, &self.user_password, &file_key, &user_hash, revision)?;
return Ok(EncryptDict {
filter: "Standard".to_string(),
sub_filter: None,
version,
length: Some((key_length * 8) as u32),
revision,
owner_password: owner_hash,
user_password: user_hash,
permissions: self.permissions,
encrypt_metadata: self.encrypt_metadata,
owner_encryption: Some(owner_encryption),
user_encryption: Some(user_encryption),
perms: None,
stream_crypt_method: None,
});
}
let owner_hash =
algorithms::compute_owner_password_hash(&owner_pass, &self.user_password, revision, key_length)?;
let encryption_key = algorithms::compute_encryption_key(
&self.user_password,
&owner_hash,
self.permissions,
file_id,
revision,
key_length,
self.encrypt_metadata,
)?;
let user_hash = algorithms::compute_user_password_hash(&encryption_key, file_id, revision)?;
Ok(EncryptDict {
filter: "Standard".to_string(),
sub_filter: None,
version,
length: Some((key_length * 8) as u32),
revision,
owner_password: owner_hash,
user_password: user_hash,
permissions: self.permissions,
encrypt_metadata: self.encrypt_metadata,
owner_encryption: None,
user_encryption: None,
perms: None,
stream_crypt_method: None,
})
}
}
#[derive(Debug, Clone, Copy)]
pub struct Permissions {
bits: i32,
}
impl Permissions {
pub fn from_bits(bits: i32) -> Self {
Self { bits }
}
#[inline]
fn decoded(&self) -> PdfPermissions {
PdfPermissions::from_p_flag(self.bits)
}
pub fn can_print(&self) -> bool {
self.decoded().print_low_res
}
pub fn can_modify(&self) -> bool {
self.decoded().modify
}
pub fn can_copy(&self) -> bool {
self.decoded().copy
}
pub fn can_annotate(&self) -> bool {
self.decoded().annotate
}
pub fn can_fill_forms(&self) -> bool {
self.decoded().fill_forms
}
pub fn can_extract_accessibility(&self) -> bool {
self.decoded().accessibility
}
pub fn can_assemble(&self) -> bool {
self.decoded().assemble
}
pub fn can_print_high_quality(&self) -> bool {
self.decoded().print_high_res
}
}
pub fn generate_file_id() -> (Vec<u8>, Vec<u8>) {
let uuid = uuid::Uuid::new_v4();
let uuid_bytes = uuid.as_bytes();
#[cfg(not(target_arch = "wasm32"))]
let now_bytes: Option<[u8; 16]> = {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default();
Some(now.as_nanos().to_le_bytes())
};
#[cfg(target_arch = "wasm32")]
let now_bytes: Option<[u8; 16]> = None;
let id = {
use md5::{Digest, Md5};
let mut h = Md5::new();
h.update(uuid_bytes);
if let Some(b) = now_bytes {
h.update(b);
}
h.finalize().to_vec()
};
(id.clone(), id)
}
#[cfg(test)]
mod tests;