/* 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/. */

#include "pkcs11.h"

#ifndef DEVM_H
#include "devm.h"
#endif /* DEVM_H */

#ifndef CKHELPER_H
#include "ckhelper.h"
#endif /* CKHELPER_H */

extern const NSSError NSS_ERROR_DEVICE_ERROR;

static const CK_BBOOL s_true = CK_TRUE;
NSS_IMPLEMENT_DATA const NSSItem
g_ck_true = { (CK_VOID_PTR)&s_true, sizeof(s_true) };

static const CK_BBOOL s_false = CK_FALSE;
NSS_IMPLEMENT_DATA const NSSItem
g_ck_false = { (CK_VOID_PTR)&s_false, sizeof(s_false) };

static const CK_OBJECT_CLASS s_class_cert = CKO_CERTIFICATE;
NSS_IMPLEMENT_DATA const NSSItem
g_ck_class_cert = { (CK_VOID_PTR)&s_class_cert, sizeof(s_class_cert) };

static const CK_OBJECT_CLASS s_class_pubkey = CKO_PUBLIC_KEY;
NSS_IMPLEMENT_DATA const NSSItem
g_ck_class_pubkey = { (CK_VOID_PTR)&s_class_pubkey, sizeof(s_class_pubkey) };

static const CK_OBJECT_CLASS s_class_privkey = CKO_PRIVATE_KEY;
NSS_IMPLEMENT_DATA const NSSItem
g_ck_class_privkey = { (CK_VOID_PTR)&s_class_privkey, sizeof(s_class_privkey) };

static PRBool
is_string_attribute (
  CK_ATTRIBUTE_TYPE aType
)
{
    PRBool isString;
    switch (aType) {
    case CKA_LABEL:
    case CKA_NSS_EMAIL:
	isString = PR_TRUE;
	break;
    default:
	isString = PR_FALSE;
	break;
    }
    return isString;
}

NSS_IMPLEMENT PRStatus 
nssCKObject_GetAttributes (
  CK_OBJECT_HANDLE object,
  CK_ATTRIBUTE_PTR obj_template,
  CK_ULONG count,
  NSSArena *arenaOpt,
  nssSession *session,
  NSSSlot *slot
)
{
    nssArenaMark *mark = NULL;
    CK_SESSION_HANDLE hSession;
    CK_ULONG i = 0;
    CK_RV ckrv;
    PRStatus nssrv;
    PRBool alloced = PR_FALSE;
    void *epv = nssSlot_GetCryptokiEPV(slot);
    hSession = session->handle; 
    if (arenaOpt) {
	mark = nssArena_Mark(arenaOpt);
	if (!mark) {
	    goto loser;
	}
    }
    nssSession_EnterMonitor(session);
    /* XXX kinda hacky, if the storage size is already in the first template
     * item, then skip the alloc portion
     */
    if (obj_template[0].ulValueLen == 0) {
	/* Get the storage size needed for each attribute */
	ckrv = CKAPI(epv)->C_GetAttributeValue(hSession,
	                                       object, obj_template, count);
	if (ckrv != CKR_OK && 
	    ckrv != CKR_ATTRIBUTE_TYPE_INVALID &&
	    ckrv != CKR_ATTRIBUTE_SENSITIVE) 
	{
	    nssSession_ExitMonitor(session);
	    nss_SetError(NSS_ERROR_DEVICE_ERROR);
	    goto loser;
	}
	/* Allocate memory for each attribute. */
	for (i=0; i<count; i++) {
	    CK_ULONG ulValueLen = obj_template[i].ulValueLen;
	    if (ulValueLen == 0 || ulValueLen == (CK_ULONG) -1) {
		obj_template[i].pValue = NULL;
		obj_template[i].ulValueLen = 0;
		continue;
	    }
	    if (is_string_attribute(obj_template[i].type)) {
		ulValueLen++;
	    }
	    obj_template[i].pValue = nss_ZAlloc(arenaOpt, ulValueLen);
	    if (!obj_template[i].pValue) {
		nssSession_ExitMonitor(session);
		goto loser;
	    }
	}
	alloced = PR_TRUE;
    }
    /* Obtain the actual attribute values. */
    ckrv = CKAPI(epv)->C_GetAttributeValue(hSession,
                                           object, obj_template, count);
    nssSession_ExitMonitor(session);
    if (ckrv != CKR_OK && 
        ckrv != CKR_ATTRIBUTE_TYPE_INVALID &&
        ckrv != CKR_ATTRIBUTE_SENSITIVE) 
    {
	nss_SetError(NSS_ERROR_DEVICE_ERROR);
	goto loser;
    }
    if (alloced && arenaOpt) {
	nssrv = nssArena_Unmark(arenaOpt, mark);
	if (nssrv != PR_SUCCESS) {
	    goto loser;
	}
    }

    if (count > 1 && ((ckrv == CKR_ATTRIBUTE_TYPE_INVALID) || 
					(ckrv == CKR_ATTRIBUTE_SENSITIVE))) {
	/* old tokens would keep the length of '0' and not deal with any
	 * of the attributes we passed. For those tokens read them one at
	 * a time */
	for (i=0; i < count; i++) {
	    if ((obj_template[i].ulValueLen == 0) 
				|| (obj_template[i].ulValueLen == -1)) {
		obj_template[i].ulValueLen=0;
		(void) nssCKObject_GetAttributes(object,&obj_template[i], 1,
			arenaOpt, session, slot);
	    }
	}
    }
    return PR_SUCCESS;
loser:
    if (alloced) {
	if (arenaOpt) {
	    /* release all arena memory allocated before the failure. */
	    (void)nssArena_Release(arenaOpt, mark);
	} else {
	    CK_ULONG j;
	    /* free each heap object that was allocated before the failure. */
	    for (j=0; j<i; j++) {
		nss_ZFreeIf(obj_template[j].pValue);
	    }
	}
    }
    return PR_FAILURE;
}

