pub const PDF_PADDING: &[u8; 32] = b"\x28\xBF\x4E\x5E\x4E\x75\x8A\x41\x64\x00\x4E\x56\xFF\xFA\x01\x08\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A";
pub struct PdfHash {
pub v: u32,
pub r: u32,
pub length: u32,
pub p: i32,
pub encrypt_meta: bool,
pub id: Vec<u8>,
pub u: Vec<u8>,
pub o: Vec<u8>,
pub ue: Vec<u8>,
pub oe: Vec<u8>,
}
pub fn extract_pdf_hash(path: &str) -> Result<String, String> {
let data = std::fs::read(path).map_err(|e| format!("Cannot read {}: {}", path, e))?;
let pdf = parse_pdf(&data)?;
Ok(format_pdf_hash(&pdf))
}
fn format_pdf_hash(pdf: &PdfHash) -> String {
let id_hex = hex::encode(&pdf.id);
let id_len = pdf.id.len();
let u_hex = hex::encode(&pdf.u);
let u_len = pdf.u.len();
let o_hex = hex::encode(&pdf.o);
let o_len = pdf.o.len();
let meta = if pdf.encrypt_meta { 1 } else { 0 };
let mut hash = format!(
"$pdf${}*{}*{}*{}*{}*{}*{}*{}*{}*{}*{}",
pdf.v, pdf.r, pdf.length, pdf.p, meta,
id_len, id_hex, u_len, u_hex, o_len, o_hex
);
if !pdf.ue.is_empty() {
let ue_hex = hex::encode(&pdf.ue);
let ue_len = pdf.ue.len();
let oe_hex = hex::encode(&pdf.oe);
let oe_len = pdf.oe.len();
hash.push_str(&format!("*{}*{}*{}*{}", ue_len, ue_hex, oe_len, oe_hex));
}
hash
}
fn parse_pdf(data: &[u8]) -> Result<PdfHash, String> {
let trailer = find_trailer(data)?;
let id = extract_id(&trailer)?;
let encrypt_ref = find_encrypt_ref(&trailer)?;
let enc_dict = get_pdf_object(data, &encrypt_ref)?;
let v = extract_int(&enc_dict, b"/V")?;
let r = extract_int(&enc_dict, b"/R")?;
let length = extract_length(&enc_dict, v);
let p = extract_int_signed(&enc_dict, b"/P")?;
let encrypt_meta = extract_encrypt_meta(&enc_dict);
let u = extract_bytes(&enc_dict, b"/U")?;
let o = extract_bytes(&enc_dict, b"/O")?;
let (ue, oe) = if v == 5 {
(extract_bytes(&enc_dict, b"/UE").unwrap_or_default(),
extract_bytes(&enc_dict, b"/OE").unwrap_or_default())
} else {
(Vec::new(), Vec::new())
};
Ok(PdfHash { v, r, length, p, encrypt_meta, id, u, o, ue, oe })
}
fn find_trailer(data: &[u8]) -> Result<Vec<u8>, String> {
let trailer_pos = find_bytes(data, b"trailer")
.ok_or_else(|| "Could not find trailer keyword".to_string())?;
let startxref_pos = find_bytes_from(data, b"startxref", trailer_pos)
.ok_or_else(|| "Could not find startxref after trailer".to_string())?;
let trailer_data = &data[trailer_pos..startxref_pos];
Ok(trailer_data.to_vec())
}
fn find_encrypt_ref(trailer: &[u8]) -> Result<String, String> {
let pattern = b"/Encrypt";
let pos = find_bytes(trailer, pattern)
.ok_or_else(|| "File is not encrypted (no /Encrypt in trailer)".to_string())?;
let rest = &trailer[pos + pattern.len()..];
let ref_str = extract_ref(rest)?;
Ok(ref_str)
}
fn extract_ref(data: &[u8]) -> Result<String, String> {
let s = std::str::from_utf8(data).map_err(|_| "Invalid UTF-8 in PDF".to_string())?;
let s = s.trim_start();
let parts: Vec<&str> = s.splitn(3, |c: char| c.is_whitespace() || c == '/' || c == '>' || c == ']')
.collect();
if parts.len() < 3 {
return Err("Cannot parse object reference".to_string());
}
let obj_num = parts[0].trim();
let gen_num = parts[1].trim();
if obj_num.parse::<u32>().is_err() || gen_num.parse::<u32>().is_err() {
return Err(format!("Invalid object reference: {} {}", obj_num, gen_num));
}
Ok(format!("{} {} R", obj_num, gen_num))
}
fn get_pdf_object(data: &[u8], ref_str: &str) -> Result<Vec<u8>, String> {
let obj_pattern = ref_str.replace(" R", " obj");
let start = find_bytes(data, obj_pattern.as_bytes())
.ok_or_else(|| format!("Could not find object {}", ref_str))?;
let end = find_bytes_from(data, b"endobj", start)
.ok_or_else(|| format!("Could not find endobj for {}", ref_str))?;
let end_obj_end = end + b"endobj".len();
let obj_data = &data[start..end_obj_end];
Ok(obj_data.to_vec())
}
fn extract_id(trailer: &[u8]) -> Result<Vec<u8>, String> {
let pattern = b"/ID";
let pos = find_bytes(trailer, pattern);
let id_data = match pos {
Some(p) => {
let rest = &trailer[p + pattern.len()..];
extract_hex_content(rest).unwrap_or_default()
}
None => Vec::new(),
};
if id_data.is_empty() {
return Err("Could not find /ID in trailer".to_string());
}
Ok(id_data)
}
fn extract_hex_content(data: &[u8]) -> Option<Vec<u8>> {
let start = find_bytes(data, b"<")?;
let end = find_bytes_from(data, b">", start + 1)?;
let hex_str = std::str::from_utf8(&data[start + 1..end]).ok()?;
let clean: String = hex_str.chars().filter(|c| c.is_ascii_hexdigit()).collect();
hex::decode(&clean).ok()
}
fn extract_int(data: &[u8], key: &[u8]) -> Result<u32, String> {
let pos = find_bytes(data, key)
.ok_or_else(|| format!("Could not find {:?} in encryption dictionary", key))?;
let rest = &data[pos + key.len()..];
let s = std::str::from_utf8(rest).map_err(|_| "Invalid UTF-8".to_string())?;
let s = s.trim_start();
let num_str: String = s.chars().take_while(|c| c.is_ascii_digit() || *c == '-').collect();
num_str.parse::<u32>().map_err(|e| format!("Cannot parse integer for {:?}: {}", key, e))
}
fn extract_int_signed(data: &[u8], key: &[u8]) -> Result<i32, String> {
let pos = find_bytes(data, key)
.ok_or_else(|| format!("Could not find {:?} in encryption dictionary", key))?;
let rest = &data[pos + key.len()..];
let s = std::str::from_utf8(rest).map_err(|_| "Invalid UTF-8".to_string())?;
let s = s.trim_start();
let num_str: String = s.chars().take_while(|c| c.is_ascii_digit() || *c == '-').collect();
num_str.parse::<i32>().map_err(|e| format!("Cannot parse signed integer for {:?}: {}", key, e))
}
fn extract_length(data: &[u8], v: u32) -> u32 {
if v == 1 {
return 40;
}
let pos = find_bytes(data, b"/Length");
match pos {
Some(p) => {
let rest = &data[p + 7..];
let s = std::str::from_utf8(rest).unwrap_or("");
let s = s.trim_start();
let num_str: String = s.chars().take_while(|c| c.is_ascii_digit()).collect();
num_str.parse::<u32>().unwrap_or(128)
}
None => 128,
}
}
fn extract_encrypt_meta(data: &[u8]) -> bool {
let pos = find_bytes(data, b"/EncryptMetadata");
match pos {
Some(p) => {
let rest = &data[p + 16..];
let s = std::str::from_utf8(rest).unwrap_or("");
let s = s.trim_start();
!s.starts_with("false")
}
None => true,
}
}
fn extract_bytes(data: &[u8], key: &[u8]) -> Result<Vec<u8>, String> {
let pos = find_bytes(data, key)
.ok_or_else(|| format!("Could not find {:?} in encryption dictionary", String::from_utf8_lossy(key)))?;
let rest = &data[pos + key.len()..];
let s = std::str::from_utf8(rest).map_err(|_| "Invalid UTF-8".to_string())?;
let s = s.trim_start();
if s.starts_with('<') {
let end = s.find('>').ok_or_else(|| "Unterminated hex string".to_string())?;
let hex_str: String = s[1..end].chars().filter(|c| c.is_ascii_hexdigit()).collect();
hex::decode(&hex_str).map_err(|e| format!("Invalid hex: {}", e))
} else if s.starts_with('(') {
let mut result = Vec::new();
let mut chars = s[1..].chars();
loop {
match chars.next() {
None => return Err("Unterminated literal string".to_string()),
Some(')') => break,
Some('\\') => {
match chars.next() {
Some('n') => result.push(b'\n'),
Some('r') => result.push(b'\r'),
Some('t') => result.push(b'\t'),
Some('(') => result.push(b'('),
Some(')') => result.push(b')'),
Some('\\') => result.push(b'\\'),
Some(c) => result.push(c as u8),
None => break,
}
}
Some(c) => result.push(c as u8),
}
}
Ok(result)
} else {
Err(format!("Unknown byte format for {:?}: starts with '{}'", key, &s[..s.len().min(10)]))
}
}
fn find_bytes(data: &[u8], pattern: &[u8]) -> Option<usize> {
data.windows(pattern.len()).position(|w| w == pattern)
}
fn find_bytes_from(data: &[u8], pattern: &[u8], start: usize) -> Option<usize> {
if start >= data.len() { return None; }
data[start..].windows(pattern.len()).position(|w| w == pattern).map(|p| p + start)
}
#[test]
fn test_extract_pdf_hash() {
let test_hash = "$pdf$2*3*128*-4*1*16*733ab0e911f8aa4c77782aa056996f57*32*0000000000000000000000000000000000000000000000000000000000000000*32*0000000000000000000000000000000000000000000000000000000000000000";
let parsed = crate::hashes::raw_pdf::parse_pdf_hash(test_hash).unwrap();
assert_eq!(parsed.v, 2);
assert_eq!(parsed.r, 3);
assert_eq!(parsed.length, 128);
assert_eq!(parsed.p, -4);
assert!(parsed.encrypt_meta);
assert_eq!(parsed.id.len(), 16);
assert_eq!(parsed.u.len(), 32);
assert_eq!(parsed.o.len(), 32);
}