/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * PKCS7 implementation -- the exported parts that are used whether
 * creating or decoding.
 */

#include "p7local.h"

#include "cert.h"
#include "secitem.h"
#include "secoid.h"
#include "pk11func.h"

/*
 * Find out (saving pointer to lookup result for future reference)
 * and return the inner content type.
 */
SECOidTag
SEC_PKCS7ContentType(SEC_PKCS7ContentInfo *cinfo)
{
    if (cinfo->contentTypeTag == NULL)
        cinfo->contentTypeTag = SECOID_FindOID(&(cinfo->contentType));

    if (cinfo->contentTypeTag == NULL)
        return SEC_OID_UNKNOWN;

    return cinfo->contentTypeTag->offset;
}

/*
 * Destroy a PKCS7 contentInfo and all of its sub-pieces.
 */
void
SEC_PKCS7DestroyContentInfo(SEC_PKCS7ContentInfo *cinfo)
{
    SECOidTag kind;
    CERTCertificate **certs;
    CERTCertificateList **certlists;
    SEC_PKCS7SignerInfo **signerinfos;
    SEC_PKCS7RecipientInfo **recipientinfos;

    PORT_Assert(cinfo->refCount > 0);
    if (cinfo->refCount <= 0)
        return;

    cinfo->refCount--;
    if (cinfo->refCount > 0)
        return;

    certs = NULL;
    certlists = NULL;
    recipientinfos = NULL;
    signerinfos = NULL;

    kind = SEC_PKCS7ContentType(cinfo);
    switch (kind) {
        case SEC_OID_PKCS7_ENVELOPED_DATA: {
            SEC_PKCS7EnvelopedData *edp;

            edp = cinfo->content.envelopedData;
            if (edp != NULL) {
                recipientinfos = edp->recipientInfos;
            }
        } break;
        case SEC_OID_PKCS7_SIGNED_DATA: {
            SEC_PKCS7SignedData *sdp;

            sdp = cinfo->content.signedData;
            if (sdp != NULL) {
                certs = sdp->certs;
                certlists = sdp->certLists;
                signerinfos = sdp->signerInfos;
            }
        } break;
        case SEC_OID_PKCS7_SIGNED_ENVELOPED_DATA: {
            SEC_PKCS7SignedAndEnvelopedData *saedp;

            saedp = cinfo->content.signedAndEnvelopedData;
            if (saedp != NULL) {
                certs = saedp->certs;
                certlists = saedp->certLists;
                recipientinfos = saedp->recipientInfos;
                signerinfos = saedp->signerInfos;
                if (saedp->sigKey != NULL)
                    PK11_FreeSymKey(saedp->sigKey);
            }
        } break;
        default:
            /* XXX Anything else that needs to be "manually" freed/destroyed? */
            break;
    }

    if (certs != NULL) {
        CERTCertificate *cert;

        while ((cert = *certs++) != NULL) {
            CERT_DestroyCertificate(cert);
        }
    }

    if (certlists != NULL) {
        CERTCertificateList *certlist;

        while ((certlist = *certlists++) != NULL) {
            CERT_DestroyCertificateList(certlist);
        }
    }

    if (recipientinfos != NULL) {
        SEC_PKCS7RecipientInfo *ri;

        while ((ri = *recipientinfos++) != NULL) {
            if (ri->cert != NULL)
                CERT_DestroyCertificate(ri->cert);
        }
    }

    if (signerinfos != NULL) {
        SEC_PKCS7SignerInfo *si;

        while ((si = *signerinfos++) != NULL) {
            if (si->cert != NULL)
                CERT_DestroyCertificate(si->cert);
            if (si->certList != NULL)
                CERT_DestroyCertificateList(si->certList);
        }
    }

    if (cinfo->poolp != NULL) {
        PORT_FreeArena(cinfo->poolp, PR_FALSE); /* XXX clear it? */
    }
}

/*
 * Return a copy of the given contentInfo.  The copy may be virtual
 * or may be real -- either way, the result needs to be passed to
 * SEC_PKCS7DestroyContentInfo later (as does the original).
 */
