1 /**
  2  *  ---------
  3  * |.##> <##.|  Open Smart Card Development Platform (www.openscdp.org)
  4  * |#       #|
  5  * |#       #|  Copyright (c) 1999-2018 CardContact Software & System Consulting
  6  * |'##> <##'|  Andreas Schwier, 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 Implementation of an ISO 7816-4 command interpreter
 25  */
 26 
 27 var TransparentEF   = require('scsh/cardsim/TransparentEF').TransparentEF;
 28 var LinearEF        = require('scsh/cardsim/LinearEF').LinearEF;
 29 var APDU            = require('scsh/cardsim/APDU').APDU;
 30 var DataUnitAPDU    = require('scsh/cardsim/DataUnitAPDU').DataUnitAPDU;
 31 
 32 
 33 
 34 /**
 35  * Create a command interpreter
 36  *
 37  * @class Class implementing a command interpreter that handles ISO 7816-4 command APDUs
 38  * @constructor
 39  * @param {FileSelector} fileSelector the file selector object
 40  */
 41 function CommandInterpreter(fileSelector) {
 42 	this.fileSelector = fileSelector;
 43 }
 44 
 45 exports.CommandInterpreter = CommandInterpreter;
 46 
 47 
 48 
 49 /**
 50  * Set secure channel
 51  *
 52  * @param {SecureChannel} secureChannel the secure channel to used for unwrapping and wrapping APDUs
 53  */
 54 CommandInterpreter.prototype.setSecureChannel = function(secureChannel) {
 55 	this.secureChannel = secureChannel;
 56 }
 57 
 58 
 59 
 60 /**
 61  * Return status of secure channel
 62  *
 63  * @type boolean
 64  * @return true if secure channel is active
 65  */
 66 CommandInterpreter.prototype.hasSecureChannel = function() {
 67 	return (typeof(this.secureChannel) != "undefined") && (this.secureChannel != null);
 68 }
 69 
 70 
 71 
 72 /**
 73  * Process a READ BINARY APDU
 74  *
 75  * @param {APDU} apdu the command and response APDU
 76  */
 77 CommandInterpreter.prototype.readBinary = function(apdu) {
 78 	if (!apdu.hasLe()) {
 79 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_WRONGLENGTH, "Wrong length - missing Le field");
 80 	}
 81 
 82 	if (apdu.isChained()) {
 83 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_CHAINNOTSUPPORTED, "Chaining not supported in READ BINARY");
 84 	}
 85 
 86 	var dua = new DataUnitAPDU(apdu);
 87 
 88 	if (dua.hasCData()) {
 89 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_INVDATA, "Command data not expected in  READ BINARY");
 90 	}
 91 
 92 	var ef;
 93 	var sfi = dua.getSFI();
 94 	var fid = dua.getFID();
 95 
 96 	if (sfi >= 0) {
 97 		ef = this.fileSelector.selectSFI(sfi);
 98 	} else if (fid != null) {
 99 		ef = this.fileSelector.selectFID(fid, false, false);
100 	} else {
101 		ef = this.fileSelector.getCurrentEF();
102 
103 		if (ef == null) {
104 			throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_COMNOTALLOWNOEF, "No current EF in READ BINARY");
105 		}
106 	}
107 
108 	if (!(ef instanceof TransparentEF)) {
109 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_COMINCOMPATIBLE, "EF is not a transparent file in READ BINARY");
110 	}
111 
112 	var ac = this.fileSelector.getMeta("accessController");
113 	if (ac && !ac.checkFileReadAccess(this, apdu, ef)) {
114 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_SECSTATNOTSAT, "Read access not allowed as determined by " + ac);
115 	}
116 
117 	var offset = dua.getOffset();
118 	var length = apdu.getNe();
119 
120 	var data = ef.readBinary(apdu, offset, length);
121 
122 	apdu.setRData(data);
123 }
124 
125 
126 
127 /**
128  * Process an UPDATE BINARY APDU
129  *
130  * @param {APDU} apdu the command and response APDU
131  */
132 CommandInterpreter.prototype.updateBinary = function(apdu) {
133 	if (apdu.isChained()) {
134 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_CHAINNOTSUPPORTED, "Chaining not supported in READ BINARY");
135 	}
136 
137 	if (!apdu.hasCData()) {
138 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_WRONGLENGTH, "No data found in UPDATE BINARY");
139 	}
140 
141 	var dua = new DataUnitAPDU(apdu);
142 
143 	if (!dua.hasCData()) {
144 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_INVDATA, "No data found in UPDATE BINARY");
145 	}
146 
147 	var ef;
148 	var sfi = dua.getSFI();
149 	var fid = dua.getFID();
150 
151 	if (sfi >= 0) {
152 		ef = this.fileSelector.selectSFI(sfi);
153 	} else if (fid != null) {
154 		ef = this.fileSelector.selectFID(fid, false, false);
155 	} else {
156 		ef = this.fileSelector.getCurrentEF();
157 
158 		if (ef == null) {
159 			throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_COMNOTALLOWNOEF, "No current EF in UPDATE BINARY");
160 		}
161 	}
162 
163 	if (!(ef instanceof TransparentEF)) {
164 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_COMINCOMPATIBLE, "EF is not a transparent file in UPDATE BINARY");
165 	}
166 
167 	var ac = this.fileSelector.getMeta("accessController");
168 	if (ac && !ac.checkFileWriteAccess(this, apdu, ef)) {
169 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_SECSTATNOTSAT, "Write access not allowed as determined by " + ac);
170 	}
171 
172 	var offset = dua.getOffset();
173 	var data = dua.getCData();
174 
175 	ef.updateBinary(apdu, offset, data);
176 }
177 
178 
179 
180 /**
181  * Process a READ RECORD APDU
182  *
183  * @param {APDU} apdu the command and response APDU
184  */
185 CommandInterpreter.prototype.readRecord = function(apdu) {
186 	if (!apdu.hasLe()) {
187 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_WRONGLENGTH, "Wrong length - missing Le field");
188 	}
189 
190 	if (apdu.isChained()) {
191 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_CHAINNOTSUPPORTED, "Chaining not supported in READ RECORD");
192 	}
193 
194 	var recno = apdu.getP1();
195 	var sfi = apdu.getP2() >> 3;
196 	var qualifier = apdu.getP2() & 0x7;
197 
198 	if (sfi > 0) {
199 		ef = this.fileSelector.selectSFI(sfi);
200 	} else {
201 		ef = this.fileSelector.getCurrentEF();
202 
203 		if (ef == null) {
204 			throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_COMNOTALLOWNOEF, "No current EF in READ RECORD");
205 		}
206 	}
207 
208 	if (!(ef instanceof LinearEF)) {
209 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_COMINCOMPATIBLE, "EF is not a linear file in READ RECORD");
210 	}
211 
212 	var ac = this.fileSelector.getMeta("accessController");
213 	if (ac && !ac.checkFileReadAccess(this, apdu, ef)) {
214 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_SECSTATNOTSAT, "Read access not allowed as determined by " + ac);
215 	}
216 
217 	var data = ef.readRecord(apdu, recno, qualifier, apdu.getNe());
218 
219 	apdu.setRData(data);
220 }
221 
222 
223 
224 /**
225  * Performs a VERIFY command
226  *
227  * @param {APDU} the apdu
228  */
229 CommandInterpreter.prototype.verify = function(apdu) {
230 	if (apdu.isChained()) {
231 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_CHAINNOTSUPPORTED, "Chaining not supported in command");
232 	}
233 
234 	var pinao = this.fileSelector.getObject(AuthenticationObject.TYPE_PIN, apdu.getP2());
235 	if (!pinao) {
236 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_RDNOTFOUND, "PIN with reference " + apdu.getP2() + " not found");
237 	}
238 
239 	if (apdu.hasCData()) {
240 		pinao.verify(apdu.getCData());
241 	} else {
242 		if (!this.fileSelector.isAuthenticated(ao)) {
243 			pinao.determineStatus();
244 		}
245 	}
246 
247 	apdu.setSW(APDU.SW_OK);
248 }
249 
250 
251 
252 /**
253  * Performs a CHANGE REFERENCE DATA command
254  *
255  * @param {APDU} the apdu
256  */
257 CommandInterpreter.prototype.changeReferenceData = function(apdu) {
258 	if (apdu.isChained()) {
259 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_CHAINNOTSUPPORTED, "Chaining not supported in command");
260 	}
261 	if (!apdu.hasCData()) {
262 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_WRONGLENGTH, "Command must have C-data");
263 	}
264 
265 	var pinao = this.fileSelector.getObject(AuthenticationObject.TYPE_PIN, apdu.getP2());
266 	if (!pinao) {
267 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_RDNOTFOUND, "PIN with reference " + apdu.getP2() + " not found");
268 	}
269 
270 	pinao.changeReferenceData(apdu.getP1(), apdu.getCData());
271 
272 	apdu.setSW(APDU.SW_OK);
273 }
274 
275 
276 
277 /**
278  * Performs a RESET RETRY COUNTER command
279  *
280  * @param {APDU} the apdu
281  */
282 CommandInterpreter.prototype.resetRetryCounter = function(apdu) {
283 	if (apdu.isChained()) {
284 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_CHAINNOTSUPPORTED, "Chaining not supported in command");
285 	}
286 	var p1 = apdu.getP1();
287 
288 	if (p1 == 0x02) {
289 		if (!apdu.hasCData()) {
290 			throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_WRONGLENGTH, "Command must have C-data");
291 		}
292 	} else if (p1 == 0x03) {
293 		if (apdu.hasCData()) {
294 			throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_WRONGLENGTH, "Command must not have C-data");
295 		}
296 	} else {
297 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_INCP1P2, "Invalid P1 or P2");
298 	}
299 
300 	var pinao = this.fileSelector.getObject(AuthenticationObject.TYPE_PIN, apdu.getP2());
301 	if (!pinao) {
302 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_RDNOTFOUND, "PIN with reference " + apdu.getP2() + " not found");
303 	}
304 
305 	pinao.resetRetryCounter(apdu.getCData());
306 
307 	apdu.setSW(APDU.SW_OK);
308 }
309 
310 
311 
312 /**
313  * Process a MANAGE SECURITY ENVIRONMENT APDU
314  *
315  * @param {APDU} apdu the command and response APDU
316  */
317 CommandInterpreter.prototype.manageSecurityEnvironment = function(apdu) {
318 	var p1 = apdu.getP1();
319 	var p2 = apdu.getP2();
320 
321 	var se = this.fileSelector.getSecurityEnvironment();
322 
323 	if ((p1 & 0x0F) != 1) { 	// SET
324 		throw new GPError("CommandInterpreter", GPError.INVALID_TYPE, APDU.APDU.SW_FUNCNOTSUPPORTED, "Only MANAGE SE set variant supported");
325 	}
326 
327 	var tlv = new ASN1(p2, apdu.getCData());
328 
329 	if (p1 & 0x80) {					// Verification, Encryption, External Authentication and Key Agreement
330 		var t = new ASN1(tlv.getBytes());		// Dirty trick to deserialize as TLV tree
331 		se.VEXK.add(t);
332 	}
333 	if (p1 & 0x40) {					// Calculation, Decryption, Internal Authentication and Key Agreement
334 		var t = new ASN1(tlv.getBytes());		// Dirty trick to deserialize as TLV tree
335 		se.CDIK.add(t);
336 	}
337 	if (p1 & 0x20) {					// Secure Messaging Response
338 		var t = new ASN1(tlv.getBytes());		// Dirty trick to deserialize as TLV tree
339 		se.SMRES.add(t);
340 	}
341 	if (p1 & 0x10) {					// Secure Messaging Command
342 		var t = new ASN1(tlv.getBytes());		// Dirty trick to deserialize as TLV tree
343 		se.SMCOM.add(t);
344 	}
345 	apdu.setSW(APDU.SW_OK);
346 }
347 
348 
349 
350 /**
351  * Process a secure messaging command APDU, if secure messaging is active.
352  *
353  * @param {APDU} apdu the command APDU
354  */
355 CommandInterpreter.prototype.handleSecMsgCommandAPDU = function(apdu) {
356 	if (apdu.isSecureMessaging()) {
357 		if (this.hasSecureChannel()) {
358 			try	{
359 				apdu.setSecureChannel(this.secureChannel);
360 				apdu.unwrap();
361 			}
362 			catch(e) {
363 				this.setSecureChannel();	// Reset secure channel
364 				throw e;
365 			}
366 		} else {
367 			throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_SMNOTSUPPORTED, "No secure messaging channel");
368 		}
369 	} else {
370 		if (this.hasSecureChannel()) {
371 			this.setSecureChannel();	// Reset secure channel
372 		}
373 	}
374 }
375 
376 
377 
378 /**
379  * Process a secure messaging response APDU, if secure messaging is active
380  *
381  * @param {APDU} apdu the response APDU
382  */
383 CommandInterpreter.prototype.handleSecMsgResponseAPDU = function(apdu) {
384 	if (apdu.isSecureMessaging() && this.hasSecureChannel()) {
385 		apdu.wrap();
386 	}
387 }
388 
389 
390 
391 /**
392  * Dispatch to command handler based on instruction code
393  *
394  * @param {APDU} apdu the command and response APDU
395  * @param {Number} ins instruction code
396  */
397 CommandInterpreter.prototype.dispatch = function(apdu, ins) {
398 	if (!apdu.isISO()) {
399 		apdu.setSW(APDU.SW_INVCLA);
400 		return;
401 	}
402 
403 	if (apdu.isChained()) {
404 		throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_CHAINNOTSUPPORTED, "Chaining not supported");
405 	}
406 
407 	switch(ins) {
408 		case APDU.INS_SELECT:
409 			this.fileSelector.processSelectAPDU(apdu);
410 			break;
411 		case APDU.INS_READ_BINARY:
412 			this.readBinary(apdu);
413 			break;
414 		case APDU.INS_UPDATE_BINARY:
415 			this.updateBinary(apdu);
416 			break;
417 		case APDU.INS_READ_RECORD:
418 			this.readRecord(apdu);
419 			break;
420 		case APDU.INS_VERIFY:
421 			this.verify(apdu);
422 			break;
423 		case APDU.INS_RESET_RETRY_COUNTER:
424 			this.resetRetryCounter(apdu);
425 			break;
426 		case APDU.INS_CHANGE_REFERENCE_DATA:
427 			this.changeReferenceData(apdu);
428 			break;
429 		case APDU.INS_MANAGE_SE:
430 			this.manageSecurityEnvironment(apdu);
431 			break;
432 		default:
433 			apdu.setSW(APDU.SW_INVINS);
434 	}
435 }
436 
437 
438 
439 /**
440  * Process a command APDU
441  *
442  * @param {APDU} apdu the command and response APDU
443  */
444 CommandInterpreter.prototype.processAPDU = function(apdu) {
445 	try	{
446 		this.handleSecMsgCommandAPDU(apdu);
447 
448 		var cla = apdu.getCLA();
449 		var ins = apdu.getINS();
450 		var tlv = (ins & 1) == 1;
451 		ins &= 0xFE;
452 
453 		var ac = this.fileSelector.getMeta("accessController");
454 		if (ac && !ac.checkCommandAccess(this, apdu)) {
455 			throw new GPError("CommandInterpreter", GPError.INVALID_DATA, APDU.SW_SECSTATNOTSAT, "Command not allowed as determined by " + ac);
456 		}
457 
458 		this.dispatch(apdu, ins);
459 	}
460 	catch(e) {
461 		GPSystem.trace(e.fileName + "#" + e.lineNumber + ": " + e);
462 		var sw = APDU.SW_GENERALERROR;
463 		if ((e instanceof GPError) && (e.reason >= 0x6200)) {
464 			apdu.setSW(e.reason);
465 		} else {
466 			apdu.setSW(APDU.SW_GENERALERROR);
467 		}
468 	}
469 	this.handleSecMsgResponseAPDU(apdu);
470 }
471 
472