/* $Id: pemfile-read.cpp $ */
/** @file
 * IPRT - Crypto - PEM file reader.
 *
 * See RFC-1341 for the original ideas for the format, but keep in mind
 * that the format was hijacked and put to different uses.  We're aiming at
 * dealing with the different uses rather than anything email related here.
 */

/*
 * Copyright (C) 2006-2020 Oracle Corporation
 *
 * This file is part of VirtualBox Open Source Edition (OSE), as
 * available from http://www.virtualbox.org. This file is free software;
 * you can redistribute it and/or modify it under the terms of the GNU
 * General Public License (GPL) as published by the Free Software
 * Foundation, in version 2 as it comes in the "COPYING" file of the
 * VirtualBox OSE distribution. VirtualBox OSE is distributed in the
 * hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
 *
 * The contents of this file may alternatively be used under the terms
 * of the Common Development and Distribution License Version 1.0
 * (CDDL) only, as it comes in the "COPYING.CDDL" file of the
 * VirtualBox OSE distribution, in which case the provisions of the
 * CDDL are applicable instead of those of the GPL.
 *
 * You may elect to license modified versions of this file under the
 * terms and conditions of either the GPL or the CDDL or both.
 */


/*********************************************************************************************************************************
*   Header Files                                                                                                                 *
*********************************************************************************************************************************/
#include "internal/iprt.h"
#include <iprt/crypto/pem.h>

#include <iprt/asm.h>
#include <iprt/base64.h>
#include <iprt/ctype.h>
#include <iprt/err.h>
#include <iprt/mem.h>
#include <iprt/memsafer.h>
#include <iprt/file.h>
#include <iprt/string.h>



/**
 * Looks for a PEM-like marker.
 *
 * @returns true if found, false if not.
 * @param   pbContent           Start of the content to search thru.
 * @param   cbContent           The size of the content to search.
 * @param   offStart            The offset into pbContent to start searching.
 * @param   pszLeadWord         The lead word (BEGIN/END).
 * @param   cchLeadWord         The length of the lead word.
 * @param   paMarkers           Pointer to an array of markers.
 * @param   cMarkers            Number of markers in the array.
 * @param   ppMatch             Where to return the pointer to the matching
 *                              marker. Optional.
 * @param   poffBegin           Where to return the start offset of the marker.
 *                              Optional.
 * @param   poffEnd             Where to return the end offset of the marker
 *                              (trailing whitespace and newlines will be
 *                              skipped).  Optional.
 */
