Skip to content

Commit 8095630

Browse files
committed
Fix embedding of encrypted attachments
DEVSIX-9183
1 parent a1afcb7 commit 8095630

File tree

5 files changed

+249
-8
lines changed

5 files changed

+249
-8
lines changed

kernel/src/main/java/com/itextpdf/kernel/pdf/PdfName.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ public class PdfName extends PdfPrimitiveObject implements Comparable<PdfName> {
232232
public static final PdfName CreatorInfo = createDirectName("CreatorInfo");
233233
public static final PdfName CropBox = createDirectName("CropBox");
234234
public static final PdfName Crypt = createDirectName("Crypt");
235+
public static final PdfName CryptFilterDecodeParms = createDirectName("CryptFilterDecodeParms");
235236
public static final PdfName CS = createDirectName("CS");
236237
public static final PdfName CT = createDirectName("CT");
237238
public static final PdfName D = createDirectName("D");

kernel/src/main/java/com/itextpdf/kernel/pdf/PdfOutputStream.java

Lines changed: 67 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -271,17 +271,25 @@ private void write(PdfStream pdfStream) {
271271
java.io.OutputStream fout = this;
272272
DeflaterOutputStream def = null;
273273
OutputStreamEncryption ose = null;
274+
275+
long beginStreamContent;
274276
if (crypto != null &&
275277
(!crypto.isEmbeddedFilesOnly() || document.doesStreamBelongToEmbeddedFile(pdfStream))) {
278+
updateCryptFilterForEmbeddedFilesOnlyMode(pdfStream);
279+
280+
// We should store current position here because crypto.getEncryptionStream(fout) may already
281+
// output something into the stream (iv vector for AES256)
282+
beginStreamContent = writePdfStreamAndGetPosition(pdfStream);
276283
fout = ose = crypto.getEncryptionStream(fout);
277-
}
278-
if (toCompress && (allowCompression || userDefinedCompression)) {
284+
} else if (toCompress && (allowCompression || userDefinedCompression)) {
279285
updateCompressionFilter(pdfStream);
286+
287+
beginStreamContent = writePdfStreamAndGetPosition(pdfStream);
280288
fout = def = new DeflaterOutputStream(fout, pdfStream.getCompressionLevel(), 0x8000);
289+
} else {
290+
beginStreamContent = writePdfStreamAndGetPosition(pdfStream);
281291
}
282-
this.write((PdfDictionary) pdfStream);
283-
writeBytes(PdfOutputStream.stream);
284-
long beginStreamContent = getCurrentPos();
292+
285293
byte[] buf = new byte[4192];
286294
while (true) {
287295
int n = pdfStream.getInputStream().read(buf);
@@ -342,6 +350,8 @@ private void write(PdfStream pdfStream) {
342350
}
343351
}
344352
if (checkEncryption(pdfStream)) {
353+
updateCryptFilterForEmbeddedFilesOnlyMode(pdfStream);
354+
345355
ByteArrayOutputStream encodedStream = new ByteArrayOutputStream();
346356
OutputStreamEncryption ose = crypto.getEncryptionStream(encodedStream);
347357
byteArrayStream.writeTo(ose);
@@ -424,7 +434,7 @@ protected void updateCompressionFilter(PdfStream pdfStream) {
424434
if (filter == null) {
425435
// Remove if any
426436
pdfStream.remove(PdfName.DecodeParms);
427-
437+
428438
pdfStream.put(PdfName.Filter, PdfName.FlateDecode);
429439
return;
430440
}
@@ -449,7 +459,50 @@ protected void updateCompressionFilter(PdfStream pdfStream) {
449459
.setMessageParams(decodeParms.getClass().toString());
450460
}
451461
}
452-
pdfStream.put(PdfName.Filter, filters);
462+
}
463+
464+
/**
465+
* Adds required Filter and DecodeParms to the pdf stream if the stream is embedded file stream
466+
* and only embedded files are expected to be encrypted. See {@link EncryptionConstants#EMBEDDED_FILES_ONLY}.
467+
*
468+
* @param pdfStream embedded file pdf stream.
469+
*/
470+
protected void updateCryptFilterForEmbeddedFilesOnlyMode(PdfStream pdfStream) {
471+
if (crypto != null && crypto.isEmbeddedFilesOnly() &&
472+
document.doesStreamBelongToEmbeddedFile(pdfStream) &&
473+
// This code works only for AES256.
474+
// All tests we currently have for earlier versions do not work with and without this code.
475+
crypto.getEncryptionAlgorithm() >= EncryptionConstants.ENCRYPTION_AES_256) {
476+
// Filter
477+
PdfObject currentFilters = pdfStream.get(PdfName.Filter);
478+
PdfArray filters = new PdfArray();
479+
filters.add(PdfName.Crypt);
480+
if (currentFilters instanceof PdfArray) {
481+
filters.addAll((PdfArray) currentFilters);
482+
} else if (currentFilters != null) {
483+
filters.add(currentFilters);
484+
}
485+
pdfStream.put(PdfName.Filter, filters);
486+
487+
// DecodeParms
488+
PdfDictionary crypt = new PdfDictionary();
489+
crypt.put(PdfName.Name, PdfName.StdCF);
490+
crypt.put(PdfName.Type, PdfName.CryptFilterDecodeParms);
491+
PdfObject decodeParms = pdfStream.get(PdfName.DecodeParms);
492+
if (decodeParms instanceof PdfDictionary || decodeParms == null) {
493+
PdfArray array = new PdfArray();
494+
array.add(crypt);
495+
if (decodeParms != null) {
496+
array.add(decodeParms);
497+
}
498+
pdfStream.put(PdfName.DecodeParms, array);
499+
} else if (decodeParms instanceof PdfArray) {
500+
((PdfArray) decodeParms).add(0, crypt);
501+
} else {
502+
throw new PdfException(KernelExceptionMessageConstant.THIS_DECODE_PARAMETER_TYPE_IS_NOT_SUPPORTED)
503+
.setMessageParams(decodeParms.getClass().toString());
504+
}
505+
}
453506
}
454507

455508
protected byte[] decodeFlateBytes(PdfStream stream, byte[] bytes) {
@@ -551,6 +604,13 @@ protected byte[] decodeFlateBytes(PdfStream stream, byte[] bytes) {
551604
return bytes;
552605
}
553606

607+
private long writePdfStreamAndGetPosition(PdfStream pdfStream) {
608+
write((PdfDictionary) pdfStream);
609+
writeBytes(PdfOutputStream.stream);
610+
611+
return getCurrentPos();
612+
}
613+
554614
private static boolean isFlushed(PdfDictionary dict, PdfName name) {
555615
PdfObject obj = dict.get(name);
556616
return obj != null && obj.isFlushed();

kernel/src/test/java/com/itextpdf/kernel/crypto/pdfencryption/PdfEncryptionTest.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,7 @@ public void encryptWithPasswordAes128EmbeddedFilesOnly() throws IOException {
391391
}
392392

393393
@Test
394-
public void encryptWithPasswordAes256EmbeddedFilesOnly() throws IOException, InterruptedException {
394+
public void encryptWithPasswordAes256EmbeddedFilesOnly() throws IOException {
395395
String filename = "encryptWithPasswordAes256EmbeddedFilesOnly.pdf";
396396
int encryptionType = EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.EMBEDDED_FILES_ONLY;
397397

@@ -424,6 +424,39 @@ public void encryptWithPasswordAes256EmbeddedFilesOnly() throws IOException, Int
424424
textContent, ERROR_IS_EXPECTED);
425425
}
426426

427+
@Test
428+
public void encryptWithPasswordAes256EmbeddedFilesOnly2() throws IOException {
429+
String filename = "encryptWithPasswordAes256EmbeddedFilesOnly2.pdf";
430+
int encryptionType = EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.EMBEDDED_FILES_ONLY;
431+
432+
String outFileName = destinationFolder + filename;
433+
int permissions = EncryptionConstants.ALLOW_SCREENREADERS;
434+
PdfWriter writer = new PdfWriter(outFileName,
435+
new WriterProperties().setStandardEncryption(PdfEncryptionTestUtils.USER, PdfEncryptionTestUtils.OWNER, permissions,
436+
encryptionType).addXmpMetadata().setPdfVersion(PdfVersion.PDF_2_0)
437+
);
438+
PdfDocument document = new PdfDocument(writer);
439+
document.getDocumentInfo().setMoreInfo(PdfEncryptionTestUtils.CUSTOM_INFO_ENTRY_KEY, PdfEncryptionTestUtils.CUSTOM_INFO_ENTRY_VALUE);
440+
PdfPage page = document.addNewPage();
441+
String textContent = "Hello world!";
442+
PdfEncryptionTestUtils.writeTextBytesOnPageContent(page, textContent);
443+
444+
String descripton = "encryptedFile";
445+
document.addFileAttachment(descripton,
446+
PdfFileSpec.createEmbeddedFileSpec(document, "TEST".getBytes(StandardCharsets.UTF_8), descripton, "test.txt", null, null));
447+
448+
page.flush();
449+
document.close();
450+
451+
//TODO DEVSIX-5355 Specific crypto filters for EFF StmF and StrF are not supported at the moment.
452+
// However we can read embedded files only mode.
453+
boolean ERROR_IS_EXPECTED = false;
454+
encryptionUtil.checkDecryptedWithPasswordContent(destinationFolder + filename, PdfEncryptionTestUtils.OWNER,
455+
textContent, ERROR_IS_EXPECTED);
456+
encryptionUtil.checkDecryptedWithPasswordContent(destinationFolder + filename, PdfEncryptionTestUtils.USER,
457+
textContent, ERROR_IS_EXPECTED);
458+
}
459+
427460
@Test
428461
public void encryptAes256Pdf2NotEncryptMetadata() throws InterruptedException, IOException {
429462
String filename = "encryptAes256Pdf2NotEncryptMetadata.pdf";

kernel/src/test/java/com/itextpdf/kernel/pdf/EncryptedEmbeddedStreamsHandlerTest.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public static void afterClass() {
6060
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
6161
ignore = true))
6262
public void noReaderStandardEncryptionAddFileAttachment() throws IOException, InterruptedException {
63+
// Test result here is wrong. Note that createEncryptedDocument adds EncryptionConstants.EMBEDDED_FILES_ONLY.
6364
String outFileName = destinationFolder + "noReaderStandardEncryptionAddFileAttachment.pdf";
6465
String cmpFileName = sourceFolder + "cmp_noReaderStandardEncryptionAddFileAttachment.pdf";
6566

@@ -78,6 +79,7 @@ public void noReaderStandardEncryptionAddFileAttachment() throws IOException, In
7879
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
7980
ignore = true))
8081
public void noReaderAesEncryptionAddFileAttachment() throws IOException, InterruptedException {
82+
// Test result here is wrong. Note that createEncryptedDocument adds EncryptionConstants.EMBEDDED_FILES_ONLY.
8183
String outFileName = destinationFolder + "noReaderAesEncryptionAddFileAttachment.pdf";
8284
String cmpFileName = sourceFolder + "cmp_noReaderAesEncryptionAddFileAttachment.pdf";
8385

@@ -117,6 +119,7 @@ public void withReaderStandardEncryptionAddFileAttachment() throws IOException,
117119
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
118120
ignore = true))
119121
public void noReaderStandardEncryptionAddAnnotation() throws IOException, InterruptedException {
122+
// Test result here is wrong. Note that createEncryptedDocument adds EncryptionConstants.EMBEDDED_FILES_ONLY.
120123
String outFileName = destinationFolder + "noReaderStandardEncryptionAddAnnotation.pdf";
121124
String cmpFileName = sourceFolder + "cmp_noReaderStandardEncryptionAddAnnotation.pdf";
122125

@@ -156,6 +159,7 @@ public void withReaderStandardEncryptionAddAnnotation() throws IOException, Inte
156159
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
157160
ignore = true))
158161
public void readerWithoutEncryptionWriterStandardEncryption() throws IOException, InterruptedException {
162+
// Test result here is wrong. Note that createEncryptedDocument adds EncryptionConstants.EMBEDDED_FILES_ONLY.
159163
String outFileName = destinationFolder + "readerWithoutEncryptionWriterStandardEncryption.pdf";
160164
String cmpFileName = sourceFolder + "cmp_readerWithoutEncryptionWriterStandardEncryption.pdf";
161165

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
This file is part of the iText (R) project.
3+
Copyright (c) 1998-2025 Apryse Group NV
4+
Authors: Apryse Software.
5+
6+
This program is offered under a commercial and under the AGPL license.
7+
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
8+
9+
AGPL licensing:
10+
This program is free software: you can redistribute it and/or modify
11+
it under the terms of the GNU Affero General Public License as published by
12+
the Free Software Foundation, either version 3 of the License, or
13+
(at your option) any later version.
14+
15+
This program is distributed in the hope that it will be useful,
16+
but WITHOUT ANY WARRANTY; without even the implied warranty of
17+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18+
GNU Affero General Public License for more details.
19+
20+
You should have received a copy of the GNU Affero General Public License
21+
along with this program. If not, see <https://www.gnu.org/licenses/>.
22+
*/
23+
package com.itextpdf.kernel.pdf;
24+
25+
import com.itextpdf.commons.utils.MessageFormatUtil;
26+
import com.itextpdf.kernel.crypto.pdfencryption.PdfEncryptionTestUtils;
27+
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
28+
import com.itextpdf.kernel.exceptions.PdfException;
29+
import com.itextpdf.kernel.pdf.filespec.PdfFileSpec;
30+
import com.itextpdf.kernel.utils.CompareTool;
31+
import com.itextpdf.test.AssertUtil;
32+
import com.itextpdf.test.ExtendedITextTest;
33+
import com.itextpdf.test.TestUtil;
34+
import org.junit.jupiter.api.Assertions;
35+
import org.junit.jupiter.api.BeforeAll;
36+
import org.junit.jupiter.api.Tag;
37+
import org.junit.jupiter.api.Test;
38+
39+
import java.io.ByteArrayOutputStream;
40+
import java.io.IOException;
41+
import java.nio.charset.StandardCharsets;
42+
43+
@Tag("IntegrationTest")
44+
public class PdfOutputStreamTest extends ExtendedITextTest {
45+
46+
public static final String DESTINATION_FOLDER = TestUtil.getOutputPath() + "/kernel/pdf/PdfOutputStreamTest/";
47+
48+
@BeforeAll
49+
public static void beforeClass() {
50+
createOrClearDestinationFolder(DESTINATION_FOLDER);
51+
}
52+
53+
@Test
54+
public void invalidDecodeParamsTest() {
55+
PdfWriter writer = new PdfWriter(new ByteArrayOutputStream(),
56+
new WriterProperties().setStandardEncryption(PdfEncryptionTestUtils.USER, PdfEncryptionTestUtils.OWNER, 0,
57+
EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.EMBEDDED_FILES_ONLY));
58+
PdfDocument document = new CustomPdfDocument1(writer);
59+
60+
document.addFileAttachment("descripton",
61+
PdfFileSpec.createEmbeddedFileSpec(document, "TEST".getBytes(StandardCharsets.UTF_8), "descripton", "test.txt", null, null));
62+
63+
Exception e = Assertions.assertThrows(PdfException.class, () -> document.close());
64+
Assertions.assertEquals(MessageFormatUtil.format(
65+
KernelExceptionMessageConstant.THIS_DECODE_PARAMETER_TYPE_IS_NOT_SUPPORTED,
66+
PdfName.class),
67+
e.getMessage());
68+
}
69+
70+
@Test
71+
public void arrayDecodeParamsTest() throws IOException {
72+
final String fileName = "arrayDecodeParamsTest.pdf";
73+
PdfWriter writer = CompareTool.createTestPdfWriter(DESTINATION_FOLDER + fileName,
74+
new WriterProperties().setStandardEncryption(PdfEncryptionTestUtils.USER, PdfEncryptionTestUtils.OWNER, 0,
75+
EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.EMBEDDED_FILES_ONLY));
76+
PdfDocument document = new CustomPdfDocument2(writer);
77+
78+
document.addFileAttachment("descripton",
79+
PdfFileSpec.createEmbeddedFileSpec(document, "TEST".getBytes(StandardCharsets.UTF_8), "descripton", "test.txt", null, null));
80+
81+
AssertUtil.doesNotThrow(() -> document.close());
82+
}
83+
84+
@Test
85+
public void dictDecodeParamsTest() throws IOException {
86+
final String fileName = "dictDecodeParamsTest.pdf";
87+
PdfWriter writer = CompareTool.createTestPdfWriter(DESTINATION_FOLDER + fileName,
88+
new WriterProperties().setStandardEncryption(PdfEncryptionTestUtils.USER, PdfEncryptionTestUtils.OWNER, 0,
89+
EncryptionConstants.ENCRYPTION_AES_256 | EncryptionConstants.EMBEDDED_FILES_ONLY));
90+
PdfDocument document = new CustomPdfDocument3(writer);
91+
92+
document.addFileAttachment("descripton",
93+
PdfFileSpec.createEmbeddedFileSpec(document, "TEST".getBytes(StandardCharsets.UTF_8), "descripton", "test.txt", null, null));
94+
95+
AssertUtil.doesNotThrow(() -> document.close());
96+
}
97+
98+
private static final class CustomPdfDocument1 extends PdfDocument {
99+
CustomPdfDocument1(PdfWriter writer) {
100+
super(writer);
101+
}
102+
103+
@Override
104+
public void markStreamAsEmbeddedFile(PdfStream stream) {
105+
stream.put(PdfName.DecodeParms, PdfName.Crypt);
106+
stream.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
107+
108+
super.markStreamAsEmbeddedFile(stream);
109+
}
110+
}
111+
112+
private static final class CustomPdfDocument2 extends PdfDocument {
113+
CustomPdfDocument2(PdfWriter writer) {
114+
super(writer);
115+
}
116+
117+
@Override
118+
public void markStreamAsEmbeddedFile(PdfStream stream) {
119+
PdfArray decodeParmsValue = new PdfArray();
120+
decodeParmsValue.add(new PdfNull());
121+
stream.put(PdfName.DecodeParms, decodeParmsValue);
122+
stream.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
123+
124+
super.markStreamAsEmbeddedFile(stream);
125+
}
126+
}
127+
128+
private static final class CustomPdfDocument3 extends PdfDocument {
129+
CustomPdfDocument3(PdfWriter writer) {
130+
super(writer);
131+
}
132+
133+
@Override
134+
public void markStreamAsEmbeddedFile(PdfStream stream) {
135+
PdfDictionary decodeParmsValue = new PdfDictionary();
136+
decodeParmsValue.put(PdfName.Name, new PdfNull());
137+
stream.put(PdfName.DecodeParms, decodeParmsValue);
138+
stream.setCompressionLevel(CompressionConstants.NO_COMPRESSION);
139+
140+
super.markStreamAsEmbeddedFile(stream);
141+
}
142+
}
143+
}

0 commit comments

Comments
 (0)