/*
 *
 *  seeld - Secure Element Manager
 *
 *  Copyright (C) 2013  Intel Corporation. All rights reserved.
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program 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 General Public License for more details.
 *
 */

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

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <errno.h>

#include <glib.h>

#include <gdbus.h>

#include "driver.h"
#include "seel.h"

#define MAX_AID_LEN 16
#define MIN_AID_LEN 5

#define APP_HASH_LEN 20

#define GP_AID_LEN 9
uint8_t gp_aid[] = {0xA0, 0x00, 0x00, 0x01,
			0x51, 0x41, 0x43, 0x4C, 0x00 };

/* Data Object Tags */
#define AID_REF_DO_ALL 0xC0
#define HASH_REF_DO    0xC1
#define AID_REF_DO     0x4F
#define REF_DO         0xE1
#define REF_AR_DO      0xE2
#define AR_DO          0xE3
#define APDU_AR_DO     0xD0
#define NFC_AR_DO      0xD1

#define APDU_STATUS_LEN 2
#define GET_ALL_DATA_CMD_LEN 2
#define GET_REFRESH_DATA_CMD_LEN 2
#define GET_REFRESH_DATA_TAG_LEN 1
#define GET_REFRESH_TAG_LEN 8

/* if ((APDU_header & rule->mask) == rule->header) then APDU is allowed */
struct seel_ace_apdu_rule {
	uint32_t header;
	uint32_t mask;
};

struct seel_ace_rule {
	uint8_t aid[MAX_AID_LEN];
	size_t aid_len;

	uint8_t hash[APP_HASH_LEN];
	size_t hash_len;

	uint8_t *apdu_rules;
	size_t apdu_rules_len;

	bool nfc_rule;
};

struct seel_ace {
	struct seel_se *se;

	size_t rules_length;
	size_t current_rules_length;
	uint8_t *rules_payload;

	uint8_t rules_tag[8];

	GSList *rules;
};

GHashTable *ace_hash;

static void free_rule(gpointer data)
{
	struct seel_ace_rule *rule = data;

	DBG("%p", rule);

	g_free(rule->apdu_rules);
	g_free(rule);
}

static void free_ace(gpointer data)
{
	struct seel_ace *ace = data;

	DBG("%p %p", ace, ace->rules);

	g_slist_free_full(ace->rules, free_rule);
	g_free(ace);
}

static void dump_rule(gpointer data, gpointer user_data)
{
	struct seel_ace_rule *rule = data;
	struct seel_ace_apdu_rule *apdu_rule;
	char aid[3 * MAX_AID_LEN + 1];
	char hash[3 * APP_HASH_LEN + 1];
	size_t i;

	DBG("ACE Rule:");

	if (rule->aid_len == 0) {
		DBG("  Hash: All SE applications");
	} else {
		for (i = 0; i < rule->aid_len; i++)
			sprintf(aid + (3 * i), "%02X ", rule->aid[i]);
		aid[3 * i] = 0;
		DBG("  AID [%zu]: %s", rule->aid_len, aid);
	}

	if (rule->hash_len == 0) {
		DBG("  Hash: All host applications");
	} else {
		for (i = 0; i < rule->hash_len; i++)
			sprintf(hash + (3 * i), "%02X ", rule->hash[i]);
		hash[3 * i] = 0;
		DBG("  Hash [%zu]: %s", rule->hash_len, hash);
	}

	if (rule->apdu_rules_len == 1) {
		DBG("  APDU: %s", rule->apdu_rules[0] ? "Always" : "Never");
	} else {
		uint8_t *header, *mask;
		size_t n_rules;

		/*
		 * (void *) to silence -Wcast-align. Code should be safe
		 * (assuming platform handles unaliagned access) as iterations
		 * go up to n_rules.
		 *
		 * TODO: Fix the problem instead of silencing with cast, so
		 * the code would be porable.
		 */
		if (rule->apdu_rules_len % sizeof(struct seel_ace_apdu_rule *)) {
			DBG("  APDU: wrong alignment (Bug, code needs fixing)");
		}
		apdu_rule = (struct seel_ace_apdu_rule *)(void *)rule->apdu_rules;
		n_rules = rule->apdu_rules_len /
				sizeof(struct seel_ace_apdu_rule);

		DBG("  APDU rules (%zu)", n_rules);
		for (i = 0; i < n_rules; i++) {
			header = (uint8_t *)&apdu_rule->header;
			mask = (uint8_t *)&apdu_rule->mask;
			DBG("    header 0x%02x:0x%02x:0x%02x:0x%02x "
			    "mask 0x%02x:0x%02x:0x%02x:0x%02x",
				header[0], header[1], header[2], header[3],
				mask[0], mask[1], mask[2], mask[3]);
			apdu_rule++;
		}
	}

	DBG("  NFC: %s", rule->nfc_rule ? "Always" : "Never");

}