static bool rtCrPemFindMarker(uint8_t const *pbContent, size_t cbContent, size_t offStart,
                              const char *pszLeadWord, size_t cchLeadWord, PCRTCRPEMMARKER paMarkers, size_t cMarkers,
                              PCRTCRPEMMARKER *ppMatch, size_t *poffBegin, size_t *poffEnd)
{
    /* Remember the start of the content for the purpose of calculating offsets. */
    uint8_t const * const pbStart = pbContent;

    /* Skip adhead by offStart */
    if (offStart >= cbContent)
        return false;
    pbContent += offStart;
    cbContent -= offStart;

    /*
     * Search the content.
     */
    while (cbContent > 6)
    {
        /*
         * Look for dashes.
         */
        uint8_t const *pbStartSearch = pbContent;
        pbContent = (uint8_t const *)memchr(pbContent, '-', cbContent);
        if (!pbContent)
            break;

        cbContent -= pbContent - pbStartSearch;
        if (cbContent < 6)
            break;

        /*
         * There must be at least three to interest us.
         */
        if (   pbContent[1] == '-'
            && pbContent[2] == '-')
        {
            unsigned cDashes = 3;
            while (cDashes < cbContent && pbContent[cDashes] == '-')
                cDashes++;

            if (poffBegin)
                *poffBegin = pbContent - pbStart;
            cbContent -= cDashes;
            pbContent += cDashes;

            /*
             * Match lead word.
             */
            if (   cbContent > cchLeadWord
                && memcmp(pbContent, pszLeadWord, cchLeadWord) == 0
                && RT_C_IS_BLANK(pbContent[cchLeadWord]) )
            {
                pbContent += cchLeadWord;
                cbContent -= cchLeadWord;
                while (cbContent > 0 && RT_C_IS_BLANK(*pbContent))
                {
                    pbContent++;
                    cbContent--;
                }

                /*
                 * Match one of the specified markers.
                 */
                uint8_t const  *pbSavedContent = pbContent;
                size_t  const   cbSavedContent = cbContent;
                for (uint32_t iMarker = 0; iMarker < cMarkers; iMarker++)
                {
                    pbContent = pbSavedContent;
                    cbContent = cbSavedContent;

                    uint32_t            cWords = paMarkers[iMarker].cWords;
                    PCRTCRPEMMARKERWORD pWord  = paMarkers[iMarker].paWords;
                    while (cWords > 0)
                    {
                        uint32_t const cchWord = pWord->cchWord;
                        if (cbContent <= cchWord)
                            break;
                        if (memcmp(pbContent, pWord->pszWord, cchWord))
                            break;
                        pbContent += cchWord;
                        cbContent -= cchWord;

                        if (!cbContent)
                            break;
                        if (RT_C_IS_BLANK(*pbContent))
                            do
                            {
                                pbContent++;
                                cbContent--;
                            } while (cbContent > 0 && RT_C_IS_BLANK(*pbContent));
                        else if (cWords > 1 || pbContent[0] != '-')
                            break;

                        cWords--;
                        if (cWords == 0)
                        {
                            /*
                             * If there are three or more dashes following now, we've got a hit.
                             */
                            if (   cbContent > 3
                                && pbContent[0] == '-'
                                && pbContent[1] == '-'
                                && pbContent[2] == '-')
                            {
                                cDashes = 3;
                                while (cDashes < cbContent && pbContent[cDashes] == '-')
                                    cDashes++;
                                cbContent -= cDashes;
                                pbContent += cDashes;

                                /*
                                 * Skip spaces and newline.
                                 */
                                while (cbContent > 0 && RT_C_IS_SPACE(*pbContent))
                                    pbContent++, cbContent--;
                                if (poffEnd)
                                    *poffEnd = pbContent - pbStart;
                                if (ppMatch)
                                    *ppMatch = &paMarkers[iMarker];
                                return true;
                            }
                            break;
                        }
                        pWord++;
                    } /* for each word in marker. */
                } /* for each marker. */
            }
        }
        else
        {
            pbContent++;
            cbContent--;
        }
    }

    return false;
}


static bool rtCrPemFindMarkerSection(uint8_t const *pbContent, size_t cbContent, size_t offStart,
                                     PCRTCRPEMMARKER paMarkers, size_t cMarkers,
                                     PCRTCRPEMMARKER *ppMatch, size_t *poffBegin, size_t *poffEnd, size_t *poffResume)
{
    /** @todo Detect BEGIN / END mismatch. */
    PCRTCRPEMMARKER pMatch;
    if (rtCrPemFindMarker(pbContent, cbContent, offStart, "BEGIN", 5, paMarkers, cMarkers,
                          &pMatch, NULL /*poffStart*/, poffBegin))
    {
        if (rtCrPemFindMarker(pbContent, cbContent, *poffBegin, "END", 3, pMatch, 1,
                              NULL /*ppMatch*/, poffEnd, poffResume))
        {
            *ppMatch = pMatch;
            return true;
        }
    }
    *ppMatch = NULL;
    return false;
}


/**
 * Parses any fields the message may contain.
 *
 * @retval  VINF_SUCCESS
 * @retval  VERR_NO_MEMORY
 * @retval  VERR_CR_MALFORMED_PEM_HEADER
 *
 * @param   pSection        The current section, where we will attach a list of
 *                          fields to the pFieldHead member.
 * @param   pbContent       The content of the PEM message being parsed.
 * @param   cbContent       The length of the PEM message.
 * @param   pcbFields       Where to return the length of the header fields we found.
 */