SEC_PKCS7ContentInfo *
SEC_PKCS7CopyContentInfo(SEC_PKCS7ContentInfo *cinfo)
{
    if (cinfo == NULL)
        return NULL;

    PORT_Assert(cinfo->refCount > 0);

    if (cinfo->created) {
        /*
         * Want to do a real copy of these; otherwise subsequent
         * changes made to either copy are likely to be a surprise.
         * XXX I suspect that this will not actually be called for yet,
         * which is why the assert, so to notice if it is...
         */
        PORT_Assert(0);
        /*
         * XXX Create a new pool here, and copy everything from
         * within.  For cert stuff, need to call the appropriate
         * copy functions, etc.
         */
    }

    cinfo->refCount++;
    return cinfo;
}

/*
 * Return a pointer to the actual content.  In the case of those types
 * which are encrypted, this returns the *plain* content.
 * XXX Needs revisiting if/when we handle nested encrypted types.
 */
SECItem *
SEC_PKCS7GetContent(SEC_PKCS7ContentInfo *cinfo)
{
    SECOidTag kind;

    kind = SEC_PKCS7ContentType(cinfo);
    switch (kind) {
        case SEC_OID_PKCS7_DATA:
            return cinfo->content.data;
        case SEC_OID_PKCS7_DIGESTED_DATA: {
            SEC_PKCS7DigestedData *digd;

            digd = cinfo->content.digestedData;
            if (digd == NULL)
                break;
            return SEC_PKCS7GetContent(&(digd->contentInfo));
        }
        case SEC_OID_PKCS7_ENCRYPTED_DATA: {
            SEC_PKCS7EncryptedData *encd;

            encd = cinfo->content.encryptedData;
            if (encd == NULL)
                break;
            return &(encd->encContentInfo.plainContent);
        }
        case SEC_OID_PKCS7_ENVELOPED_DATA: {
            SEC_PKCS7EnvelopedData *envd;

            envd = cinfo->content.envelopedData;
            if (envd == NULL)
                break;
            return &(envd->encContentInfo.plainContent);
        }
        case SEC_OID_PKCS7_SIGNED_DATA: {
            SEC_PKCS7SignedData *sigd;

            sigd = cinfo->content.signedData;
            if (sigd == NULL)
                break;
            return SEC_PKCS7GetContent(&(sigd->contentInfo));
        }
        case SEC_OID_PKCS7_SIGNED_ENVELOPED_DATA: {
            SEC_PKCS7SignedAndEnvelopedData *saed;

            saed = cinfo->content.signedAndEnvelopedData;
            if (saed == NULL)
                break;
            return &(saed->encContentInfo.plainContent);
        }
        default:
            PORT_Assert(0);
            break;
    }

    return NULL;
}

/*
 * XXX Fix the placement and formatting of the
 * following routines (i.e. make them consistent with the rest of
 * the pkcs7 code -- I think some/many belong in other files and
 * they all need a formatting/style rehaul)
 */

/* retrieve the algorithm identifier for encrypted data.
 * the identifier returned is a copy of the algorithm identifier
 * in the content info and needs to be freed after being used.
 *
 *   cinfo is the content info for which to retrieve the
 *     encryption algorithm.
 *
 * if the content info is not encrypted data or an error
 * occurs NULL is returned.
 */
SECAlgorithmID *
SEC_PKCS7GetEncryptionAlgorithm(SEC_PKCS7ContentInfo *cinfo)
{
    SECAlgorithmID *alg = 0;
    switch (SEC_PKCS7ContentType(cinfo)) {
        case SEC_OID_PKCS7_ENCRYPTED_DATA:
            alg = &cinfo->content.encryptedData->encContentInfo.contentEncAlg;
            break;
        case SEC_OID_PKCS7_ENVELOPED_DATA:
            alg = &cinfo->content.envelopedData->encContentInfo.contentEncAlg;
            break;
        case SEC_OID_PKCS7_SIGNED_ENVELOPED_DATA:
            alg = &cinfo->content.signedAndEnvelopedData
                       ->encContentInfo.contentEncAlg;
            break;
        default:
            alg = 0;
            break;
    }

    return alg;
}

/* set the content of the content info.  For data content infos,
 * the data is set.  For encrytped content infos, the plainContent
 * is set, and is expected to be encrypted later.
 *
 * cinfo is the content info where the data will be set
 *
 * buf is a buffer of the data to set
 *
 * len is the length of the data being set.
 *
 * in the event of an error, SECFailure is returned.  SECSuccess
 * indicates the content was successfully set.
 */
