/**
 *  ---------
 * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
 * |#       #|
 * |#       #|  Copyright (c) 1999-2026 CardContact Systems GmbH
 * |'##> <##'|  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 File encryptor.
 */

var CVC = require("scsh/eac/CVC").CVC;
var PKIXCommon = require("scsh/x509/PKIXCommon").PKIXCommon;



/**
 * Craate a FileEncryptor instance.
 *
 * @class Class implementing file encryption
 * @constructor
 * @param {SmartCardHSM} sc the SmartCardHSM object used to access the token or undefined at the sender in the hybrid scheme.
 * @param {Number} mode the mode to use (One of FileEncryptor.NATIVE, .DERIVED or .HYBRID). Must be specified at the sending side.
 */
 function FileEncryptor(sc, mode) {
	this.mode = mode;
	this.sc = sc;
	this.crypto = new Crypto();

	this.keyId = new ByteString("UNKNOWN", ASCII);
}

exports.FileEncryptor = FileEncryptor;

FileEncryptor.NATIVE = 1;
FileEncryptor.DERIVED = 2;
FileEncryptor.HYBRID = 3;



/**
 * Get the crypto object.
 *
 * @private
 * @type Crypto
 * @return the crypto object.
 */
FileEncryptor.prototype.getCrypto = function() {
	if (typeof(this.sc) != "undefined") {
		return this.sc.getCrypto();
	}
	return this.crypto;
}



/**
 * Generate the IV
 *
 * @private
 */
FileEncryptor.prototype.generateIV = function() {
	this.iv = this.crypto.generateRandom(16);
}



/**
 * Calculate the hash of the encrypted payload.
 *
 * @private
 * @param {ByteString} content the encrypted payload
 * @type ByteString
 * @return the hash value.
 */
FileEncryptor.prototype.calculateHash = function(content) {
	return this.crypto.digest(Crypto.SHA_256, content);
}



/**
 * Create the TLV encoded header of the encrypted file.
 *
 * @private
 * @type ASN1
 * @return the header.
 */
FileEncryptor.prototype.createHeader = function() {
	var tbs = new ASN1("tbs", ASN1.SEQUENCE,
				new ASN1("oid", ASN1.OBJECT_IDENTIFIER, new ByteString("CardContact 4 6 " + this.mode, OID)),
				new ASN1("keyId", ASN1.OCTET_STRING, this.keyId),
				new ASN1("hash", ASN1.OCTET_STRING, this.hash),
				new ASN1("iv", ASN1.OCTET_STRING, this.iv),
				new ASN1("extensions", ASN1.SEQUENCE)
			);

	if (this.mode == FileEncryptor.HYBRID) {
		tbs.add(new ASN1("senderPublicKey", 0xA0, PKIXCommon.createECSubjectPublicKeyInfo(this.pukey)));
	}

	var mac = this.getCrypto().sign(this.mackey, Crypto.AES_CMAC, tbs.getBytes());

	var header =
		new ASN1("EncryptionHeader", ASN1.SEQUENCE,
			tbs,
			new ASN1("mac", ASN1.OCTET_STRING, mac)
		);

	return header;
}



/**
 * Parse the header of the encrypted file.
 *
 * @param {ByteString} buffer the encrypted file with header.
 * @type Number
 * @return the offset at which the payload starts.
 */
