/**
 *  ---------
 * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
 * |#       #|
 * |#       #|  Copyright (c) 1999-2009 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 Common classes and functions for PKIX
 */



/**
 * Common functions and constants
 */
function PKIXCommon() {
}

exports.PKIXCommon = PKIXCommon;


PKIXCommon.digitalSignature	= 0x0080;
PKIXCommon.nonRepudiation	= 0x0040;
PKIXCommon.keyEncipherment	= 0x0020;
PKIXCommon.dataEncipherment	= 0x0010;
PKIXCommon.keyAgreement		= 0x0008;
PKIXCommon.keyCertSign		= 0x0004;
PKIXCommon.cRLSign		= 0x0002;
PKIXCommon.encipherOnly		= 0x0001;
PKIXCommon.decipherOnly		= 0x8000;



/**
 * Convert integer to fixed length string with leading zeros.
 *
 * @private
 * @param {Number} value the value to convert to a string.
 * @param {Number} digits the number of digits in output string. Must be <= 20.
 * @return the 0-padded string
 * @type String
 */
PKIXCommon.itos = function(value, digits) {
	if (digits > 20) {
		throw new Error("Digits must be <= 20");
	}
	var str = "" + value;
	str = "0000000000000000000".substr(19 - (digits - str.length)).concat(str);
	return str;
}



/**
 * Convert date and time to UTC string with format YYMMDDHHMMSSZ.
 *
 * @param {Date} d the date object.
 * @return the date/time string.
 * @type String
 */
PKIXCommon.dtoUTC = function(d) {
	var s = PKIXCommon.itos(d.getUTCFullYear() % 100, 2) +
			PKIXCommon.itos(d.getUTCMonth() + 1, 2) +
			PKIXCommon.itos(d.getUTCDate(), 2) +
			PKIXCommon.itos(d.getUTCHours(), 2) +
			PKIXCommon.itos(d.getUTCMinutes(), 2) +
			PKIXCommon.itos(d.getUTCSeconds(), 2) + "Z";
	return s;
}



/**
 * Convert date and time to UTC string with format YYYYMMDDHHMMSSZ.
 *
 * @param {Date} d the date object.
 * @return the date/time string.
 * @type String
 */
PKIXCommon.dtoUTCFullYear = function(d) {
	var s = 	d.getUTCFullYear() +
			PKIXCommon.itos(d.getUTCMonth() + 1, 2) +
			PKIXCommon.itos(d.getUTCDate(), 2) +
			PKIXCommon.itos(d.getUTCHours(), 2) +
			PKIXCommon.itos(d.getUTCMinutes(), 2) +
			PKIXCommon.itos(d.getUTCSeconds(), 2) + "Z";
	return s;
}



/**
 * Add the specified number of days to a date object
 *
 * @param {Date} d the date object
 * @param {Number} days the number of days to add, may be negative
 * @type Date
 * @return a new Date object
 */
PKIXCommon.addDays = function(d, days) {
	var hour = d.getUTCHours();
	var minute = d.getUTCMinutes();
	var second = d.getUTCSeconds();
	var cd = new Date(d);
	cd.setHours(12);

	var ts = cd.getTime();
	ts += days * 24 * 60 * 60 * 1000;
	var nd = new Date(ts);

	nd.setUTCHours(hour);
	nd.setUTCMinutes(minute);
	nd.setUTCSeconds(second);
	return nd;
}



/**
 * Converts the integer value into a BIT STRING value.
 * <p>The function interprets the integer value as bitmap, where
 * bit 0 is the most significant bit of the least significant byte.</p>
 * <p>The function adds the minimum number of bytes to the final bit string
 * and encodes the "number of unused bits at the beginning.</p>
 *
 * @param {Number} val the value to convert
 * @return the bit string
 * @type ByteString
 */
PKIXCommon.bitstringForInteger = function(val) {
	var bb = new ByteBuffer();
	var b = 0;

	// Encode starting with the least significant byte
	while (val > 0) {
		b = val & 0xFF;
		bb.append(b);
		val = val >> 8;
	}

	// Determine number of unused bits
	var i = 0;
	while ((i < 8) && !(b & 1)) {
		i++;
		b >>= 1;
	}

	bb.insert(0, i);
	return bb.toByteString();
}



/**
 * Removes leading zeros and prepends a single '00' to ByteStrings which have the most significant bit set.
 *
 * This prevent interpretation of the integer representation if converted into
 * a signed ASN1 INTEGER.
 *
 * @param {ByteString} value the value to convert
 * @return the converted value
 * @type ByteString
 */
PKIXCommon.convertUnsignedInteger = function(value) {
	assert(value.length > 0);

	var i = 0;
	for (var i = 0; (i < value.length - 1) && (value.byteAt(i) == 0); i++);

	if (value.byteAt(i) >= 0x80) {
		value = (new ByteString("00", HEX)).concat(value.bytes(i));
	} else {
		value = value.bytes(i);
	}

	return value;
}



/**
 * Removes leading zero byte.
 *
 * @param {ByteString} value the value to process
 * @return the processed ByteString without leading zero byte
 * @type ByteString
 */
PKIXCommon.removeLeadingZeroBytes = function(value) {
	if (value.length > 0 && value.byteAt(0) == 0x00) {
		value = value.bytes(1);
	}

	return value;
}



/**
 * Encode PSS algorithm parameter
 */