static void dump_ace(struct seel_ace *ace)
{
	DBG("ACE Tag [0x%02x:0x%02x:0x%02x:0x%02x:0x%02x:0x%02x:0x%02x:0x%02x]",
	ace->rules_tag[0], ace->rules_tag[1], ace->rules_tag[2], ace->rules_tag[3],
	ace->rules_tag[4], ace->rules_tag[5], ace->rules_tag[6], ace->rules_tag[7]);

	g_slist_foreach(ace->rules, dump_rule, NULL);
}

static int build_ref(struct seel_ace_rule *ace_rule,
			uint8_t *rule, size_t rule_length)
{
	uint8_t *rule_ptr;
	size_t remaining, do_length;

	remaining = rule_length;
	rule_ptr = rule;
	while (remaining) {

		switch (rule_ptr[0]) {
		case AID_REF_DO:
			do_length = rule_ptr[1];
			DBG("AID_REF_DO %zu", do_length);
			rule_ptr += 2;

			if (remaining < do_length)
				return -EINVAL;

			if (do_length > MAX_AID_LEN)
				return -EINVAL;

			memcpy(ace_rule->aid, rule_ptr, do_length);
			ace_rule->aid_len = do_length;

			remaining -= do_length + 2;
			rule_ptr += do_length;

			break;

		case HASH_REF_DO:
			do_length = rule_ptr[1];
			DBG("HASH_REF_DO %zu", do_length);
			rule_ptr += 2;

			if (remaining < do_length)
				return -EINVAL;

			if (do_length > APP_HASH_LEN)
				return -EINVAL;

			memcpy(ace_rule->hash, rule_ptr, do_length);
			ace_rule->hash_len = do_length;

			remaining -= do_length + 2;
			rule_ptr += do_length;

			break;

		default:
			return -EINVAL;
		}
	}

	return 0;
}

static int build_ar(struct seel_ace_rule *ace_rule,
			uint8_t *rule, size_t rule_length)
{
	uint8_t *rule_ptr;
	size_t remaining, do_length;

	remaining = rule_length;
	rule_ptr = rule;
	while (remaining) {

		switch (rule_ptr[0]) {
		case APDU_AR_DO:
			do_length = rule_ptr[1];
			DBG("APDU_AR_DO %zu", do_length);
			rule_ptr += 2;

			if (remaining < do_length)
				return -EINVAL;

			ace_rule->apdu_rules = g_try_malloc0(do_length);
			if (!ace_rule->apdu_rules)
				return -ENOMEM;

			memcpy(ace_rule->apdu_rules, rule_ptr, do_length);
			ace_rule->apdu_rules_len = do_length;

			remaining -= do_length + 2;
			rule_ptr += do_length;

			break;

		case NFC_AR_DO:
			do_length = rule_ptr[1];
			DBG("NFC_AR_DO %zu", do_length);
			rule_ptr += 2;

			if (do_length != 1)
				return -EINVAL;

			ace_rule->nfc_rule = !!rule_ptr[0];

			remaining -= do_length + 2;
			rule_ptr += do_length;

			break;

		default:
			return -EINVAL;
		}
	}

	return 0;
}

