      1 /**
      2  *  ---------
      3  * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
      4  * |#       #|
      5  * |#       #|  Copyright (c) 1999-2026 CardContact Systems GmbH
      6  * |'##> <##'|  32429 Minden, Germany (www.cardcontact.de)
      7  *  ---------
      8  *
      9  *  This file is part of OpenSCDP.
     10  *
     11  *  OpenSCDP is free software; you can redistribute it and/or modify
     12  *  it under the terms of the GNU General Public License version 2 as
     13  *  published by the Free Software Foundation.
     14  *
     15  *  OpenSCDP is distributed in the hope that it will be useful,
     16  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
     17  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     18  *  GNU General Public License for more details.
     19  *
     20  *  You should have received a copy of the GNU General Public License
     21  *  along with OpenSCDP; if not, write to the Free Software
     22  *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
     23  *
     24  * @fileoverview File encryptor.
     25  */
     26
     27 var CVC = require("scsh/eac/CVC").CVC;
     28 var PKIXCommon = require("scsh/x509/PKIXCommon").PKIXCommon;
     29
     30
     31
     32 /**
     33  * Craate a FileEncryptor instance.
     34  *
     35  * @class Class implementing file encryption
     36  * @constructor
     37  * @param {SmartCardHSM} sc the SmartCardHSM object used to access the token or undefined at the sender in the hybrid scheme.
     38  * @param {Number} mode the mode to use (One of FileEncryptor.NATIVE, .DERIVED or .HYBRID). Must be specified at the sending side.
     39  */
     40  function FileEncryptor(sc, mode) {
     41 	this.mode = mode;
     42 	this.sc = sc;
     43 	this.crypto = new Crypto();
     44
     45 	this.keyId = new ByteString("UNKNOWN", ASCII);
     46 }
     47
     48 exports.FileEncryptor = FileEncryptor;
     49
     50 FileEncryptor.NATIVE = 1;
     51 FileEncryptor.DERIVED = 2;
     52 FileEncryptor.HYBRID = 3;
     53
     54
     55
     56 /**
     57  * Get the crypto object.
     58  *
     59  * @private
     60  * @type Crypto
     61  * @return the crypto object.
     62  */
     63 FileEncryptor.prototype.getCrypto = function() {
     64 	if (typeof(this.sc) != "undefined") {
     65 		return this.sc.getCrypto();
     66 	}
     67 	return this.crypto;
     68 }
     69
     70
     71
     72 /**
     73  * Generate the IV
     74  *
     75  * @private
     76  */
     77 FileEncryptor.prototype.generateIV = function() {
     78 	this.iv = this.crypto.generateRandom(16);
     79 }
     80
     81
     82
     83 /**
     84  * Calculate the hash of the encrypted payload.
     85  *
     86  * @private
     87  * @param {ByteString} content the encrypted payload
     88  * @type ByteString
     89  * @return the hash value.
     90  */
     91 FileEncryptor.prototype.calculateHash = function(content) {
     92 	return this.crypto.digest(Crypto.SHA_256, content);
     93 }
     94
     95
     96
     97 /**
     98  * Create the TLV encoded header of the encrypted file.
     99  *
    100  * @private
    101  * @type ASN1
    102  * @return the header.
    103  */
    104 FileEncryptor.prototype.createHeader = function() {
    105 	var tbs = new ASN1("tbs", ASN1.SEQUENCE,
    106 				new ASN1("oid", ASN1.OBJECT_IDENTIFIER, new ByteString("CardContact 4 6 " + this.mode, OID)),
    107 				new ASN1("keyId", ASN1.OCTET_STRING, this.keyId),
    108 				new ASN1("hash", ASN1.OCTET_STRING, this.hash),
    109 				new ASN1("iv", ASN1.OCTET_STRING, this.iv),
    110 				new ASN1("extensions", ASN1.SEQUENCE)
    111 			);
    112
    113 	if (this.mode == FileEncryptor.HYBRID) {
    114 		tbs.add(new ASN1("senderPublicKey", 0xA0, PKIXCommon.createECSubjectPublicKeyInfo(this.pukey)));
    115 	}
    116
    117 	var mac = this.getCrypto().sign(this.mackey, Crypto.AES_CMAC, tbs.getBytes());
    118
    119 	var header =
    120 		new ASN1("EncryptionHeader", ASN1.SEQUENCE,
    121 			tbs,
    122 			new ASN1("mac", ASN1.OCTET_STRING, mac)
    123 		);
    124
    125 	return header;
    126 }
    127
    128
    129
    130 /**
    131  * Parse the header of the encrypted file.
    132  *
    133  * @param {ByteString} buffer the encrypted file with header.
    134  * @type Number
    135  * @return the offset at which the payload starts.
    136  */
    137 FileEncryptor.prototype.parseHeader = function(buffer) {
    138 	if (buffer.byteAt(0) != 0x30) {
    139 		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Header corrupted");
    140 	}
    141
    142 	var a = new ASN1(buffer);
    143
    144 	if (a.elements != 2) {
    145 		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Header corrupted");
    146 	}
    147
    148 	this.mac = a.get(1).value;
    149 	this.tbs = a.get(0);
    150 	if ((this.tbs.elements < 5) || (this.tbs.elements > 6)) {
    151 		throw new GPError(module.id, GPError.INVALID_DATA, 0, "Header corrupted");
    152 	}
    153
    154 	var oid = this.tbs.get(0).value;
    155 	if (!oid.startsWith(new ByteString("CardContact 4 6", OID))) {
    156 		throw new GPError(module.id, GPError.INVALID_DATA, 0, "File format not supported");
    157 	}
    158
    159 	this.mode = oid.byteAt(oid.length - 1);
    160 	this.keyId = this.tbs.get(1).value;
    161 	this.hash = this.tbs.get(2).value;
    162 	this.iv = this.tbs.get(3).value;
    163
    164 	if (this.tbs.elements == 6) {
    165 		// Parse sender public key
    166 		var spki = this.tbs.get(5).get(0);
    167 		var oid = spki.get(0).get(1).value;
    168 		var pk = spki.get(1).value.bytes(2);
    169 		this.pukey = new Key();
    170 		this.pukey.setType(Key.PUBLIC);
    171 		this.pukey.setComponent(Key.ECC_CURVE_OID, oid);
    172 		this.pukey.setComponent(Key.ECC_QX, pk.left(pk.length >> 1));
    173 		this.pukey.setComponent(Key.ECC_QY, pk.right(pk.length >> 1));
    174 	}
    175
    176 	return a.size;
    177 }
    178
    179
    180
    181 /**
    182  * Validate the Message Authentication Code (MAC) over the header.
    183  *
    184  * @throws GPError is MAC verification failed.
    185  */
    186 FileEncryptor.prototype.validateHeader = function() {
    187 	var mac = this.getCrypto().sign(this.mackey, Crypto.AES_CMAC, this.tbs.getBytes());
    188 	if (!mac.equals(this.mac)) {
    189 		throw new GPError(module.id, GPError.CRYPTO_FAILED, 0, "Integrity check failed (Invalid MAC).");
    190 	}
    191 }
    192
    193
    194
    195 /**
    196  * Generate ephemeral EC key pair at the sending side in hybrid scheme.
    197  *
    198  * @param {Key} key the domain parameter.
    199  */
    200 FileEncryptor.prototype.generateEphemeralKeyPair = function(dp) {
    201 	this.prkey = new Key(dp);
    202 	this.prkey.setType(Key.PRIVATE);
    203
    204 	this.pukey = new Key(dp);
    205 	this.pukey.setType(Key.PUBLIC);
    206
    207 	this.crypto.generateKeyPair(Crypto.EC, this.pukey, this.prkey);
    208 }
    209
    210
    211
    212 /**
    213  * Derive a AES key from a AES master key using a CMAC based Key Derivation Function in
    214  * Counter Mode as per NIST SP 800-108.
    215  *
    216  * @param {Key} key the AES master key.
    217  * @param {Number} size the requested number of derived bytes.
    218  * @param {ByteString} context the derivation context used to differ keys derived from the same master.
    219  * @type ByteString
    220  * @return the derived key value.
    221  */
    222 FileEncryptor.prototype.keyDerivationFunction1 = function(key, size, context) {
    223 	var dd = new ByteBuffer();
    224 	var os = size;
    225 	var iter = 1;
    226 	while (os > 0) {
    227 		var dp = new ByteBuffer();
    228 		dp.append(ByteString.valueOf(iter, 4));
    229 		dp.append(context);
    230 		dp.append(ByteString.valueOf(size << 3, 4));
    231
    232 		var mac = this.getCrypto().sign(key, Crypto.AES_CMAC, dp.toByteString());
    233
    234 		dd.append(mac.left(os > mac.length ? mac.length : os));
    235 		os -= mac.length;
    236 		mac.clear();
    237 		iter++;
    238 	}
    239 	var result = dd.toByteString();
    240 	dd.clear();
    241 	return result;
    242 }
    243
    244
    245
    246 /**
    247  * Derive an AES key from the shared secret resulting from performing the ECDH using
    248  * a one-step-kdf with SHA-256 (Option 1) as defined in NIST SP 800-56C.
    249  *
    250  * @param {Number} id the key identifier (0x01 for K.ENC, 0x02 for K.MAC).
    251  * @param {ByteString} sharedSecret the shared secret resulting from ECDH.
    252  * @return the key value as 32 bytes.
    253  */
    254 FileEncryptor.prototype.keyDerivationFunction2 = function(id, sharedSecret) {
    255 	var inp = ByteString.valueOf(1, 4).concat(sharedSecret).concat(ByteString.valueOf(id));
    256 	var res = this.crypto.digest(Crypto.SHA_256, inp);
    257 	inp.clear();
    258 	return res;
    259 }
    260
    261
    262
    263 /**
    264  * Derive K.ENC and K.MAC based on the selected scheme.
    265  *
    266  * @param {Key} key the AES key or private EC key.
    267  * @param {CVC} key the CVC with the public of the receiver.
    268  */
    269 FileEncryptor.prototype.deriveKey = function(key) {
    270 	if (typeof(this.iv) == "undefined") {
    271 		this.generateIV();
    272 	}
    273
    274 	switch(this.mode) {
    275 		case FileEncryptor.NATIVE:
    276 			this.mackey = key;
    277 			this.enckey = key;
    278 			if (typeof(key.getPKCS15Id) == "function") {
    279 				this.keyId = key.getPKCS15Id();
    280 			}
    281 			break;
    282 		case FileEncryptor.DERIVED:
    283 			this.mackey = key;
    284 			this.enckey = new Key();
    285 			var kv = this.keyDerivationFunction1(key, key.getSize() >> 3, this.iv);
    286 			this.enckey.setComponent(Key.AES, kv);
    287 			kv.clear();
    288
    289 			if (typeof(key.getPKCS15Id) == "function") {
    290 				this.keyId = key.getPKCS15Id();
    291 			}
    292 			break;
    293 		case FileEncryptor.HYBRID:
    294 			var ss;
    295 			if (key instanceof CVC) {
    296 				var pk = key.getPublicKey();
    297 				this.generateEphemeralKeyPair(pk);
    298 				var point = pk.getComponent(Key.ECC_QX).concat(pk.getComponent(Key.ECC_QY));
    299 				ss = this.crypto.decrypt(this.prkey, Crypto.ECDHP, point, new ByteString("", HEX));
    300 				this.keyId = key.determineKeyIdentifier();
    301 			} else {
    302 				var point = this.pukey.getComponent(Key.ECC_QX).concat(this.pukey.getComponent(Key.ECC_QY));
    303 				ss = this.getCrypto().decrypt(key, Crypto.ECDHP, point, new ByteString("", HEX));
    304 			}
    305
    306 			this.enckey = new Key();
    307 			var kv = this.keyDerivationFunction2(1, ss);
    308 			this.enckey.setComponent(Key.AES, kv);
    309 			kv.clear();
    310
    311 			this.mackey = new Key();
    312 			var kv = this.keyDerivationFunction2(2, ss);
    313 			this.mackey.setComponent(Key.AES, kv);
    314 			kv.clear();
    315 			ss.clear();
    316 			break;
    317 		default:
    318 			throw new GPError(module.id, GPError.INVALID_DATA, 0, "Mode not defined or invalid");
    319 	}
    320 }
    321
    322
    323
    324 /**
    325  * Encrypt the buffer with K.ENC previously derived with deriveKey() and prepend header.
    326  *
    327  * @param {ByteString} plain the plain input. The caller should clear the buffer after use with plain.clear().
    328  * @type ByteString
    329  * @return the header concatenated with the encrypted payload.
    330  */
    331 FileEncryptor.prototype.encrypt = function(plain) {
    332
    333 	var p = plain.pad(Crypto.ISO9797_METHOD_2_16);
    334
    335 	var c = this.getCrypto().encrypt(this.enckey, Crypto.AES_CBC, p, this.iv);
    336 	p.clear();
    337
    338 	this.hash = this.calculateHash(c);
    339
    340 	var header = this.createHeader();
    341
    342 	return header.getBytes().concat(c);
    343 }
    344
    345
    346
    347 /**
    348  * Validate and strip the ISO padding.
    349  *
    350  * @param {ByteString} buffer the decrypted buffer.
    351  * @throw GPError is padding is invalid, indicating the wrong key or corrupted cipher.
    352  * @type ByteString
    353  * @return the buffer without padding.
    354  */
    355 FileEncryptor.stripPadding = function(buffer) {
    356
    357 	var ofs = buffer.length - 1;
    358 	while((ofs > 0) && (buffer.byteAt(ofs) == 0)) {
    359 		ofs--;
    360 	}
    361
    362 	if ((ofs <= 0) || (buffer.byteAt(ofs) != 0x80)) {
    363 		throw new GPError(module.id, GPError.CRYPTO_FAILED, 0, "Plain block has invalid padding. Key probably wrong.");
    364 	}
    365
    366 	return buffer.left(ofs);
    367 }
    368
    369
    370
    371 /**
    372  * Validate and decrypt the payload.
    373  *
    374  * @param {ByteString} buffer the encrypted payload.
    375  * @type ByteString
    376  * @return the plain content.
    377  */
    378 FileEncryptor.prototype.decrypt = function(buffer) {
    379
    380 	this.validateHeader();
    381
    382 	var hash = this.calculateHash(buffer);
    383 	if (!hash.equals(this.hash)) {
    384 		throw new GPError(module.id, GPError.CRYPTO_FAILED, 0, "Integrity check failed (Invalid hash).");
    385 	}
    386
    387 	var pp = this.getCrypto().decrypt(this.enckey, Crypto.AES_CBC, buffer, this.iv);
    388 	p = FileEncryptor.stripPadding(pp);
    389 	pp.clear();
    390
    391 	return p;
    392 }
    393