/* $Id: pemfile.cpp $ */
/** @file
 * IPRT - Crypto - PEM file reader / writer.
 */

/*
 * Copyright (C) 2006-2014 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/base64.h>
#include <iprt/ctype.h>
#include <iprt/err.h>
#include <iprt/mem.h>
#include <iprt/file.h>
#include <iprt/string.h>



/**
 * Looks for a PEM-like marker.
 *
 * @returns true if found, fasle 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;
                uint32_t        iMarker = 0;
                while (iMarker < cMarkers)
                {
                    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 || !RT_C_IS_BLANK(*pbContent))
                            break;
                        do
                        {
                            pbContent++;
                            cbContent--;
                        } while (cbContent > 0 && RT_C_IS_BLANK(*pbContent));

                        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;
                        }
                    } /* 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;
}



/**
 * 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   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, 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 = RTMemAlloc(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;
    }
    RTMemFree(pvDecoded);
    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.
 */
static bool rtCrPemIsBinaryFile(uint8_t *pbFile, size_t cbFile)
{
    /*
     * Assume a well formed PEM file contains only 7-bit ASCII and restricts
     * itself to the following control characters:
     *      tab, newline, return, form feed
     */
    while (cbFile-- > 0)
    {
        uint8_t const b = *pbFile++;
        if (   b >= 0x7f
            || (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 true;
            if (b == 0 && cbFile == 0)
                return true;
            return false;
        }
    }
    return true;
}


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

        if (pFree->pMarker)
        {
            if (pFree->pbData)
            {
                RTMemFree(pFree->pbData);
                pFree->pbData = NULL;
                pFree->cbData = 0;
            }

            if (pFree->pszPreamble)
            {
                RTMemFree(pFree->pszPreamble);
                pFree->pszPreamble = NULL;
                pFree->cchPreamble = 0;
            }
        }
        else
        {
            RTFileReadAllFree(pFree->pbData, pFree->cbData);
            Assert(!pFree->pszPreamble);
        }
        pFree->pbData = NULL;
        pFree->cbData = 0;
    }
    return VINF_SUCCESS;
}


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

    size_t      cbContent;
    uint8_t    *pbContent;
    int rc = RTFileReadAllEx(pszFilename, 0, 64U*_1M, RTFILE_RDALL_O_DENY_WRITE, (void **)&pbContent, &cbContent);
    if (RT_SUCCESS(rc))
    {
        PRTCRPEMSECTION pSection = (PRTCRPEMSECTION)RTMemAllocZ(sizeof(*pSection));
        if (pSection)
        {
            /*
             * Try locate the first section.
             */
            size_t          offBegin, offEnd, offResume;
            PCRTCRPEMMARKER pMatch;
            if (   !rtCrPemIsBinaryFile(pbContent, cbContent)
                && 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->pszPreamble   = NULL;
                    //pSection->cchPreamble   = 0;

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

                    /* Decode the section. */
                    /** @todo copy the preamble as well. */
                    rc = rtCrPemDecodeBase64(pbContent + offBegin, offEnd - offBegin,
                                             (void **)&pSection->pbData, &pSection->cbData);
                    if (RT_FAILURE(rc))
                    {
                        pSection->pbData = NULL;
                        pSection->cbData = 0;
                        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))
                {
                    RTFileReadAllFree(pbContent, cbContent);
                    return rc;
                }

                RTCrPemFreeSections(*ppSectionHead);
            }
            else
            {
                /*
                 * No PEM section found.  Return the whole file as one binary section.
                 */
                //pSection->pNext         = NULL;
                //pSection->pMarker       = NULL;
                pSection->pbData        = pbContent;
                pSection->cbData        = cbContent;
                //pSection->pszPreamble   = NULL;
                //pSection->cchPreamble   = 0;
                *ppSectionHead = pSection;
                return VINF_SUCCESS;
            }
        }
        else
            rc = VERR_NO_MEMORY;
        RTFileReadAllFree(pbContent, cbContent);
    }
    *ppSectionHead = NULL;
    return rc;
}

