/**
 *  ---------
 * |.##> <##.|  SmartCard-HSM Support Scripts
 * |#       #|
 * |#       #|  Copyright (c) 2011-2012 CardContact Software & System Consulting
 * |'##> <##'|  Andreas Schwier, 32429 Minden, Germany (www.cardcontact.de)
 *  ---------
 *
 * Consult your license package for usage terms and conditions.
 *
 * @fileoverview A simple X.509 CA setup
 */

var CAGUI = require('pki-as-a-service/ui/CAGUI').CAGUI;
var CVC = require('scsh/eac/CVC').CVC;
var Subject = require('scsh/pki-db/Subject').Subject;
var X509CertificateIssuer = require('scsh/x509/X509CertificateIssuer').X509CertificateIssuer;
var SmartCardHSMUpdateService = require('pki-as-a-service/service/SmartCardHSMUpdateService').SmartCardHSMUpdateService;
var BackendRegistry = require('pki-as-a-service/service/BackendRegistry').BackendRegistry;



/**
 * Create a CA service instance
 *
 * @class Class implementing a basic online CA service
 * @param {X509CA} ca the ca instance
 * @param {DAOFactory} daof the data access object factory to use
 */
function CAService(hsmService, name, daof) {
	GPSystem.log(GPSystem.DEBUG, module.id, "new (" + hsmService + "," + name + "," + daof + ")");

	this.daof = daof;
	this.type = "X509 CA";
	this.name = name;
	this.crypto = new Crypto();
	this.hsmService = hsmService;
	this.backendRegistry = new BackendRegistry(this);
	this.selfRegistrationMode = 2;		// 0 - Disabled, 1 - with approval, 2 - without approval
}

exports.CAService = CAService;



CAService.prototype.getCertificateProfileList = function() {
	var profiles = [
		{ id: 1, name: "SubCA" },
		{ id: 2, name: "TLSServer" },
		{ id: 3, name: "TLSClient" },
		{ id: 4, name: "EmailAndTLSClient" }
	];
	return profiles;
}



CAService.KeySpecification = [
	{ id:  1, name: "RSA 2048 V1.5 SHA-256", type: "RSA", keysize: 2048, sigalg: Crypto.RSA_SHA256 },
	{ id:  2, name: "RSA 2048 PSS SHA-256",  type: "RSA", keysize: 2048, sigalg: Crypto.RSA_PSS_SHA256 },
	{ id: 50, name: "RSA 2048 PSS SHA-384",  type: "RSA", keysize: 2048, sigalg: Crypto.RSA_PSS_SHA384 },
	{ id: 51, name: "RSA 2048 PSS SHA-512",  type: "RSA", keysize: 2048, sigalg: Crypto.RSA_PSS_SHA512 },
	{ id:  3, name: "RSA 3072 V1.5 SHA-256", type: "RSA", keysize: 3072, sigalg: Crypto.RSA_SHA256 },
	{ id:  4, name: "RSA 3072 PSS SHA-256",  type: "RSA", keysize: 3072, sigalg: Crypto.RSA_PSS_SHA256 },
	{ id:  7, name: "RSA 3072 PSS SHA-384",  type: "RSA", keysize: 3072, sigalg: Crypto.RSA_PSS_SHA384 },
	{ id:  5, name: "RSA 4096 V1.5 SHA-256", type: "RSA", keysize: 4096, sigalg: Crypto.RSA_SHA256 },
	{ id:  6, name: "RSA 4096 PSS SHA-256",  type: "RSA", keysize: 4096, sigalg: Crypto.RSA_PSS_SHA256 },
	{ id:  8, name: "RSA 4096 PSS SHA-512",  type: "RSA", keysize: 4096, sigalg: Crypto.RSA_PSS_SHA512 },

	{ id: 10, name: "EC prime256v1 SHA-256", type: "EC", curve: "secp256r1", sigalg: Crypto.ECDSA_SHA256 },
	{ id: 11, name: "EC secp384r1 SHA-256",  type: "EC", curve: "secp384r1", sigalg: Crypto.ECDSA_SHA256 },
	{ id: 12, name: "EC secp521r1 SHA-256",  type: "EC", curve: "secp521r1", sigalg: Crypto.ECDSA_SHA256 },
	{ id: 13, name: "EC secp384r1 SHA-384",  type: "EC", curve: "secp384r1", sigalg: Crypto.ECDSA_SHA384 },
	{ id: 12, name: "EC secp521r1 SHA-512",  type: "EC", curve: "secp521r1", sigalg: Crypto.ECDSA_SHA512 },

	{ id: 20, name: "EC brainpoolP256r1 SHA-256", type: "EC", curve: "brainpoolP256r1", sigalg: Crypto.ECDSA_SHA256 },
	{ id: 21, name: "EC brainpoolP320r1 SHA-256", type: "EC", curve: "brainpoolP320r1", sigalg: Crypto.ECDSA_SHA256 },
	{ id: 22, name: "EC brainpoolP384r1 SHA-256", type: "EC", curve: "brainpoolP384r1", sigalg: Crypto.ECDSA_SHA256 },
	{ id: 23, name: "EC brainpoolP512r1 SHA-256", type: "EC", curve: "brainpoolP512r1", sigalg: Crypto.ECDSA_SHA256 },
	{ id: 24, name: "EC brainpoolP384r1 SHA-384", type: "EC", curve: "brainpoolP384r1", sigalg: Crypto.ECDSA_SHA384 },
	{ id: 25, name: "EC brainpoolP512r1 SHA-512", type: "EC", curve: "brainpoolP512r1", sigalg: Crypto.ECDSA_SHA512 }
];

