GordianPEMParser.java

/*
 * GordianKnot: Security Suite
 * Copyright 2012-2026. Tony Washer
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License.  You may obtain a copy
 * of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package io.github.tonywasher.joceanus.gordianknot.impl.core.keystore;

import io.github.tonywasher.joceanus.gordianknot.api.base.GordianException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.base.GordianDataConverter;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianDataException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.exc.GordianIOException;
import io.github.tonywasher.joceanus.gordianknot.impl.core.keystore.GordianPEMObject.GordianPEMObjectType;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;

/**
 * PEM Parser.
 */
public class GordianPEMParser {
    /**
     * The bracket sequence.
     */
    private static final String BRACKET = "-----";

    /**
     * The begin header.
     */
    private static final String BEGIN = "BEGIN ";

    /**
     * The end header.
     */
    private static final String END = "END ";

    /**
     * The newLine.
     */
    private static final char NEWLINE = '\n';

    /**
     * The PEM line length.
     */
    private static final int PEMLEN = 64;

    /**
     * the active ObjectType.
     */
    private GordianPEMObjectType theObjectType;

    /**
     * Write PEM objects to stream.
     *
     * @param pStream  the stream
     * @param pObjects the list of objects
     * @throws GordianException on error
     */
    void writePEMFile(final OutputStream pStream,
                      final List<GordianPEMObject> pObjects) throws GordianException {
        /* Protect against exceptions */
        try (OutputStreamWriter myOutputWriter = new OutputStreamWriter(pStream, StandardCharsets.UTF_8);
             BufferedWriter myWriter = new BufferedWriter(myOutputWriter)) {
            /* Write the objects to the file */
            writeObjects(myWriter, pObjects);

            /* Catch exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to write to stream", e);
        }
    }

    /**
     * Parse PEM Object stream.
     *
     * @param pStream the stream
     * @return the list of parsed objects
     * @throws GordianException on error
     */
    List<GordianPEMObject> parsePEMFile(final InputStream pStream) throws GordianException {
        /* Protect against exceptions */
        try (InputStreamReader myInputReader = new InputStreamReader(pStream, StandardCharsets.UTF_8);
             BufferedReader myReader = new BufferedReader(myInputReader)) {
            /* Parse the objects from the file */
            return parseObjects(myReader);

            /* Catch exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to process stream", e);
        }
    }

    /**
     * Write PEM Objects.
     *
     * @param pWriter  the writer
     * @param pObjects the list of objects
     * @throws GordianException on error
     */
    private static void writeObjects(final BufferedWriter pWriter,
                                     final List<GordianPEMObject> pObjects) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Loop through the objects */
            for (GordianPEMObject myObject : pObjects) {
                /* Determine header type */
                final String myType = myObject.getObjectType().getId();

                /* Write the object header */
                pWriter.write(BRACKET);
                pWriter.write(BEGIN);
                pWriter.write(myType);
                pWriter.write(BRACKET);
                pWriter.write(NEWLINE);

                /* Access base64 data */
                final String myBase64 = GordianDataConverter.byteArrayToBase64(myObject.getEncoded());
                int myLen = myBase64.length();
                for (int i = 0; myLen > 0; i += PEMLEN, myLen -= PEMLEN) {
                    pWriter.write(myBase64, i, Math.min(myLen, PEMLEN));
                    pWriter.write(NEWLINE);
                }

                /* Write the object trailer */
                pWriter.write(BRACKET);
                pWriter.write(END);
                pWriter.write(myType);
                pWriter.write(BRACKET);
                pWriter.write(NEWLINE);
                pWriter.write(NEWLINE);
            }

            /* Catch exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to write to stream", e);
        }
    }

    /**
     * Parse certificates.
     *
     * @param pReader the reader
     * @return the list of objects
     * @throws GordianException on error
     */
    private List<GordianPEMObject> parseObjects(final BufferedReader pReader) throws GordianException {
        /* Protect against exceptions */
        try {
            /* Create variables */
            final List<GordianPEMObject> myObjects = new ArrayList<>();
            final StringBuilder myCurrent = new StringBuilder();

            /* Read the lines */
            while (true) {
                /* Read next line */
                String myLine = pReader.readLine();
                if (myLine == null) {
                    break;
                }

                /* If the line is a start/end element */
                if (myLine.startsWith(BRACKET)) {
                    /* Process the boundary */
                    myLine = myLine.substring(BRACKET.length());
                    if (theObjectType != null) {
                        processEndBoundary(myLine, myCurrent, myObjects);
                    } else {
                        processStartBoundary(myLine);
                    }

                    /* else if we are parsing, add line to buffer */
                } else if (theObjectType != null) {
                    myCurrent.append(myLine);
                }

                /* Ignore other lines */
            }

            /* Return the objects */
            return myObjects;

            /* Catch exceptions */
        } catch (IOException e) {
            throw new GordianIOException("Failed to parse stream", e);
        }
    }

    /**
     * Process the start boundary.
     *
     * @param pBoundary the boundary
     * @throws GordianException on error
     */
    private void processStartBoundary(final String pBoundary) throws GordianException {
        /* If this is not a begin boundary */
        if (!pBoundary.startsWith(BEGIN)) {
            throw new GordianDataException("Sequencing error");
        }

        /* Check object type */
        final String myLine = pBoundary.substring(BEGIN.length());
        theObjectType = GordianPEMObjectType.getObjectType(myLine);
    }

    /**
     * Process the end boundary.
     *
     * @param pBoundary the boundary
     * @param pCurrent  the current item
     * @param pList     the list of parsed objects
     * @throws GordianException on error
     */
    private void processEndBoundary(final String pBoundary,
                                    final StringBuilder pCurrent,
                                    final List<GordianPEMObject> pList) throws GordianException {
        /* If this is not an end boundary */
        if (!pBoundary.startsWith(END)) {
            throw new GordianDataException("Sequencing error");
        }

        /* Check object type */
        final String myLine = pBoundary.substring(END.length());
        final GordianPEMObjectType myType = GordianPEMObjectType.getObjectType(myLine);
        if (theObjectType != myType) {
            throw new GordianDataException("Mixed dataTypes");
        }

        /* Parse the data and add certificate to list */
        final String myData = pCurrent.toString();
        final byte[] myBytes = GordianDataConverter.base64ToByteArray(myData);
        pList.add(new GordianPEMObject(theObjectType, myBytes));
        theObjectType = null;
        pCurrent.setLength(0);
    }
}