static int rtCrPemProcessFields(PRTCRPEMSECTION pSection, uint8_t const *pbContent, size_t cbContent, size_t *pcbFields)
{
    uint8_t const * const pbContentStart = pbContent;

    /*
     * Work the encapulated header protion field by field.
     *
     * This is optional, so currently we don't throw errors here but leave that
     * to when we work the text portion with the base64 decoder.  Also, as a reader
     * we don't go all pedanic on confirming to specification (RFC-1421), especially
     * given that it's used for crypto certificates, keys and the like not email. :-)
     */
    PCRTCRPEMFIELD *ppNext = &pSection->pFieldHead;
    while (cbContent > 0)
    {
        /* Just look for a colon first. */
        const uint8_t *pbColon = (const uint8_t *)memchr(pbContent, ':', cbContent);
        if (!pbColon)
            break;
        size_t offColon = pbColon - pbContent;

        /* Check that the colon is within the first line. */
        if (!memchr(pbContent, '\n', cbContent - offColon))
            return VERR_CR_MALFORMED_PEM_HEADER;

        /* Skip leading spaces (there shouldn't be any, but just in case). */
        while (RT_C_IS_BLANK(*pbContent) && /*paranoia:*/ offColon > 0)
        {
            offColon--;
            cbContent--;
            pbContent++;
        }

        /* There shouldn't be any spaces before the colon, but just in case */
        size_t cchName = offColon;
        while (cchName > 0 && RT_C_IS_BLANK(pbContent[cchName - 1]))
            cchName--;

        /* Skip leading value spaces (there typically is at least one). */
        size_t offValue = offColon + 1;
        while (offValue < cbContent && RT_C_IS_BLANK(pbContent[offValue]))
            offValue++;

        /* Find the newline the field value ends with and where the next iteration should start later on. */
        size_t         cbLeft;
        uint8_t const *pbNext = (uint8_t const *)memchr(&pbContent[offValue], '\n', cbContent - offValue);
        while (   pbNext
               && (cbLeft = pbNext - pbContent) < cbContent
               && RT_C_IS_BLANK(pbNext[1]) /* next line must start with a space or tab */)
            pbNext = (uint8_t const *)memchr(&pbNext[1], '\n', cbLeft - 1);

        size_t cchValue;
        if (pbNext)
        {
            cchValue = pbNext - &pbContent[offValue];
            if (cchValue > 0 && pbNext[-1] == '\r')
                cchValue--;
            pbNext++;
        }
        else
        {
            cchValue = cbContent - offValue;
            pbNext = &pbContent[cbContent];
        }

        /* Strip trailing spaces. */
        while (cchValue > 0 && RT_C_IS_BLANK(pbContent[offValue + cchValue - 1]))
            cchValue--;

        /*
         * Allocate a field instance.
         *
         * Note! We don't consider field data sensitive at the moment.  This
         *       mainly because the fields are chiefly used to indicate the
         *       encryption parameters to the body.
         */
        PRTCRPEMFIELD pNewField = (PRTCRPEMFIELD)RTMemAllocZVar(sizeof(*pNewField) + cchName + 1 + cchValue + 1);
        if (!pNewField)
            return VERR_NO_MEMORY;
        pNewField->cchName         = cchName;
        pNewField->cchValue        = cchValue;
        memcpy(pNewField->szName, pbContent, cchName);
        pNewField->szName[cchName] = '\0';
        char *pszDst = (char *)memcpy(&pNewField->szName[cchName + 1], &pbContent[offValue], cchValue);
        pNewField->pszValue        = pszDst;
        pszDst[cchValue]           = '\0';
        pNewField->pNext           = NULL;

        *ppNext = pNewField;
        ppNext = &pNewField->pNext;

        /*
         * Advance past the field.
         */
        cbContent -= pbNext - pbContent;
        pbContent  = pbNext;
    }

    /*
     * Skip blank line(s) before the body.
     */
    while (cbContent >= 1)
    {
        size_t cbSkip;
        if (pbContent[0] == '\n')
            cbSkip = 1;
        else if (   pbContent[0] == '\r'
                 && cbContent >= 2
                 && pbContent[1] == '\n')
            cbSkip = 2;
        else
            break;
        pbContent += cbSkip;
        cbContent -= cbSkip;
    }

    *pcbFields = pbContent - pbContentStart;
    return VINF_SUCCESS;
}