static struct seel_ace_rule *build_rule(struct seel_ace *ace,
				uint8_t *rule, size_t rule_length)
{
	struct seel_ace_rule *ace_rule;
	uint8_t *rule_ptr;
	size_t remaining, do_length;
	int err;

	DBG("");

	ace_rule = g_try_malloc0(sizeof(struct seel_ace_rule));
	if (!ace_rule)
		return NULL;

	remaining = rule_length;
	rule_ptr = rule;
	while (remaining) {

		switch (rule_ptr[0]) {
		case REF_DO:
			do_length = rule_ptr[1];
			DBG("REF_DO %zu", do_length);
			rule_ptr += 2;

			err = build_ref(ace_rule, rule_ptr, do_length);
			if (err)
				goto error;

			remaining -= do_length + 2;
			rule_ptr += do_length;

			break;

		case AR_DO:
			do_length = rule_ptr[1];
			DBG("AR_DO %zu", do_length);
			rule_ptr += 2;

			err = build_ar(ace_rule, rule_ptr, do_length);
			if (err)
				goto error;

			remaining -= do_length + 2;
			rule_ptr += do_length;

			break;
		default:
			goto error;
		}
	}

	return ace_rule;

error:
	free_rule(ace_rule);
	return NULL;
}

static int build_ace_rules(struct seel_ace *ace,
			uint8_t *rules, size_t rules_length)
{
	struct seel_ace_rule *rule;
	uint8_t *rule_ptr;
	size_t remaining = rules_length, ref_ar_do_length;
	int err;

	DBG("");

	err = 0;
	remaining = rules_length;
	rule_ptr = rules;
	while (remaining) {
		if (rule_ptr[0] != REF_AR_DO) {
			rule_ptr++;
			remaining--;
			continue;
		}

		ref_ar_do_length = rule_ptr[1];
		/* Tag + Length + value length */
		remaining -= ref_ar_do_length + 2;
		rule_ptr += 2;

		DBG("REF_AR_DO %zu bytes", ref_ar_do_length);

		rule = build_rule(ace, rule_ptr, ref_ar_do_length);
		if (!rule) {
			err = -EINVAL;
			break;
		}

		ace->rules = g_slist_append(ace->rules, rule);

		rule_ptr += ref_ar_do_length;
	}

	return err;
}

static int ace_rule_length(uint8_t *apdu, size_t apdu_length,
						size_t *length_length)
{
	size_t length;

	if (apdu_length < 3)
		return -EINVAL;

	length = apdu[2];
	*length_length = 1;

	/*
	 * BER: If length bit 8 is one, bit [0..7] is the number
	 * of bytes used for encoding the length.
	 */
	if (length & 0x80) {
		size_t _length = length & 0x7f, i, base;

		DBG("%zu", _length);

		if (apdu_length < 3 + _length)
			return -EINVAL;

		length = 0;
		base = 1 << 8 * _length;
		for (i = 0; i < _length; i++) {
			base >>= 8;
			length += apdu[3 + i] * base;
		}

		*length_length = _length + 1;
	}

	DBG("length 0x%zx", length);

	return length;
}

static void get_next_gp_data(struct seel_ace *ace);

static void get_next_gp_data_cb(void *context,
			uint8_t *apdu, size_t apdu_length,
			int err)
{
	struct seel_ace *ace  = context;
	size_t payload_length;

	DBG("Current %zu Total %zu Got %zu",
		ace->current_rules_length, ace->rules_length, apdu_length);

	if (err)
		goto out;

	payload_length = apdu_length - APDU_STATUS_LEN;

	if (ace->current_rules_length + payload_length > ace->rules_length)
		goto out;

	memcpy(ace->rules_payload + ace->current_rules_length,
						apdu, payload_length);
	ace->current_rules_length += apdu_length - APDU_STATUS_LEN;

	if (ace->current_rules_length < ace->rules_length)
		return get_next_gp_data(ace);

	if (build_ace_rules(ace, ace->rules_payload, ace->rules_length))
		goto out;

	dump_ace(ace);

	g_hash_table_replace(ace_hash, ace->se, ace);

out:
	g_free(ace->rules_payload);

	return;
}

