/**
 *  ---------
 * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
 * |#       #|
 * |#       #|  Copyright (c) 1999-2025 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 Wizard for public key authentication using the PKI API
 */

var CVC			= require('scsh/eac/CVC').CVC;
var SmartCardHSM	= require('scsh/sc-hsm/SmartCardHSM').SmartCardHSM;
var HSMKeyStore		= require('scsh/sc-hsm/HSMKeyStore').HSMKeyStore;

var CardAction		= require('scsh/srv-cc1/CardAction').CardAction;



/**
 * Wizard to perform a public key authentication with a SmartCard-HSM
 *
 */
function PKAWizardForPKIAPI(ui, subjectId, tokenId, api, redirectURL) {
	CardAction.call(this, ui);
    this.subjectId = subjectId;
    this.tokenId = tokenId;
    this.api = api;
    this.redirectURL = redirectURL;

	this.tokenInfo = this.api.getToken(this.subjectId, this.tokenId, this.ui.session.user.id);
}

PKAWizardForPKIAPI.prototype = new CardAction();
PKAWizardForPKIAPI.constructor = PKAWizardForPKIAPI;

exports.PKAWizardForPKIAPI = PKAWizardForPKIAPI;



PKAWizardForPKIAPI.prototype.getPage = function(req, res, url) {
	GPSystem.trace("PKAWizardForPKIAPI state: " + this.state);
	switch(this.state) {
		case "new":
			var page = this.getClientActivationPage(req, res, url);
			this.state = "enumerateKeys";
			break;
		case "enumerateKeys":
			// no client found
			var page = this.getAuthenticationTokenPage(req, res, url);
			this.state = "new";
			break;
		case "noMatchingKey":
			if (req.method == "POST") {
				this.state = "new";
			} else {
				var page = this.getNoMatchingKeyPage(req, res, url);
			}
			break;
		case "enterData":
			if (req.method == "POST") {
				if (!req.parameters.key) {
					this.state = "new";
				} else if (!this.keys.includes(req.parameters.key)) {
					this.reportError("Input validation failed")
				} else {
					this.key = req.parameters.key;
					this.state = "startPKA";
				}
			} else {
				var page = this.getKeySelectionPage(req, res, url);
			}
			break;
		case "startPKA":
			var page = this.getClientActivationPageWithPIN(req, res, url);
			this.state = "performPKA";
			break;
		case "performPKA":
			var page = this.getErrorPage(req, res, url);
			this.state = "startPKA";
			break;
        case CardAction.SUCCESS:
			this.ui.redirectTo(req, res, req.url, this.redirectURL);
			return;
		default:
			var page = CardAction.prototype.getPage.call(this, req, res, this.redirectURL);
	}
	return page;
}



/**
 * Serve the error page if no local client was found
 *
 * @param {HttpRequest} req the request object
 * @param {HttpResponse} req the response object
 */
PKAWizardForPKIAPI.prototype.getAuthenticationTokenPage = function(req, res) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var page =
		<div>
			<h1>Public Key Authentication Wizard</h1>
			<p>This wizard performs public key authentication on token {this.tokenInfo.path}.</p>
			<p>Please connect the authentication token with your local client and press <a href="">refresh</a> to proceed.</p>
			<p>
			If you have problems connecting your token see the <a href="https://www.openscdp.org/ocf/faq.html">FAQ</a> for more details
			   and troubleshooting hints.</p>
		</div>

	return page;
}



PKAWizardForPKIAPI.prototype.getClientActivationPage = function(req, res, url) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var page =
		<div>
			<h1>Public Key Authentication Wizard</h1>
			<p>Enumerate keys... { this.getActivationLink(req, res, -1) }</p>
			<p>Please wait for the green tick to appear. The page will be reloaded automatically if JavaScript is enabled.</p>
			<p>If JavaScript is disabled, then press <a href="">continue</a> after the green tick is shown.</p>
		</div>

	return page;
}



PKAWizardForPKIAPI.prototype.getNoMatchingKeyPage = function(req, res, url) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var page =
		<div>
			<div id="error"/>
			<h1>Public Key Authentication Wizard</h1>

			<h2>No Matching Key Found on Authentication Token</h2>

			<p>The authentication token {this.path} in this session doesn't contain any of the following public keys of token {this.tokenInfo.path}:</p>
			<td>
				<ul/>
			</td>

			<p>Please connect another authentication token and press
				<form class="pure-form" action="" method="post" enctype="multipart/form-data">
					<input name="action" type="hidden" value="enumerate"/>
					<button class="pure-button" type="submit">Enumerate Keys</button>
				</form>
			</p>
		</div>

	for (var i = 0; i < this.tokenInfo.pkaStatus.publicKeys.length; i++) {
		var keyInfo = this.tokenInfo.pkaStatus.publicKeys[i];
		if (keyInfo.authenticated) {
            continue;
        }
		var chr = keyInfo.chr;
		page.td.ul.li += <li>{chr}</li>;
	}

	return page;
}