NSS_IMPLEMENT PRStatus
nssCKObject_GetAttributeItem (
  CK_OBJECT_HANDLE object,
  CK_ATTRIBUTE_TYPE attribute,
  NSSArena *arenaOpt,
  nssSession *session,
  NSSSlot *slot,
  NSSItem *rvItem
)
{
    CK_ATTRIBUTE attr = { 0, NULL, 0 };
    PRStatus nssrv;
    attr.type = attribute;
    nssrv = nssCKObject_GetAttributes(object, &attr, 1, 
                                      arenaOpt, session, slot);
    if (nssrv != PR_SUCCESS) {
	return nssrv;
    }
    rvItem->data = (void *)attr.pValue;
    rvItem->size = (PRUint32)attr.ulValueLen;
    return PR_SUCCESS;
}

NSS_IMPLEMENT PRBool
nssCKObject_IsAttributeTrue (
  CK_OBJECT_HANDLE object,
  CK_ATTRIBUTE_TYPE attribute,
  nssSession *session,
  NSSSlot *slot,
  PRStatus *rvStatus
)
{
    CK_BBOOL bool;
    CK_ATTRIBUTE_PTR attr;
    CK_ATTRIBUTE atemplate = { 0, NULL, 0 };
    CK_RV ckrv;
    void *epv = nssSlot_GetCryptokiEPV(slot);
    attr = &atemplate;
    NSS_CK_SET_ATTRIBUTE_VAR(attr, attribute, bool);
    nssSession_EnterMonitor(session);
    ckrv = CKAPI(epv)->C_GetAttributeValue(session->handle, object, 
                                           &atemplate, 1);
    nssSession_ExitMonitor(session);
    if (ckrv != CKR_OK) {
	*rvStatus = PR_FAILURE;
	return PR_FALSE;
    }
    *rvStatus = PR_SUCCESS;
    return (PRBool)(bool == CK_TRUE);
}

NSS_IMPLEMENT PRStatus 
nssCKObject_SetAttributes (
  CK_OBJECT_HANDLE object,
  CK_ATTRIBUTE_PTR obj_template,
  CK_ULONG count,
  nssSession *session,
  NSSSlot  *slot
)
{
    CK_RV ckrv;
    void *epv = nssSlot_GetCryptokiEPV(slot);
    nssSession_EnterMonitor(session);
    ckrv = CKAPI(epv)->C_SetAttributeValue(session->handle, object, 
                                           obj_template, count);
    nssSession_ExitMonitor(session);
    if (ckrv == CKR_OK) {
	return PR_SUCCESS;
    } else {
	return PR_FAILURE;
    }
}

NSS_IMPLEMENT PRBool
nssCKObject_IsTokenObjectTemplate (
  CK_ATTRIBUTE_PTR objectTemplate, 
  CK_ULONG otsize
)
{
    CK_ULONG ul;
    for (ul=0; ul<otsize; ul++) {
	if (objectTemplate[ul].type == CKA_TOKEN) {
	    return (*((CK_BBOOL*)objectTemplate[ul].pValue) == CK_TRUE);
	}
    }
    return PR_FALSE;
}

