MacIntegrityProtectorCreationTest.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.mac;

import com.itextpdf.bouncycastleconnector.BouncyCastleFactoryCreator;
import com.itextpdf.commons.bouncycastle.operator.AbstractOperatorCreationException;
import com.itextpdf.commons.bouncycastle.pkcs.AbstractPKCSException;
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.util.EnumUtil;
import com.itextpdf.kernel.crypto.CryptoUtil;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.logs.KernelLogMessageConstant;
import com.itextpdf.kernel.mac.MacProperties.KeyWrappingAlgorithm;
import com.itextpdf.kernel.mac.MacProperties.MacAlgorithm;
import com.itextpdf.kernel.mac.MacProperties.MacDigestAlgorithm;
import com.itextpdf.kernel.pdf.EncryptionConstants;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfVersion;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.ReaderProperties;
import com.itextpdf.kernel.pdf.StampingProperties;
import com.itextpdf.kernel.pdf.VersionConforming;
import com.itextpdf.kernel.pdf.WriterProperties;
import com.itextpdf.kernel.pdf.annot.PdfTextAnnotation;
import com.itextpdf.kernel.utils.CompareTool;
import com.itextpdf.kernel.utils.PemFileHelper;
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.ByteArrayOutputStream;
import java.io.IOException;
import java.security.PrivateKey;
import java.security.Security;
import java.security.cert.Certificate;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Tag("BouncyCastleIntegrationTest")
public class MacIntegrityProtectorCreationTest extends ExtendedITextTest {
    private static final String SOURCE_FOLDER = "./src/test/resources/com/itextpdf/kernel/mac/MacIntegrityProtectorCreationTest/";
    private static final String DESTINATION_FOLDER = TestUtil.getOutputPath() + "/kernel/mac/MacIntegrityProtectorCreationTest/";
    private static final String CERTS_SRC = "./src/test/resources/com/itextpdf/kernel/mac/MacIntegrityProtectorCreationTest/certs/";
    private static final byte[] PASSWORD = "123".getBytes();
    private static final String PROVIDER_NAME = BouncyCastleFactoryCreator.getFactory().getProviderName();

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

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

    @Test
    public void standaloneMacStandardEncryptionTest() throws IOException, InterruptedException {
        String fileName = "standaloneMacStandardEncryptionTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_256,
                        new MacProperties(MacDigestAlgorithm.SHA_256));

