Skip to content

Commit 93a0273

Browse files
author
Eugene Bochilo
committed
Support encrypting embedded files only
DEVSIX-1433
1 parent 0310e5e commit 93a0273

18 files changed

+347
-7
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.itextpdf.kernel.pdf;
2+
3+
import java.io.Serializable;
4+
import java.util.HashSet;
5+
import java.util.Set;
6+
7+
class EncryptedEmbeddedStreamsHandler implements Serializable {
8+
9+
private static final long serialVersionUID = -4542644924377740467L;
10+
11+
private final PdfDocument document;
12+
13+
private final Set<PdfStream> embeddedStreams = new HashSet<>();
14+
15+
/**
16+
* Creates {@link EncryptedEmbeddedStreamsHandler} instance.
17+
*
18+
* @param document {@link PdfDocument} associated with this handler
19+
*/
20+
EncryptedEmbeddedStreamsHandler(PdfDocument document) {
21+
this.document = document;
22+
}
23+
24+
/**
25+
* Stores all embedded streams present in the {@link PdfDocument}.
26+
* Note that during this method we traverse through every indirect object of the document.
27+
*/
28+
void storeAllEmbeddedStreams() {
29+
for (int i = 0; i < document.getNumberOfPdfObjects(); ++i) {
30+
PdfObject indirectObject = document.getPdfObject(i);
31+
if (indirectObject instanceof PdfDictionary) {
32+
PdfStream embeddedStream = getEmbeddedFileStreamFromDictionary((PdfDictionary) indirectObject);
33+
if (embeddedStream != null) {
34+
storeEmbeddedStream(embeddedStream);
35+
}
36+
}
37+
}
38+
}
39+
40+
void storeEmbeddedStream(PdfStream embeddedStream) {
41+
embeddedStreams.add(embeddedStream);
42+
}
43+
44+
/**
45+
* Checks, whether this {@link PdfStream} was stored as embedded stream.
46+
*
47+
* @param stream to be checked
48+
* @return true if this stream is embedded, false otherwise
49+
*/
50+
boolean isStreamStoredAsEmbedded(PdfStream stream) {
51+
return embeddedStreams.contains(stream);
52+
}
53+
54+
private static PdfStream getEmbeddedFileStreamFromDictionary(PdfDictionary dictionary) {
55+
PdfDictionary embeddedFileDictionary = dictionary.getAsDictionary(PdfName.EF);
56+
if (PdfName.Filespec.equals(dictionary.getAsName(PdfName.Type)) && embeddedFileDictionary != null) {
57+
return embeddedFileDictionary.getAsStream(PdfName.F);
58+
}
59+
return null;
60+
}
61+
}

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,8 @@ public class PdfDocument implements IEventDispatcher, Closeable, Serializable {
230230
*/
231231
MemoryLimitsAwareHandler memoryLimitsAwareHandler = null;
232232

233+
private EncryptedEmbeddedStreamsHandler encryptedEmbeddedStreamsHandler;
234+
233235
/**
234236
* Open PDF document in reading mode.
235237
*
@@ -460,6 +462,17 @@ public PdfPage getLastPage() {
460462
return getPage(getNumberOfPages());
461463
}
462464

465+
/**
466+
* Marks {@link PdfStream} object as embedded file stream. Note that this method is for internal usage.
467+
* To add an embedded file to the PDF document please use specialized API for file attachments.
468+
* (e.g. {@link PdfDocument#addFileAttachment(String, PdfFileSpec)}, {@link PdfPage#addAnnotation(PdfAnnotation)})
469+
*
470+
* @param stream to be marked as embedded file stream
471+
*/
472+
public void markStreamAsEmbeddedFile(PdfStream stream) {
473+
encryptedEmbeddedStreamsHandler.storeEmbeddedStream(stream);
474+
}
475+
463476
/**
464477
* Creates and adds new page to the end of document.
465478
*
@@ -1913,9 +1926,11 @@ protected void flushObject(PdfObject pdfObject, boolean canBeInObjStm) throws IO
19131926
*/
19141927
protected void open(PdfVersion newPdfVersion) {
19151928
this.fingerPrint = new FingerPrint();
1929+
this.encryptedEmbeddedStreamsHandler = new EncryptedEmbeddedStreamsHandler(this);
19161930

19171931
try {
19181932
EventCounterHandler.getInstance().onEvent(CoreEvent.PROCESS, properties.metaInfo, getClass());
1933+
boolean embeddedStreamsSavedOnReading = false;
19191934
if (reader != null) {
19201935
if (reader.pdfDocument != null) {
19211936
throw new PdfException(PdfException.PdfReaderHasBeenAlreadyUtilized);
@@ -1926,6 +1941,10 @@ protected void open(PdfVersion newPdfVersion) {
19261941
memoryLimitsAwareHandler = new MemoryLimitsAwareHandler(reader.tokens.getSafeFile().length());
19271942
}
19281943
reader.readPdf();
1944+
if (reader.decrypt != null && reader.decrypt.isEmbeddedFilesOnly()) {
1945+
encryptedEmbeddedStreamsHandler.storeAllEmbeddedStreams();
1946+
embeddedStreamsSavedOnReading = true;
1947+
}
19291948
for (ICounter counter : getCounters()) {
19301949
counter.onDocumentRead(reader.getFileLength());
19311950
}
@@ -2053,6 +2072,9 @@ protected void open(PdfVersion newPdfVersion) {
20532072
writer.initCryptoIfSpecified(pdfVersion);
20542073
}
20552074
if (writer.crypto != null) {
2075+
if (!embeddedStreamsSavedOnReading && writer.crypto.isEmbeddedFilesOnly()) {
2076+
encryptedEmbeddedStreamsHandler.storeAllEmbeddedStreams();
2077+
}
20562078
if (writer.crypto.getCryptoMode() < EncryptionConstants.ENCRYPTION_AES_256) {
20572079
VersionConforming.validatePdfVersionForDeprecatedFeatureLogWarn(this, PdfVersion.PDF_2_0, VersionConforming.DEPRECATED_ENCRYPTION_ALGORITHMS);
20582080
} else if (writer.crypto.getCryptoMode() == EncryptionConstants.ENCRYPTION_AES_256) {
@@ -2195,6 +2217,10 @@ long getDocumentId() {
21952217
return documentId;
21962218
}
21972219

2220+
boolean doesStreamBelongToEmbeddedFile(PdfStream stream) {
2221+
return encryptedEmbeddedStreamsHandler.isStreamStoredAsEmbedded(stream);
2222+
}
2223+
21982224
/**
21992225
* Gets iText version info.
22002226
*

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,6 @@ private int setCryptoMode(int mode, int length) {
484484
revision = STANDARD_ENCRYPTION_40;
485485
break;
486486
case EncryptionConstants.STANDARD_ENCRYPTION_128:
487-
embeddedFilesOnly = false;
488487
if (length > 0) {
489488
setKeyLength(length);
490489
} else {
@@ -514,6 +513,7 @@ private int readAndSetCryptoModeForStdHandler(PdfDictionary encDict) {
514513
if (rValue == null)
515514
throw new PdfException(PdfException.IllegalRValue);
516515
int revision = rValue.intValue();
516+
boolean embeddedFilesOnlyMode = readEmbeddedFilesOnlyFromEncryptDictionary(encDict);
517517
switch (revision) {
518518
case 2:
519519
cryptoMode = EncryptionConstants.STANDARD_ENCRYPTION_40;
@@ -545,6 +545,9 @@ private int readAndSetCryptoModeForStdHandler(PdfDictionary encDict) {
545545
if (em != null && !em.getValue()) {
546546
cryptoMode |= EncryptionConstants.DO_NOT_ENCRYPT_METADATA;
547547
}
548+
if (embeddedFilesOnlyMode) {
549+
cryptoMode |= EncryptionConstants.EMBEDDED_FILES_ONLY;
550+
}
548551
break;
549552
case 5:
550553
case 6:
@@ -553,6 +556,9 @@ private int readAndSetCryptoModeForStdHandler(PdfDictionary encDict) {
553556
if (em5 != null && !em5.getValue()) {
554557
cryptoMode |= EncryptionConstants.DO_NOT_ENCRYPT_METADATA;
555558
}
559+
if (embeddedFilesOnlyMode) {
560+
cryptoMode |= EncryptionConstants.EMBEDDED_FILES_ONLY;
561+
}
556562
break;
557563
default:
558564
throw new PdfException(PdfException.UnknownEncryptionTypeREq1).setMessageParams(rValue);
@@ -570,6 +576,7 @@ private int readAndSetCryptoModeForPubSecHandler(PdfDictionary encDict) {
570576
if (vValue == null)
571577
throw new PdfException(PdfException.IllegalVValue);
572578
int v = vValue.intValue();
579+
boolean embeddedFilesOnlyMode = readEmbeddedFilesOnlyFromEncryptDictionary(encDict);
573580
switch (v) {
574581
case 1:
575582
cryptoMode = EncryptionConstants.STANDARD_ENCRYPTION_40;
@@ -608,13 +615,33 @@ private int readAndSetCryptoModeForPubSecHandler(PdfDictionary encDict) {
608615
if (em != null && !em.getValue()) {
609616
cryptoMode |= EncryptionConstants.DO_NOT_ENCRYPT_METADATA;
610617
}
618+
if (embeddedFilesOnlyMode) {
619+
cryptoMode |= EncryptionConstants.EMBEDDED_FILES_ONLY;
620+
}
611621
break;
612622
default:
613623
throw new PdfException(PdfException.UnknownEncryptionTypeVEq1, vValue);
614624
}
615625
return setCryptoMode(cryptoMode, length);
616626
}
617627

628+
static boolean readEmbeddedFilesOnlyFromEncryptDictionary(PdfDictionary encDict) {
629+
PdfName embeddedFilesFilter = encDict.getAsName(PdfName.EFF);
630+
boolean encryptEmbeddedFiles = !PdfName.Identity.equals(embeddedFilesFilter) && embeddedFilesFilter != null;
631+
boolean encryptStreams = !PdfName.Identity.equals(encDict.getAsName(PdfName.StmF));
632+
boolean encryptStrings = !PdfName.Identity.equals(encDict.getAsName(PdfName.StrF));
633+
if (encryptStreams || encryptStrings || !encryptEmbeddedFiles) {
634+
return false;
635+
}
636+
637+
PdfDictionary cfDictionary = encDict.getAsDictionary(PdfName.CF);
638+
if (cfDictionary != null) {
639+
// Here we check if the crypt filter for embedded files and the filter in the CF dictionary are the same
640+
return cfDictionary.getAsDictionary(embeddedFilesFilter) != null;
641+
}
642+
return false;
643+
}
644+
618645
private int fixAccessibilityPermissionPdf20(int permissions) {
619646
// This bit was previously used to determine whether
620647
// content could be extracted for the purposes of accessibility,

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,8 @@ private void write(PdfStream pdfStream) {
298298
java.io.OutputStream fout = this;
299299
DeflaterOutputStream def = null;
300300
OutputStreamEncryption ose = null;
301-
if (crypto != null && !crypto.isEmbeddedFilesOnly()) {
301+
if (crypto != null &&
302+
(!crypto.isEmbeddedFilesOnly() || document.doesStreamBelongToEmbeddedFile(pdfStream))) {
302303
fout = ose = crypto.getEncryptionStream(fout);
303304
}
304305
if (toCompress && (allowCompression || userDefinedCompression)) {
@@ -390,7 +391,7 @@ private void write(PdfStream pdfStream) {
390391
}
391392

392393
protected boolean checkEncryption(PdfStream pdfStream) {
393-
if (crypto == null || crypto.isEmbeddedFilesOnly()) {
394+
if (crypto == null || (crypto.isEmbeddedFilesOnly() && !document.doesStreamBelongToEmbeddedFile(pdfStream))) {
394395
return false;
395396
} else if (isXRefStream(pdfStream)) {
396397
// The cross-reference stream shall not be encrypted

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -390,7 +390,8 @@ public byte[] readStreamBytesRaw(PdfStream stream) throws IOException {
390390
file.seek(stream.getOffset());
391391
bytes = new byte[length];
392392
file.readFully(bytes);
393-
if (decrypt != null && !decrypt.isEmbeddedFilesOnly()) {
393+
boolean embeddedStream = pdfDocument.doesStreamBelongToEmbeddedFile(stream);
394+
if (decrypt != null && (!decrypt.isEmbeddedFilesOnly() || embeddedStream)) {
394395
PdfObject filter = stream.get(PdfName.Filter, true);
395396
boolean skip = false;
396397
if (filter != null) {

kernel/src/main/java/com/itextpdf/kernel/pdf/filespec/PdfFileSpec.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ private static PdfFileSpec createEmbeddedFileSpec(PdfDocument doc, PdfStream str
387387
ef.put(PdfName.F, stream);
388388
ef.put(PdfName.UF, stream);
389389
dict.put(PdfName.EF, ef);
390+
doc.markStreamAsEmbeddedFile(stream);
390391

391392
return (PdfFileSpec) new PdfFileSpec(dict).makeIndirect(doc);
392393
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -421,9 +421,9 @@ public void encryptWithPasswordAes128EmbeddedFilesOnly() throws IOException {
421421
document.close();
422422

423423

424-
//NOTE: Specific crypto filters for EFF StmF and StrF are not supported at the moment. iText don't distinguish objects based on their semantic role
425-
// because of this we can't read streams correctly and corrupt such documents on stamping.
426-
boolean ERROR_IS_EXPECTED = true;
424+
//TODO DEVSIX-5355 Specific crypto filters for EFF StmF and StrF are not supported at the moment.
425+
// However we can read embedded files only mode.
426+
boolean ERROR_IS_EXPECTED = false;
427427
checkDecryptedWithPasswordContent(destinationFolder + filename, OWNER, textContent, ERROR_IS_EXPECTED);
428428
checkDecryptedWithPasswordContent(destinationFolder + filename, USER, textContent, ERROR_IS_EXPECTED);
429429
}

0 commit comments

Comments
 (0)