SECStatus
SEC_PKCS7SetContent(SEC_PKCS7ContentInfo *cinfo,
                    const char *buf,
                    unsigned long len)
{
    SECOidTag cinfo_type;
    SECStatus rv;
    SECItem content;
    SECOidData *contentTypeTag = NULL;

    content.type = siBuffer;
    content.data = (unsigned char *)buf;
    content.len = len;

    cinfo_type = SEC_PKCS7ContentType(cinfo);

    /* set inner content */
    switch (cinfo_type) {
        case SEC_OID_PKCS7_SIGNED_DATA:
            if (content.len > 0) {
                /* we "leak" the old content here, but as it's all in the pool */
                /* it does not really matter */

                /* create content item if necessary */
                if (cinfo->content.signedData->contentInfo.content.data == NULL)
                    cinfo->content.signedData->contentInfo.content.data = SECITEM_AllocItem(cinfo->poolp, NULL, 0);
                rv = SECITEM_CopyItem(cinfo->poolp,
                                      cinfo->content.signedData->contentInfo.content.data,
                                      &content);
            } else {
                cinfo->content.signedData->contentInfo.content.data->data = NULL;
                cinfo->content.signedData->contentInfo.content.data->len = 0;
                rv = SECSuccess;
            }
            if (rv == SECFailure)
                goto loser;

            break;
        case SEC_OID_PKCS7_ENCRYPTED_DATA:
            /* XXX this forces the inner content type to be "data" */
            /* do we really want to override without asking or reason? */
            contentTypeTag = SECOID_FindOIDByTag(SEC_OID_PKCS7_DATA);
            if (contentTypeTag == NULL)
                goto loser;
            rv = SECITEM_CopyItem(cinfo->poolp,
                                  &(cinfo->content.encryptedData->encContentInfo.contentType),
                                  &(contentTypeTag->oid));
            if (rv == SECFailure)
                goto loser;
            if (content.len > 0) {
                rv = SECITEM_CopyItem(cinfo->poolp,
                                      &(cinfo->content.encryptedData->encContentInfo.plainContent),
                                      &content);
            } else {
                cinfo->content.encryptedData->encContentInfo.plainContent.data = NULL;
                cinfo->content.encryptedData->encContentInfo.encContent.data = NULL;
                cinfo->content.encryptedData->encContentInfo.plainContent.len = 0;
                cinfo->content.encryptedData->encContentInfo.encContent.len = 0;
                rv = SECSuccess;
            }
            if (rv == SECFailure)
                goto loser;
            break;
        case SEC_OID_PKCS7_DATA:
            cinfo->content.data = (SECItem *)PORT_ArenaZAlloc(cinfo->poolp,
                                                              sizeof(SECItem));
            if (cinfo->content.data == NULL)
                goto loser;
            if (content.len > 0) {
                rv = SECITEM_CopyItem(cinfo->poolp,
                                      cinfo->content.data, &content);
            } else {
                /* handle case with NULL content */
                rv = SECSuccess;
            }
            if (rv == SECFailure)
                goto loser;
            break;
        default:
            goto loser;
    }

    return SECSuccess;

loser:

    return SECFailure;
}

/* the content of an encrypted data content info is encrypted.
 * it is assumed that for encrypted data, that the data has already
 * been set and is in the "plainContent" field of the content info.
 *
 * cinfo is the content info to encrypt
 *
 * key is the key with which to perform the encryption.  if the
 *     algorithm is a password based encryption algorithm, the
 *     key is actually a password which will be processed per
 *     PKCS #5.
 *
 * in the event of an error, SECFailure is returned.  SECSuccess
 * indicates a success.
 */