        try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outputFileName, writerProperties))) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare(false).compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    public void noMacProtectionTest() throws IOException, InterruptedException {
        String fileName = "noMacProtectionTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_256, null);

        try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outputFileName, writerProperties))) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare().compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
            ignore = true))
    public void macEncryptionWithAesGcmTest() throws IOException, InterruptedException {
        String fileName = "macEncryptionWithAesGsmTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_GCM,
                        new MacProperties(MacDigestAlgorithm.SHA_256));

        try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outputFileName, writerProperties))) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare(false).compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = IoLogMessageConstant.PDF_WRITER_CLOSING_FAILED)
    })
    public void standaloneMacUnwritableStreamTest() throws IOException {
        WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_256,
                        new MacProperties(MacDigestAlgorithm.SHA_256));
        ByteArrayOutputStream unwritableStream = new ByteArrayOutputStream() {
            @Override
            public void write(byte[] b, int off, int len) {
                throw new RuntimeException("expected");
            }
        };

        try (PdfDocument pdfDoc = new PdfDocument(new PdfWriter(unwritableStream, writerProperties))) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }

        unwritableStream.close();
    }

    @Test
    public void standaloneMacWithAllHashAlgorithmsTest() throws IOException, InterruptedException {
        for (int i = 0; i < EnumUtil.getAllValuesOfEnum(MacDigestAlgorithm.class).size(); i++) {
            String fileName = "standaloneMacWithAllHashAlgorithmsTest" + (i + 1) + ".pdf";
            String outputFileName = DESTINATION_FOLDER + fileName;
            String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

            MacProperties macProperties = new MacProperties(EnumUtil.getAllValuesOfEnum(MacDigestAlgorithm.class).get(i),
                    MacAlgorithm.HMAC_WITH_SHA_256, KeyWrappingAlgorithm.AES_256_NO_PADD);
            WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                    .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_256,
                            macProperties);

            try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outputFileName, writerProperties))) {
                pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
            }
            Assertions.assertNull(new CompareTool().enableEncryptionCompare(false).compareByContent(
                    outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
        }
    }

    @Test
    public void standaloneMacPdfVersionNotSetTest() {
        String fileName = "standaloneMacPdfVersionNotSetTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;

        MacProperties macProperties = new MacProperties(MacDigestAlgorithm.SHA_256, MacAlgorithm.HMAC_WITH_SHA_256,
                KeyWrappingAlgorithm.AES_256_NO_PADD);
        WriterProperties writerProperties = new WriterProperties()
                .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_256, macProperties);

        String exceptionMessage = Assertions.assertThrows(PdfException.class, () -> {
            try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outputFileName, writerProperties))) {
                pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
            }
        }).getMessage();
        Assertions.assertEquals(KernelExceptionMessageConstant.MAC_FOR_PDF_2, exceptionMessage);
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
            ignore = true))
    public void addMacOnPreserveEncryptionTest() throws IOException, InterruptedException {
        String fileName = "addMacOnPreserveEncryptionTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "noMacProtectionDocument.pdf",
                new ReaderProperties().setPassword(PASSWORD)),
                CompareTool.createTestPdfWriter(outputFileName, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)),
                new StampingProperties().preserveEncryption())) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare(false).compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
            ignore = true))
    public void addMacOnAppendModeTest() throws IOException, InterruptedException {
        // MAC should not be added in append mode
        String fileName = "addMacOnAppendModeTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "noMacProtectionDocument.pdf",
                new ReaderProperties().setPassword(PASSWORD)),
                CompareTool.createTestPdfWriter(outputFileName, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)),
                new StampingProperties().useAppendMode())) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare().compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    public void addMacWithDisableMacPropertyTest() throws IOException, InterruptedException {
        // MAC should not be added in disable MAC mode even if it was provided with writer properties
        String fileName = "addMacWithDisableMacPropertyTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        MacProperties macProperties = new MacProperties(MacDigestAlgorithm.SHA_384);
        WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_256, macProperties);
        try (PdfDocument pdfDoc = new PdfDocument(
                new PdfReader(SOURCE_FOLDER + "noMacProtectionDocument.pdf", new ReaderProperties().setPassword(PASSWORD)),
                new PdfWriter(outputFileName, writerProperties), new StampingProperties().disableMac())) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare().compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
            ignore = true))
    public void addMacOnPreserveEncryptionWhileDowngradingTest() throws IOException, InterruptedException {
        String fileName = "addMacOnPreserveEncryptionWhileDowngradingTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "noMacProtectionDocument.pdf",
                new ReaderProperties().setPassword(PASSWORD)),
                CompareTool.createTestPdfWriter(outputFileName, new WriterProperties().setPdfVersion(PdfVersion.PDF_1_7)),
                new StampingProperties().preserveEncryption())) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare().compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    @LogMessages(messages = {
            @LogMessage(messageTemplate = VersionConforming.DEPRECATED_AES256_REVISION),
            @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT, ignore = true)})
    public void addMacOnPreserveEncryptionFor17DocTest() throws IOException, InterruptedException {
        // We can't embed MAC into encrypted documents during the conversion from earlier PDF version
        // because their encryption does not support this. So WriterProperties should be used iso preserveEncryption
        String fileName = "addMacOnPreserveEncryptionFor17DocTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        try (PdfDocument pdfDoc = new PdfDocument(new PdfReader(SOURCE_FOLDER + "noMacProtectionDocument_1_7.pdf",
                new ReaderProperties().setPassword(PASSWORD)),
                CompareTool.createTestPdfWriter(outputFileName, new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)),
                new StampingProperties().preserveEncryption())) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        Assertions.assertNull(new CompareTool().enableEncryptionCompare().compareByContent(
                outputFileName, cmpFileName, DESTINATION_FOLDER, "diff", PASSWORD, PASSWORD));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
            ignore = true))
    public void standaloneMacOldEncryptionAlgorithmTest() {
        String fileName = "standaloneMacOldEncryptionAlgorithmTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;

        MacProperties macProperties = new MacProperties(MacDigestAlgorithm.SHA_256, MacAlgorithm.HMAC_WITH_SHA_256,
                KeyWrappingAlgorithm.AES_256_NO_PADD);
        WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                .setStandardEncryption(PASSWORD, PASSWORD, 0, EncryptionConstants.ENCRYPTION_AES_128, macProperties);

        String exceptionMessage = Assertions.assertThrows(PdfException.class, () -> {
            try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outputFileName, writerProperties))) {
                pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
            }
        }).getMessage();
        Assertions.assertEquals(KernelExceptionMessageConstant.MAC_FOR_ENCRYPTION_5, exceptionMessage);
    }

    @Test
    public void standaloneMacPublicKeyEncryptionTest() throws Exception {
        try {
            BouncyCastleFactoryCreator.getFactory().isEncryptionFeatureSupported(0, true);
        } catch (Exception ignored) {
            Assumptions.assumeTrue(false);
        }
        Assumptions.assumeTrue(!BouncyCastleFactoryCreator.getFactory().isInApprovedOnlyMode());
        String fileName = "standaloneMacPublicKeyEncryptionTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        Certificate certificate = CryptoUtil.readPublicCertificate(FileUtil.getInputStreamForFile(CERTS_SRC + "SHA256withRSA.cer"));

        WriterProperties writerProperties = new WriterProperties().setPdfVersion(PdfVersion.PDF_2_0)
                .setPublicKeyEncryption(new Certificate[] {certificate}, new int[] {-1}, EncryptionConstants.ENCRYPTION_AES_256,
                        new MacProperties(MacDigestAlgorithm.SHA_256));
        try (PdfDocument pdfDoc = new PdfDocument(CompareTool.createTestPdfWriter(outputFileName, writerProperties))) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }
        PrivateKey privateKey = getPrivateKey(CERTS_SRC + "SHA256withRSA.key");
        CompareTool compareTool = new CompareTool();
        compareTool.getCmpReaderProperties().setPublicKeySecurityParams(certificate, privateKey, PROVIDER_NAME, null);
        compareTool.getOutReaderProperties().setPublicKeySecurityParams(certificate, privateKey, PROVIDER_NAME, null);

        Assertions.assertNull(compareTool.compareByContent(outputFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
    }

    @Test
    @LogMessages(messages = @LogMessage(messageTemplate = KernelLogMessageConstant.MD5_IS_NOT_FIPS_COMPLIANT,
            ignore = true))
    // TODO DEVSIX-8635 - Verify MAC permission and embed MAC in stamping mode for public key encryption
    public void addMacOnPreservePublicKeyEncryptionTest() throws Exception {
        try {
            BouncyCastleFactoryCreator.getFactory().isEncryptionFeatureSupported(0, true);
        } catch (Exception ignored) {
            Assumptions.assumeTrue(false);
        }

        String fileName = "addMacOnPreservePublicKeyEncryptionTest.pdf";
        String outputFileName = DESTINATION_FOLDER + fileName;
        String cmpFileName = SOURCE_FOLDER + "cmp_" + fileName;

        Certificate certificate = CryptoUtil.readPublicCertificate(
                FileUtil.getInputStreamForFile(CERTS_SRC + "SHA256withRSA.cer"));
        PrivateKey privateKey = getPrivateKey(CERTS_SRC + "SHA256withRSA.key");
        ReaderProperties readerProperties = new ReaderProperties();
        readerProperties.setPublicKeySecurityParams(certificate, privateKey, PROVIDER_NAME, null);
        try (PdfDocument pdfDoc = new PdfDocument(
                new PdfReader(SOURCE_FOLDER + "noMacProtectionPublicKeyEncryptionDocument.pdf", readerProperties),
                CompareTool.createTestPdfWriter(outputFileName), new StampingProperties().preserveEncryption())) {
            pdfDoc.addNewPage().addAnnotation(new PdfTextAnnotation(new Rectangle(100, 100, 100, 100)));
        }

        CompareTool compareTool = new CompareTool();
        compareTool.getCmpReaderProperties().setPublicKeySecurityParams(certificate, privateKey, PROVIDER_NAME, null);
        compareTool.getOutReaderProperties().setPublicKeySecurityParams(certificate, privateKey, PROVIDER_NAME, null);

        Assertions.assertNull(compareTool.compareByContent(outputFileName, cmpFileName, DESTINATION_FOLDER, "diff"));
    }

    public static PrivateKey getPrivateKey(String keyName) throws IOException, AbstractPKCSException, AbstractOperatorCreationException {
        return PemFileHelper.readPrivateKeyFromPemFile(
                FileUtil.getInputStreamForFile(keyName), "testpassphrase".toCharArray());
    }
}