StandardHandlerUsingAesGcmTest.java
/*
This file is part of the iText (R) project.
Copyright (c) 1998-2025 Apryse Group NV
Authors: Apryse Software.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at https://itextpdf.com/sales. For AGPL licensing, see below.
AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itextpdf.kernel.crypto.securityhandler;
import com.itextpdf.bouncycastleconnector.BouncyCastleFactoryCreator;
import com.itextpdf.commons.bouncycastle.IBouncyCastleFactory;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfBoolean;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfEncryption;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfNumber;
import com.itextpdf.kernel.pdf.PdfObject;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfStream;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;
import com.itextpdf.kernel.pdf.VersionConforming;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.kernel.utils.objectpathitems.ObjectPath;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.TestUtil;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.Security;
import java.util.HashMap;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
@Tag("BouncyCastleIntegrationTest")
public class StandardHandlerUsingAesGcmTest extends ExtendedITextTest {
public static final String SRC =
"./src/test/resources/com/itextpdf/kernel/crypto/securityhandler/StandardHandlerUsingAesGcmTest/";
public static final String DEST =
TestUtil.getOutputPath() + "/kernel/crypto/securityhandler/StandardHandlerUsingAesGcmTest/";
private static final IBouncyCastleFactory FACTORY = BouncyCastleFactoryCreator.getFactory();
private static final byte[] OWNER_PASSWORD = "supersecret".getBytes(StandardCharsets.UTF_8);
private static final byte[] USER_PASSWORD = "secret".getBytes(StandardCharsets.UTF_8);
@BeforeAll
public static void setUp() {
createOrClearDestinationFolder(DEST);
Security.addProvider(FACTORY.getProvider());
}
@Test
public void simpleEncryptDecryptTest() throws Exception {
String srcFile = SRC + "simpleDocument.pdf";
String encryptedCmpFile = SRC + "cmp_encryptedSimpleDocument.pdf";
String outFile = DEST + "simpleEncryptDecrypt.pdf";
// Set usage permissions.
int perms = EncryptionConstants.ALLOW_PRINTING | EncryptionConstants.ALLOW_DEGRADED_PRINTING;
WriterProperties wProps = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
.setStandardEncryption(USER_PASSWORD, OWNER_PASSWORD, perms, EncryptionConstants.ENCRYPTION_AES_GCM);
// Instantiate input/output document.
try (PdfDocument docIn = new PdfDocument(new PdfReader(srcFile));
PdfDocument docOut = new PdfDocument(new PdfWriter(outFile, wProps))) {
// Copy one page from input to output.
docIn.copyPagesTo(1, 1, docOut);
}
new CToolNoDeveloperExtension().compareByContent(outFile, srcFile, DEST, "diff", USER_PASSWORD, null);
new CompareTool().compareByContent(outFile, encryptedCmpFile, DEST, "diff", USER_PASSWORD, USER_PASSWORD);
}
@Test
@LogMessages(messages = {@LogMessage(messageTemplate = VersionConforming.NOT_SUPPORTED_AES_GCM)})
public void simpleEncryptDecryptPdf15Test() throws Exception {
String srcFile = SRC + "simpleDocument.pdf";
String outFile = DEST + "notSupportedVersionDocument.pdf";
int perms = EncryptionConstants.ALLOW_PRINTING | EncryptionConstants.ALLOW_DEGRADED_PRINTING;
WriterProperties wProps = new WriterProperties()
.setStandardEncryption(USER_PASSWORD, OWNER_PASSWORD, perms, EncryptionConstants.ENCRYPTION_AES_GCM);
PdfDocument ignored = new PdfDocument(new PdfReader(srcFile), new PdfWriter(outFile, wProps));
ignored.close();
new CToolNoDeveloperExtension().compareByContent(outFile, srcFile, DEST, "diff", USER_PASSWORD, null);
}
@Test
public void knownOutputTest() throws Exception {
String srcFile = SRC + "encryptedDocument.pdf";
String outFile = DEST + "encryptedDocument.pdf";
String cmpFile = SRC + "simpleDocument.pdf";
try (PdfDocument ignored = new PdfDocument(new PdfReader(srcFile,
new ReaderProperties().setPassword(OWNER_PASSWORD)), new PdfWriter(outFile))) {
// We need to copy the source file to the destination folder to be able to compare pdf files in android.
}
new CompareTool().compareByContent(outFile, cmpFile, DEST, "diff", USER_PASSWORD, null);
}
// In all these tampered files, the stream content of object 14 has been modified.
@Test
public void macTamperedTest() throws IOException {
String srcFile = SRC + "encryptedDocumentTamperedMac.pdf";
assertTampered(srcFile);
}
@Test
public void initVectorTamperedTest() throws IOException {
String srcFile = SRC + "encryptedDocumentTamperedIv.pdf";
assertTampered(srcFile);
}
@Test
public void ciphertextTamperedTest() throws IOException {
String srcFile = SRC + "encryptedDocumentTamperedCiphertext.pdf";
assertTampered(srcFile);
}
@Test
@LogMessages(messages = {@LogMessage(messageTemplate = IoLogMessageConstant.ENCRYPTION_ENTRIES_P_AND_ENCRYPT_METADATA_NOT_CORRESPOND_PERMS_ENTRY)})
public void pdfEncryptionWithEmbeddedFilesTest() {
byte[] documentId = new byte[]{(byte)88, (byte)189, (byte)192, (byte)48, (byte)240, (byte)200, (byte)87,
(byte)183, (byte)244, (byte)119, (byte)224, (byte)109, (byte)226, (byte)173, (byte)32, (byte)90};
byte[] password = new byte[]{(byte)115, (byte)101, (byte)99, (byte)114, (byte)101, (byte)116};
HashMap<PdfName, PdfObject> encMap = new HashMap<PdfName, PdfObject>();
encMap.put(PdfName.R, new PdfNumber(7));
encMap.put(PdfName.V, new PdfNumber(6));
encMap.put(PdfName.P, new PdfNumber(-1852));
encMap.put(PdfName.EFF, PdfName.FlateDecode);
encMap.put(PdfName.StmF, PdfName.Identity);
encMap.put(PdfName.StrF, PdfName.Identity);
PdfDictionary embeddedFilesDict = new PdfDictionary();
embeddedFilesDict.put(PdfName.FlateDecode, new PdfDictionary());
PdfDictionary cfmDict = new PdfDictionary();
cfmDict.put(PdfName.CFM, PdfName.AESV4);
embeddedFilesDict.put(PdfName.StdCF, cfmDict);
encMap.put(PdfName.CF, embeddedFilesDict);
encMap.put(PdfName.EncryptMetadata, PdfBoolean.FALSE);
encMap.put(PdfName.O, new PdfString("\u0006����\u009A<@\u009D��G\u0013&\u008C5r\u0096\u0081i!\u0091\u000F����h=��\u0091\u0006A����\u008D\"��\u0018?��\u001DN����{y\u0091)\u0090v����"));
encMap.put(PdfName.U, new PdfString("��Y\u009D��\u0017������\u0097v��\fJ\u0099c\u0004��������B\u0084��9��\u008F\u009D-��xnk��\u0086��\u0088��\u0086��T��������\u0018\u009D\u0016-"));
encMap.put(PdfName.OE, new PdfString("5��\u009EU����\u0007��N����\u0094��\u001D��_wn��\u001AK��-\u007F\u00ADQ������\u001FSJ"));
encMap.put(PdfName.UE, new PdfString("\u000B:\r��\u0004\u0094����k��,��BS9��\u001E��\u0088\u001D(\u0098����\u0010��\u0082.'`k��"));
encMap.put(PdfName.Perms, new PdfString("\u008F��\u0080.����\u0011\u001Et\u0012\u00905\u001B\u0019\u0014��"));
PdfDictionary dictionary = new PdfDictionary(encMap);
PdfEncryption encryption = new PdfEncryption(dictionary, password, documentId);
Assertions.assertTrue(encryption.isEmbeddedFilesOnly());
}
@Test
public void pdfEncryptionWithMetadataTest() {
byte[] documentId = new byte[]{(byte)88, (byte)189, (byte)192, (byte)48, (byte)240, (byte)200, (byte)87,
(byte)183, (byte)244, (byte)119, (byte)224, (byte)109, (byte)226, (byte)173, (byte)32, (byte)90};
byte[] password = new byte[]{(byte)115, (byte)101, (byte)99, (byte)114, (byte)101, (byte)116};
HashMap<PdfName, PdfObject> encMap = new HashMap<PdfName, PdfObject>();
encMap.put(PdfName.R, new PdfNumber(7));
encMap.put(PdfName.V, new PdfNumber(6));
encMap.put(PdfName.P, new PdfNumber(-1852));
encMap.put(PdfName.StmF, PdfName.StdCF);
encMap.put(PdfName.StrF, PdfName.StdCF);
PdfDictionary embeddedFilesDict = new PdfDictionary();
embeddedFilesDict.put(PdfName.FlateDecode, new PdfDictionary());
PdfDictionary cfmDict = new PdfDictionary();
cfmDict.put(PdfName.CFM, PdfName.AESV4);
embeddedFilesDict.put(PdfName.StdCF, cfmDict);
encMap.put(PdfName.CF, embeddedFilesDict);
encMap.put(PdfName.EncryptMetadata, PdfBoolean.TRUE);
encMap.put(PdfName.O, new PdfString("\u0006����\u009A<@\u009D��G\u0013&\u008C5r\u0096\u0081i!\u0091\u000F����h=��\u0091\u0006A����\u008D\"��\u0018?��\u001DN����{y\u0091)\u0090v����"));
encMap.put(PdfName.U, new PdfString("��Y\u009D��\u0017������\u0097v��\fJ\u0099c\u0004��������B\u0084��9��\u008F\u009D-��xnk��\u0086��\u0088��\u0086��T��������\u0018\u009D\u0016-"));
encMap.put(PdfName.OE, new PdfString("5��\u009EU����\u0007��N����\u0094��\u001D��_wn��\u001AK��-\u007F\u00ADQ������\u001FSJ"));
encMap.put(PdfName.UE, new PdfString("\u000B:\r��\u0004\u0094����k��,��BS9��\u001E��\u0088\u001D(\u0098����\u0010��\u0082.'`k��"));
encMap.put(PdfName.Perms, new PdfString("\u008F��\u0080.����\u0011\u001Et\u0012\u00905\u001B\u0019\u0014��"));
PdfDictionary dictionary = new PdfDictionary(encMap);
PdfEncryption encryption = new PdfEncryption(dictionary, password, documentId);
Assertions.assertTrue(encryption.isMetadataEncrypted());
}
@Test
public void encryptPdfWithMissingCFTest() {
byte[] documentId = new byte[]{(byte)88, (byte)189, (byte)192, (byte)48, (byte)240, (byte)200, (byte)87,
(byte)183, (byte)244, (byte)119, (byte)224, (byte)109, (byte)226, (byte)173, (byte)32, (byte)90};
byte[] password = new byte[]{(byte)115, (byte)101, (byte)99, (byte)114, (byte)101, (byte)116};
HashMap<PdfName, PdfObject> encMap = new HashMap<PdfName, PdfObject>();
encMap.put(PdfName.R, new PdfNumber(7));
encMap.put(PdfName.V, new PdfNumber(6));
PdfDictionary dictionary = new PdfDictionary(encMap);
Exception e = Assertions.assertThrows(PdfException.class, () -> new PdfEncryption(dictionary, password, documentId));
Assertions.assertEquals(KernelExceptionMessageConstant.CF_NOT_FOUND_ENCRYPTION, e.getMessage());
}
@Test
public void encryptPdfWithMissingStdCFTest() {
byte[] documentId = new byte[]{(byte)88, (byte)189, (byte)192, (byte)48, (byte)240, (byte)200, (byte)87,
(byte)183, (byte)244, (byte)119, (byte)224, (byte)109, (byte)226, (byte)173, (byte)32, (byte)90};
byte[] password = new byte[]{(byte)115, (byte)101, (byte)99, (byte)114, (byte)101, (byte)116};
HashMap<PdfName, PdfObject> encMap = new HashMap<PdfName, PdfObject>();
encMap.put(PdfName.R, new PdfNumber(7));
encMap.put(PdfName.V, new PdfNumber(6));
PdfDictionary embeddedFilesDict = new PdfDictionary();
embeddedFilesDict.put(PdfName.FlateDecode, new PdfDictionary());
PdfDictionary cfmDict = new PdfDictionary();
cfmDict.put(PdfName.CFM, PdfName.AESV4);
encMap.put(PdfName.CF, embeddedFilesDict);
PdfDictionary dictionary = new PdfDictionary(encMap);
Exception e = Assertions.assertThrows(PdfException.class, () -> new PdfEncryption(dictionary, password, documentId));
Assertions.assertEquals(KernelExceptionMessageConstant.STDCF_NOT_FOUND_ENCRYPTION, e.getMessage());
}
@Test
public void encryptPdfWithMissingCFMTest() {
byte[] documentId = new byte[]{(byte)88, (byte)189, (byte)192, (byte)48, (byte)240, (byte)200, (byte)87,
(byte)183, (byte)244, (byte)119, (byte)224, (byte)109, (byte)226, (byte)173, (byte)32, (byte)90};
byte[] password = new byte[]{(byte)115, (byte)101, (byte)99, (byte)114, (byte)101, (byte)116};
HashMap<PdfName, PdfObject> encMap = new HashMap<PdfName, PdfObject>();
encMap.put(PdfName.R, new PdfNumber(7));
encMap.put(PdfName.V, new PdfNumber(6));
encMap.put(PdfName.P, new PdfNumber(-1852));
encMap.put(PdfName.StmF, PdfName.StdCF);
encMap.put(PdfName.StrF, PdfName.StdCF);
PdfDictionary embeddedFilesDict = new PdfDictionary();
embeddedFilesDict.put(PdfName.FlateDecode, new PdfDictionary());
PdfDictionary cfmDict = new PdfDictionary();
embeddedFilesDict.put(PdfName.StdCF, cfmDict);
encMap.put(PdfName.CF, embeddedFilesDict);
PdfDictionary dictionary = new PdfDictionary(encMap);
Exception e = Assertions.assertThrows(PdfException.class, () -> new PdfEncryption(dictionary, password, documentId));
Assertions.assertEquals(KernelExceptionMessageConstant.NO_COMPATIBLE_ENCRYPTION_FOUND, e.getMessage());
}
private void assertTampered(String outFile) throws IOException {
try (PdfDocument pdfDoc =
new PdfDocument(new PdfReader(outFile, new ReaderProperties().setPassword(USER_PASSWORD)))) {
PdfObject obj = pdfDoc.getPdfObject(14);
if (obj != null && obj.isStream()) {
// Get decoded stream bytes.
Assertions.assertThrows(Exception.class, () -> ((PdfStream) obj).getBytes());
}
}
}
}
// Outside test class for porting
class CToolNoDeveloperExtension extends CompareTool {
@Override
protected boolean compareObjects(PdfObject outObj, PdfObject cmpObj, ObjectPath currentPath, CompareResult compareResult) {
if (outObj != null && outObj.isDictionary()) {
if (((PdfDictionary) outObj).get(PdfName.ISO_) != null) {
return true;
}
}
if (cmpObj != null && cmpObj.isDictionary()) {
if (((PdfDictionary) cmpObj).get(PdfName.ISO_) != null) {
return true;
}
}
return super.compareObjects(outObj, cmpObj, currentPath, compareResult);
}
}