FileEncryptor.prototype.parseHeader = function(buffer) {
	if (buffer.byteAt(0) != 0x30) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Header corrupted");
	}

	var a = new ASN1(buffer);

	if (a.elements != 2) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Header corrupted");
	}

	this.mac = a.get(1).value;
	this.tbs = a.get(0);
	if ((this.tbs.elements < 5) || (this.tbs.elements > 6)) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Header corrupted");
	}

	var oid = this.tbs.get(0).value;
	if (!oid.startsWith(new ByteString("CardContact 4 6", OID))) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "File format not supported");
	}

	this.mode = oid.byteAt(oid.length - 1);
	this.keyId = this.tbs.get(1).value;
	this.hash = this.tbs.get(2).value;
	this.iv = this.tbs.get(3).value;

	if (this.tbs.elements == 6) {
		// Parse sender public key
		var spki = this.tbs.get(5).get(0);
		var oid = spki.get(0).get(1).value;
		var pk = spki.get(1).value.bytes(2);
		this.pukey = new Key();
		this.pukey.setType(Key.PUBLIC);
		this.pukey.setComponent(Key.ECC_CURVE_OID, oid);
		this.pukey.setComponent(Key.ECC_QX, pk.left(pk.length >> 1));
		this.pukey.setComponent(Key.ECC_QY, pk.right(pk.length >> 1));
	}

	return a.size;
}



/**
 * Validate the Message Authentication Code (MAC) over the header.
 *
 * @throws GPError is MAC verification failed.
 */
FileEncryptor.prototype.validateHeader = function() {
	var mac = this.getCrypto().sign(this.mackey, Crypto.AES_CMAC, this.tbs.getBytes());
	if (!mac.equals(this.mac)) {
		throw new GPError(module.id, GPError.CRYPTO_FAILED, 0, "Integrity check failed (Invalid MAC).");
	}
}



/**
 * Generate ephemeral EC key pair at the sending side in hybrid scheme.
 *
 * @param {Key} key the domain parameter.
 */
FileEncryptor.prototype.generateEphemeralKeyPair = function(dp) {
	this.prkey = new Key(dp);
	this.prkey.setType(Key.PRIVATE);

	this.pukey = new Key(dp);
	this.pukey.setType(Key.PUBLIC);

	this.crypto.generateKeyPair(Crypto.EC, this.pukey, this.prkey);
}



/**
 * Derive a AES key from a AES master key using a CMAC based Key Derivation Function in
 * Counter Mode as per NIST SP 800-108.
 *
 * @param {Key} key the AES master key.
 * @param {Number} size the requested number of derived bytes.
 * @param {ByteString} context the derivation context used to differ keys derived from the same master.
 * @type ByteString
 * @return the derived key value.
 */
FileEncryptor.prototype.keyDerivationFunction1 = function(key, size, context) {
	var dd = new ByteBuffer();
	var os = size;
	var iter = 1;
	while (os > 0) {
		var dp = new ByteBuffer();
		dp.append(ByteString.valueOf(iter, 4));
		dp.append(context);
		dp.append(ByteString.valueOf(size << 3, 4));

		var mac = this.getCrypto().sign(key, Crypto.AES_CMAC, dp.toByteString());

		dd.append(mac.left(os > mac.length ? mac.length : os));
		os -= mac.length;
		mac.clear();
		iter++;
	}
	var result = dd.toByteString();
	dd.clear();
	return result;
}



/**
 * Derive an AES key from the shared secret resulting from performing the ECDH using
 * a one-step-kdf with SHA-256 (Option 1) as defined in NIST SP 800-56C.
 *
 * @param {Number} id the key identifier (0x01 for K.ENC, 0x02 for K.MAC).
 * @param {ByteString} sharedSecret the shared secret resulting from ECDH.
 * @return the key value as 32 bytes.
 */
FileEncryptor.prototype.keyDerivationFunction2 = function(id, sharedSecret) {
	var inp = ByteString.valueOf(1, 4).concat(sharedSecret).concat(ByteString.valueOf(id));
	var res = this.crypto.digest(Crypto.SHA_256, inp);
	inp.clear();
	return res;
}



/**
 * Derive K.ENC and K.MAC based on the selected scheme.
 *
 * @param {Key} key the AES key or private EC key.
 * @param {CVC} key the CVC with the public of the receiver.
 */
