Skip to content

Commit

Permalink
Align /ID entry creation with 14.4 File Identifiers (#876)
Browse files Browse the repository at this point in the history
Co-authored-by: Andreas Rosdal <41650711+andreasrosdal@users.noreply.github.com>
  • Loading branch information
bsanchezb and andreasrosdal committed Nov 1, 2023
1 parent b9a3ea1 commit 1d5325f
Show file tree
Hide file tree
Showing 8 changed files with 274 additions and 28 deletions.
78 changes: 72 additions & 6 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfEncryption.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,22 @@
package com.lowagie.text.pdf;

import com.lowagie.text.ExceptionConverter;
import com.lowagie.text.error_messages.MessageLocalization;
import com.lowagie.text.pdf.crypto.ARCFOUREncryption;
import com.lowagie.text.pdf.crypto.IVGenerator;
import com.lowagie.text.error_messages.MessageLocalization;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.cert.Certificate;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;



/**
Expand Down Expand Up @@ -490,6 +489,73 @@ public static PdfObject createInfoId(byte[] idPartOne, byte[] idPartTwo) {
}
}

/**
* This method returns a changing part of the {@code fileId} when can be identified.
* Returns a complete {@code fileId} of the changing part is not found.
*
* @param fileId {@link PdfObject}
* @return byte array representing the changing part of the document identifier
*/
public static byte[] getFileIdChangingPart(PdfObject fileId) {
byte[] bytes = fileId.getBytes();
boolean firstPartFound = false;
int start = 0;
int end = bytes.length;
for (int i = 0; i < bytes.length; i++) {
if (bytes[i] == '<') {
if (firstPartFound) {
start = i + 1;
}
firstPartFound = true;
}
else if (start > 0 && bytes[i] == '>') {
end = i;
break;
}
}
if (firstPartFound && start > 0) {
byte[] secondPartValue = new byte[end - start];
System.arraycopy(bytes, start, secondPartValue, 0, end - start);
if (isHexEncoded(secondPartValue)) {
return decodeHex(secondPartValue);
}
else {
return secondPartValue;
}

}
else {
// otherwise return provided value
return bytes;
}
}

private static boolean isHexEncoded(byte[] str) {
if (str.length == 0 || str.length % 2 != 0) {
return false;
}
for (int c : str) {
if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
return false;
}
}
return true;
}

private static byte[] decodeHex(byte[] hexEncoded) {
try (ByteBuffer byteBuffer = new ByteBuffer(hexEncoded.length / 2)) {
for (int i = 0; i < hexEncoded.length; i += 2) {
int firstDigit = Character.digit(hexEncoded[i], 16);
int secondDigit = Character.digit(hexEncoded[i + 1], 16);
byteBuffer.append((byte) ((firstDigit << 4) + secondDigit));
}
return byteBuffer.toByteArray();

} catch (IOException e) {
throw new ExceptionConverter(e);
}
}

public PdfDictionary getEncryptionDictionary() {
PdfDictionary dic = new PdfDictionary();

Expand Down
18 changes: 18 additions & 0 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -4094,4 +4094,22 @@ public byte[] computeUserPassword() {
return null;
return decrypt.computeUserPassword(password);
}

/**
* Returns a permanent document identifier extracted from trailer /ID entry, when present
*
* @return byte array representing the document permanent identifier
*/
public byte[] getDocumentId() {
if (trailer == null) {
return null;
}
PdfArray documentIDs = trailer.getAsArray(PdfName.ID);
if (documentIDs == null || documentIDs.size() == 0) {
return null;
}
PdfObject o = documentIDs.getPdfObject(0);
return com.lowagie.text.DocWriter.getISOBytes(o.toString());
}

}
31 changes: 19 additions & 12 deletions openpdf/src/main/java/com/lowagie/text/pdf/PdfStamperImp.java
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ else if (!producer.contains(Document.getProduct())) {
// empty on purpose
}
}