/**
 * Does the decoding of a PEM-like data blob after it has been located.
 *
 * @returns IPRT status ocde
 * @param   pbContent           The start of the PEM-like content (text).
 * @param   cbContent           The max size of the PEM-like content.
 * @param   fSensitive          Set if the safer allocator should be used.
 * @param   ppvDecoded          Where to return a heap block containing the
 *                              decoded content.
 * @param   pcbDecoded          Where to return the size of the decoded content.
 */
static int rtCrPemDecodeBase64(uint8_t const *pbContent, size_t cbContent, bool fSensitive,
                               void **ppvDecoded, size_t *pcbDecoded)
{
    ssize_t cbDecoded = RTBase64DecodedSizeEx((const char *)pbContent, cbContent, NULL);
    if (cbDecoded < 0)
        return VERR_INVALID_BASE64_ENCODING;

    *pcbDecoded = cbDecoded;
    void *pvDecoded = !fSensitive ? RTMemAlloc(cbDecoded) : RTMemSaferAllocZ(cbDecoded);
    if (!pvDecoded)
        return VERR_NO_MEMORY;

    size_t cbActual;
    int rc = RTBase64DecodeEx((const char *)pbContent, cbContent, pvDecoded, cbDecoded, &cbActual, NULL);
    if (RT_SUCCESS(rc))
    {
        if (cbActual == (size_t)cbDecoded)
        {
            *ppvDecoded = pvDecoded;
            return VINF_SUCCESS;
        }

        rc = VERR_INTERNAL_ERROR_3;
    }
    if (!fSensitive)
        RTMemFree(pvDecoded);
    else
        RTMemSaferFree(pvDecoded, cbDecoded);
    return rc;
}


/**
 * Checks if the content of a file looks to be binary or not.
 *
 * @returns true if likely to be binary, false if not binary.
 * @param   pbFile              The file bytes to scan.
 * @param   cbFile              The number of bytes.
 * @param   fFlags              RTCRPEMREADFILE_F_XXX
 */
static bool rtCrPemIsBinaryBlob(uint8_t const *pbFile, size_t cbFile, uint32_t fFlags)
{
    if (fFlags & RTCRPEMREADFILE_F_ONLY_PEM)
        return false;

    /*
     * Well formed PEM files should probably only contain 7-bit ASCII and
     * restrict thenselfs to the following control characters:
     *      tab, newline, return, form feed
     *
     * However, if we want to read PEM files which contains human readable
     * certificate details before or after each base-64 section, we can't stick
     * to 7-bit ASCII.  We could say it must be UTF-8, but that's probably to
     * limited as well.  So, we'll settle for detecting binary files by control
     * characters alone (safe enough for DER encoded stuff, I think).
     */
    while (cbFile-- > 0)
    {
        uint8_t const b = *pbFile++;
        if (b < 32 && b != '\t' && b != '\n' && b != '\r' && b != '\f')
        {
            /* Ignore EOT (4), SUB (26) and NUL (0) at the end of the file. */
            if (   (b == 4 || b == 26)
                && (   cbFile == 0
                    || (   cbFile == 1
                        && *pbFile == '\0')))
                return false;

            if (b == 0 && cbFile == 0)
                return false;

            return true;
        }
    }
    return false;
}