SECStatus
SEC_PKCS7EncryptContents(PLArenaPool *poolp,
                         SEC_PKCS7ContentInfo *cinfo,
                         SECItem *key,
                         void *wincx)
{
    SECAlgorithmID *algid = NULL;
    SECItem *src;
    SECItem *dest;
    SECItem *blocked_data = NULL;
    void *mark;
    void *cx;
    PK11SymKey *eKey = NULL;
    PK11SlotInfo *slot = NULL;

    CK_MECHANISM_TYPE cryptoMechType;
    int bs;
    SECStatus rv = SECFailure;
    SECItem *c_param = NULL;

    if ((cinfo == NULL) || (key == NULL))
        return SECFailure;

    if (SEC_PKCS7ContentType(cinfo) != SEC_OID_PKCS7_ENCRYPTED_DATA)
        return SECFailure;

    algid = SEC_PKCS7GetEncryptionAlgorithm(cinfo);
    if (algid == NULL)
        return SECFailure;

    if (poolp == NULL)
        poolp = cinfo->poolp;

    mark = PORT_ArenaMark(poolp);

    src = &cinfo->content.encryptedData->encContentInfo.plainContent;
    dest = &cinfo->content.encryptedData->encContentInfo.encContent;
    dest->data = (unsigned char *)PORT_ArenaZAlloc(poolp, (src->len + 64));
    dest->len = (src->len + 64);
    if (dest->data == NULL) {
        rv = SECFailure;
        goto loser;
    }

    slot = PK11_GetInternalKeySlot();
    if (slot == NULL) {
        rv = SECFailure;
        goto loser;
    }

    eKey = PK11_PBEKeyGen(slot, algid, key, PR_FALSE, wincx);
    if (eKey == NULL) {
        rv = SECFailure;
        goto loser;
    }

    cryptoMechType = PK11_GetPBECryptoMechanism(algid, &c_param, key);
    if (cryptoMechType == CKM_INVALID_MECHANISM) {
        rv = SECFailure;
        goto loser;
    }

    /* block according to PKCS 8 */
    bs = PK11_GetBlockSize(cryptoMechType, c_param);
    rv = SECSuccess;
    if (bs) {
        char pad_char;
        pad_char = (char)(bs - (src->len % bs));
        if (src->len % bs) {
            rv = SECSuccess;
            blocked_data = PK11_BlockData(src, bs);
            if (blocked_data) {
                PORT_Memset((blocked_data->data + blocked_data->len - (int)pad_char),
                            pad_char, (int)pad_char);
            } else {
                rv = SECFailure;
                goto loser;
            }
        } else {
            blocked_data = SECITEM_DupItem(src);
            if (blocked_data) {
                blocked_data->data = (unsigned char *)PORT_Realloc(
                    blocked_data->data,
                    blocked_data->len + bs);
                if (blocked_data->data) {
                    blocked_data->len += bs;
                    PORT_Memset((blocked_data->data + src->len), (char)bs, bs);
                } else {
                    rv = SECFailure;
                    goto loser;
                }
            } else {
                rv = SECFailure;
                goto loser;
            }
        }
    } else {
        blocked_data = SECITEM_DupItem(src);
        if (!blocked_data) {
            rv = SECFailure;
            goto loser;
        }
    }

    cx = PK11_CreateContextBySymKey(cryptoMechType, CKA_ENCRYPT,
                                    eKey, c_param);
    if (cx == NULL) {
        rv = SECFailure;
        goto loser;
    }

    rv = PK11_CipherOp((PK11Context *)cx, dest->data, (int *)(&dest->len),
                       (int)(src->len + 64), blocked_data->data,
                       (int)blocked_data->len);
    PK11_DestroyContext((PK11Context *)cx, PR_TRUE);

loser:
    /* let success fall through */
    if (blocked_data != NULL)
        SECITEM_ZfreeItem(blocked_data, PR_TRUE);

    if (rv == SECFailure)
        PORT_ArenaRelease(poolp, mark);
    else
        PORT_ArenaUnmark(poolp, mark);

    if (eKey != NULL)
        PK11_FreeSymKey(eKey);

    if (slot != NULL)
        PK11_FreeSlot(slot);

    if (c_param != NULL)
        SECITEM_ZfreeItem(c_param, PR_TRUE);

    return rv;
}

/* the content of an encrypted data content info is decrypted.
 * it is assumed that for encrypted data, that the data has already
 * been set and is in the "encContent" field of the content info.
 *
 * cinfo is the content info to decrypt
 *
 * key is the key with which to perform the decryption.  if the
 *     algorithm is a password based encryption algorithm, the
 *     key is actually a password which will be processed per
 *     PKCS #5.
 *
 * in the event of an error, SECFailure is returned.  SECSuccess
 * indicates a success.
 */