static NSSCertificateType
nss_cert_type_from_ck_attrib(CK_ATTRIBUTE_PTR attrib)
{
    CK_CERTIFICATE_TYPE ckCertType;
    if (!attrib->pValue) {
	/* default to PKIX */
	return NSSCertificateType_PKIX;
    }
    ckCertType = *((CK_ULONG *)attrib->pValue);
    switch (ckCertType) {
    case CKC_X_509:
	return NSSCertificateType_PKIX;
    default:
	break;
    }
    return NSSCertificateType_Unknown;
}

/* incoming pointers must be valid */
NSS_IMPLEMENT PRStatus
nssCryptokiCertificate_GetAttributes (
  nssCryptokiObject *certObject,
  nssSession *sessionOpt,
  NSSArena *arenaOpt,
  NSSCertificateType *certTypeOpt,
  NSSItem *idOpt,
  NSSDER *encodingOpt,
  NSSDER *issuerOpt,
  NSSDER *serialOpt,
  NSSDER *subjectOpt
)
{
    PRStatus status;
    PRUint32 i;
    nssSession *session;
    NSSSlot *slot;
    CK_ULONG template_size;
    CK_ATTRIBUTE_PTR attr;
    CK_ATTRIBUTE cert_template[6];
    /* Set up a template of all options chosen by caller */
    NSS_CK_TEMPLATE_START(cert_template, attr, template_size);
    if (certTypeOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_CERTIFICATE_TYPE);
    }
    if (idOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_ID);
    }
    if (encodingOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_VALUE);
    }
    if (issuerOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_ISSUER);
    }
    if (serialOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_SERIAL_NUMBER);
    }
    if (subjectOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_SUBJECT);
    }
    NSS_CK_TEMPLATE_FINISH(cert_template, attr, template_size);
    if (template_size == 0) {
	/* caller didn't want anything */
	return PR_SUCCESS;
    }

    status = nssToken_GetCachedObjectAttributes(certObject->token, arenaOpt,
                                                certObject, CKO_CERTIFICATE,
                                                cert_template, template_size);
    if (status != PR_SUCCESS) {

	session = sessionOpt ? 
	          sessionOpt : 
	          nssToken_GetDefaultSession(certObject->token);
	if (!session) {
	    nss_SetError(NSS_ERROR_INVALID_ARGUMENT);
	    return PR_FAILURE;
	}

	slot = nssToken_GetSlot(certObject->token);
	status = nssCKObject_GetAttributes(certObject->handle, 
	                                   cert_template, template_size,
	                                   arenaOpt, session, slot);
	nssSlot_Destroy(slot);
	if (status != PR_SUCCESS) {
	    return status;
	}
    }

    i=0;
    if (certTypeOpt) {
	*certTypeOpt = nss_cert_type_from_ck_attrib(&cert_template[i]); i++;
    }
    if (idOpt) {
	NSS_CK_ATTRIBUTE_TO_ITEM(&cert_template[i], idOpt); i++;
    }
    if (encodingOpt) {
	NSS_CK_ATTRIBUTE_TO_ITEM(&cert_template[i], encodingOpt); i++;
    }
    if (issuerOpt) {
	NSS_CK_ATTRIBUTE_TO_ITEM(&cert_template[i], issuerOpt); i++;
    }
    if (serialOpt) {
	NSS_CK_ATTRIBUTE_TO_ITEM(&cert_template[i], serialOpt); i++;
    }
    if (subjectOpt) {
	NSS_CK_ATTRIBUTE_TO_ITEM(&cert_template[i], subjectOpt); i++;
    }
    return PR_SUCCESS;
}

static nssTrustLevel 
get_nss_trust (
  CK_TRUST ckt
)
{
    nssTrustLevel t;
    switch (ckt) {
    case CKT_NSS_NOT_TRUSTED: t = nssTrustLevel_NotTrusted; break;
    case CKT_NSS_TRUSTED_DELEGATOR: t = nssTrustLevel_TrustedDelegator; 
	break;
    case CKT_NSS_VALID_DELEGATOR: t = nssTrustLevel_ValidDelegator; break;
    case CKT_NSS_TRUSTED: t = nssTrustLevel_Trusted; break;
    case CKT_NSS_MUST_VERIFY_TRUST: t = nssTrustLevel_MustVerify; break;
    case CKT_NSS_TRUST_UNKNOWN:
    default:
	t = nssTrustLevel_Unknown; break;
    }
    return t;
}

