StandaloneMacIntegrityProtector.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.io.source.IRandomAccessSource;
import com.itextpdf.io.source.RASInputStream;
import com.itextpdf.io.source.RandomAccessSourceFactory;
import com.itextpdf.kernel.pdf.event.AbstractPdfDocumentEventHandler;
import com.itextpdf.kernel.pdf.event.AbstractPdfDocumentEvent;
import com.itextpdf.kernel.pdf.event.PdfDocumentEvent;
import com.itextpdf.kernel.exceptions.KernelExceptionMessageConstant;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.pdf.PdfDictionary;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfName;
import com.itextpdf.kernel.pdf.PdfOutputStream;
import com.itextpdf.kernel.pdf.PdfString;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.GeneralSecurityException;

/**
 * Class responsible for integrity protection in encrypted documents, which uses MAC container in the standalone mode.
 */
class StandaloneMacIntegrityProtector extends AbstractMacIntegrityProtector {
    private MacPdfObject macPdfObject;

    StandaloneMacIntegrityProtector(PdfDocument document, MacProperties macProperties) {
        super(document, macProperties);
    }

    StandaloneMacIntegrityProtector(PdfDocument document, PdfDictionary authDictionary) {
        super(document, authDictionary);
    }

    void prepareDocument() {
        document.addEventHandler(PdfDocumentEvent.START_DOCUMENT_CLOSING,
                new StandaloneMacIntegrityProtector.StandaloneMacPdfObjectAdder());
        document.addEventHandler(PdfDocumentEvent.START_WRITER_CLOSING,
                new StandaloneMacIntegrityProtector.StandaloneMacContainerEmbedder());
    }

    private void embedMacContainerInTrailer() throws IOException {
        byte[] documentBytes = getDocumentByteArrayOutputStream().toByteArray();
        long[] byteRange = macPdfObject.computeByteRange(documentBytes.length);

        long byteRangePosition = macPdfObject.getByteRangePosition();
        ByteArrayOutputStream localBaos = new ByteArrayOutputStream();
        PdfOutputStream os = new PdfOutputStream(localBaos);
        os.write('[');
        for (long l : byteRange) {
            os.writeLong(l).write(' ');
        }
        os.write(']');
        System.arraycopy(localBaos.toByteArray(), 0, documentBytes, (int) byteRangePosition, localBaos.size());

        byte[] mac = createDocumentDigestAndMacContainer(documentBytes, byteRange);
        PdfString macString = new PdfString(mac).setHexWriting(true);

        // fill in the MAC
        localBaos.reset();
        os.write(macString);
        System.arraycopy(localBaos.toByteArray(), 0, documentBytes, (int) byteRange[1], localBaos.size());
        getDocumentByteArrayOutputStream().reset();
        document.getWriter().getOutputStream().write(documentBytes, 0, documentBytes.length);
    }

    private byte[] createDocumentDigestAndMacContainer(byte[] documentBytes, long[] byteRange) throws IOException {
        IRandomAccessSource ras = new RandomAccessSourceFactory().createSource(documentBytes);
        try (InputStream rg = new RASInputStream(new RandomAccessSourceFactory().createRanged(ras, byteRange))) {
            byte[] dataDigest = digestBytes(rg);
            return createMacContainer(dataDigest, generateRandomBytes(32), null).getEncoded();
        } catch (GeneralSecurityException e) {
            throw new PdfException(KernelExceptionMessageConstant.CONTAINER_GENERATION_EXCEPTION, e);
        }
    }

    private int getContainerSizeEstimate() {
        try {
            return createMacContainer(digestBytes(new byte[0]), generateRandomBytes(32), null)
                    .getEncoded().length * 2 + 2;
        } catch (GeneralSecurityException | IOException e) {
            throw new PdfException(KernelExceptionMessageConstant.CONTAINER_GENERATION_EXCEPTION, e);
        }
    }

    private ByteArrayOutputStream getDocumentByteArrayOutputStream() {
        return ((ByteArrayOutputStream) document.getWriter().getOutputStream());
    }

    private final class StandaloneMacPdfObjectAdder extends AbstractPdfDocumentEventHandler {
        @Override
        public void onAcceptedEvent(AbstractPdfDocumentEvent event) {
            macPdfObject = new MacPdfObject(getContainerSizeEstimate());
            document.getTrailer().put(PdfName.AuthCode, macPdfObject.getPdfObject());
        }
    }

    private final class StandaloneMacContainerEmbedder extends AbstractPdfDocumentEventHandler {
        @Override
        public void onAcceptedEvent(AbstractPdfDocumentEvent event) {
            try {
                embedMacContainerInTrailer();
            } catch (IOException e) {
                throw new PdfException(KernelExceptionMessageConstant.CONTAINER_EMBEDDING_EXCEPTION, e);
            }
        }
    }
}