static void get_next_gp_data(struct seel_ace *ace)
{
	struct seel_apdu *get_next_gp_data;
	size_t req_length;
	int err;

	if (ace->rules_length - ace->current_rules_length < 0x100)
		req_length = ace->rules_length - ace->current_rules_length;
	else
		req_length = 0;

	get_next_gp_data = __seel_apdu_get_next_gp_data(req_length);
	if (!get_next_gp_data) {
		g_free(ace->rules_payload);
		return;
	}

	err = __seel_se_queue_io(ace->se, get_next_gp_data,
					get_next_gp_data_cb, ace);
	if (err < 0) {
		near_error("GET NEXT ALL err %d", err);
		return;
	}

	return;
}

static void get_all_gp_data_cb(void *context,
			uint8_t *apdu, size_t apdu_length,
			int err)
{
	struct seel_ace *ace = context;
	size_t length_length, payload_length;
	int rule_length;

	DBG("");

	if (err)
		return;

	rule_length = ace_rule_length(apdu, apdu_length, &length_length);
	if (rule_length < 0)
		return;

	if (apdu_length > GET_ALL_DATA_CMD_LEN + length_length +
					rule_length + APDU_STATUS_LEN)
		return;

	payload_length = apdu_length - GET_ALL_DATA_CMD_LEN -
					length_length - APDU_STATUS_LEN;

	DBG("Received %zu bytes of payload", payload_length);

	if (payload_length < (size_t)rule_length) {
		ace->rules_length = rule_length;
		ace->current_rules_length = payload_length;
		ace->rules_payload = g_try_malloc0(rule_length);
		if (!ace->rules_payload)
			return;

		memcpy(ace->rules_payload,
				apdu + GET_ALL_DATA_CMD_LEN + length_length,
								payload_length);

		return get_next_gp_data(ace);
	}

	if (build_ace_rules(ace, apdu + GET_ALL_DATA_CMD_LEN + length_length,
								payload_length))
		return;

	dump_ace(ace);

	g_hash_table_replace(ace_hash, ace->se, ace);

	return;
}

static void get_refresh_gp_data_cb(void *context,
			uint8_t *apdu, size_t apdu_length,
			int err)
{
	struct seel_se *se = context;
	struct seel_ace *ace;
	struct seel_apdu *get_all_gp_data;

	DBG("");

	if (err)
		return;

	if (apdu_length != GET_REFRESH_DATA_CMD_LEN + GET_REFRESH_DATA_TAG_LEN
				 + GET_REFRESH_TAG_LEN + APDU_STATUS_LEN)
		return;

	if (apdu[0] != 0xDF || apdu[1] != 0x20 || apdu[2] != 0x08)
		return;

	ace = g_try_malloc0(sizeof(struct seel_ace));
	if (!ace)
		return;

	ace->se = se;

	memcpy(ace->rules_tag, apdu + GET_REFRESH_DATA_CMD_LEN
			+ GET_REFRESH_DATA_TAG_LEN, GET_REFRESH_TAG_LEN);

	get_all_gp_data = __seel_apdu_get_all_gp_data();
	if (!get_all_gp_data)
		return;

	err = __seel_se_queue_io(se, get_all_gp_data, get_all_gp_data_cb, ace);
	if (err < 0) {
		near_error("GET DATA ALL err %d", err);
		return;
	}

	return ;
}

static void select_gp_aid_cb(void *context,
			uint8_t *apdu, size_t apdu_length,
			int err)
{
	struct seel_se *se = context;
	struct seel_apdu *get_refresh_gp_data;

	DBG("");

	if (err)
		return;

	get_refresh_gp_data = __seel_apdu_get_refresh_gp_data();
	if (!get_refresh_gp_data)
		return;

	err = __seel_se_queue_io(se, get_refresh_gp_data, get_refresh_gp_data_cb, se);
	if (err < 0) {
		near_error("GET REFRESH DATA err %d", err);
		return;
	}

	return ;
}