NSS_IMPLEMENT PRStatus
nssCryptokiTrust_GetAttributes (
  nssCryptokiObject *trustObject,
  nssSession *sessionOpt,
  NSSItem *sha1_hash,
  nssTrustLevel *serverAuth,
  nssTrustLevel *clientAuth,
  nssTrustLevel *codeSigning,
  nssTrustLevel *emailProtection,
  PRBool *stepUpApproved
)
{
    PRStatus status;
    NSSSlot *slot;
    nssSession *session;
    CK_BBOOL isToken = PR_FALSE;
    CK_BBOOL stepUp = PR_FALSE;
    CK_TRUST saTrust = CKT_NSS_TRUST_UNKNOWN;
    CK_TRUST caTrust = CKT_NSS_TRUST_UNKNOWN;
    CK_TRUST epTrust = CKT_NSS_TRUST_UNKNOWN;
    CK_TRUST csTrust = CKT_NSS_TRUST_UNKNOWN;
    CK_ATTRIBUTE_PTR attr;
    CK_ATTRIBUTE trust_template[7];
    CK_ATTRIBUTE_PTR sha1_hash_attr;
    CK_ULONG trust_size;

    /* Use the trust object to find the trust settings */
    NSS_CK_TEMPLATE_START(trust_template, attr, trust_size);
    NSS_CK_SET_ATTRIBUTE_VAR(attr, CKA_TOKEN,                  isToken);
    NSS_CK_SET_ATTRIBUTE_VAR(attr, CKA_TRUST_SERVER_AUTH,      saTrust);
    NSS_CK_SET_ATTRIBUTE_VAR(attr, CKA_TRUST_CLIENT_AUTH,      caTrust);
    NSS_CK_SET_ATTRIBUTE_VAR(attr, CKA_TRUST_EMAIL_PROTECTION, epTrust);
    NSS_CK_SET_ATTRIBUTE_VAR(attr, CKA_TRUST_CODE_SIGNING,     csTrust);
    NSS_CK_SET_ATTRIBUTE_VAR(attr, CKA_TRUST_STEP_UP_APPROVED, stepUp);
    sha1_hash_attr = attr;
    NSS_CK_SET_ATTRIBUTE_ITEM(attr, CKA_CERT_SHA1_HASH,     sha1_hash);
    NSS_CK_TEMPLATE_FINISH(trust_template, attr, trust_size);

    status = nssToken_GetCachedObjectAttributes(trustObject->token, NULL,
                                                trustObject, 
                                                CKO_NSS_TRUST,
                                                trust_template, trust_size);
    if (status != PR_SUCCESS) {
	session = sessionOpt ? 
	          sessionOpt : 
	          nssToken_GetDefaultSession(trustObject->token);
	if (!session) {
	    nss_SetError(NSS_ERROR_INVALID_ARGUMENT);
	    return PR_FAILURE;
	}

	slot = nssToken_GetSlot(trustObject->token);
	status = nssCKObject_GetAttributes(trustObject->handle,
	                                   trust_template, trust_size,
	                                   NULL, session, slot);
	nssSlot_Destroy(slot);
	if (status != PR_SUCCESS) {
	    return status;
	}
    }

    if (sha1_hash_attr->ulValueLen == -1) {
	/* The trust object does not have the CKA_CERT_SHA1_HASH attribute. */
	sha1_hash_attr->ulValueLen = 0;
    }
    sha1_hash->size = sha1_hash_attr->ulValueLen;
    *serverAuth = get_nss_trust(saTrust);
    *clientAuth = get_nss_trust(caTrust);
    *emailProtection = get_nss_trust(epTrust);
    *codeSigning = get_nss_trust(csTrust);
    *stepUpApproved = stepUp;
    return PR_SUCCESS;
}

