/*
 * sm-global-platform.c: Global Platform related procedures
 *
 * Copyright (C) 2010  Viktor Tarasov <vtarasov@opentrust.com>
 *                      OpenTrust <www.opentrust.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <stdio.h>
#include <stdlib.h>

#ifdef HAVE_UNISTD_H
#include <unistd.h>
#endif

#include <string.h>
#include <assert.h>
#include <errno.h>
#include <ctype.h>
#include <sys/stat.h>

#include <openssl/des.h>
#include <openssl/rand.h>

#include "libopensc/opensc.h"
#include "libopensc/sm.h"
#include "libopensc/log.h"
#include "libopensc/asn1.h"

#include "sm-module.h"

int
sm_gp_decode_card_answer(struct sc_context *ctx, struct sc_remote_data *rdata, unsigned char *out, size_t out_len)
{
	LOG_FUNC_RETURN(ctx, SC_ERROR_NOT_SUPPORTED);
}


int
sm_gp_initialize(struct sc_context *ctx, struct sm_info *sm_info,  struct sc_remote_data *rdata)
{
	struct sc_serial_number sn = sm_info->serialnr;
	struct sm_gp_session *gp_session = &sm_info->session.gp;
	struct sm_gp_keyset *gp_keyset = &sm_info->session.gp.gp_keyset;
	struct sc_remote_apdu *new_rapdu = NULL;
	struct sc_apdu *apdu = NULL;
	int rv;

	LOG_FUNC_CALLED(ctx);
	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP initialize: serial:%s", sc_dump_hex(sn.value, sn.len));
	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP initialize: current_df_path %s", sc_print_path(&sm_info->current_path_df));
	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP initialize: KMC length %i", gp_keyset->kmc_len);

	if (!rdata || !rdata->alloc)
		LOG_FUNC_RETURN(ctx, SC_ERROR_INVALID_ARGUMENTS);

	rv = rdata->alloc(rdata, &new_rapdu);
	LOG_TEST_RET(ctx, rv, "SM GP decode card answer: cannot allocate remote APDU");
	apdu = &new_rapdu->apdu;

	rv = RAND_bytes(gp_session->host_challenge, SM_SMALL_CHALLENGE_LEN);
	if (!rv)
		LOG_FUNC_RETURN(ctx, SC_ERROR_SM_RAND_FAILED);

	apdu->cse = SC_APDU_CASE_4_SHORT;
	apdu->cla = 0x80;
	apdu->ins = 0x50;
	apdu->p1 = 0x0;
	apdu->p2 = 0x0;
	apdu->lc = SM_SMALL_CHALLENGE_LEN;
	apdu->le = 0x1C;
	apdu->datalen = SM_SMALL_CHALLENGE_LEN;
	memcpy(&new_rapdu->sbuf[0], gp_session->host_challenge, SM_SMALL_CHALLENGE_LEN);

	LOG_FUNC_RETURN(ctx, SC_SUCCESS);
}


static unsigned char *
sc_gp_get_session_key(struct sc_context *ctx, struct sm_gp_session *gp_session,
		unsigned char *key)
{
	int out_len;
	unsigned char *out;
	unsigned char deriv[16];

	memcpy(deriv,		gp_session->card_challenge + 4,	4);
	memcpy(deriv + 4,	gp_session->host_challenge,	4);
	memcpy(deriv + 8,	gp_session->card_challenge,	4);
	memcpy(deriv + 12,	gp_session->host_challenge + 4,	4);

	if (sm_encrypt_des_ecb3(key, deriv, 16, &out, &out_len))   {
		if (ctx)
			sc_debug(ctx, SC_LOG_DEBUG_VERBOSE, "SM GP get session key: des_ecb3 encryption error");
		free(out);
		return NULL;
	}
	else if (out==NULL  || out_len!=16)   {
		if (ctx)
			sc_debug(ctx, SC_LOG_DEBUG_VERBOSE, "SM GP get session key: des_ecb3 encryption error: out(%p,len:%i)", out, out_len);
		if (out)
			free(out);
		return NULL;
	}

	return out;
}


int
sm_gp_get_cryptogram(unsigned char *session_key,
		unsigned char *left, unsigned char *right,
		unsigned char *out, int out_len)
{
	unsigned char block[24];
	DES_cblock cksum={0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};

	if (out_len!=8)
		return SC_ERROR_INVALID_ARGUMENTS;

	memcpy(block + 0, left, 8);
	memcpy(block + 8, right, 8);
	memcpy(block + 16, "\x80\0\0\0\0\0\0\0",8);

	DES_cbc_cksum_3des(block,&cksum, sizeof(block), session_key, &cksum);

	memcpy(out, cksum, 8);

	return 0;
}


int
sm_gp_get_mac(unsigned char *key, DES_cblock *icv,
		unsigned char *in, int in_len, DES_cblock *out)
{
	int len;
	unsigned char *block;

	block = malloc(in_len + 8);
	if (!block)
		return SC_ERROR_OUT_OF_MEMORY;

	memcpy(block, in, in_len);
	memcpy(block + in_len, "\x80\0\0\0\0\0\0\0", 8);
	len = in_len + 8;
	len -= (len%8);

	DES_cbc_cksum_3des(block, out, len, key, icv);

	free(block);
	return 0;
}


static int
sm_gp_parse_init_data(struct sc_context *ctx, struct sm_gp_session *gp_session,
		unsigned char *init_data, size_t init_len)
{
	struct sm_gp_keyset *gp_keyset = &gp_session->gp_keyset;

	if(init_len != 0x1C)
		return SC_ERROR_INVALID_DATA;

	gp_keyset->version = *(init_data + 10);
	gp_keyset->index = *(init_data + 11);
	memcpy(gp_session->card_challenge, init_data + 12, SM_SMALL_CHALLENGE_LEN);

	return SC_SUCCESS;
}


static int
sm_gp_init_session(struct sc_context *ctx, struct sm_gp_session *gp_session,
		unsigned char *adata, size_t adata_len)
{
	struct sm_gp_keyset *gp_keyset = &gp_session->gp_keyset;
	unsigned char cksum[8];
	int rv;

	LOG_FUNC_CALLED(ctx);
	if (!adata || adata_len < 8)
		LOG_FUNC_RETURN(ctx, SC_ERROR_INVALID_ARGUMENTS);

	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP init session: auth.data %s", sc_dump_hex(adata, 8));

	gp_session->session_enc = sc_gp_get_session_key(ctx, gp_session, gp_keyset->enc);
	gp_session->session_mac = sc_gp_get_session_key(ctx, gp_session, gp_keyset->mac);
	gp_session->session_kek = sc_gp_get_session_key(ctx, gp_session, gp_keyset->kek);
	if (!gp_session->session_enc || !gp_session->session_mac || !gp_session->session_kek)
		LOG_TEST_RET(ctx, SC_ERROR_SM_NO_SESSION_KEYS, "SM GP init session: get session keys error");
	memcpy(gp_session->session_kek, gp_keyset->kek, 16);

	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP init session: session ENC: %s", sc_dump_hex(gp_session->session_enc, 16));
	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP init session: session MAC: %s", sc_dump_hex(gp_session->session_mac, 16));
	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP init session: session KEK: %s", sc_dump_hex(gp_session->session_kek, 16));

	memset(cksum, 0, sizeof(cksum));
	rv = sm_gp_get_cryptogram(gp_session->session_enc, gp_session->host_challenge, gp_session->card_challenge, cksum, sizeof(cksum));
	LOG_TEST_RET(ctx, rv, "SM GP init session: cannot get cryptogram");

	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP init session: cryptogram: %s", sc_dump_hex(cksum, 8));
	if (memcmp(cksum, adata, adata_len))
		LOG_FUNC_RETURN(ctx, SC_ERROR_SM_AUTHENTICATION_FAILED);

	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP init session: card authenticated");
	LOG_FUNC_RETURN(ctx, SC_SUCCESS);
}


void
sm_gp_close_session(struct sc_context *ctx, struct sm_gp_session *gp_session)
{
	free(gp_session->session_enc);
	free(gp_session->session_mac);
	free(gp_session->session_kek);
}


int
sm_gp_external_authentication(struct sc_context *ctx, struct sm_info *sm_info,
		unsigned char *init_data, size_t init_len, struct sc_remote_data *rdata,
		int (*diversify_keyset)(struct sc_context *ctx,
				struct sm_info *sm_info,
				unsigned char *idata, size_t idata_len))
{
	struct sc_remote_apdu *new_rapdu = NULL;
	struct sc_apdu *apdu = NULL;
	unsigned char host_cryptogram[8], raw_apdu[SC_MAX_APDU_BUFFER_SIZE];
	struct sm_gp_session *gp_session = &sm_info->session.gp;
	DES_cblock mac;
	int rv, offs = 0;

	LOG_FUNC_CALLED(ctx);
	if (!sm_info || !init_data || !rdata || !rdata->alloc)
		LOG_FUNC_RETURN(ctx, SC_ERROR_INVALID_ARGUMENTS);

	if (init_len != 0x1C)
		LOG_TEST_RET(ctx, SC_ERROR_UNKNOWN_DATA_RECEIVED, "SM GP authentication: invalid auth data length");

	rv = sm_gp_parse_init_data(ctx, gp_session, init_data, init_len);
	LOG_TEST_RET(ctx, rv, "SM GP authentication: 'INIT DATA' parse error");

	if (diversify_keyset)   {
		rv = (*diversify_keyset)(ctx, sm_info, init_data, init_len);
		LOG_TEST_RET(ctx, rv, "SM GP authentication: keyset diversification error");
	}

	rv = sm_gp_init_session(ctx, gp_session, init_data + 20, 8);
	LOG_TEST_RET(ctx, rv, "SM GP authentication: init session error");

	rv = sm_gp_get_cryptogram(gp_session->session_enc,
			gp_session->card_challenge, gp_session->host_challenge,
			host_cryptogram, sizeof(host_cryptogram));
	LOG_TEST_RET(ctx, rv, "SM GP authentication: get host cryptogram error");

	sc_debug(ctx, SC_LOG_DEBUG_SM, "SM GP authentication: host_cryptogram:%s", sc_dump_hex(host_cryptogram, 8));

	rv = rdata->alloc(rdata, &new_rapdu);
	LOG_TEST_RET(ctx, rv, "SM GP authentication: cannot allocate remote APDU");
	apdu = &new_rapdu->apdu;

	offs = 0;
	apdu->cse = SC_APDU_CASE_3_SHORT;
	apdu->cla = raw_apdu[offs++] = 0x84;
	apdu->ins = raw_apdu[offs++] = 0x82;
	apdu->p1  = raw_apdu[offs++] = gp_session->params.level;
	apdu->p2  = raw_apdu[offs++] = 0;
	apdu->lc  = raw_apdu[offs++] = 0x10;
	apdu->datalen = 0x10;

	memcpy(raw_apdu + offs, host_cryptogram, 8);
	offs += 8;
	rv = sm_gp_get_mac(gp_session->session_mac, &gp_session->mac_icv, raw_apdu, offs, &mac);
	LOG_TEST_RET(ctx, rv, "SM GP authentication: get MAC error");

	memcpy(new_rapdu->sbuf, host_cryptogram, 8);
	memcpy(new_rapdu->sbuf + 8, mac, 8);
	memcpy(gp_session->mac_icv, mac, 8);

	LOG_FUNC_RETURN(ctx, 1);
}


static int
sm_gp_encrypt_command_data(struct sc_context *ctx, unsigned char *session_key,
		const unsigned char *in, size_t in_len, unsigned char **out, size_t *out_len)
{
	unsigned char *data = NULL;
	int rv, len;

	if (!out || !out_len)
		LOG_TEST_RET(ctx, SC_ERROR_INVALID_ARGUMENTS, "SM GP encrypt command data error");

	sc_debug(ctx, SC_LOG_DEBUG_SM,
	       "SM GP encrypt command data(len:%"SC_FORMAT_LEN_SIZE_T"u,%p)",
	       in_len, in);
	if (in==NULL || in_len==0)   {
		*out = NULL;
		*out_len = 0;
		LOG_FUNC_RETURN(ctx, SC_SUCCESS);
	}

	len = in_len + 8;
	len -= (len%8);

	data = calloc(1, len);
	if (!data)
		LOG_FUNC_RETURN(ctx, SC_ERROR_OUT_OF_MEMORY);

	*data = in_len;
	memcpy(data + 1, in, in_len);

	rv = sm_encrypt_des_cbc3(ctx, session_key, data, in_len + 1, out, out_len, 1);
	free(data);
	LOG_TEST_RET(ctx, rv, "SM GP encrypt command data: encryption error");

	LOG_FUNC_RETURN(ctx, SC_SUCCESS);
}


int
sm_gp_securize_apdu(struct sc_context *ctx, struct sm_info *sm_info,
		char *init_data, struct sc_apdu *apdu)
{
	unsigned char  buff[SC_MAX_APDU_BUFFER_SIZE + 24];
	unsigned char *apdu_data = NULL;
	struct sm_gp_session *gp_session = &sm_info->session.gp;
	unsigned gp_level = sm_info->session.gp.params.level;
	unsigned gp_index = sm_info->session.gp.params.index;
	DES_cblock mac;
	unsigned char *encrypted = NULL;
	size_t encrypted_len = 0;
	int rv;

	LOG_FUNC_CALLED(ctx);

	apdu_data = (unsigned char *)apdu->data;
	sc_debug(ctx, SC_LOG_DEBUG_SM,
	       "SM GP securize APDU(cse:%X,cla:%X,ins:%X,data(len:%"SC_FORMAT_LEN_SIZE_T"u,%p),lc:%"SC_FORMAT_LEN_SIZE_T"u,GP level:%X,GP index:%X",
	       apdu->cse, apdu->cla, apdu->ins, apdu->datalen, apdu->data,
	       apdu->lc, gp_level, gp_index);

	if (gp_level == 0 || (apdu->cla & 0x04))
		return 0;

	if (gp_level == SM_GP_SECURITY_MAC)   {
		if (apdu->datalen + 8 > SC_MAX_APDU_BUFFER_SIZE)
			LOG_TEST_RET(ctx, SC_ERROR_WRONG_LENGTH, "SM GP securize APDU: too much data");
	}
	else if (gp_level == SM_GP_SECURITY_ENC)   {
		if (!gp_session->session_enc)
			LOG_TEST_RET(ctx, SC_ERROR_SM_INVALID_SESSION_KEY, "SM GP securize APDU: no ENC session key found");

		if (sm_gp_encrypt_command_data(ctx, gp_session->session_enc, apdu->data, apdu->datalen, &encrypted, &encrypted_len))
			LOG_TEST_RET(ctx, SC_ERROR_SM_ENCRYPT_FAILED, "SM GP securize APDU: data encryption error");

		if (encrypted_len + 8 > SC_MAX_APDU_BUFFER_SIZE) {
			rv = SC_ERROR_BUFFER_TOO_SMALL;
			LOG_TEST_GOTO_ERR(ctx, rv, "SM GP securize APDU: not enough place for encrypted data");
		}

		sc_debug(ctx, SC_LOG_DEBUG_SM,
		       "SM GP securize APDU: encrypted length %"SC_FORMAT_LEN_SIZE_T"u",
		       encrypted_len);
	}
	else   {
		LOG_TEST_RET(ctx, SC_ERROR_SM_INVALID_LEVEL, "SM GP securize APDU: invalid SM level");
	}

	buff[0] = apdu->cla | 0x04;
	buff[1] = apdu->ins;
	buff[2] = apdu->p1;
	buff[3] = apdu->p2;
	buff[4] = apdu->lc + 8;

	memcpy(buff + 5, apdu_data, apdu->datalen);

	rv = sm_gp_get_mac(gp_session->session_mac, &gp_session->mac_icv, buff, 5 + apdu->datalen, &mac);
	LOG_TEST_GOTO_ERR(ctx, rv, "SM GP securize APDU: get MAC error");

	if (gp_level == SM_GP_SECURITY_MAC)   {
		memcpy(apdu_data + apdu->datalen, mac, 8);

		apdu->cla |= 0x04;
		apdu->datalen += 8;
		apdu->lc = apdu->datalen;

		if (apdu->cse==SC_APDU_CASE_2_SHORT)
			apdu->cse = SC_APDU_CASE_4_SHORT;
	}
	else if (gp_level == SM_GP_SECURITY_ENC)   {
		memcpy(apdu_data + encrypted_len, mac, 8);
		if (encrypted_len)
			memcpy(apdu_data, encrypted, encrypted_len);

		apdu->cla |= 0x04;
		apdu->datalen = encrypted_len + 8;
		apdu->lc = encrypted_len + 8;

		if (apdu->cse == SC_APDU_CASE_2_SHORT)
			apdu->cse = SC_APDU_CASE_4_SHORT;

		if (apdu->cse == SC_APDU_CASE_1)
			apdu->cse = SC_APDU_CASE_3_SHORT;

		free(encrypted);
		encrypted = NULL;
	}

	memcpy(sm_info->session.gp.mac_icv, mac, 8);

err:
	free(encrypted);
	LOG_FUNC_RETURN(ctx, rv);
}


