From fecb3ea832972630ebba2d4703be91a59e791fa6 Mon Sep 17 00:00:00 2001 From: Matt Zimmerman Date: Tue, 18 Aug 2026 12:26:35 -0700 Subject: [PATCH] fix: https://github.com/ledongthuc/pdf/issues/82 Implement step (g) of PDF 32000-1:2008 Algorithm 2: when R>=4 and /EncryptMetadata is false, append 0xFFFFFFFF to the MD5 hash before deriving the encryption key. Without this, AES-128 PDFs with cleartext metadata (common from e-signature tools) fail with "invalid password" even when the actual password is empty. Co-Authored-By: Claude Sonnet 5 --- read.go | 5 +++++ read_test.go | 25 +++++++++++++++++++++++++ testdata/encrypted_metadata_false.pdf | Bin 0 -> 1100 bytes 3 files changed, 30 insertions(+) create mode 100644 testdata/encrypted_metadata_false.pdf diff --git a/read.go b/read.go index 5cadd7c5..31148b3e 100644 --- a/read.go +++ b/read.go @@ -982,6 +982,11 @@ func (r *Reader) initEncrypt(password string) error { h.Write([]byte(O)) h.Write([]byte{byte(P), byte(P >> 8), byte(P >> 16), byte(P >> 24)}) h.Write([]byte(ID)) + if R >= 4 { + if encryptMetadata, ok := encrypt["EncryptMetadata"].(bool); ok && !encryptMetadata { + h.Write([]byte{0xff, 0xff, 0xff, 0xff}) + } + } key := h.Sum(nil) if R >= 3 { diff --git a/read_test.go b/read_test.go index edea184e..32966764 100644 --- a/read_test.go +++ b/read_test.go @@ -2,6 +2,8 @@ package pdf import ( "bytes" + "context" + "io" "strings" "testing" ) @@ -151,3 +153,26 @@ func TestNewReaderMaliciousPDF(t *testing.T) { t.Fatal("expected error from malicious PDF, got nil") } } + +// Regression test for https://github.com/ledongthuc/pdf/issues/82: +// AES-128 encrypted PDFs (V=4, R=4) with /EncryptMetadata false and an empty +// user password must open successfully instead of returning ErrInvalidPassword. +func TestOpenEncryptedMetadataFalse(t *testing.T) { + f, r, err := Open("testdata/encrypted_metadata_false.pdf") + if err != nil { + t.Fatalf("Open: %v", err) + } + defer f.Close() + + b, err := r.GetPlainText(context.Background()) + if err != nil { + t.Fatalf("GetPlainText: %v", err) + } + text, err := io.ReadAll(b) + if err != nil { + t.Fatalf("ReadAll: %v", err) + } + if !strings.Contains(string(text), "hello world") { + t.Fatalf("unexpected text: %q", text) + } +} diff --git a/testdata/encrypted_metadata_false.pdf b/testdata/encrypted_metadata_false.pdf new file mode 100644 index 0000000000000000000000000000000000000000..1431736cd734ae6fdc05bb6b2a2ebce0d974e703 GIT binary patch literal 1100 zcmZuwJ!=#}7;db#5K&MBi?>N7$^%`MATcB*&eJXh?KsXEr&x?8d#h5VZ?} zos|kH6g*JDT5K&NR{jQS3mXwEf-`$}xe%OXcV?fN_hX*-c_$ik^+`O%y@@Y>`Uf5c z1i5_RRVv^&#JcW6V38*Hw;pY2@Mnb)oqQdtRZnNi9dz3xgW0^78NlG*Xsa%)04YsZ zN5%txV^g<1HqDIAOc&@N+`Lw8F_Z6DFUURtLV)dSeKka35027m>TcdE5}T*XxSrdc za)E)Bj;d9g>(K;`r0znE;@{NSy4ipTStsp|(S@Bm!st1jd%~B z?cbm3|A?>dzrMJ7Wv_nr{nOo3cU~;K`PSck`}yPAlZ9u`-<|sK^Y$kFbz%S9y}RV{ z_N6s?<3!It`2GF-aql0B$Qd2GxrKjLM!UUJehYPg#2nT^`EW6__>lf^Ba*~-@E!Jn?RmILsIWMJ;N zbM)1+q9c@T5 kg@jZJ;V7l5YP)Wz0C2i$4%$H!req~Tp*Jy6TdaHk0Ee?PXaE2J literal 0 HcmV?d00001