PKIXCommon.encodePSSAlgorithmParameter = function(hashAlgorithm, saltLength) {
	var param =  new ASN1("parameters", ASN1.SEQUENCE,
			new ASN1("hashAlgorithm", 0xA0,
				new ASN1("sha256Identifier", ASN1.SEQUENCE,
					new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, hashAlgorithm),
					new ASN1("parameter", ASN1.NULL, new ByteString("", HEX)))),
			new ASN1("maskGenAlgorithm", 0xA1,
				new ASN1("mgf1SHA256Identifier", ASN1.SEQUENCE,
					new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("pkcs-1 8", OID)), // id-mgf1
					new ASN1("parameters", ASN1.SEQUENCE,
						new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, hashAlgorithm),
						new ASN1("parameter", ASN1.NULL, new ByteString("", HEX))))),
			new ASN1("hashAlgorithm", 0xA2,
				new ASN1("saltLength", ASN1.INTEGER, ByteString.valueOf(saltLength)))
			);

	return param;
}



/**
 * Gets the signature algorithm TLV object
 *
 * @return the signature algorithm object
 * @type ASN1
 */
PKIXCommon.encodeSignatureAlgorithm = function(signatureAlgorithm) {
	var t = new ASN1("signatureAlgorithm", ASN1.SEQUENCE);

	if ((signatureAlgorithm == Crypto.RSA) || (signatureAlgorithm == Crypto.RSA_SHA1)) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("sha1WithRSAEncryption", OID)));
		t.add(new ASN1("parameters", ASN1.NULL, new ByteString("", HEX)));
	} else if (signatureAlgorithm == Crypto.RSA_SHA256) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("sha256WithRSAEncryption", OID)));
		t.add(new ASN1("parameters", ASN1.NULL, new ByteString("", HEX)));
	} else if (signatureAlgorithm == Crypto.RSA_SHA384) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("sha384WithRSAEncryption", OID)));
		t.add(new ASN1("parameters", ASN1.NULL, new ByteString("", HEX)));
	} else if (signatureAlgorithm == Crypto.RSA_SHA512) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("sha512WithRSAEncryption", OID)));
		t.add(new ASN1("parameters", ASN1.NULL, new ByteString("", HEX)));
	} else if (signatureAlgorithm == Crypto.RSA_PSS_SHA1) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("id-RSASSA-PSS", OID)));
		t.add(new ASN1("parameters", ASN1.NULL, new ByteString("", HEX)));
	} else if (signatureAlgorithm == Crypto.RSA_PSS_SHA256) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("id-RSASSA-PSS", OID)));
		t.add(PKIXCommon.encodePSSAlgorithmParameter(new ByteString("id-sha256", OID), 32));
	} else if (signatureAlgorithm == Crypto.RSA_PSS_SHA384) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("id-RSASSA-PSS", OID)));
		t.add(PKIXCommon.encodePSSAlgorithmParameter(new ByteString("id-sha384", OID), 48));
	} else if (signatureAlgorithm == Crypto.RSA_PSS_SHA512) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("id-RSASSA-PSS", OID)));
		t.add(PKIXCommon.encodePSSAlgorithmParameter(new ByteString("id-sha512", OID), 64));
	} else if (signatureAlgorithm == Crypto.ECDSA_SHA1) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("ecdsa-with-SHA1", OID)));
	} else if (signatureAlgorithm == Crypto.ECDSA_SHA224) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("ecdsa-with-SHA224", OID)));
	} else if (signatureAlgorithm == Crypto.ECDSA_SHA256) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("ecdsa-with-SHA256", OID)));
	} else if (signatureAlgorithm == Crypto.ECDSA_SHA384) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("ecdsa-with-SHA384", OID)));
	} else if (signatureAlgorithm == Crypto.ECDSA_SHA512) {
		t.add(new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("ecdsa-with-SHA512", OID)));
	} else {
		throw new GPError(module.id, GPError.INVALID_MECH, signatureAlgorithm, "Invalid algorithm");
	}

	return t;
}



PKIXCommon.algIdMap = {};
PKIXCommon.algIdMap[(new ByteString("sha1WithRSAEncryption", OID)).toString(HEX)] = Crypto.RSA_SHA1;
PKIXCommon.algIdMap[(new ByteString("sha256WithRSAEncryption", OID)).toString(HEX)] = Crypto.RSA_SHA256;
PKIXCommon.algIdMap[(new ByteString("sha384WithRSAEncryption", OID)).toString(HEX)] = Crypto.RSA_SHA384;
PKIXCommon.algIdMap[(new ByteString("sha512WithRSAEncryption", OID)).toString(HEX)] = Crypto.RSA_SHA512;
PKIXCommon.algIdMap[(new ByteString("id-RSASSA-PSS", OID)).toString(HEX)] = Crypto.RSA_PSS_SHA1;
PKIXCommon.algIdMap[(new ByteString("ecdsa-with-SHA1", OID)).toString(HEX)] = Crypto.ECDSA_SHA1;
PKIXCommon.algIdMap[(new ByteString("ecdsa-with-SHA224", OID)).toString(HEX)] = Crypto.ECDSA_SHA224;
PKIXCommon.algIdMap[(new ByteString("ecdsa-with-SHA256", OID)).toString(HEX)] = Crypto.ECDSA_SHA256;
PKIXCommon.algIdMap[(new ByteString("ecdsa-with-SHA384", OID)).toString(HEX)] = Crypto.ECDSA_SHA384;
PKIXCommon.algIdMap[(new ByteString("ecdsa-with-SHA512", OID)).toString(HEX)] = Crypto.ECDSA_SHA512;



/**
 * Return Crypto.x constant for given signatureAlgorithm
 *
 * @param {ByteString} signatureAlgorithm the algorithm
 * @param {ByteString} param the parameter (optional)
 * @type Number
 * @return the Crypto.X constant
 */