CAService.prototype.getKeySpecificationList = function() {
	return CAService.KeySpecification;
}



CAService.prototype.getKeySpecification = function(id) {
	for (var i = 0; i < CAService.KeySpecification.length; i++) {
		if (id == CAService.KeySpecification[i].id) {
			return CAService.KeySpecification[i];
		}
	}
	throw new GPError(module.id, GPError.INVALID_DATA, 0, "No key specification found for id " + id);
}



/**
 * Return a subject object based on the type attribute in the database
 *
 * @type {Number} id the subject id
 * @return Object
 * @return Object with a class derived from SubjectModel
 */
CAService.prototype.getSubject = function(id) {

	var dao = this.daof.getSubjectDAO();
	var bo = dao.getSubject(id);
	if (!bo) {
		throw new GPError(module.id, GPError.OBJECT_NOT_FOUND, 0, "No subject found with id " + id);
	}

	var factory = this.pluginRegistry.getFactoryForSubject(bo.type);
	if (!factory) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Can not find factory for subject type " + bo.type);
	}

	var subject = factory.getSubject(bo);
	return subject;
}



CAService.prototype.getServiceRequestById = function(id) {
	if (typeof(id) != "number" || isNaN(id)) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Invalid argument: " + id);
	}

	var srdao = this.daof.getServiceRequestDAO();
	var bo = srdao.getServiceRequestById(id);
	if (!bo) {
		throw new GPError(module.id, GPError.OBJECT_NOT_FOUND, 0, "No service request found with id " + id);
	}

	var factory = this.pluginRegistry.getFactoryForProcess(bo.process);
	if (!factory) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Can not find factory for process " + bo.process);
	}

	var sr = factory.getServiceRequest(bo);
	return sr;
}



CAService.prototype.getControllerForProcess = function(process) {
	if (process == null || process.length == 0) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Invalid argument: " + process);
	}

	var factory = this.pluginRegistry.getFactoryForProcess(process);
	if (!factory) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Can not find factory for process " + process);
	}

	var controller = factory.getControllerForProcess(process);
	return controller;
}



/**
 * Get the token object identified by the given path
 *
 * @param {String} path the token's path
 * @returns the token
 * @type Token
 */
CAService.prototype.getToken = function(path) {
	var tokenDAO = this.daof.getTokenDAO();
	var token = tokenDAO.getToken(path);
	if (token == null) {
		token = tokenDAO.newToken(path);
	}
	return token;
}