RTDECL(int) RTCrPemFreeSections(PCRTCRPEMSECTION pSectionHead)
{
    while (pSectionHead != NULL)
    {
        PRTCRPEMSECTION pFree = (PRTCRPEMSECTION)pSectionHead;
        pSectionHead = pSectionHead->pNext;
        ASMCompilerBarrier(); /* paranoia */

        if (pFree->pbData)
        {
            if (!pFree->fSensitive)
                RTMemFree(pFree->pbData);
            else
                RTMemSaferFree(pFree->pbData, pFree->cbData);
            pFree->pbData = NULL;
            pFree->cbData = 0;
        }

        PRTCRPEMFIELD pField = (PRTCRPEMFIELD)pFree->pFieldHead;
        if (pField)
        {
            pFree->pFieldHead = NULL;
            do
            {
                PRTCRPEMFIELD pFreeField = pField;
                pField = (PRTCRPEMFIELD)pField->pNext;
                ASMCompilerBarrier(); /* paranoia */

                pFreeField->pszValue = NULL;
                RTMemFree(pFreeField);
            } while (pField);
        }

        RTMemFree(pFree);
    }
    return VINF_SUCCESS;
}


RTDECL(int) RTCrPemParseContent(void const *pvContent, size_t cbContent, uint32_t fFlags,
                                PCRTCRPEMMARKER paMarkers, size_t cMarkers,
                                PCRTCRPEMSECTION *ppSectionHead, PRTERRINFO pErrInfo)
{
    RT_NOREF_PV(pErrInfo);

    /*
     * Input validation.
     */
    AssertPtr(ppSectionHead);
    *ppSectionHead = NULL;
    AssertReturn(cbContent, VINF_EOF);
    AssertPtr(pvContent);
    AssertPtr(paMarkers);
    AssertReturn(!(fFlags & ~RTCRPEMREADFILE_F_VALID_MASK), VERR_INVALID_FLAGS);

    /*
     * Pre-allocate a section.
     */
    int rc = VINF_SUCCESS;
    PRTCRPEMSECTION pSection = (PRTCRPEMSECTION)RTMemAllocZ(sizeof(*pSection));
    if (pSection)
    {
        bool const fSensitive = RT_BOOL(fFlags & RTCRPEMREADFILE_F_SENSITIVE);

        /*
         * Try locate the first section.
         */
        uint8_t const  *pbContent = (uint8_t const *)pvContent;
        size_t          offBegin, offEnd, offResume;
        PCRTCRPEMMARKER pMatch;
        if (   !rtCrPemIsBinaryBlob(pbContent, cbContent, fFlags)
            && rtCrPemFindMarkerSection(pbContent, cbContent, 0 /*offStart*/, paMarkers, cMarkers,
                                        &pMatch, &offBegin, &offEnd, &offResume) )
        {
            PCRTCRPEMSECTION *ppNext = ppSectionHead;
            for (;;)
            {
                //pSection->pNext       = NULL;
                pSection->pMarker       = pMatch;
                //pSection->pbData      = NULL;
                //pSection->cbData      = 0;
                //pSection->pFieldHead  = NULL;
                pSection->fSensitive    = fSensitive;

                *ppNext = pSection;
                ppNext = &pSection->pNext;

                /*
                 * Decode the section.
                 */
                size_t cbFields = 0;
                int rc2 = rtCrPemProcessFields(pSection, pbContent + offBegin, offEnd - offBegin, &cbFields);
                offBegin += cbFields;
                if (RT_SUCCESS(rc2))
                    rc2 = rtCrPemDecodeBase64(pbContent + offBegin, offEnd - offBegin, fSensitive,
                                              (void **)&pSection->pbData, &pSection->cbData);
                if (RT_FAILURE(rc2))
                {
                    pSection->pbData = NULL;
                    pSection->cbData = 0;
                    if (   rc2 == VERR_INVALID_BASE64_ENCODING
                        && (fFlags & RTCRPEMREADFILE_F_CONTINUE_ON_ENCODING_ERROR))
                        rc = -rc2;
                    else
                    {
                        rc = rc2;
                        break;
                    }
                }

                /*
                 * More sections?
                 */
                if (   offResume + 12 >= cbContent
                    || offResume      >= cbContent
                    || !rtCrPemFindMarkerSection(pbContent, cbContent, offResume, paMarkers, cMarkers,
                                                 &pMatch, &offBegin, &offEnd, &offResume) )
                    break; /* No. */

                /* Ok, allocate a new record for it. */
                pSection = (PRTCRPEMSECTION)RTMemAllocZ(sizeof(*pSection));
                if (RT_UNLIKELY(!pSection))
                {
                    rc = VERR_NO_MEMORY;
                    break;
                }
            }
            if (RT_SUCCESS(rc))
                return rc;

            RTCrPemFreeSections(*ppSectionHead);
        }
        else
        {
            if (!(fFlags & RTCRPEMREADFILE_F_ONLY_PEM))
            {
                /*
                 * No PEM section found.  Return the whole file as one binary section.
                 */
                //pSection->pNext       = NULL;
                //pSection->pMarker     = NULL;
                //pSection->pFieldHead  = NULL;
                pSection->cbData        = cbContent;
                pSection->fSensitive    = fSensitive;
                if (!fSensitive)
                    pSection->pbData    = (uint8_t *)RTMemDup(pbContent, cbContent);
                else
                {
                    pSection->pbData    = (uint8_t *)RTMemSaferAllocZ(cbContent);
                    if (pSection->pbData)
                        memcpy(pSection->pbData, pbContent, cbContent);
                }
                if (pSection->pbData)
                {
                    *ppSectionHead = pSection;
                    return VINF_SUCCESS;
                }

                rc = VERR_NO_MEMORY;
            }
            else
                rc = VWRN_NOT_FOUND;
            RTMemFree(pSection);
        }
    }
    else
        rc = VERR_NO_MEMORY;
    *ppSectionHead = NULL;
    return rc;
}