PKIXCommon.decodeSignatureAlgorithm = function(signatureAlgorithm, param) {
	if (signatureAlgorithm.equals(new ByteString("id-RSASSA-PSS", OID))) {
		if (typeof(param) == "undefined") {
			return Crypto.RSA_PSS_SHA1;
		}

		var a = new ASN1(param);
		var hashAlgorithm = null;

		for (var i = 0; i < a.elements; i++) {
			var o = a.get(i);
			if (o.tag == 0xA0) { // hashAlgorithm
				var oid = o.get(0).get(0).value;
				hashAlgorithm = this.decodeHashAlgorithm(oid);
			} else if (o.tag == 0xA1) { // MGF
				// Check MGF
				var m = o.get(0).get(0).value;
				if (!m.equals(new ByteString("pkcs-1 8", OID))) {
					throw new GPError(module.id, GPError.INVALID_MECH, 2, "Unsupported MGF");
				}

				// Check hashAlgorithm
				var oid = o.get(0).get(1).get(0).value;
				var h = this.decodeHashAlgorithm(oid);
				if (hashAlgorithm == null) {
					hashAlgorithm = h;
				} else if (hashAlgorithm != h) {
					throw new GPError(module.id, GPError.INVALID_MECH, 2, "Signature hash algorithm and MGF hash algorithm must be the same");
				}
			} else if (o.tag == 0xA2) { // saltLength
				switch(hashAlgorithm) {
					case Crypto.SHA_224:
						var expectedSalt = 28;
						break;
					case Crypto.SHA_256:
						var expectedSalt = 32;
						break;
					case Crypto.SHA_384:
						var expectedSalt = 48;
						break;
					case Crypto.SHA_512:
						var expectedSalt = 64;
						break;
					case Crypto.SHA_1:
					default:
						var expectedSalt = 20;
				}

				var salt = o.get(0).value.toUnsigned();
				if (salt != expectedSalt) {
					throw new GPError(module.id, GPError.INVALID_MECH, 2, "saltLength (" + salt + ") must match hash size (" + expectedSalt + ")");
				}
			} else if (o.tag == 0xA3) { // trailerField
				var trailerField = o.get(0).value.toUnsigned();
				if (trailerField != 1) {
					throw new GPError(module.id, GPError.INVALID_MECH, 2, "trailerField value must be 1");
				}
			}
		}

		switch(hashAlgorithm) {
			case Crypto.SHA_224:
				return Crypto.RSA_PSS_SHA224;
			case Crypto.SHA_256:
				return Crypto.RSA_PSS_SHA256;
			case Crypto.SHA_384:
				return Crypto.RSA_PSS_SHA384;
			case Crypto.SHA_512:
				return Crypto.RSA_PSS_SHA512;
			case Crypto.SHA_1:
			default:
				return Crypto.RSA_PSS_SHA1;
		}
	}

	var alg = PKIXCommon.algIdMap[signatureAlgorithm.toString(HEX)];
	return alg;
}


/**
 * Return Crypto.SHA_x constant for given OID
 *
 * @param {ByteString} oid the hash OID
 * @type Number
 * @return the Crypto.SHA_x constant
 */
PKIXCommon.decodeHashAlgorithm = function(oid) {
	//var oid = algorithmId.get(0).get(0).value.toString(HEX);

	switch (oid.toString(HEX)) {
		case new ByteString("id-sha1", OID).toString(HEX):
			return Crypto.SHA_1;
		case new ByteString("id-sha224", OID).toString(HEX):
			return Crypto.SHA_224;
		case new ByteString("id-sha256", OID).toString(HEX):
			return Crypto.SHA_256;
		case new ByteString("id-sha384", OID).toString(HEX):
			return Crypto.SHA_384;
		case new ByteString("id-sha512", OID).toString(HEX):
			return Crypto.SHA_512;
		default:
			throw new GPError(module.id, GPError.INVALID_MECH, 1, "Invalid algorithm");
	}
}



/**
 * Return Crypto.RSA or Crypto.EC depending on signature algorithm
 *
 * @param {Number} alg signature algorithm (one of Crypto.X)
 * @type Number
 * @return either Crypto.RSA or Crypto.EC
 */
PKIXCommon.keyTypeForAlgorithm = function(alg) {
	if     ((alg == Crypto.RSA) ||
		(alg == Crypto.RSA_SHA256) ||
		(alg == Crypto.RSA_SHA384) ||
		(alg == Crypto.RSA_SHA512) ||
		(alg == Crypto.RSA_PSS_SHA1) ||
		(alg == Crypto.RSA_PSS_SHA224) ||
		(alg == Crypto.RSA_PSS_SHA256) ||
		(alg == Crypto.RSA_PSS_SHA384) ||
		(alg == Crypto.RSA_PSS_SHA512)) {
		return Crypto.RSA;
	} else {
		return Crypto.EC;
	}
}



/**
 * Create tailored AlgorithmIdentifier that restricts usage of the public key.
 *
 * @param{String} restriction The restriction to apply.
 * @type ASN1
 * @return the tailored AlgorithmIdentifier;
 */
PKIXCommon.restrictedPublicKeyAlgorithmIdentifier = function(restriction) {
	switch(restriction) {
		case "PSS-SHA1":
			var puboid = new ByteString("id-RSASSA-PSS", OID);
			var hash = new ByteString("id-sha1", OID);
			break;
		case "PSS-SHA224":
			var puboid = new ByteString("id-RSASSA-PSS", OID);
			var hash = new ByteString("id-sha224", OID);
			break;
		case "PSS-SHA256":
			var puboid = new ByteString("id-RSASSA-PSS", OID);
			var hash = new ByteString("id-sha256", OID);
			break;
		case "PSS-SHA384":
			var puboid = new ByteString("id-RSASSA-PSS", OID);
			var hash = new ByteString("id-sha384", OID);
			break;
		case "PSS-SHA512":
			var puboid = new ByteString("id-RSASSA-PSS", OID);
			var hash = new ByteString("id-sha512", OID);
			break;
		case "OEAP":
		default:
			throw new GPError(module.id, GPError.INVALID_DATA, 0, "Unknown public key restriction " + this.policy.restrictPublicKey);
	}

	var info = PKIXCommon.encodePSSAlgorithmParameter(hash, 0);
	var algorithm = new ASN1("algorithm", ASN1.SEQUENCE,
		new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, puboid),
		info
	);

	return algorithm;
}



