diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 63dcce44b8..dd5c4aafa0 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -5104,7 +5104,9 @@ impl Session { if opts.date.is_empty() { opts.date = self.signing_date(); } - Ok(Arc::new(pdfcraft_sign::sign(&editor.cos, id, &opts)?)) + // A typed Password-field value is not signed into the file (#3926), as it isn't saved. + let cos = writable(&editor.cos)?; + Ok(Arc::new(pdfcraft_sign::sign(&cos, id, &opts)?)) } /// [`Session::sign`], embedding an RFC 3161 signature timestamp (PAdES B-T) produced by @@ -5121,7 +5123,8 @@ impl Session { if opts.date.is_empty() { opts.date = self.signing_date(); } - let signed = Arc::new(pdfcraft_sign::sign_with_timestamp(&editor.cos, id, &opts, tsa)?); + let cos = writable(&editor.cos)?; + let signed = Arc::new(pdfcraft_sign::sign_with_timestamp(&cos, id, &opts, tsa)?); // Checked like a save before anyone writes it (#2166). self.check_save(doc, &signed)?; Ok(signed) @@ -5133,7 +5136,8 @@ impl Session { let d = self.get(doc).ok_or(EditError::NoDocument)?; let editor = d.editor.as_ref().ok_or_else(|| EditError::ReadOnly(d.read_only_reason.clone().unwrap_or_default()))?; let date = if date.is_empty() { self.signing_date() } else { date }; - let stamped = Arc::new(pdfcraft_sign::timestamp_document(&editor.cos, tsa, &date)?); + let cos = writable(&editor.cos)?; + let stamped = Arc::new(pdfcraft_sign::timestamp_document(&cos, tsa, &date)?); self.check_save(doc, &stamped)?; Ok(stamped) } @@ -5143,7 +5147,8 @@ impl Session { pub fn embed_ltv(&self, doc: DocId, evidence: &pdfcraft_sign::dss::Evidence) -> Result>, EditError> { let d = self.get(doc).ok_or(EditError::NoDocument)?; let editor = d.editor.as_ref().ok_or_else(|| EditError::ReadOnly(d.read_only_reason.clone().unwrap_or_default()))?; - let embedded = Arc::new(pdfcraft_sign::dss::embed(&editor.cos, evidence)?); + let cos = writable(&editor.cos)?; + let embedded = Arc::new(pdfcraft_sign::dss::embed(&cos, evidence)?); self.check_save(doc, &embedded)?; Ok(embedded) } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index df8039fc29..e8016fb8c6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2081,6 +2081,23 @@ fn password_field_values_are_kept_in_memory_but_written_to_no_file() { assert!(t.get(reopened).unwrap().form.iter().find(|f| f.name == "name").unwrap().value.iter().all(|v| v.is_empty())); } +/// #3926 / #3943 review: signing writes the signed file from the same copy a save does, so a +/// typed Password-field value is not signed into the file either. +#[test] +fn a_signed_file_carries_no_password_field_value() { + let p12 = std::fs::read(concat!(env!("CARGO_MANIFEST_DIR"), "/../sign/tests/data/ec-p256.p12")).unwrap(); + let digital_id = sign::pkcs12::open(&p12, "test").unwrap(); + let fixture = String::from_utf8(form_fixture()).unwrap().replace("/T (name)", "/T (name) /Ff 8192"); + // After the test certificate's start date (2026-10-02). + let mut s = Session::new().with_clock(|| 1_800_000_000); + let id = s.open("form.pdf", None, Arc::new(fixture.into_bytes()), None).unwrap(); + s.apply(id, Edit::SetFieldValue { name: "name".into(), value: FieldValue::Text("hunter2".into()) }).unwrap(); + assert!(appears_in_pdf(&s.get(id).unwrap().bytes, "hunter2"), "the working bytes hold it"); + let signed = s.sign(id, &digital_id, SignOptions { rect: None, ..SignOptions::default() }).unwrap(); + assert!(!appears_in_pdf(&signed, "hunter2"), "the signed file"); + assert!(appears_in_pdf(&signed, "*******"), "the masked appearance is signed in"); +} + /// A value its field's font can't draw is kept, and the edit that drew it reports it in /// `edit_notices` (the interface shows it, tools return it): not a later unrelated edit, not a /// failed one, and not a value that can be drawn. The form (an inline AcroForm here) asks other