gboolean __seel_ace_add(gpointer user_data)
{
	struct seel_se *se = user_data;
	struct seel_apdu *select_gp_aid;
	int err;

	DBG("");

	select_gp_aid = __seel_apdu_select_aid(0, gp_aid, GP_AID_LEN);
	if (!select_gp_aid)
		return FALSE;

	/* Send the GP AID selection APDU */
	err = __seel_se_queue_io(se, select_gp_aid, select_gp_aid_cb, se);
	if (err < 0) {
		near_error("GP AID err %d", err);
		return FALSE;
	}

	return FALSE;
}

int __seel_ace_remove(struct seel_se *se)
{
	DBG("%p", se);

	if (!g_hash_table_remove(ace_hash, se))
		return -ENODEV;

	return 0;
}

static struct seel_ace_rule *find_specific_rule(struct seel_ace *ace,
						uint8_t *aid, size_t aid_len,
						uint8_t *hash)
{
	GSList *list;

	DBG("%zu", aid_len);

	if (!hash || !aid)
		return false;

	for (list = ace->rules; list; list = list->next) {
		struct seel_ace_rule *rule = list->data;

		if (rule->aid_len != aid_len)
			continue;

		if (rule->hash_len != APP_HASH_LEN)
			continue;

		if (!memcmp(rule->aid, aid, aid_len) &&
				!memcmp(rule->hash, hash, APP_HASH_LEN)) {
			dump_rule(rule, NULL);
			return rule;
		}
	}

	return NULL;
}

static bool find_specific_rule_for_aid(struct seel_ace *ace,
					uint8_t *aid, size_t aid_len)
{
	GSList *list;

	DBG("%zu", aid_len);

	if (!aid)
		return false;

	for (list = ace->rules; list; list = list->next) {
		struct seel_ace_rule *rule = list->data;

		if (rule->aid_len != aid_len)
			continue;

		if (rule->hash_len != APP_HASH_LEN)
			continue;

		if (!memcmp(rule->aid, aid, aid_len)) {
			dump_rule(rule, NULL);
			return true;
		}
	}

	return false;
}

static struct seel_ace_rule *find_generic_rule_for_aid(struct seel_ace *ace,
						uint8_t *aid, size_t aid_len)

{
	GSList *list;

	DBG("%zu", aid_len);

	if (!aid)
		return false;

	for (list = ace->rules; list; list = list->next) {
		struct seel_ace_rule *rule = list->data;

		if (rule->aid_len != aid_len)
			continue;

		/* This is a specific rule */
		if (rule->hash_len)
			continue;

		if (!memcmp(rule->aid, aid, aid_len)) {
			dump_rule(rule, NULL);
			return rule;
		}
	}

	return NULL;
}

static struct seel_ace_rule *find_generic_rule_for_hash(struct seel_ace *ace,
								uint8_t *hash)
{
	GSList *list;

	DBG("");

	if (!hash)
		return false;

	for (list = ace->rules; list; list = list->next) {
		struct seel_ace_rule *rule = list->data;

		/* This is a generic hash rule */
		if (rule->hash_len != APP_HASH_LEN)
			continue;

		/* This is a specific AID rule */
		if (rule->aid_len)
			continue;

		if (!memcmp(rule->hash, hash, APP_HASH_LEN)) {
			dump_rule(rule, NULL);
			return rule;
		}
	}

	return NULL;
}

static struct seel_ace_rule *find_generic_rule(struct seel_ace *ace)
{
	GSList *list;

	DBG("");

	for (list = ace->rules; list; list = list->next) {
		struct seel_ace_rule *rule = list->data;

		if (rule->hash_len || rule->aid_len)
			continue;

		dump_rule(rule, NULL);
		return rule;
	}

	return NULL;
}