/**
 * Encode EC Parameter for inclusion in the AlgorithmIdentifier structure.
 *
 * @param{Key} publicKey the public key.
 * @type ASN1
 * @return the encoded ECParameter.
 */
PKIXCommon.encodeECParameter = function(publicKey) {
	if (!publicKey.getComponent(Key.ECC_P)) {		// Make sure curve components are available if only curve oid is defined
		publicKey.setComponent(Key.ECC_CURVE_OID, publicKey.getComponent(Key.ECC_CURVE_OID));
	}
	var ecParameter =
		new ASN1("ecParameters", ASN1.SEQUENCE,
			new ASN1("version", ASN1.INTEGER, new ByteString("01", HEX)),
			new ASN1("fieldID", ASN1.SEQUENCE,
				new ASN1("fieldType", ASN1.OBJECT_IDENTIFIER, new ByteString("prime-field", OID)),
				new ASN1("prime", ASN1.INTEGER,
					PKIXCommon.convertUnsignedInteger(publicKey.getComponent(Key.ECC_P)))
			),
			new ASN1("curve", ASN1.SEQUENCE,
				new ASN1("a", ASN1.OCTET_STRING, publicKey.getComponent(Key.ECC_A)),
				new ASN1("b", ASN1.OCTET_STRING, publicKey.getComponent(Key.ECC_B))
			),
			new ASN1("base", ASN1.OCTET_STRING,
					(new ByteString("04", HEX)).concat(publicKey.getComponent(Key.ECC_GX)).concat(publicKey.getComponent(Key.ECC_GY))),
			new ASN1("order", ASN1.INTEGER,
				PKIXCommon.convertUnsignedInteger(publicKey.getComponent(Key.ECC_N)))
		);

	var cofactor = publicKey.getComponent(Key.ECC_H);
	var i = 0;
	for (; (i < cofactor.length) && (cofactor.byteAt(i) == 0); i++);
	if (i < cofactor.length) {
		ecParameter.add(new ASN1("cofactor", ASN1.INTEGER, cofactor.bytes(i)));
	}
	return ecParameter;
}



/**
 * Creates the EC Public Key as subjectPublicKeyInfo TLV structure object.
 *
 * <p>The structure is defined as:</p>
 * <pre>
 *	SubjectPublicKeyInfo  ::=  SEQUENCE  {
 *		algorithm            AlgorithmIdentifier,
 *		subjectPublicKey     BIT STRING  }
 *
 *	AlgorithmIdentifier  ::=  SEQUENCE  {
 *		algorithm               OBJECT IDENTIFIER,
 *		parameters              ANY DEFINED BY algorithm OPTIONAL  }
 *
 *	id-ecPublicKey OBJECT IDENTIFIER ::= {
 *		iso(1) member-body(2) us(840) ansi-X9-62(10045) keyType(2) 1 }
 *
 *	ECParameters ::= CHOICE {
 *		namedCurve         OBJECT IDENTIFIER,
 *		implicitCurve      NULL,
 *		specifiedCurve     SpecifiedECDomain }
 * </pre>
 * @return the subjectPublicKey TLV structure
 * @type ASN1
 */
PKIXCommon.createECSubjectPublicKeyInfo = function(publicKey, encodeECDomainParameter) {
	var t = new ASN1("subjectPublicKeyInfo", ASN1.SEQUENCE);

	var algorithm = publicKey.algorithmIdentifier;
	if (typeof(algorithm) == "undefined") {
		algorithm = new ASN1("algorithm", ASN1.SEQUENCE,
				new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("id-ecPublicKey", OID))

			);
		if (encodeECDomainParameter) {
			algorithm.add(PKIXCommon.encodeECParameter(publicKey));
		} else {
			algorithm.add(new ASN1("parameters", ASN1.OBJECT_IDENTIFIER, publicKey.getComponent(Key.ECC_CURVE_OID)));
		}
	}

	t.add(algorithm);

	// Prefix a 00 to form correct bitstring
	// Prefix a 04 to indicate uncompressed format
	var keybin = new ByteString("0004", HEX);
	keybin = keybin.concat(publicKey.getComponent(Key.ECC_QX));
	keybin = keybin.concat(publicKey.getComponent(Key.ECC_QY));
	t.add(new ASN1("subjectPublicKey", ASN1.BIT_STRING, keybin));

	return t;
}



/**
 * Creates the RSA Public Key as subjectPublicKeyInfo TLV structure object.
 *
 * <p>The structure is defined as:</p>
 * <pre>
 *	SubjectPublicKeyInfo  ::=  SEQUENCE  {
 *		algorithm            AlgorithmIdentifier,
 *		subjectPublicKey     BIT STRING  }
 *
 *	AlgorithmIdentifier  ::=  SEQUENCE  {
 *		algorithm               OBJECT IDENTIFIER,
 *		parameters              ANY DEFINED BY algorithm OPTIONAL  }
 *
 *	pkcs-1 OBJECT IDENTIFIER ::= { iso(1) member-body(2) us(840) rsadsi(113549) pkcs(1) 1 }
 *
 *	rsaEncryption OBJECT IDENTIFIER ::=  { pkcs-1 1}
 *
 *	RSAPublicKey ::= SEQUENCE {
 *		modulus            INTEGER,    -- n
 *		publicExponent     INTEGER  }  -- e
 * </pre>
 *
 * @return the subjectPublicKey TLV structure
 * @type ASN1
 */