PdfIndirectReference encryption = null;
PdfObject fileID = null;
if (crypto != null) {
Expand All @@ -340,24 +341,30 @@ else if (!producer.contains(Document.getProduct())) {
encryption = encryptionObject.getIndirectReference();
}
if (includeFileID) {
byte[] fileIDPartOne = crypto.documentID;
byte[] fileIDPartTwo;
if (overrideFileId != null) {
fileIDPartTwo = overrideFileId.getBytes();
} else {
fileIDPartTwo = PdfEncryption.createDocumentId();
}
fileID = PdfEncryption.createInfoId(fileIDPartOne, fileIDPartTwo);
byte[] fileIDPartTwo = overrideFileId != null ?
PdfEncryption.getFileIdChangingPart(overrideFileId) : PdfEncryption.createDocumentId();
fileID = PdfEncryption.createInfoId(crypto.documentID, fileIDPartTwo);
}
}
else if (includeFileID) {
byte[] documentId = reader.getDocumentId();
if (overrideFileId != null) {
fileID = overrideFileId;
} else {
fileID = PdfEncryption.createInfoId(PdfEncryption.createDocumentId());
if (documentId != null) {
fileID = PdfEncryption.createInfoId(documentId, PdfEncryption.getFileIdChangingPart(overrideFileId));
}
else {
fileID = overrideFileId;
}
}
else if (documentId != null) {
fileID = PdfEncryption.createInfoId(documentId);
}
else {
byte[] fileIDPart = PdfEncryption.createDocumentId();
fileID = PdfEncryption.createInfoId(fileIDPart, fileIDPart);
}

}

PRIndirectReference iRoot = (PRIndirectReference) reader.trailer.get(PdfName.ROOT);
PdfIndirectReference root = new PdfIndirectReference(0, getNewObjectNumber(reader, iRoot.getNumber(), 0));

Expand Down
4 changes: 3 additions & 1 deletion openpdf/src/main/java/com/lowagie/text/pdf/PdfWriter.java
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,9 @@ public void close() {
} else if (getInfo().contains(PdfName.FILEID)) {
fileID = getInfo().get(PdfName.FILEID);
} else {
fileID = PdfEncryption.createInfoId(PdfEncryption.createDocumentId());
// the same documentId shall be provided to the first version
byte[] documentId = PdfEncryption.createDocumentId();
fileID = PdfEncryption.createInfoId(documentId, documentId);
}

// write the cross-reference table of the body
Expand Down
37 changes: 37 additions & 0 deletions openpdf/src/test/java/com/lowagie/text/pdf/PdfEncryptionTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.lowagie.text.pdf;

import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;

public class PdfEncryptionTest {

@Test
public void getFileIdChangingPartTest() {
// HEX encoded, valid format
assertArrayEquals(Hex.decode("c547234acb212b7cd65ba961f9e9acae"),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("<c547234acb212b7cd65ba961f9e9acae><c547234acb212b7cd65ba961f9e9acae>")));
assertArrayEquals(Hex.decode("1527b6788ff5fb71d8c0bd6642fcbe47"),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("<c547234acb212b7cd65ba961f9e9acae><1527b6788ff5fb71d8c0bd6642fcbe47>")));
assertArrayEquals(Hex.decode("c547234acb212b7cd65ba961f9e9acae"),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("[<c547234acb212b7cd65ba961f9e9acae><c547234acb212b7cd65ba961f9e9acae>]")));
assertArrayEquals(Hex.decode("1527b6788ff5fb71d8c0bd6642fcbe47"),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("[<c547234acb212b7cd65ba961f9e9acae><1527b6788ff5fb71d8c0bd6642fcbe47>]")));

// not-HEX encoded, valid format
assertArrayEquals("abc".getBytes(),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("<abc><abc>")));
assertArrayEquals("def".getBytes(),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("<abc><def>")));