SECStatus
SEC_PKCS7DecryptContents(PLArenaPool *poolp,
                         SEC_PKCS7ContentInfo *cinfo,
                         SECItem *key,
                         void *wincx)
{
    SECAlgorithmID *algid = NULL;
    SECStatus rv = SECFailure;
    SECItem *dest, *src;
    void *mark;

    PK11SymKey *eKey = NULL;
    PK11SlotInfo *slot = NULL;
    CK_MECHANISM_TYPE cryptoMechType;
    void *cx;
    SECItem *c_param = NULL;
    int bs;

    if ((cinfo == NULL) || (key == NULL))
        return SECFailure;

    if (SEC_PKCS7ContentType(cinfo) != SEC_OID_PKCS7_ENCRYPTED_DATA)
        return SECFailure;

    algid = SEC_PKCS7GetEncryptionAlgorithm(cinfo);
    if (algid == NULL)
        return SECFailure;

    if (poolp == NULL)
        poolp = cinfo->poolp;

    mark = PORT_ArenaMark(poolp);

    src = &cinfo->content.encryptedData->encContentInfo.encContent;
    dest = &cinfo->content.encryptedData->encContentInfo.plainContent;
    dest->data = (unsigned char *)PORT_ArenaZAlloc(poolp, (src->len + 64));
    dest->len = (src->len + 64);
    if (dest->data == NULL) {
        rv = SECFailure;
        goto loser;
    }

    slot = PK11_GetInternalKeySlot();
    if (slot == NULL) {
        rv = SECFailure;
        goto loser;
    }

    eKey = PK11_PBEKeyGen(slot, algid, key, PR_FALSE, wincx);
    if (eKey == NULL) {
        rv = SECFailure;
        goto loser;
    }

    cryptoMechType = PK11_GetPBECryptoMechanism(algid, &c_param, key);
    if (cryptoMechType == CKM_INVALID_MECHANISM) {
        rv = SECFailure;
        goto loser;
    }

    cx = PK11_CreateContextBySymKey(cryptoMechType, CKA_DECRYPT,
                                    eKey, c_param);
    if (cx == NULL) {
        rv = SECFailure;
        goto loser;
    }

    rv = PK11_CipherOp((PK11Context *)cx, dest->data, (int *)(&dest->len),
                       (int)(src->len + 64), src->data, (int)src->len);
    PK11_DestroyContext((PK11Context *)cx, PR_TRUE);
    /* PK11_CipherOp may leave dest->len at 0; bail out before the padding
     * check so dest->len - 1 does not underflow. */
    if (rv != SECSuccess) {
        goto loser;
    }

    bs = PK11_GetBlockSize(cryptoMechType, c_param);
    if (bs) {
        /* Check for proper PKCS#7 padding: the last byte gives the number of
         * pad bytes, which must be in [1, bs]. */
        unsigned int pad = dest->data[dest->len - 1];
        if ((dest->len >= (unsigned int)bs) &&
            pad >= 1 && pad <= (unsigned int)bs) {
            dest->len -= pad;
        } else {
            rv = SECFailure;
            /* set an error ? */
        }
    }

loser:
    /* let success fall through */
    if (rv == SECFailure)
        PORT_ArenaRelease(poolp, mark);
    else
        PORT_ArenaUnmark(poolp, mark);

    if (eKey != NULL)
        PK11_FreeSymKey(eKey);

    if (slot != NULL)
        PK11_FreeSlot(slot);

    if (c_param != NULL)
        SECITEM_ZfreeItem(c_param, PR_TRUE);

    return rv;
}

SECItem **
SEC_PKCS7GetCertificateList(SEC_PKCS7ContentInfo *cinfo)
{
    switch (SEC_PKCS7ContentType(cinfo)) {
        case SEC_OID_PKCS7_SIGNED_DATA:
            return cinfo->content.signedData->rawCerts;
            break;
        default:
            return NULL;
            break;
    }
}

int
SEC_PKCS7GetKeyLength(SEC_PKCS7ContentInfo *cinfo)
{
    if (cinfo->contentTypeTag->offset == SEC_OID_PKCS7_ENVELOPED_DATA)
        return cinfo->content.envelopedData->encContentInfo.keysize;
    else
        return 0;
}
