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

    bs = PK11_GetBlockSize(cryptoMechType, c_param);
    if(bs) {
	/* check for proper badding in block algorithms.  this assumes
	 * RC2 cbc or a DES cbc variant.  and the padding is thus defined
	 */
	if(((int)dest->data[dest->len-1] <= bs) && 
	   ((int)dest->data[dest->len-1] > 0)) {
	    dest->len -= (int)dest->data[dest->len-1];
	} 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;
}