FileEncryptor.prototype.deriveKey = function(key) {
	if (typeof(this.iv) == "undefined") {
		this.generateIV();
	}

	switch(this.mode) {
		case FileEncryptor.NATIVE:
			this.mackey = key;
			this.enckey = key;
			if (typeof(key.getPKCS15Id) == "function") {
				this.keyId = key.getPKCS15Id();
			}
			break;
		case FileEncryptor.DERIVED:
			this.mackey = key;
			this.enckey = new Key();
			var kv = this.keyDerivationFunction1(key, key.getSize() >> 3, this.iv);
			this.enckey.setComponent(Key.AES, kv);
			kv.clear();

			if (typeof(key.getPKCS15Id) == "function") {
				this.keyId = key.getPKCS15Id();
			}
			break;
		case FileEncryptor.HYBRID:
			var ss;
			if (key instanceof CVC) {
				var pk = key.getPublicKey();
				this.generateEphemeralKeyPair(pk);
				var point = pk.getComponent(Key.ECC_QX).concat(pk.getComponent(Key.ECC_QY));
				ss = this.crypto.decrypt(this.prkey, Crypto.ECDHP, point, new ByteString("", HEX));
				this.keyId = key.determineKeyIdentifier();
			} else {
				var point = this.pukey.getComponent(Key.ECC_QX).concat(this.pukey.getComponent(Key.ECC_QY));
				ss = this.getCrypto().decrypt(key, Crypto.ECDHP, point, new ByteString("", HEX));
			}

			this.enckey = new Key();
			var kv = this.keyDerivationFunction2(1, ss);
			this.enckey.setComponent(Key.AES, kv);
			kv.clear();

			this.mackey = new Key();
			var kv = this.keyDerivationFunction2(2, ss);
			this.mackey.setComponent(Key.AES, kv);
			kv.clear();
			ss.clear();
			break;
		default:
			throw new GPError(module.id, GPError.INVALID_DATA, 0, "Mode not defined or invalid");
	}
}



/**
 * Encrypt the buffer with K.ENC previously derived with deriveKey() and prepend header.
 *
 * @param {ByteString} plain the plain input. The caller should clear the buffer after use with plain.clear().
 * @type ByteString
 * @return the header concatenated with the encrypted payload.
 */
FileEncryptor.prototype.encrypt = function(plain) {

	var p = plain.pad(Crypto.ISO9797_METHOD_2_16);

	var c = this.getCrypto().encrypt(this.enckey, Crypto.AES_CBC, p, this.iv);
	p.clear();

	this.hash = this.calculateHash(c);

	var header = this.createHeader();

	return header.getBytes().concat(c);
}



/**
 * Validate and strip the ISO padding.
 *
 * @param {ByteString} buffer the decrypted buffer.
 * @throw GPError is padding is invalid, indicating the wrong key or corrupted cipher.
 * @type ByteString
 * @return the buffer without padding.
 */
FileEncryptor.stripPadding = function(buffer) {

	var ofs = buffer.length - 1;
	while((ofs > 0) && (buffer.byteAt(ofs) == 0)) {
		ofs--;
	}

	if ((ofs <= 0) || (buffer.byteAt(ofs) != 0x80)) {
		throw new GPError(module.id, GPError.CRYPTO_FAILED, 0, "Plain block has invalid padding. Key probably wrong.");
	}

	return buffer.left(ofs);
}



/**
 * Validate and decrypt the payload.
 *
 * @param {ByteString} buffer the encrypted payload.
 * @type ByteString
 * @return the plain content.
 */
FileEncryptor.prototype.decrypt = function(buffer) {

	this.validateHeader();

	var hash = this.calculateHash(buffer);
	if (!hash.equals(this.hash)) {
		throw new GPError(module.id, GPError.CRYPTO_FAILED, 0, "Integrity check failed (Invalid hash).");
	}

	var pp = this.getCrypto().decrypt(this.enckey, Crypto.AES_CBC, buffer, this.iv);
	var p = FileEncryptor.stripPadding(pp);
	pp.clear();

	return p;
}