/**
 * Check the authorization status for the given token
 *
 * @param {Token} token the token
 * @returns true if the token is authorized, false otherwise
 */
CAService.prototype.isAuthorized = function(token) {
	return token.subjectId != null;
}



/**
 * Get the Register Token Service Request linked to the given token
 * or create a new service request.
 *
 * @param {Token} token the token to be registered
 * @returns the request
 * @type RegisterMyTokenServiceRequest
 */
CAService.prototype.getRegisterMyTokenServiceRequest = function(tokenId, lang) {
	GPSystem.log(GPSystem.DEBUG, module.id, "getRegisterMyTokenServiceRequest(tokenId=" + tokenId + ", lang=" + lang + ")");

	var factory = this.pluginRegistry.getFactoryForProcess("RegisterMyTokenServiceRequest");
	if (!factory) {
		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Can not find factory for process RegisterMyTokenServiceRequest");
	}

	var tokenDAO = this.daof.getTokenDAO();
	var token = tokenDAO.getToken(tokenId);

	if (token.subjectId) {
		GPSystem.log(GPSystem.WARN, module.id, "Attempt to register token already assigned to subject " + token.subjectId);
		return;
	}

	var registrationBO = this.getRegistrationBOFromTokenActions(token);
	if (!registrationBO) {
		if (this.selfRegistrationMode == 0) {
			GPSystem.log(GPSystem.WARN, module.id, "Self-registration is disabled");
			return;
		}

		var ctrl = factory.getControllerForProcess("RegisterMyTokenServiceRequest");
		var parameter = {lang: lang, token: token};
		var registrationBO = ctrl.createRequest(this, parameter);
	}

	var sr = factory.getServiceRequest(registrationBO);
	return sr;
}



CAService.prototype.getRegistrationBOFromTokenActions = function(token) {
	var tokenDAO = this.daof.getTokenDAO();
	var actions = tokenDAO.enumerateTokenAction(token.id);
	var srdao = this.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;
		}

		if (bo.process == "RegisterMyTokenServiceRequest") {
			return bo;
		}
	}

	return null;
}



CAService.prototype.removeRegistrationTokenAction = function(tokenId) {
	GPSystem.log(GPSystem.DEBUG, module.id, "removeRegistrationTokenAction(" + tokenId + ")");
	var tokenDAO = this.daof.getTokenDAO();
	var token = tokenDAO.getToken(tokenId);
	var actions = tokenDAO.enumerateTokenAction(token.id);

	var srdao = this.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;
		}

		if (bo.process == "RegisterMyTokenServiceRequest") {
			tokenDAO.removeTokenAction(actions[i].id);
			return actions[i].id;
		}
	}

	return -1;
}



/**
 * Define configuration for outbund e-mails
 *
 * @param {String} smtpserver the SMTP server hostname
 * @param {String} emailfrom the e-mail address the message shall be send from
 * @param {String} emailAdmin the e-mail address where user messages will be forwarded to
 */
CAService.prototype.setSMTPConfig = function(smtpserver, emailfrom, emailAdmin) {
	this.smtpserver = smtpserver;
	this.emailfrom = emailfrom;
	this.emailAdmin = emailAdmin;
}



/**
 * Return the card update handler
 *
 * @type Object
 * @return the object handling the card updates
 */
CAService.prototype.getCardUpdateHandler = function() {
	if (typeof(this.cardUpdateHandler) == "undefined") {
		this.cardUpdateHandler = new SmartCardHSMUpdateService(this);
	}

	return this.cardUpdateHandler;
}



/**
 * Create a new UI session
 */
CAService.prototype.newUI = function(session) {
	return new CAGUI(this, session);
}



/**
 * Generate random data and decimalize into a sequence of digits
 *
 * @param {digits} number of digits
 * @type String
 * @return the random activation code
 */