// invalid format
assertArrayEquals("c547234acb212b7cd65ba961f9e9acae".getBytes(),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("c547234acb212b7cd65ba961f9e9acae")));
assertArrayEquals("<c547234acb212b7cd65ba961f9e9acae>1527b6788ff5fb71d8c0bd6642fcbe47".getBytes(),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("<c547234acb212b7cd65ba961f9e9acae>1527b6788ff5fb71d8c0bd6642fcbe47")));
assertArrayEquals("c547234acb212b7cd65ba961f9e9acae".getBytes(),
PdfEncryption.getFileIdChangingPart(new PdfLiteral("c547234acb212b7cd65ba961f9e9acae")));
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.lowagie.text.pdf;

import com.lowagie.text.DocWriter;
import com.lowagie.text.Utilities;
import org.junit.jupiter.api.Test;

Expand All @@ -14,6 +15,7 @@
import java.util.Map;

import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;

Expand All @@ -27,11 +29,16 @@ void signPasswordProtected() throws Exception {
byte[] expectedDigestPreClose = null;
byte[] expectedDigestClose = null;

byte[] originalDocId = null;
byte[] changingId = null;

// Sign and compare the generated range
for (int i = 0; i < 10; i++) {
try (InputStream is = getClass().getResourceAsStream("/open_protected.pdf");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfReader reader = new PdfReader(is, new byte[]{' '})) {
originalDocId = reader.getDocumentId();

PdfStamper stp = PdfStamper.createSignature(reader, baos, '\0', null, true);
stp.setEnforcedModificationDate(signDate);

Expand Down Expand Up @@ -79,6 +86,20 @@ void signPasswordProtected() throws Exception {
try (InputStream is = new ByteArrayInputStream(documentBytes);
PdfReader reader = new PdfReader(is, new byte[]{' '})) {
assertNotNull(reader);

byte[] documentId = reader.getDocumentId();
assertNotNull(documentId);
assertArrayEquals(originalDocId, documentId);

PdfArray idArray = reader.getTrailer().getAsArray(PdfName.ID);
assertEquals(2, idArray.size());
assertArrayEquals(documentId, com.lowagie.text.DocWriter.getISOBytes(idArray.getPdfObject(0).toString()));

byte[] currentChangingId = DocWriter.getISOBytes(idArray.getPdfObject(1).toString());
if (changingId != null) {
assertFalse(Arrays.equals(changingId, currentChangingId));
}
changingId = currentChangingId;
}
}

Expand All @@ -89,6 +110,7 @@ void signPasswordProtectedOverrideFileId() throws Exception {
Calendar signDate = Calendar.getInstance();

// override with custom FileID to ensure deterministic behaviour
byte[] originalDocId = null;
PdfObject overrideFileId = new PdfLiteral("<123><123>".getBytes());

byte[] documentBytes;
Expand All @@ -100,6 +122,8 @@ void signPasswordProtectedOverrideFileId() throws Exception {
try (InputStream is = getClass().getResourceAsStream("/open_protected.pdf");
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PdfReader reader = new PdfReader(is, new byte[]{' '})) {
originalDocId = reader.getDocumentId();

PdfStamper stp = PdfStamper.createSignature(reader, baos, '\0', null, true);
stp.setEnforcedModificationDate(signDate);
stp.setOverrideFileId(overrideFileId);
Expand Down Expand Up @@ -148,6 +172,15 @@ void signPasswordProtectedOverrideFileId() throws Exception {
try (InputStream is = new ByteArrayInputStream(documentBytes);
PdfReader reader = new PdfReader(is, new byte[]{' '})) {
assertNotNull(reader);

byte[] documentId = reader.getDocumentId();
assertNotNull(documentId);
assertArrayEquals(originalDocId, documentId);

PdfArray idArray = reader.getTrailer().getAsArray(PdfName.ID);
assertEquals(2, idArray.size());
assertArrayEquals(documentId, com.lowagie.text.DocWriter.getISOBytes(idArray.getPdfObject(0).toString()));
assertEquals("123", idArray.getPdfObject(1).toString());
}
}

Expand Down
Loading

0 comments on commit 1d5325f

Please sign in to comment.