/**
 *  ---------
 * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
 * |#       #|
 * |#       #|  Copyright (c) 1999-2016 CardContact Software & System Consulting
 * |'##> <##'|  Andreas Schwier, 32429 Minden, Germany (www.cardcontact.de)
 *  ---------
 *
 *  This file is part of OpenSCDP.
 *
 *  OpenSCDP 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.
 *
 *  OpenSCDP 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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with OpenSCDP; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * @fileoverview
 */

var SmartCardHSM		= require('scsh/sc-hsm/SmartCardHSM').SmartCardHSM;
var ServiceRequestModel	= require('pki-as-a-service/service/ServiceRequestModel').ServiceRequestModel;
var KeyDomain			= require('pki-as-a-service/service/KeyDomain').KeyDomain;


function SmartCardHSMUpdateService(service) {
	this.service = service;
}

exports.SmartCardHSMUpdateService = SmartCardHSMUpdateService;



SmartCardHSMUpdateService.prototype.setAuthenticationKey = function(key) {
	this.authenticationKey = key;
}



SmartCardHSMUpdateService.prototype.calculateAuthenticationToken = function(srid) {
	var crypto = new Crypto();
	var inp = ByteString.valueOf(srid);
	var mac = crypto.sign(this.authenticationKey, Crypto.AES_CMAC, inp);
	var token = inp.concat(mac.left(8));
	return token.toString(HEX);
}



/**
 * This method is a special construction to bypass the SmartCard-HSM authentication in the case
 * where a firmware update failed, leaving the SmartCard-HSM in a state where authentication is not
 * possible.
 *
 * Rather than authenticating with the Device Certificate, an authentication token is used that contains
 * a service request id concatenated with a MAC.
 *
 * If the authentication token is valid, then the references service request is activated, possibly recovering
 * from the firmware update.
 */
SmartCardHSMUpdateService.prototype.handleAuthenticatedRequest = function(card, queryInfo) {
	GPSystem.log(GPSystem.DEBUG, module.id, "AuthenticationToken " + queryInfo);

	var inp = new ByteString(queryInfo, HEX);
	if (inp.length < 9) {
		GPSystem.log(GPSystem.ERROR, module.id, "Authentication token too short");
		return;
	}

	var mac = inp.right(8);
	var inp = inp.bytes(0, inp.length - mac.length);

	var crypto = new Crypto();

	if (!crypto.verify(this.authenticationKey, Crypto.AES_CMAC, inp, mac)) {
		GPSystem.log(GPSystem.ERROR, module.id, "Authentication token failed in validation");
		return;
	}

	var id = inp.toUnsigned();

	var srdao = this.service.daof.getServiceRequestDAO();
	var bo = srdao.getServiceRequestById(id);
	if (!bo) {
		GPSystem.log(GPSystem.ERROR, module.id, "Service request with id " + id + " not found");
		return;
	}

	var factory = this.service.pluginRegistry.getFactoryForProcess(bo.process);
	if (!factory) {
		GPSystem.log(GPSystem.ERROR, module.id, "Can not find factory for process " + bo.process);
		return;
	}

	var sr = factory.getServiceRequest(bo);

	try	{
		sr.handleSmartCardHSM( { card: card } );
	}
	catch(e if e instanceof GPError) {
		sr.addMessage(e);
		sr.setLifeCycle(ServiceRequestModel.LIFECYCLE_ERROR);
		sr.setStatusInfo("Scheduled processing failed");
		sr.commit(0);
	}
}



SmartCardHSMUpdateService.prototype.handleCard = function(session, pathInfo, queryInfo) {
	GPSystem.log(GPSystem.DEBUG, module.id, "Handling card for session " + session.id);

	try	{
		if (session.cardTerminal) {
			var card = new Card(session.cardTerminal);
		} else {
			var card = new Card(session.id);
		}

		try	{
			var sc = new SmartCardHSM(card);

			// Read version infos
			sc.getFreeMemory();
			var type = 1; // Type SC-HSM

			var devAutCert = sc.readBinary(SmartCardHSM.C_DevAut);
			var crypto = new Crypto();
			var chain = SmartCardHSM.validateCertificateChain(crypto, devAutCert);
			if (chain == null) {
				GPSystem.log(GPSystem.ERROR, module.id, "Could not authenticate SmartCard-HSM");
				return;
			}

			sc.openSecureChannel(crypto, chain.publicKey);
		}
		catch(e if e instanceof GPError) {
			// Special handling for recovery from firmware update
			if ((typeof(this.authenticationKey) == "object") && (queryInfo != null)) {
				this.handleAuthenticatedRequest(card, queryInfo);
				return;
			}

			throw e;
		}

		var tokenDAO = this.service.daof.getTokenDAO();
		var token = tokenDAO.getToken(chain.path);

		if (token == null) {
			GPSystem.log(GPSystem.ERROR, module.id, "SmartCard-HSM not found in database");
			return;
		}

		tokenDAO.updateTypeAndLastSeen(chain.path, type, sc.platform, sc.major, sc.minor);

		var actions = tokenDAO.enumerateTokenAction(token.id);
		if (actions.length == 0) {
			var msg = "Nothing to do";
			GPSystem.log(GPSystem.ERROR, module.id, msg);
			card.nativeCardTerminal.sendNotification(0, msg);
		}

		var srdao = this.service.daof.getServiceRequestDAO();
		for (var i = 0; i < actions.length; i++) {
			var id = actions[i].serviceRequestId;

			var bo = srdao.getServiceRequestById(id);
			if (!bo) {
				GPSystem.log(GPSystem.ERROR, module.id, "Service request with id " + id + " not found");
				continue;
			}

			var factory = this.service.pluginRegistry.getFactoryForProcess(bo.process);
			if (!factory) {
				GPSystem.log(GPSystem.ERROR, module.id, "Can not find factory for process " + bo.process);
				continue;
			}

			var sr = factory.getServiceRequest(bo);

			try	{
				var keepTokenAction = false;
				if (typeof(sr.handleSmartCardHSM) == "function") {
					keepTokenAction = sr.handleSmartCardHSM(sc, chain);
				}
			}
			catch(e if e instanceof GPError) {
				GPSystem.log(GPSystem.ERROR, module.id, e.message + "\n" + e.stack);
				sr.addMessage(e);
				sr.setLifeCycle(ServiceRequestModel.LIFECYCLE_ERROR);
				sr.setStatusInfo("Scheduled processing failed");
				sr.commit(0);
			}
			finally	{
				if (!keepTokenAction) {
					tokenDAO.removeTokenAction(actions[i].id);
				}
			}
		}

		// Preserve authentication state after SM termination
		sc.card.sendSecMsgApdu(Card.ALL, 0x00, 0x20, 0x01, 0x81, 0, [0x9000,0x6985]);
	}
	catch(e if e instanceof GPError) {
		GPSystem.log(GPSystem.ERROR, module.id, e);
	}
	catch(e) {
		GPSystem.log(GPSystem.ERROR, module.id, e);
		GPSystem.log(GPSystem.ERROR, module.id, e.stack);
	}
	finally {
		card.close();
	}
}



