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