Skip to content

Commit 17a5c49

Browse files
authored
Cover encryption param parsing (crypt)
encryptParamsFromDict and initEncrypt were only exercised for the V2/R3 RC4 case. Add coverage at the Open() surface: - a V4/R4 fixture using a /CF StdCF crypt filter (CFM /V2), decrypting a stream end-to-end through the /CF + /StmF parsing path; - rejection of a non-Standard security handler; - a malformed-/Encrypt sweep (wrong type, missing /V and /R, short O/U, short V5 OE/UE/Perms) that must error, never panic. Factor the shared PDF assembly out of buildRC4EncryptedStreamPDF so the V2 and V4 builders reuse it. initEncrypt 78.9% -> 89.5%; encryptParamsFromDict 65.9% -> 95.1%.
1 parent 87f8b9a commit 17a5c49

1 file changed

Lines changed: 113 additions & 17 deletions

File tree

crypt_test.go

Lines changed: 113 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,10 @@ func rc4Crypt(key, data []byte) []byte {
129129
return out
130130
}
131131

132-
// buildRC4EncryptedStreamPDF builds a V2/R3 RC4-encrypted PDF (empty password)
133-
// whose object 4 is a stream carrying RC4-encrypted plaintext.
134-
func buildRC4EncryptedStreamPDF(t *testing.T, plaintext []byte) []byte {
135-
t.Helper()
136-
owner := bytes.Repeat([]byte{0x5a}, 32)
137-
id0 := bytes.Repeat([]byte{0x7c}, 16)
138-
const bits = 128
139-
var p int32 = -44
140-
fileKey := emptyPwRC4Key(owner, id0, p, bits)
141-
u := emptyPwU(fileKey, id0)
142-
enc := rc4Crypt(objKeyRC4(fileKey, 4, 0), plaintext)
143-
132+
// assembleEncryptedPDF builds a classical-xref PDF — catalog (1), pages (2),
133+
// the given /Encrypt dict body (3), and an already-encrypted stream (4) — with
134+
// the trailer wired to /Encrypt 3 0 R and /ID [id0 id0].
135+
func assembleEncryptedPDF(encryptBody string, encStream, id0 []byte) []byte {
144136
var buf bytes.Buffer
145137
off := func() int { return buf.Len() }
146138
fmt.Fprint(&buf, "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n")
@@ -150,12 +142,10 @@ func buildRC4EncryptedStreamPDF(t *testing.T, plaintext []byte) []byte {
150142
offsets[2] = off()
151143
fmt.Fprint(&buf, "2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n")
152144
offsets[3] = off()
153-
fmt.Fprintf(&buf,
154-
"3 0 obj\n<< /Filter /Standard /V 2 /R 3 /Length %d /O <%s> /U <%s> /P %d >>\nendobj\n",
155-
bits, hex.EncodeToString(owner), hex.EncodeToString(u), p)
145+
fmt.Fprintf(&buf, "3 0 obj\n%s\nendobj\n", encryptBody)
156146
offsets[4] = off()
157-
fmt.Fprintf(&buf, "4 0 obj\n<< /Length %d >>\nstream\n", len(enc))
158-
buf.Write(enc)
147+
fmt.Fprintf(&buf, "4 0 obj\n<< /Length %d >>\nstream\n", len(encStream))
148+
buf.Write(encStream)
159149
fmt.Fprint(&buf, "\nendstream\nendobj\n")
160150

161151
xrefOff := off()
@@ -171,6 +161,40 @@ func buildRC4EncryptedStreamPDF(t *testing.T, plaintext []byte) []byte {
171161
return buf.Bytes()
172162
}
173163

164+
// buildRC4EncryptedStreamPDF builds a V2/R3 RC4-encrypted PDF (empty password)
165+
// whose object 4 is a stream carrying RC4-encrypted plaintext.
166+
func buildRC4EncryptedStreamPDF(t *testing.T, plaintext []byte) []byte {
167+
t.Helper()
168+
owner := bytes.Repeat([]byte{0x5a}, 32)
169+
id0 := bytes.Repeat([]byte{0x7c}, 16)
170+
const bits = 128
171+
var p int32 = -44
172+
fileKey := emptyPwRC4Key(owner, id0, p, bits)
173+
u := emptyPwU(fileKey, id0)
174+
enc := rc4Crypt(objKeyRC4(fileKey, 4, 0), plaintext)
175+
body := fmt.Sprintf("<< /Filter /Standard /V 2 /R 3 /Length %d /O <%s> /U <%s> /P %d >>",
176+
bits, hex.EncodeToString(owner), hex.EncodeToString(u), p)
177+
return assembleEncryptedPDF(body, enc, id0)
178+
}
179+
180+
// buildV4RC4EncryptedStreamPDF builds a V4/R4 PDF whose StdCF crypt filter uses
181+
// CFM /V2 (RC4) — same empty-password key derivation as V2/R3, reached through
182+
// the V4 /CF + /StmF parsing path.
183+
func buildV4RC4EncryptedStreamPDF(t *testing.T, plaintext []byte) []byte {
184+
t.Helper()
185+
owner := bytes.Repeat([]byte{0x5a}, 32)
186+
id0 := bytes.Repeat([]byte{0x7c}, 16)
187+
const bits = 128
188+
var p int32 = -44
189+
fileKey := emptyPwRC4Key(owner, id0, p, bits)
190+
u := emptyPwU(fileKey, id0)
191+
enc := rc4Crypt(objKeyRC4(fileKey, 4, 0), plaintext)
192+
body := fmt.Sprintf("<< /Filter /Standard /V 4 /R 4 /Length %d /O <%s> /U <%s> /P %d "+
193+
"/CF << /StdCF << /CFM /V2 /Length 16 >> >> /StmF /StdCF /StrF /StdCF /EncryptMetadata true >>",
194+
bits, hex.EncodeToString(owner), hex.EncodeToString(u), p)
195+
return assembleEncryptedPDF(body, enc, id0)
196+
}
197+
174198
// Open must accept an RC4-encrypted PDF secured with the empty user password
175199
// and decrypt its stream content end-to-end.
176200
func TestOpenDecryptsRC4Stream(t *testing.T) {
@@ -189,3 +213,75 @@ func TestOpenDecryptsRC4Stream(t *testing.T) {
189213
t.Fatalf("decrypted stream mismatch:\n got %q\nwant %q", got, plaintext)
190214
}
191215
}
216+
217+
// The V4 path resolves the stream cipher through /CF + /StmF rather than /V
218+
// directly, so it must be exercised end-to-end too.
219+
func TestOpenDecryptsV4RC4Stream(t *testing.T) {
220+
plaintext := []byte("BT (V4 crypt filter) Tj ET")
221+
data := buildV4RC4EncryptedStreamPDF(t, plaintext)
222+
r, err := Open(bytes.NewReader(data))
223+
if err != nil {
224+
t.Fatalf("Open: %v", err)
225+
}
226+
defer r.Close()
227+
got, err := r.DecodeStream(Reference{Number: 4, Generation: 0})
228+
if err != nil {
229+
t.Fatalf("DecodeStream: %v", err)
230+
}
231+
if !bytes.Equal(got, plaintext) {
232+
t.Fatalf("V4 decrypted stream mismatch:\n got %q\nwant %q", got, plaintext)
233+
}
234+
}
235+
236+
// buildPDFWithEncryptObj wraps an arbitrary /Encrypt dict body as object 3 of a
237+
// classical-xref PDF, with the trailer pointing /Encrypt at it.
238+
func buildPDFWithEncryptObj(t *testing.T, encryptBody string) []byte {
239+
t.Helper()
240+
var buf bytes.Buffer
241+
off := func() int { return buf.Len() }
242+
fmt.Fprint(&buf, "%PDF-1.7\n%\xE2\xE3\xCF\xD3\n")
243+
offsets := make([]int, 4)
244+
offsets[1] = off()
245+
fmt.Fprint(&buf, "1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")
246+
offsets[2] = off()
247+
fmt.Fprint(&buf, "2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n")
248+
offsets[3] = off()
249+
fmt.Fprintf(&buf, "3 0 obj\n%s\nendobj\n", encryptBody)
250+
xrefOff := off()
251+
fmt.Fprint(&buf, "xref\n0 4\n")
252+
fmt.Fprintf(&buf, "%010d %05d f \n", 0, 65535)
253+
for i := 1; i <= 3; i++ {
254+
fmt.Fprintf(&buf, "%010d %05d n \n", offsets[i], 0)
255+
}
256+
id := "<00112233445566778899aabbccddeeff>"
257+
fmt.Fprintf(&buf, "trailer\n<< /Size 4 /Root 1 0 R /Encrypt 3 0 R /ID [%s %s] >>\n", id, id)
258+
fmt.Fprintf(&buf, "startxref\n%d\n%%%%EOF\n", xrefOff)
259+
return buf.Bytes()
260+
}
261+
262+
// Only the /Standard security handler is supported; any other /Filter must be
263+
// rejected rather than silently treated as unencrypted.
264+
func TestEncryptNonStandardFilterRejected(t *testing.T) {
265+
data := buildPDFWithEncryptObj(t, "<< /Filter /FooSecurity /V 2 /R 3 /Length 128 >>")
266+
if _, err := Open(bytes.NewReader(data)); err == nil {
267+
t.Fatal("expected an error for a non-Standard /Filter, got nil")
268+
}
269+
}
270+
271+
// Malformed /Encrypt dictionaries (wrong type, missing fields, short V5 entries)
272+
// must surface as errors during Open, never panics.
273+
func TestEncryptMalformedNoPanic(t *testing.T) {
274+
cases := map[string]string{
275+
"encrypt not a dict": "42",
276+
"missing V and R": "<< /Filter /Standard >>",
277+
"short O and U": "<< /Filter /Standard /V 2 /R 3 /Length 128 /O <00> /U <00> /P 0 >>",
278+
"v5 short entries": "<< /Filter /Standard /V 5 /R 6 /Length 256 /O <00> /U <00> /OE <00> /UE <00> /Perms <00> /P 0 >>",
279+
}
280+
for name, body := range cases {
281+
t.Run(name, func(t *testing.T) {
282+
if _, err := Open(bytes.NewReader(buildPDFWithEncryptObj(t, body))); err == nil {
283+
t.Fatal("expected an error, got nil")
284+
}
285+
})
286+
}
287+
}

0 commit comments

Comments
 (0)