PKIXCommon.createRSASubjectPublicKeyInfo = function(publicKey) {
	var t = new ASN1("subjectPublicKeyInfo", ASN1.SEQUENCE);

	var algorithm = publicKey.algorithmIdentifier;
	if (typeof(algorithm) == "undefined") {
		algorithm = new ASN1("algorithm", ASN1.SEQUENCE,
				new ASN1("algorithm", ASN1.OBJECT_IDENTIFIER, new ByteString("rsaEncryption", OID)),
				new ASN1("parameters", ASN1.NULL, new ByteString("", HEX))
			);
	}

	t.add(algorithm);
	// Prefix a 00 to form correct bitstring
	var keybin = new ByteString("00", HEX);

	var modulus = publicKey.getComponent(Key.MODULUS);
	modulus = PKIXCommon.convertUnsignedInteger(modulus);

	var exponent = publicKey.getComponent(Key.EXPONENT);
	exponent = PKIXCommon.convertUnsignedInteger(exponent);

	var rsapub = new ASN1("RSAPublicKey", ASN1.SEQUENCE,
			new ASN1("modulus", ASN1.INTEGER, modulus),
			new ASN1("publicKeyExponent", ASN1.INTEGER, exponent));

	keybin = keybin.concat(rsapub.getBytes());
	t.add(new ASN1("subjectPublicKey", ASN1.BIT_STRING, keybin));

	return t;
}



/**
 * Determine key identifier according to RFC5280 using SHA-1 over the SubjectPublicKeyInfo
 *
 * @param {Key} publicKey the public key
 * @param {Boolean} encodeECDomainParameter true to include domain parameter in SPKI of EC key
 */
PKIXCommon.determineKeyIdentifier = function(publicKey, encodeECDomainParameter) {
	if (typeof(encodeECDomainParameter) == "undefined") {
		encodeECDomainParameter = true;
	}

	var spki;

	if (publicKey.getComponent(Key.MODULUS)) {
		spki = PKIXCommon.createRSASubjectPublicKeyInfo(publicKey);
	} else {
		spki = PKIXCommon.createECSubjectPublicKeyInfo(publicKey, encodeECDomainParameter);
	}

	var keyvalue = spki.get(1).value.bytes(1);
	var crypto = new Crypto();
	return (crypto.digest(Crypto.SHA_1, keyvalue));
}



/**
 * Creates a relative distinguished name component.
 *
 * <p>The structure is defined as:</p>
 * <pre>
 *	RelativeDistinguishedName ::=
 *		SET SIZE (1..MAX) OF AttributeTypeAndValue
 *
 *	AttributeTypeAndValue ::= SEQUENCE {
 *		type     AttributeType,
 *		value    AttributeValue }
 *
 *	AttributeType ::= OBJECT IDENTIFIER
 *
 *	AttributeValue ::= ANY -- DEFINED BY AttributeType
 *
 *	DirectoryString ::= CHOICE {
 *		teletexString           TeletexString (SIZE (1..MAX)),
 *		printableString         PrintableString (SIZE (1..MAX)),
 *		universalString         UniversalString (SIZE (1..MAX)),
 *		utf8String              UTF8String (SIZE (1..MAX)),
 *		bmpString               BMPString (SIZE (1..MAX)) }
 *</pre>
 *
 * @param {String} name the components name
 * @param {String} oid the oid for the RDN
 * @param {ASN1} value the value object
 * @return the
 */
PKIXCommon.makeRDN = function(name, oid, value) {
	return new ASN1(name, ASN1.SET,
				new ASN1(ASN1.SEQUENCE,
					new ASN1(ASN1.OBJECT_IDENTIFIER, new ByteString(oid, OID)),
					value
				)
			);
}



/**
 * Adds names from the name object to the RDNSequence.
 *
 * @param {ASN1} t the sequence object
 * @param {Object} name the name object
 */
PKIXCommon.addNames = function(t, name) {
	if (name.C) {
		t.add(PKIXCommon.makeRDN("country", "id-at-countryName", new ASN1(ASN1.PrintableString, new ByteString(name.C, ASCII))));
	}
	if (name.ST) {
		t.add(PKIXCommon.makeRDN("stateOrProvinceName", "id-at-stateOrProvinceName", new ASN1(ASN1.UTF8String, new ByteString(name.ST, UTF8))));
	}
	if (name.O) {
		t.add(PKIXCommon.makeRDN("organization", "id-at-organizationName", new ASN1(ASN1.UTF8String, new ByteString(name.O, UTF8))));
	}
	if (name.OU) {
		t.add(PKIXCommon.makeRDN("organizationalUnit", "id-at-organizationalUnitName", new ASN1(ASN1.UTF8String, new ByteString(name.OU, UTF8))));
	}
	if (name.S) {
		t.add(PKIXCommon.makeRDN("stateOrProvince", "id-at-stateOrProvinceName", new ASN1(ASN1.UTF8String, new ByteString(name.S, UTF8))));
	}
	if (name.L) {
		t.add(PKIXCommon.makeRDN("locality", "id-at-localityName", new ASN1(ASN1.UTF8String, new ByteString(name.L, UTF8))));
	}
	if (name.DC) {
		t.add(PKIXCommon.makeRDN("domainComponent", "id-domainComponent", new ASN1(ASN1.UTF8String, new ByteString(name.DC, UTF8))));
	}
	if (name.T) {
		t.add(PKIXCommon.makeRDN("title", "id-at-title", new ASN1(ASN1.UTF8String, new ByteString(name.T, UTF8))));
	}
	if (name.G) {
		t.add(PKIXCommon.makeRDN("givenName", "id-at-givenName", new ASN1(ASN1.UTF8String, new ByteString(name.G, UTF8))));
	}
	if (name.SN) {
		t.add(PKIXCommon.makeRDN("surname", "id-at-surname", new ASN1(ASN1.UTF8String, new ByteString(name.SN, UTF8))));
	}
	if (name.CN) {
		t.add(PKIXCommon.makeRDN("commonName", "id-at-commonName", new ASN1(ASN1.UTF8String, new ByteString(name.CN, UTF8))));
	}
	if (name.SERIALNUMBER) {
		t.add(PKIXCommon.makeRDN("serialNumber", "id-at-serialNumber", new ASN1(ASN1.UTF8String, new ByteString(name.SERIALNUMBER, UTF8))));
	}
	if (name.DNQ) {
		t.add(PKIXCommon.makeRDN("dnQualifier", "id-at-dnQualifier", new ASN1(ASN1.UTF8String, new ByteString(name.DNQ, UTF8))));
	}
	if (name.E) {
		t.add(PKIXCommon.makeRDN("emailAddress", "id-emailAddress", new ASN1(ASN1.IA5String, new ByteString(name.E, ASCII))));
	}
	if (name.UID) {
		t.add(PKIXCommon.makeRDN("userId", "id-userId", new ASN1(ASN1.UTF8String, new ByteString(name.UID, UTF8))));
	}
}