RTDECL(int) RTCrPemReadFile(const char *pszFilename, uint32_t fFlags, PCRTCRPEMMARKER paMarkers, size_t cMarkers,
                            PCRTCRPEMSECTION *ppSectionHead, PRTERRINFO pErrInfo)
{
    *ppSectionHead = NULL;
    AssertReturn(!(fFlags & ~RTCRPEMREADFILE_F_VALID_MASK), VERR_INVALID_FLAGS);

    size_t      cbContent;
    void        *pvContent;
    int rc = RTFileReadAllEx(pszFilename, 0, 64U*_1M, RTFILE_RDALL_O_DENY_WRITE, &pvContent, &cbContent);
    if (RT_SUCCESS(rc))
    {
        rc = RTCrPemParseContent(pvContent, cbContent, fFlags, paMarkers, cMarkers, ppSectionHead, pErrInfo);
        if (fFlags & RTCRPEMREADFILE_F_SENSITIVE)
            RTMemWipeThoroughly(pvContent, cbContent, 3);
        RTFileReadAllFree(pvContent, cbContent);
    }
    else
        rc = RTErrInfoSetF(pErrInfo, rc, "RTFileReadAllEx failed with %Rrc on '%s'", rc, pszFilename);
    return rc;
}


RTDECL(const char *) RTCrPemFindFirstSectionInContent(void const *pvContent, size_t cbContent,
                                                      PCRTCRPEMMARKER paMarkers, size_t cMarkers)
{
    size_t offBegin;
    if (rtCrPemFindMarker((uint8_t *)pvContent, cbContent, 0, "BEGIN", 5, paMarkers, cMarkers, NULL, &offBegin, NULL))
        return (const char *)pvContent + offBegin;
    return NULL;
}