NSS_IMPLEMENT PRStatus
nssCryptokiCRL_GetAttributes (
  nssCryptokiObject *crlObject,
  nssSession *sessionOpt,
  NSSArena *arenaOpt,
  NSSItem *encodingOpt,
  NSSItem *subjectOpt,
  CK_ULONG* crl_class,
  NSSUTF8 **urlOpt,
  PRBool *isKRLOpt
)
{
    PRStatus status;
    NSSSlot *slot;
    nssSession *session;
    CK_ATTRIBUTE_PTR attr;
    CK_ATTRIBUTE crl_template[7];
    CK_ULONG crl_size;
    PRUint32 i;

    NSS_CK_TEMPLATE_START(crl_template, attr, crl_size);
    if (crl_class) {
        NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_CLASS);
    }
    if (encodingOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_VALUE);
    }
    if (urlOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_NSS_URL);
    }
    if (isKRLOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_NSS_KRL);
    }
    if (subjectOpt) {
	NSS_CK_SET_ATTRIBUTE_NULL(attr, CKA_SUBJECT);
    }
    NSS_CK_TEMPLATE_FINISH(crl_template, attr, crl_size);

    status = nssToken_GetCachedObjectAttributes(crlObject->token, NULL,
                                                crlObject, 
                                                CKO_NSS_CRL,
                                                crl_template, crl_size);
    if (status != PR_SUCCESS) {
	session = sessionOpt ? 
	          sessionOpt : 
	          nssToken_GetDefaultSession(crlObject->token);
	if (session == NULL) {
	    nss_SetError(NSS_ERROR_INVALID_ARGUMENT);
	    return PR_FAILURE;
	}

	slot = nssToken_GetSlot(crlObject->token);
	status = nssCKObject_GetAttributes(crlObject->handle, 
	                                   crl_template, crl_size,
	                                   arenaOpt, session, slot);
	nssSlot_Destroy(slot);
	if (status != PR_SUCCESS) {
	    return status;
	}
    }

    i=0;
    if (crl_class) {
        NSS_CK_ATTRIBUTE_TO_ULONG(&crl_template[i], *crl_class); i++;
    }
    if (encodingOpt) {
	NSS_CK_ATTRIBUTE_TO_ITEM(&crl_template[i], encodingOpt); i++;
    }
    if (urlOpt) {
	NSS_CK_ATTRIBUTE_TO_UTF8(&crl_template[i], *urlOpt); i++;
    }
    if (isKRLOpt) {
	NSS_CK_ATTRIBUTE_TO_BOOL(&crl_template[i], *isKRLOpt); i++;
    }
    if (subjectOpt) {
	NSS_CK_ATTRIBUTE_TO_ITEM(&crl_template[i], subjectOpt); i++;
    }
    return PR_SUCCESS;
}

NSS_IMPLEMENT PRStatus
nssCryptokiPrivateKey_SetCertificate (
  nssCryptokiObject *keyObject,
  nssSession *sessionOpt,
  const NSSUTF8 *nickname,
  NSSItem *id,
  NSSDER *subject
)
{
    CK_RV ckrv;
    CK_ATTRIBUTE_PTR attr;
    CK_ATTRIBUTE key_template[3];
    CK_ULONG key_size;
    void *epv = nssToken_GetCryptokiEPV(keyObject->token);
    nssSession *session;
    NSSToken *token = keyObject->token;
    nssSession *defaultSession = nssToken_GetDefaultSession(token);
    PRBool createdSession = PR_FALSE;

    NSS_CK_TEMPLATE_START(key_template, attr, key_size);
    NSS_CK_SET_ATTRIBUTE_UTF8(attr, CKA_LABEL, nickname);
    NSS_CK_SET_ATTRIBUTE_ITEM(attr, CKA_ID, id);
    NSS_CK_SET_ATTRIBUTE_ITEM(attr, CKA_SUBJECT, subject);
    NSS_CK_TEMPLATE_FINISH(key_template, attr, key_size);

    if (sessionOpt) {
	if (!nssSession_IsReadWrite(sessionOpt)) {
	    return PR_FAILURE;
	} 
	session = sessionOpt;
    } else if (defaultSession && nssSession_IsReadWrite(defaultSession)) {
	session = defaultSession;
    } else {
	NSSSlot *slot = nssToken_GetSlot(token);
	session = nssSlot_CreateSession(token->slot, NULL, PR_TRUE);
	nssSlot_Destroy(slot);
	if (!session) {
	    return PR_FAILURE;
	}
	createdSession = PR_TRUE;
    }

    ckrv = CKAPI(epv)->C_SetAttributeValue(session->handle,
                                           keyObject->handle,
                                           key_template,
                                           key_size);

    if (createdSession) {
	nssSession_Destroy(session);
    }

    return (ckrv == CKR_OK) ? PR_SUCCESS : PR_FAILURE;
}