static bool apdu_allowed(struct seel_ace_rule *rule,
				uint8_t *apdu, size_t apdu_len)
{
	size_t i, n_rules;
	uint32_t apdu_header;
	struct seel_ace_apdu_rule *apdu_rule;

	if (rule->apdu_rules_len == 1)
		return rule->apdu_rules[0] ? true : false;

	n_rules = rule->apdu_rules_len /
				sizeof(struct seel_ace_apdu_rule);
	/*
	 * FIXME: apdu comes from message and where is checking for apdu_len?
	 * The (void *) is to fix -Wcast-align but the actual problem is
	 * whether the apdu contains enough of data.
	 */
	apdu_header = *((uint32_t *)(void *) apdu);
	/*
	 * (void *) to silence -Wcast-align. Code should be safe
	 * (assuming platform handles unaliagned access) as iterations
	 * go up to n_rules.
	 *
	 * TODO: Fix the problem instead of silencing with cast, so
	 * the code would be porable.
	 */
	apdu_rule = (struct seel_ace_apdu_rule *)(void *)rule->apdu_rules;

	for (i = 0; i < n_rules; i++) {
		if ((apdu_header & apdu_rule->mask) == apdu_rule->header)
			return true;

		apdu_rule++;
	}

	return false;
}

bool __seel_ace_apdu_allowed(struct seel_channel *channel, uint8_t *hash,
				uint8_t *apdu, size_t apdu_len)
{
	struct seel_se *se;
	struct seel_ace *ace;
	struct seel_ace_rule *rule;
	uint8_t *aid;
	size_t aid_len;

	DBG("%zu", apdu_len);

	/* XXX Do we need to do some filtering on the basic channel ?*/
	if (__seel_channel_is_basic(channel))
	   return true;

	se = __seel_channel_get_se(channel);
	if (!se)
		return false;

	ace = g_hash_table_lookup(ace_hash, se);
	if (!ace)
		return false;

	if (!ace->rules)
		return false;

	/* Ref. GP SE Access Control specification, Chapter 4: Device Interface
	 * If the secure element is a uicc, a device application can access an SE
	 * application in all of the following case:
	 * - The ARA-M is accessible and provides a rule which explicitly allows the
	 * access.
	 * - The ARA-M is not accessible but the ARF contains a rule which allows the
	 * access.
	 *
	 * If the secure element is not a uicc, a device application can access an SE
	 * application in all of the following case:
	 * - The ARA-M is not accessible on the SE (e.g not installed, not selectable,
	 * or locked).
	 * - The ARA-M is accessible and provides a rule which explicitly allows the
	 * access.
	 */
	aid = __seel_channel_get_aid(channel, &aid_len);
	if (!aid)
		return __seel_se_get_type(se) != SEEL_SE_UICC;

	/* a) Try to find a specific rule */
	rule = find_specific_rule(ace, aid, aid_len, hash);
	if (rule)
		return apdu_allowed(rule, apdu, apdu_len) ? true : false;

	/*
	 * a') Try to find a specific rule for another hash
	 * If there is such a rule, then access is denied for the
	 * current hash: Specific rule precedence over generic ones.
	 */
	if (find_specific_rule_for_aid(ace, aid, aid_len))
		return false;

	/* b) Search for a generic rule for this specific AID */
	rule = find_generic_rule_for_aid(ace, aid, aid_len);
	if (rule)
		return apdu_allowed(rule, apdu, apdu_len) ? true : false;

	/* c) Search for a generic rule for this specific hash */
	rule = find_generic_rule_for_hash(ace, hash);
	if (rule)
		return apdu_allowed(rule, apdu, apdu_len) ? true : false;

	/* d) Search for a generic rule: All apps, all AIDs */
	rule = find_generic_rule(ace);
	if (rule)
		return apdu_allowed(rule, apdu, apdu_len) ? true : false;

	return false;
}

int __seel_ace_init(void)
{
	DBG("");

	ace_hash = g_hash_table_new_full(g_direct_hash, g_direct_equal,
							NULL, free_ace);
	return 0;
}

void __seel_ace_cleanup(void)
{
	DBG("");

	g_hash_table_destroy(ace_hash);
	ace_hash = NULL;
}
