      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