PKAWizardForPKIAPI.prototype.getKeySelectionPage = function(req, res, url) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var page =
		<div>
			<div id="error"/>
			<h1>Public Key Authentication Wizard</h1>

			<p>
			<form class="pure-form" action="" method="post" enctype="multipart/form-data">
				<p>Token: {this.tokenInfo.path}</p>
				<label>Authentication Token: {this.path}</label>
				<input name="action" type="hidden" value="enumerate"/>
				<button class="pure-button" type="submit">Enumerate Keys</button>
			</form>
			</p>

			<form class="pure-form" action="" method="post" enctype="multipart/form-data">
				<p>Select the key for authentication:</p>
				<select name="key"/>
				<button class="pure-button" type="submit">Authenticate</button>
			</form>
		</div>

	var l = page.form.select;
	for each (var alias in this.keys) {
		l[0].appendChild(<option>{alias}</option>);
	}

	return page;
}



PKAWizardForPKIAPI.prototype.getClientActivationPageWithPIN = function(req, res, url) {
	default xml namespace = "http://www.w3.org/1999/xhtml";

	var page =
		<div>
			<h1>Public Key Authentication Wizard</h1>
			<p>Start authentication... { this.getActivationLink(req, res, 1) }</p>
			<p>This operation requires PIN verification. Please watch the PIN dialog on your screen or on your card readers PIN pad.</p>
			<p>Please wait for the green tick to appear. The page will be reloaded automatically if JavaScript is enabled.</p>
			<p>If JavaScript is disabled, then press <a href="">continue</a> after the green tick is shown.</p>
		</div>

	return page;
}



PKAWizardForPKIAPI.prototype.determinePublicKeys = function(sc) {
    this.pkaStatus = this.tokenInfo.pkaStatus;

    if (this.pkaStatus.authenticated) {
        this.result = "Token already authenticated";
        this.state = "success";
        return
    }

    var labels = sc.enumerateKeys();
    GPSystem.log(GPSystem.DEBUG, module.id, "found keys: " + labels);
    if (labels.length == 0) {
        this.reportError("No keys found on card");
    }

    this.keys = [];
    var chrMap = {};

    var ks = new HSMKeyStore(sc);

    for (var i = 0; i < labels.length; i++) {
        var label = labels[i];
        if (ks.hasCertificate(label)) {
            var cert = ks.getCertificate(label);
            try {
                var cvc = new CVC(cert);
                GPSystem.log(GPSystem.DEBUG, module.id, "Add cvc " + cvc + " for label "+ label);

				if (typeof(chrMap[cvc.getCHR().toString()]) == "undefined") {
					chrMap[cvc.getCHR().toString()] = label;
				} else if (typeof(chrMap[cvc.getCHR().toString()]) == "string") {
					var l = chrMap[cvc.getCHR().toString()];
					chrMap[cvc.getCHR().toString()] = [l, label];
				} else {
					chrMap[cvc.getCHR().toString()].push(label);
				}
				GPSystem.log(GPSystem.DEBUG, module.id, "### " + chrMap[cvc.getCHR().toString()]);

            } catch (e) {
                GPSystem.log(GPSystem.DEBUG, module.id, "Label " + label + " is not a CVC");
                continue;
            }
        }
    }


    for (var i = 0; i < this.pkaStatus.publicKeys.length; i++) {
        var keyInfo = this.pkaStatus.publicKeys[i];

        if (keyInfo.authenticated) {
            continue;
        }

        var chr = keyInfo.chr;
        if (typeof(chrMap[chr]) == "string") {
            var label = chrMap[chr];
            this.keys.push(label);
		} else if (Array.isArray(chrMap[chr])) {
			this.keys = this.keys.concat(chrMap[chr]);
		} else if (labels.includes(chr)) {
            this.keys.push(chr);
        }
    }

    if (this.keys.length == 0) {
		this.state = "noMatchingKey";
    } else if (this.keys.length == 1) {
        this.key = this.keys[0];
        this.state = "startPKA";
    } else {
        this.state = "enterData";
    }
}



PKAWizardForPKIAPI.prototype.handleCardAction = function(card, reqInfo) {
	var sc = new SmartCardHSM(card);

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

	sc.openSecureChannel(crypto, chain.publicKey);

	switch(this.state) {
		case "enumerateKeys":
			this.path = chain.path;
			this.determinePublicKeys(sc);
			break;
		case "performPKA":
			var ks = new HSMKeyStore(sc);
			var key = ks.getKey(this.key);
			var cert = ks.getCertificate(this.key);
			var cvc = new CVC(cert);

            var challengeRsp = this.api.getPKAChallenge(this.subjectId, this.tokenId, this.ui.session.user.id);

            var challenge = ByteString.fromJSON(challengeRsp.pkaChallenge);
            var input = new ByteString(challengeRsp.deviceId, ASCII).concat(challenge);
            var crypto = sc.getCrypto();
            var signature = crypto.sign(key, Crypto.ECDSA_SHA256, input);
            signature = CVC.unwrapSignature(signature, 32);

            var newStatus = this.api.performPublicKeyAuthentication(this.subjectId, this.tokenId, this.ui.session.user.id, cvc.getCHR(), signature);

			if (newStatus.numberOfAuthenticatedPublicKeys <= this.pkaStatus.numberOfAuthenticatedPublicKeys) {
                this.reportError("Authentication with key " + this.key + " failed");
                return;
            }

			this.result = "Public key authentication successful";
			this.state = "success";
			break;
		default:
			GPSystem.trace("Unexpected handleCardAction in state " + this.state);
	}
}