PKIXCommon.validRDN = [ "C","ST","L","O","OU","S","DC","T","G","SN","PSEUDONYM","CN","SERIALNUMBER","DNQ","E","UID" ];

/**
 * Gets the dn as TLV object
 *
 * <p>This function support two format for names</p>
 * <pre>
 *  var issuer = { C:"UT", O:"ACME Corporation", CN:"Test-CA" };
 * or
 *  var issuer = [ { C:"UT"}, { O:"ACME Corporation" }, { CN:"Test-CA"} ];
 * </pre>
 *
 * <p>It supports the following RDNs:</p>
 * <ul>
 * <li>C - country</li>
 * <li>ST - state</li>
 * <li>O - organization</li>
 * <li>OU - organizational unit</li>
 * <li>S - state or province</li>
 * <li>L - locality</li>
 * <li>DC - domain component</li>
 * <li>T - title</li>
 * <li>G - given name</li>
 * <li>SN - surname</li>
 * <li>CN - common name</li>
 * <li>SERIALNUMBER - serial number</li>
 * <li>DNQ - domain name qualifier</li>
 * <li>E - e-mail address</li>
 * <li>UID - user Id</li>
 * <p>The first format sorts the RDS in the sequence C,ST,L,O,OU,S,DC,T,G,SN,PSEUDONYM,CN,SERIALNUMBER,DNQ,E,UID</p>
 * </ul>
 * @param {Object} name the name object
 * @return the RDNSequence
 * @type ASN1
 */
PKIXCommon.encodeName = function(name) {
	var t = new ASN1("subject", ASN1.SEQUENCE);
	if (typeof(name.C) == "undefined") {
		for (var i = 0; i < name.length; i++) {
			PKIXCommon.addNames(t, name[i]);
		}
	} else {
		PKIXCommon.addNames(t, name);
	}
	return t;
}



/**
 * Return the value of the last RDN
 *
 * @param {Object[]} dn the dn array
 * @type String
 * @return the value of the last RDN
 */
PKIXCommon.makeName = function(rdnlist) {
	if (rdnlist.length == 0) {
		return undefined;
	}
	var l = rdnlist[rdnlist.length - 1];

	for (var i in l) {
		return l[i];
	}

	return undefined;
}



PKIXCommon.parseDN = function(dn) {
	assert(typeof(dn) == "string", "Parameter dn must be string");

	var e = dn.split(",");
	var rdnlist = [];

	for (var i = 0; i < e.length; i++) {
		var p = e[i].split("=");

		if (p.length < 2) {
			throw new GPError(module.id, GPError.INVALID_DATA, i, "Missing '=' in RDN " + e[i]);
		}

		if (p.length > 2) {
			throw new GPError(module.id, GPError.INVALID_DATA, i, "Too many '=' in RDN " + e[i]);
		}

		var key = p[0].trim().toUpperCase();

		if (key.length == 0) {
			throw new GPError(module.id, GPError.INVALID_DATA, i, "Key in RDN " + e[i] + " can't be empty");
		}

		if (PKIXCommon.validRDN.indexOf(key) == -1) {
			throw new GPError(module.id, GPError.INVALID_DATA, i, key + " is not a supported RDN (Valid RDNs are " + PKIXCommon.validRDN + ")");
		}

		var value = p[1].trim();

		if (value.length == 0) {
			throw new GPError(module.id, GPError.INVALID_DATA, i, "Value for " + key + " can't be empty");
		}
		var rdn = { };
		rdn[key] = value;
		rdnlist.push(rdn);
	}
	return rdnlist;
}



/**
 * Convert a DN parsed with parseDN() back into the string format
 *
 * @param {Object[]} dn the dn array
 * @type String
 * @return the DN in string format
 */
PKIXCommon.dnToString = function(dn) {
	var str = "";

	for (var i = 0; i < dn.length; i++) {
		if (str.length > 0) {
			str += ",";
		}

		for (var j in dn[i]) {
			str += j + "=" + dn[i][j];
		}
	}
	return str;
}



PKIXCommon.findRDN = function(rdnlist, c) {
	for (var i = 0; i < rdnlist.length; i++) {
		var v = rdnlist[i][c];
		if (v) {
			return v;
		}
	}
}



