PdfEncryptionManuallyPortedTest.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;
import com.itextpdf.bouncycastleconnector.BouncyCastleFactoryCreator;
import com.itextpdf.commons.bouncycastle.IBouncyCastleFactory;
import com.itextpdf.commons.bouncycastle.crypto.fips.AbstractFipsUnapprovedOperationError;
import com.itextpdf.commons.bouncycastle.operator.AbstractOperatorCreationException;
import com.itextpdf.commons.bouncycastle.pkcs.AbstractPKCSException;
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.font.PdfFontFactory;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.pdf.CompressionConstants;
import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfString;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.kernel.utils.PemFileHelper;
import com.itextpdf.test.ExtendedITextTest;
import com.itextpdf.test.ITextTest;
import com.itextpdf.test.TestUtil;
import com.itextpdf.test.annotations.LogMessage;
import com.itextpdf.test.annotations.LogMessages;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.Tag;
/**
* Due to import control restrictions by the governments of a few countries,
* the encryption libraries shipped by default with the Java SDK restrict the
* length, and as a result the strength, of encryption keys. Be aware that in
* this test by using {@link ITextTest#removeCryptographyRestrictions()} we
* remove cryptography restrictions via reflection for testing purposes.
* <br/>
* For more conventional way of solving this problem you need to replace the
* default security JARs in your Java installation with the Java Cryptography
* Extension (JCE) Unlimited Strength Jurisdiction Policy Files. These JARs
* are available for download from http://java.oracle.com/ in eligible countries.
*/
@Tag("BouncyCastleIntegrationTest")
public class PdfEncryptionManuallyPortedTest extends ExtendedITextTest {
private static final IBouncyCastleFactory FACTORY = BouncyCastleFactoryCreator.getFactory();
public static final String destinationFolder = TestUtil.getOutputPath() + "/kernel/crypto/PdfEncryptionManuallyPortedTest/";
public static final String sourceFolder = "./src/test/resources/com/itextpdf/kernel/crypto/PdfEncryptionManuallyPortedTest/";
public static final char[] PRIVATE_KEY_PASS = "testpassphrase".toCharArray();
// There is also test.pfx to add to Acrobat to be able to open result pdf files. Password for it is also
// testpassphrase
public static final String CERT = sourceFolder + "test.cer";
public static final String PRIVATE_KEY = sourceFolder + "test.pem";
static final String pageTextContent = "Hello world!";
// Custom entry in Info dictionary is used because standard entried are gone into metadata in PDF 2.0
static final String customInfoEntryKey = "Custom";
static final String customInfoEntryValue = "String";
private PrivateKey privateKey;
@BeforeAll
public static void beforeClass() {
createOrClearDestinationFolder(destinationFolder);
Security.addProvider(FACTORY.getProvider());
}
@AfterAll
public static void afterClass() {
CompareTool.cleanup(destinationFolder);
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateStandard128() throws IOException, InterruptedException, GeneralSecurityException,
AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateStandard128.pdf";
int encryptionType = EncryptionConstants.STANDARD_ENCRYPTION_128;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateStandard40() throws IOException, InterruptedException, GeneralSecurityException,
AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateStandard40.pdf";
int encryptionType = EncryptionConstants.STANDARD_ENCRYPTION_40;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateStandard128NoCompression() throws IOException, InterruptedException,
GeneralSecurityException, AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateStandard128NoCompression.pdf";
int encryptionType = EncryptionConstants.STANDARD_ENCRYPTION_128;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateStandard40NoCompression() throws IOException, InterruptedException,
GeneralSecurityException, AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateStandard40NoCompression.pdf";
int encryptionType = EncryptionConstants.STANDARD_ENCRYPTION_40;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateAes128() throws IOException, InterruptedException, GeneralSecurityException,
AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateAes128.pdf";
int encryptionType = EncryptionConstants.ENCRYPTION_AES_128;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateAes256() throws IOException, InterruptedException, GeneralSecurityException,
AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateAes256.pdf";
int encryptionType = EncryptionConstants.ENCRYPTION_AES_256;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.DEFAULT_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateAes128NoCompression() throws IOException, InterruptedException,
GeneralSecurityException, AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateAes128NoCompression.pdf";
int encryptionType = EncryptionConstants.ENCRYPTION_AES_128;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void encryptWithCertificateAes256NoCompression() throws IOException, InterruptedException,
GeneralSecurityException, AbstractPKCSException, AbstractOperatorCreationException {
String filename = "encryptWithCertificateAes256NoCompression.pdf";
int encryptionType = EncryptionConstants.ENCRYPTION_AES_256;
if (FACTORY.isInApprovedOnlyMode()) {
// RSA PKCS1.5 encryption disallowed
Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
() -> encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION));
} else {
encryptWithCertificate(filename, encryptionType, CompressionConstants.NO_COMPRESSION);
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void openEncryptedDocWithWrongPrivateKey()
throws IOException, GeneralSecurityException, AbstractPKCSException, AbstractOperatorCreationException {
try (PdfReader reader = new PdfReader(sourceFolder + "encryptedWithCertificateAes128.pdf",
new ReaderProperties()
.setPublicKeySecurityParams(
getPublicCertificate(CERT),
PemFileHelper.readPrivateKeyFromPemFile(
FileUtil.getInputStreamForFile(sourceFolder + "wrong.pem"), PRIVATE_KEY_PASS),
FACTORY.getProviderName(),
null))) {
Exception e = Assertions.assertThrows(PdfException.class,
() -> new PdfDocument(reader)
);
Assertions.assertEquals(KernelExceptionMessageConstant.PDF_DECRYPTION, e.getMessage());
}
}
@Test
@LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
ignore = true))
public void openEncryptedWithCertificateDocWithDefaultKeyLength() throws IOException, CertificateException,
AbstractOperatorCreationException, AbstractPKCSException {
Certificate cert = getPublicCertificate(CERT);
try (PdfReader reader = new PdfReader(sourceFolder + "encryptedWithCertificateWithDefaultKeyLength.pdf",
new ReaderProperties().setPublicKeySecurityParams(cert, getPrivateKey(),
FACTORY.getProviderName(), null));
PdfDocument document = new PdfDocument(reader)) {
Assertions.assertFalse(document.getTrailer().getAsDictionary(PdfName.Encrypt).containsKey(PdfName.Length));
}
}
public void encryptWithCertificate(String filename, int encryptionType, int compression) throws IOException,
InterruptedException, GeneralSecurityException, AbstractPKCSException, AbstractOperatorCreationException {
ITextTest.removeCryptographyRestrictions();
String outFileName = destinationFolder + filename;
int permissions = EncryptionConstants.ALLOW_SCREENREADERS;
Certificate cert = getPublicCertificate(CERT);
PdfWriter writer = CompareTool.createTestPdfWriter(outFileName, new WriterProperties()
.setPublicKeyEncryption(new Certificate[] {cert}, new int[] {permissions}, encryptionType)
.addXmpMetadata());
writer.setCompressionLevel(compression);
PdfDocument document = new PdfDocument(writer);
document.getDocumentInfo().setMoreInfo(customInfoEntryKey, customInfoEntryValue);
PdfPage page = document.addNewPage();
writeTextBytesOnPageContent(page, pageTextContent);
page.flush();
document.close();
checkDecryptedWithCertificateContent(filename, cert, pageTextContent);
CompareTool compareTool = new CompareTool().enableEncryptionCompare();
compareTool.getOutReaderProperties()
.setPublicKeySecurityParams(cert, getPrivateKey(), FACTORY.getProviderName(), null);
compareTool.getCmpReaderProperties()
.setPublicKeySecurityParams(cert, getPrivateKey(), FACTORY.getProviderName(), null);
String compareResult = compareTool.compareByContent(outFileName, sourceFolder + "cmp_" + filename,
destinationFolder, "diff_");
if (compareResult != null) {
Assertions.fail(compareResult);
}
checkEncryptedWithCertificateDocumentStamping(filename, cert);
checkEncryptedWithCertificateDocumentAppending(filename, cert);
ITextTest.restoreCryptographyRestrictions();
}
public Certificate getPublicCertificate(String path) throws IOException, CertificateException {
InputStream is = FileUtil.getInputStreamForFile(path);
return CryptoUtil.readPublicCertificate(is);
}
public PrivateKey getPrivateKey() throws IOException, AbstractPKCSException, AbstractOperatorCreationException {
if (privateKey == null) {
privateKey = PemFileHelper.readPrivateKeyFromPemFile(FileUtil.getInputStreamForFile(PRIVATE_KEY), PRIVATE_KEY_PASS);
}
return privateKey;
}
public void checkDecryptedWithCertificateContent(String filename, Certificate certificate, String pageContent)
throws IOException, AbstractPKCSException, AbstractOperatorCreationException {
String src = destinationFolder + filename;
PdfReader reader = CompareTool.createOutputReader(src, new ReaderProperties()
.setPublicKeySecurityParams(certificate, getPrivateKey(), FACTORY.getProviderName(), null));
PdfDocument document = new PdfDocument(reader);
PdfPage page = document.getPage(1);
String s = new String(page.getStreamBytes(0));
Assertions.assertTrue(s.contains(pageContent), "Expected content: \n" + pageContent);
Assertions.assertEquals(customInfoEntryValue,
document.getTrailer().getAsDictionary(PdfName.Info).getAsString(new PdfName(customInfoEntryKey))
.toUnicodeString(), "Encrypted custom");
document.close();
}
// basically this is comparing content of decrypted by itext document with content of encrypted document
public void checkEncryptedWithCertificateDocumentStamping(String filename, Certificate certificate)
throws IOException, InterruptedException, AbstractPKCSException, AbstractOperatorCreationException {
String srcFileName = destinationFolder + filename;
String outFileName = destinationFolder + "stamped_" + filename;
PdfReader reader = CompareTool.createOutputReader(srcFileName, new ReaderProperties()
.setPublicKeySecurityParams(certificate, getPrivateKey(), FACTORY.getProviderName(), null));
PdfWriter writer = CompareTool.createTestPdfWriter(outFileName);
PdfDocument document = new PdfDocument(reader, writer);
document.close();
CompareTool compareTool = new CompareTool();
compareTool.getCmpReaderProperties()
.setPublicKeySecurityParams(certificate, getPrivateKey(), FACTORY.getProviderName(), null);
String compareResult = compareTool.compareByContent(outFileName, sourceFolder + "cmp_" + filename,
destinationFolder, "diff_");
if (compareResult != null) {
Assertions.fail(compareResult);
}
}
public void checkEncryptedWithCertificateDocumentAppending(String filename, Certificate certificate)
throws IOException, InterruptedException, AbstractPKCSException, AbstractOperatorCreationException {
String srcFileName = destinationFolder + filename;
String outFileName = destinationFolder + "appended_" + filename;
PdfReader reader = CompareTool.createOutputReader(srcFileName, new ReaderProperties()
.setPublicKeySecurityParams(certificate, getPrivateKey(), FACTORY.getProviderName(), null));
PdfWriter writer = CompareTool.createTestPdfWriter(outFileName);
PdfDocument document = new PdfDocument(reader, writer,
new StampingProperties().useAppendMode());
PdfPage newPage = document.addNewPage();
String helloWorldStringValue = "Hello world string";
newPage.put(PdfName.Default, new PdfString(helloWorldStringValue));
writeTextBytesOnPageContent(newPage, "Hello world page_2!");
document.close();
PdfReader appendedDocReader = CompareTool.createOutputReader(outFileName, new ReaderProperties()
.setPublicKeySecurityParams(certificate, getPrivateKey(), FACTORY.getProviderName(), null));
PdfDocument appendedDoc = new PdfDocument(appendedDocReader);
PdfPage secondPage = appendedDoc.getPage(2);
PdfString helloWorldPdfString = secondPage.getPdfObject().getAsString(PdfName.Default);
String actualHelloWorldStringValue = helloWorldPdfString != null ? helloWorldPdfString.getValue() : null;
Assertions.assertEquals(actualHelloWorldStringValue, helloWorldStringValue);
appendedDoc.close();
CompareTool compareTool = new CompareTool().enableEncryptionCompare();
compareTool.getOutReaderProperties()
.setPublicKeySecurityParams(certificate, getPrivateKey(), FACTORY.getProviderName(), null);
compareTool.getCmpReaderProperties()
.setPublicKeySecurityParams(certificate, getPrivateKey(), FACTORY.getProviderName(), null);
String compareResult = compareTool.compareByContent(outFileName, sourceFolder + "cmp_appended_" + filename,
destinationFolder, "diff_");
if (compareResult != null) {
Assertions.fail(compareResult);
}
}
static void writeTextBytesOnPageContent(PdfPage page, String text) throws IOException {
page.getFirstContentStream().getOutputStream().writeBytes(("q\n" +
"BT\n" +
"36 706 Td\n" +
"0 0 Td\n" +
"/F1 24 Tf\n" +
"(" + text + ")Tj\n" +
"0 0 Td\n" +
"ET\n" +
"Q ").getBytes(StandardCharsets.ISO_8859_1));
page.getResources().addFont(page.getDocument(), PdfFontFactory.createFont(StandardFonts.HELVETICA));
}
}