CAService.prototype.generateActivationCode = function(digits) {
	var raw = this.crypto.generateRandom(digits);

	var c;
	do	{
		var str = raw.toString(HEX);
		c = digits;
		var result = "";
		var i = 0;

		while ((i < str.length) && (c > 0)) {
			var d = str.charCodeAt(i);
			if ((d >= 0x30) && (d <= 0x39)) {
				result = result.concat(String.fromCharCode(d));
				c--;
			}
			i++;
		}
		if (c > 0) {
			raw = raw.not();
		}

	} while (c > 0);

	return result;
}



/**
 * Add a blacklist
 *
 * @param{String[]} list list of entries or regular expressions
 */
CAService.prototype.addBlackList = function(list) {
	if (typeof(this.blacklist) == "undefined") {
		this.blacklist = [];
		this.blacklistfilter = [];
	}
	for each (var e in list) {
		if (typeof(e) == "string") {
			this.blacklist[e] = true;
		} else {
			this.blacklistfilter.push(e);
		}
	}
}



/**
 * Check if device is on blacklist
 *
 * @param {Object} req request containing the authenticated certificate signing request and the
 *                     device certificate and issuer certificate read from the SmartCard-HSM
 * @type boolean
 * @return true if blacklisted
 */
CAService.prototype.isBlackListed = function(req) {
	if (typeof(this.blacklist) == "undefined") {
		return false;
	}

	// Check list
	if (typeof(this.blacklist[req.path]) != "undefined") {
		return true;
	}

	// Check regular expressions
	for each (var regex in this.blacklistfilter) {
		if (req.path.match(regex)) {
			return true;
		}
	}
	return false;
}



/**
 * Send an e-mail
 *
 * @param {String} recipient the recipient e-mail address
 * @param {String} subject the e-mail subject
 * @param {String} text the text
 */
CAService.prototype.sendMail = function(recipient, subject, text) {
	if (!this.smtpserver) {
		GPSystem.log(GPSystem.DEBUG, module.id, "SMTP server disabled. Skip sending e-mail.");
		return;
	}

	var prop = java.lang.System.getProperties();
	prop.setProperty("mail.smtp.host", this.smtpserver);

	var session = javax.mail.Session.getDefaultInstance(prop);

	var msg = new javax.mail.internet.MimeMessage(session);
	msg.setFrom(new javax.mail.internet.InternetAddress(this.emailfrom));

	msg.addRecipient(javax.mail.Message.RecipientType.TO,new javax.mail.internet.InternetAddress(recipient));
	msg.setSubject(subject);
	msg.setText(text);
	javax.mail.Transport.send(msg);
}



/**
 * Send an e-mail with the activation code
 *
 * @param {Object} req request containing the authenticated certificate signing request and the
 *                     device certificate and issuer certificate read from the SmartCard-HSM
 */
CAService.prototype.sendActivationCode = function(sr) {
	GPSystem.log(GPSystem.DEBUG, module.id, "send activation code " + sr.getActivationCode() + " at " + sr.getEmail());

	var subject = this.name + " Activation Code";
	var text =	"Dear user,\n\n" +
			"please use the code " + sr.getActivationCode() + " to continue the activation process.\n\n" +
			"If you received this e-mail unexpectedly, then someone used your e-mail address at the PKI-as-a-Service Portal.\n\n" +
			"In that case please forward this e-mail to " + this.emailAdmin + "\n\n" +
			"Kind regards,\n\n" +
			this.name + "\n\n";

	this.sendMail(sr.getEmail(), subject, text);
}



/**
 * Send an user message with related service request to the registered admin
 *
 * @param {ServiceRequest} sr the service request
 * @param {String} name the name
 * @param {String} email the e-mail address
 * @param {String} message the message
 */
CAService.prototype.contactAdmin = function(sr, name, email, message) {
	var subject = this.name + "Reset Aborted Service Request";
	var text =	"Service Request: " + sr + ",\n\n" +
			"Name: " + name + ",\n\n" +
			"E-mail: " + email + ",\n\n" +
			"Message: " + message;
	GPSystem.log(GPSystem.DEBUG, module.id, "Contact Admin " + this.emailAdmin + "\n\n" + text);

	this.sendMail(this.emailAdmin, subject, text);
}
