mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 21:48:09 -06:00
fix(crypt): derive a new file id from the encryption seed
/ID is SHA-256 of the seed under the label id, not an unkeyed DefaultHasher digest. The salts test is a characterization; failing closed when randomness is missing stays the regression guard. Co-authored-by: jonathanwxh-cell <jonathanwxh-cell@users.noreply.github.com>
This commit is contained in:
co-authored by
jonathanwxh-cell
parent
c05f00c67a
commit
0a6d0f3689
+33
-14
@@ -792,8 +792,7 @@ impl Document {
|
||||
let id0 = match self.trailer.get(b"ID") {
|
||||
Some(Object::Array(a)) if a.len() == 2 => a[0].as_string().map(|s| s.bytes.clone()).unwrap_or_default(),
|
||||
_ => {
|
||||
let mut id = generated_id(¶ms.seed);
|
||||
id.truncate(16);
|
||||
let id = pdfcraft_crypt::file_id(¶ms.seed);
|
||||
let s = Object::String(crate::PdfString { bytes: id.clone(), hex: true });
|
||||
self.trailer.set(b"ID".to_vec(), Object::Array(vec![s.clone(), s]));
|
||||
id
|
||||
@@ -2191,18 +2190,6 @@ fn rfind(hay: &[u8], needle: &[u8], from: usize) -> Option<usize> {
|
||||
(from..=hay.len() - needle.len()).rev().find(|&i| &hay[i..i + needle.len()] == needle)
|
||||
}
|
||||
|
||||
/// A file identifier derived from entropy supplied by the caller.
|
||||
fn generated_id(seed: &[u8; 32]) -> Vec<u8> {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut out = Vec::new();
|
||||
for i in 0..2u8 {
|
||||
let mut h = std::collections::hash_map::DefaultHasher::new();
|
||||
(seed, i, b"pdfcraft id").hash(&mut h);
|
||||
out.extend_from_slice(&h.finish().to_be_bytes());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The object number a classic xref subsection really starts at. A common producer bug heads the
|
||||
/// table `1 n` and then writes object 0's free entry (`0000000000 65535 f`) as its first row, so
|
||||
/// every row would be read one number too high and object 1 as free. Such a subsection is read
|
||||
@@ -2250,6 +2237,38 @@ const MAX_XREF_OBJECTS: usize = 8_388_607;
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn a_missing_file_id_is_derived_from_the_encryption_seed() {
|
||||
let mut doc = Document::new_empty();
|
||||
let seed = [9u8; 32];
|
||||
doc.set_encryption(&pdfcraft_crypt::NewEncryption {
|
||||
algorithm: pdfcraft_crypt::Algorithm::Aes256,
|
||||
user_password: "user",
|
||||
owner_password: "owner",
|
||||
permissions: -4,
|
||||
encrypt_metadata: true,
|
||||
seed,
|
||||
})
|
||||
.unwrap();
|
||||
let id = doc.trailer().get(b"ID").and_then(|o| o.as_array()).and_then(|a| a.first()).and_then(|o| o.as_string()).map(|s| s.bytes.clone());
|
||||
assert_eq!(id.as_deref(), Some(pdfcraft_crypt::file_id(&seed).as_slice()));
|
||||
|
||||
let mut kept = Document::new_empty();
|
||||
let fixed = Object::String(crate::PdfString { bytes: b"0123456789abcdef".to_vec(), hex: true });
|
||||
kept.trailer_mut().set(b"ID".to_vec(), Object::Array(vec![fixed.clone(), fixed]));
|
||||
kept.set_encryption(&pdfcraft_crypt::NewEncryption {
|
||||
algorithm: pdfcraft_crypt::Algorithm::Aes256,
|
||||
user_password: "user",
|
||||
owner_password: "owner",
|
||||
permissions: -4,
|
||||
encrypt_metadata: true,
|
||||
seed,
|
||||
})
|
||||
.unwrap();
|
||||
let still = kept.trailer().get(b"ID").and_then(|o| o.as_array()).and_then(|a| a.first()).and_then(|o| o.as_string()).map(|s| s.bytes.clone());
|
||||
assert_eq!(still.as_deref(), Some(b"0123456789abcdef".as_slice()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_read_state_matches_a_clone_and_no_changed_document() {
|
||||
let mut doc = Document::new_empty();
|
||||
|
||||
@@ -602,12 +602,20 @@ fn expand(seed: &[u8; 32], label: &[u8], n: usize) -> Vec<u8> {
|
||||
h.update(label);
|
||||
h.update(counter.to_le_bytes());
|
||||
out.extend_from_slice(&h.finalize());
|
||||
counter += 1;
|
||||
counter = counter.saturating_add(1);
|
||||
}
|
||||
out.truncate(n);
|
||||
out
|
||||
}
|
||||
|
||||
/// Sixteen bytes of file identifier from `seed` (SHA-256 in counter mode, label `id`).
|
||||
///
|
||||
/// `/ID` is written in the clear. Deriving it the same way as the file key keeps that public
|
||||
/// value from being an unkeyed hash of the seed.
|
||||
pub fn file_id(seed: &[u8; 32]) -> Vec<u8> {
|
||||
expand(seed, b"id", 16)
|
||||
}
|
||||
|
||||
/// Build the `/Encrypt` values and an authenticated (owner) handler for a new encryption.
|
||||
pub fn create(params: &NewEncryption, id0: &[u8]) -> Result<SecurityHandler, CryptError> {
|
||||
// Bits 7–8 and 13–32 must be 1; bits 1–2 must be 0 (§7.6.4.2).
|
||||
|
||||
@@ -4,6 +4,16 @@ fn hex(s: &str) -> Vec<u8> {
|
||||
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn file_id_is_a_one_way_16_byte_identifier() {
|
||||
let seed = [7u8; 32];
|
||||
let id = file_id(&seed);
|
||||
assert_eq!(id.len(), 16);
|
||||
assert_eq!(id, file_id(&seed));
|
||||
assert_ne!(id, seed[..16]);
|
||||
assert_ne!(id, file_id(&[8u8; 32]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rc4_reference_vectors() {
|
||||
assert_eq!(rc4(b"Key", b"Plaintext"), hex("bbf316e8d940af0ad3"));
|
||||
|
||||
@@ -1886,6 +1886,12 @@ fn passwords_that_encryption_would_make_empty_or_equal_are_refused() {
|
||||
|
||||
#[test]
|
||||
fn fresh_sessions_protecting_identical_documents_produce_different_salts() {
|
||||
// Characterization only: two sessions' salts differ. A per-call RandomState already did
|
||||
// that before Protect drew from the platform CSPRNG, so this does not prove the source.
|
||||
// `protect_fails_closed_when_randomness_is_unavailable` is the guard. It does not compile
|
||||
// against a Protect with no CSPRNG hook, and it fails closed when that hook errors or
|
||||
// returns zeros. Comparing the stored function with `getrandom::fill` is not reliable:
|
||||
// one function can have different addresses in different codegen units.
|
||||
let raw = Arc::new(fixture(1));
|
||||
let encrypt_bytes = |saved: Arc<Vec<u8>>, key: &[u8]| -> Vec<u8> {
|
||||
let doc = pdfcraft_cos::Document::open_with_password(saved, Some("secret")).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user