PKIXCommon.countryNames = {
AF:"Afghanistan",
AX:"Aeland Islands",
AL:"Albania",
DZ:"Algeria",
AS:"American Samoa",
AD:"Andorra",
AO:"Angola",
AI:"Anguilla",
AQ:"Antarctica",
AG:"Antigua and Barbuda",
AR:"Argentina",
AM:"Armenia",
AW:"Aruba",
AU:"Australia",
AT:"Austria",
AZ:"Azerbaijan",
BS:"Bahamas",
BH:"Bahrain",
BD:"Bangladesh",
BB:"Barbados",
BY:"Belarus",
BE:"Belgium",
BZ:"Belize",
BJ:"Benin",
BM:"Bermuda",
BT:"Bhutan",
BO:"Bolivia, Plurinational State of",
BQ:"Bonaire, Sint Eustatius and Saba",
BA:"Bosnia and Herzegovina",
BW:"Botswana",
BV:"Bouvet Island",
BR:"Brazil",
IO:"British Indian Ocean Territory",
BN:"Brunei Darussalam",
BG:"Bulgaria",
BF:"Burkina Faso",
BI:"Burundi",
KH:"Cambodia",
CM:"Cameroon",
CA:"Canada",
CV:"Cape Verde",
KY:"Cayman Islands",
CF:"Central African Republic",
TD:"Chad",
CL:"Chile",
CN:"China",
CX:"Christmas Island",
CC:"Cocos (Keeling) Islands",
CO:"Colombia",
KM:"Comoros",
CG:"Congo",
CD:"Congo, the Democratic Republic of the",
CK:"Cook Islands",
CR:"Costa Rica",
CI:"Cote d'Ivoire",
HR:"Croatia",
CU:"Cuba",
CW:"Curacao",
CY:"Cyprus",
CZ:"Czech Republic",
DK:"Denmark",
DJ:"Djibouti",
DM:"Dominica",
DO:"Dominican Republic",
EC:"Ecuador",
EG:"Egypt",
SV:"El Salvador",
GQ:"Equatorial Guinea",
ER:"Eritrea",
EE:"Estonia",
ET:"Ethiopia",
FK:"Falkland Islands (Malvinas)",
FO:"Faroe Islands",
FJ:"Fiji",
FI:"Finland",
FR:"France",
GF:"French Guiana",
PF:"French Polynesia",
TF:"French Southern Territories",
GA:"Gabon",
GM:"Gambia",
GE:"Georgia",
DE:"Germany",
GH:"Ghana",
GI:"Gibraltar",
GR:"Greece",
GL:"Greenland",
GD:"Grenada",
GP:"Guadeloupe",
GU:"Guam",
GT:"Guatemala",
GG:"Guernsey",
GN:"Guinea",
GW:"Guinea-Bissau",
GY:"Guyana",
HT:"Haiti",
HM:"Heard Island and McDonald Islands",
VA:"Holy See (Vatican City State)",
HN:"Honduras",
HK:"Hong Kong",
HU:"Hungary",
IS:"Iceland",
IN:"India",
ID:"Indonesia",
IR:"Iran, Islamic Republic of",
IQ:"Iraq",
IE:"Ireland",
IM:"Isle of Man",
IL:"Israel",
IT:"Italy",
JM:"Jamaica",
JP:"Japan",
JE:"Jersey",
JO:"Jordan",
KZ:"Kazakhstan",
KE:"Kenya",
KI:"Kiribati",
KP:"Korea, Democratic People's Republic of",
KR:"Korea, Republic of",
KW:"Kuwait",
KG:"Kyrgyzstan",
LA:"Lao People's Democratic Republic",
LV:"Latvia",
LB:"Lebanon",
LS:"Lesotho",
LR:"Liberia",
LY:"Libya",
LI:"Liechtenstein",
LT:"Lithuania",
LU:"Luxembourg",
MO:"Macao",
MK:"Macedonia, the Former Yugoslav Republic of",
MG:"Madagascar",
MW:"Malawi",
MY:"Malaysia",
MV:"Maldives",
ML:"Mali",
MT:"Malta",
MH:"Marshall Islands",
MQ:"Martinique",
MR:"Mauritania",
MU:"Mauritius",
YT:"Mayotte",
MX:"Mexico",
FM:"Micronesia, Federated States of",
MD:"Moldova, Republic of",
MC:"Monaco",
MN:"Mongolia",
ME:"Montenegro",
MS:"Montserrat",
MA:"Morocco",
MZ:"Mozambique",
MM:"Myanmar",
NA:"Namibia",
NR:"Nauru",
NP:"Nepal",
NL:"Netherlands",
NC:"New Caledonia",
NZ:"New Zealand",
NI:"Nicaragua",
NE:"Niger",
NG:"Nigeria",
NU:"Niue",
NF:"Norfolk Island",
MP:"Northern Mariana Islands",
NO:"Norway",
OM:"Oman",
PK:"Pakistan",
PW:"Palau",
PS:"Palestine, State of",
PA:"Panama",
PG:"Papua New Guinea",
PY:"Paraguay",
PE:"Peru",
PH:"Philippines",
PN:"Pitcairn",
PL:"Poland",
PT:"Portugal",
PR:"Puerto Rico",
QA:"Qatar",
RE:"Reunion",
RO:"Romania",
RU:"Russian Federation",
RW:"Rwanda",
BL:"Saint Bartholemy",
SH:"Saint Helena, Ascension and Tristan da Cunha",
KN:"Saint Kitts and Nevis",
LC:"Saint Lucia",
MF:"Saint Martin (French part)",
PM:"Saint Pierre and Miquelon",
VC:"Saint Vincent and the Grenadines",
WS:"Samoa",
SM:"San Marino",
ST:"Sao Tome and Principe",
SA:"Saudi Arabia",
SN:"Senegal",
RS:"Serbia",
SC:"Seychelles",
SL:"Sierra Leone",
SG:"Singapore",
SX:"Sint Maarten (Dutch part)",
SK:"Slovakia",
SI:"Slovenia",
SB:"Solomon Islands",
SO:"Somalia",
ZA:"South Africa",
GS:"South Georgia and the South Sandwich Islands",
SS:"South Sudan",
ES:"Spain",
LK:"Sri Lanka",
SD:"Sudan",
SR:"Suriname",
SJ:"Svalbard and Jan Mayen",
SZ:"Swaziland",
SE:"Sweden",
CH:"Switzerland",
SY:"Syrian Arab Republic",
TW:"Taiwan, Province of China",
TJ:"Tajikistan",
TZ:"Tanzania, United Republic of",
TH:"Thailand",
TL:"Timor-Leste",
TG:"Togo",
TK:"Tokelau",
TO:"Tonga",
TT:"Trinidad and Tobago",
TN:"Tunisia",
TR:"Turkey",
TM:"Turkmenistan",
TC:"Turks and Caicos Islands",
TV:"Tuvalu",
UG:"Uganda",
UA:"Ukraine",
AE:"United Arab Emirates",
GB:"United Kingdom",
US:"United States",
UM:"United States Minor Outlying Islands",
UY:"Uruguay",
UZ:"Uzbekistan",
VU:"Vanuatu",
VE:"Venezuela, Bolivarian Republic of",
VN:"Viet Nam",
VG:"Virgin Islands, British",
VI:"Virgin Islands, U.S.",
WF:"Wallis and Futuna",
EH:"Western Sahara",
YE:"Yemen",
ZM:"Zambia",
ZW:"Zimbabwe"
};



