PdfEncryptingTest.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.crypto.fips.AbstractFipsUnapprovedOperationError;
import com.itextpdf.commons.utils.EncodingUtil;
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.font.constants.StandardFonts;
import com.itextpdf.io.util.StreamUtil;
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.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.kernel.utils.CompareTool;
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.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.spec.PKCS8EncodedKeySpec;
import org.junit.jupiter.api.AfterAll;
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 PdfEncryptingTest extends ExtendedITextTest {
    private static final String CERTS_SRC = "./src/test/resources/com/itextpdf/kernel/crypto/PdfEncryptingTest/certs/";
    private static final String DESTINATION_FOLDER = TestUtil.getOutputPath() + "/kernel/crypto/PdfEncryptingTest/";
    private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/kernel/crypto/PdfEncryptingTest/";

    private static final byte[] USER_PASSWORD = "user".getBytes(StandardCharsets.UTF_8);
    private static final byte[] OWNER_PASSWORD = "owner".getBytes(StandardCharsets.UTF_8);

    private static final String PROVIDER_NAME = BouncyCastleFactoryCreator.getFactory().getProviderName();

    @BeforeAll
    public static void setUpBeforeClass() {
        createOrClearDestinationFolder(DESTINATION_FOLDER);
        Security.addProvider(BouncyCastleFactoryCreator.getFactory().getProvider());
    }

    @AfterAll
    public static void afterClass() {
        CompareTool.cleanup(DESTINATION_FOLDER);
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT), ignore = true)
    public void encryptWithPasswordStandard40() throws IOException, InterruptedException {
        encryptWithPassword("encryptWithPasswordStandard40.pdf",
                EncryptionConstants.STANDARD_ENCRYPTION_40, false);
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT), ignore = true)
    public void encryptWithPasswordStandard128() throws IOException, InterruptedException {
        encryptWithPassword("encryptWithPasswordStandard128.pdf",
                EncryptionConstants.STANDARD_ENCRYPTION_128, false);
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT), ignore = true)
    public void encryptWithPasswordAes128() throws IOException, InterruptedException {
        encryptWithPassword("encryptWithPasswordAes128.pdf", EncryptionConstants.ENCRYPTION_AES_128,
                false);
    }

    @Test
    public void encryptWithPasswordAes256() throws IOException, InterruptedException {
        encryptWithPassword("encryptWithPasswordAes256.pdf", EncryptionConstants.ENCRYPTION_AES_256,
                false);
    }

    @Test
    public void encryptWithPasswordAes256Pdf2() throws IOException, InterruptedException {
        encryptWithPassword("encryptWithPasswordAes256Pdf2.pdf", EncryptionConstants.ENCRYPTION_AES_256,
                true);
    }

    @Test
    public void encryptWithCertificateAes256Rsa() throws GeneralSecurityException, IOException, InterruptedException {
        if (BouncyCastleFactoryCreator.getFactory().isInApprovedOnlyMode()) {
            // RSA PKCS1.5 encryption disallowed
            Assertions.assertThrows(AbstractFipsUnapprovedOperationError.class,
                    () -> encryptWithCertificate("encryptWithCertificateAes256Rsa.pdf", "SHA256withRSA.crt"));
        } else {
            encryptWithCertificate("encryptWithCertificateAes256Rsa.pdf", "SHA256withRSA.crt");
        }
    }

    @Test
    public void encryptWithCertificateAes256EcdsaP256() {
        String exceptionTest = Assertions.assertThrows(PdfException.class,
                () -> encryptWithCertificate("encryptWithCertificateAes256EcdsaP256.pdf", "SHA256withECDSA_P256.crt"))
                .getMessage();
        Assertions.assertEquals(MessageFormatUtil.format(
                KernelExceptionMessageConstant.ALGORITHM_IS_NOT_SUPPORTED, "1.2.840.10045.2.1"), exceptionTest);
    }

    @Test
    public void encryptWithCertificateAes256EcdsaBrainpoolP256R1() {
        String exceptionTest = Assertions.assertThrows(PdfException.class,
                () -> encryptWithCertificate(
                        "encryptWithCertificateAes256EcdsaBrainpoolP256R1.pdf", "SHA256withECDSA_brainpoolP256r1.crt"))
                .getMessage();
        Assertions.assertEquals(MessageFormatUtil.format(
                KernelExceptionMessageConstant.ALGORITHM_IS_NOT_SUPPORTED, "1.2.840.10045.2.1"), exceptionTest);
    }

    private void encryptWithPassword(String fileName, int encryptionType, boolean pdf2)
            throws IOException, InterruptedException {
        WriterProperties writerProperties = new WriterProperties()
                .setStandardEncryption(USER_PASSWORD, OWNER_PASSWORD, -1, encryptionType);
        if (pdf2) {
            writerProperties.setPdfVersion(PdfVersion.PDF_2_0);
        }

        try (PdfWriter writer = CompareTool.createTestPdfWriter(DESTINATION_FOLDER + fileName, writerProperties.addXmpMetadata());
                PdfDocument document = new PdfDocument(writer)) {
            writeTextToDocument(document);
        }
        Assertions.assertNull(new CompareTool().compareByContent(DESTINATION_FOLDER + fileName,
                SOURCE_FOLDER + "cmp_" + fileName, DESTINATION_FOLDER, "diff", USER_PASSWORD, USER_PASSWORD));
    }

    private void encryptWithCertificate(String fileName, String certificatePath)
            throws IOException, GeneralSecurityException, InterruptedException {
        Certificate certificate = CryptoUtil.readPublicCertificate(FileUtil.getInputStreamForFile(CERTS_SRC + certificatePath));
        WriterProperties writerProperties = new WriterProperties().setPublicKeyEncryption(
                new Certificate[] {certificate}, new int[] {-1}, EncryptionConstants.ENCRYPTION_AES_256);
        try (PdfWriter writer = CompareTool.createTestPdfWriter(DESTINATION_FOLDER + fileName, writerProperties.addXmpMetadata());
                PdfDocument document = new PdfDocument(writer)) {
            writeTextToDocument(document);
        }
        CompareTool compareTool = new CompareTool();
        PrivateKey privateKey = readPrivateKey("SHA256withRSA.key", "RSA");
        compareTool.getCmpReaderProperties().setPublicKeySecurityParams(certificate, privateKey, PROVIDER_NAME, null);
        compareTool.getOutReaderProperties().setPublicKeySecurityParams(certificate, privateKey, PROVIDER_NAME, null);
        Assertions.assertNull(compareTool
                .compareByContent(DESTINATION_FOLDER + fileName, SOURCE_FOLDER + "cmp_" + fileName, DESTINATION_FOLDER,
                        "diff"));
    }
    
    private void writeTextToDocument(PdfDocument document) throws IOException {
        PdfCanvas canvas = new PdfCanvas(document.addNewPage());
        canvas.saveState()
                .beginText()
                .moveText(36, 750)
                .setFontAndSize(PdfFontFactory.createFont(StandardFonts.HELVETICA), 16)
                .showText("Content encrypted by iText")
                .endText()
                .restoreState();
    }

    private PrivateKey readPrivateKey(String privateKeyName, String algorithm)
            throws GeneralSecurityException, IOException {
        try (InputStream pemFile = FileUtil.getInputStreamForFile(CERTS_SRC + privateKeyName)) {
            String start = "-----BEGIN PRIVATE KEY-----";
            String end = "-----END PRIVATE KEY-----";

            String pemContent = new String(StreamUtil.inputStreamToArray(pemFile));
            int startPos = pemContent.indexOf(start);
            int endPos = pemContent.indexOf(end);
            pemContent = pemContent.substring(startPos + start.length(), endPos);
            byte[] encoded = EncodingUtil.fromBase64(pemContent);

            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
            return KeyFactory.getInstance(algorithm, BouncyCastleFactoryCreator.getFactory().getProviderName())
                    .generatePrivate(keySpec);
        }
    }
}