/**
 * Extract subject from certificate as ASN1 object
 *
 * @param {X509} cert the certificate
 * @type ASN1
 * @return the subject as ASN1
 */
PKIXCommon.getSubjectAsASN1 = function(cert) {
	var a = new ASN1(cert.getBytes());
	return a.get(0).get(5);
}



/**
 * Extract issuer from certificate as ASN1 object
 *
 * @param {X509} cert the certificate
 * @type ASN1
 * @return the issuer as ASN1
 */
PKIXCommon.getIssuerAsASN1 = function(cert) {
	var a = new ASN1(cert.getBytes());
	return a.get(0).get(3);
}



/**
 * Convert binary data to PEM format
 *
 * @param {String} label the label to be used in the MIME header and footer
 * @param {ByteString} bin the binary data
 * @type String
 * @return the MIME/BASE64 encoded string
 */
PKIXCommon.toPEM = function(label, bin) {
	assert(typeof(label) == "string", "Parameter label must be string");
	assert(bin instanceof ByteString, "Parameter bin must be ByteString");

	var str = "-----BEGIN " + label + "-----\n";

	var b64 = bin.toString(BASE64);
	while(b64.length > 0) {
		str += b64.substr(0, 64) + "\n";
		b64 = b64.substr(64);
	}

	str += "-----END " + label + "-----\n";
	return str;
}



/**
 * Parse PEM format and extract binary data
 *
 * @param {String} label the label of the section
 * @param {String} bin the binary data
 * @type String
 * @return the MIME/BASE64 encoded string
 */
PKIXCommon.parsePEM = function(label, pem) {
	assert(typeof(label) == "string", "Parameter label must be string");
	assert(typeof(pem) == "string", "Parameter pem must be string");

	var lines = pem.split("\n");

	var b64str = "";
	var parse = false;
	for (var i = 0; i < lines.length; i++) {
		var str = lines[i];

		if (str.endsWith("\r")) {
			str = str.substring(0, str.length - 1);
		}

		if (str == "-----BEGIN " + label + "-----") {
			parse = true;
		} else if (str == "-----END " + label + "-----") {
			break;
		} else if (parse) {
			b64str += str;
		}
	}

	return new ByteString(b64str, BASE64);
}



/**
 * Writes a byte string object to file
 *
 * <p>The filename is mapped to the workspace location.</p>
 *
 * @param {String} filename the fully qualified name of the file
 * @param {ByteString} content the content to write
 */
PKIXCommon.writeFileToDisk = function(filename, content) {
	var file = new java.io.FileOutputStream(filename);
	file.write(content);
	file.close();
}



/**
 * Loads a binary file from disk
 *
 * @param {String} filename the fully qualified file name
 * @return the binary content
 * @type ByteString
 */
PKIXCommon.readFileFromDisk = function(filename) {
	// Open stream
	var f = new java.io.FileInputStream(filename);

	// Determine file size
	var flen = f.available();

	// Allocate native byte array
	var bs = java.lang.reflect.Array.newInstance(java.lang.Byte.TYPE, flen);

	// Read into byte array
	var len = f.read(bs);

	f.close();

	// Allocate JavaScript ByteBuffer from native/wrapped byte array
	var bb = new ByteBuffer(bs);

	// Convert to JavaScript ByteString
	var data = bb.toByteString();

	return data;
}



PKIXCommon.test = function() {
	var issuer = { C:"C", O:"O", OU:"OU", SP:"SP", L:"L", DC:"DC", T:"T", G:"G", SN:"SN", CN:"CN", SERIALNUMBER:"serial", DNQ:"DNQ" };
	var dn = PKIXCommon.encodeName(issuer);
	print(dn);

	var r = PKIXCommon.convertUnsignedInteger(new ByteString("00", HEX));
	assert(r.toString(HEX) == "00");
	var r = PKIXCommon.convertUnsignedInteger(new ByteString("80", HEX));
	assert(r.toString(HEX) == "0080");
	var r = PKIXCommon.convertUnsignedInteger(new ByteString("FF", HEX));
	assert(r.toString(HEX) == "00FF");
	var r = PKIXCommon.convertUnsignedInteger(new ByteString("0000", HEX));
	assert(r.toString(HEX) == "00");
	var r = PKIXCommon.convertUnsignedInteger(new ByteString("0080", HEX));
	assert(r.toString(HEX) == "0080");
	var r = PKIXCommon.convertUnsignedInteger(new ByteString("000080", HEX));
	assert(r.toString(HEX) == "0080");
}
