diff options
Diffstat (limited to 'dom/secureelement/gonk/GPAccessRulesManager.js')
-rw-r--r-- | dom/secureelement/gonk/GPAccessRulesManager.js | 436 |
1 files changed, 436 insertions, 0 deletions
diff --git a/dom/secureelement/gonk/GPAccessRulesManager.js b/dom/secureelement/gonk/GPAccessRulesManager.js new file mode 100644 index 000000000..dce11ec09 --- /dev/null +++ b/dom/secureelement/gonk/GPAccessRulesManager.js @@ -0,0 +1,436 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Copyright © 2015, Deutsche Telekom, Inc. */ + +"use strict"; + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); + +XPCOMUtils.defineLazyServiceGetter(this, "UiccConnector", + "@mozilla.org/secureelement/connector/uicc;1", + "nsISecureElementConnector"); + +XPCOMUtils.defineLazyModuleGetter(this, "SEUtils", + "resource://gre/modules/SEUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "SE", function() { + let obj = {}; + Cu.import("resource://gre/modules/se_consts.js", obj); + return obj; +}); + +XPCOMUtils.defineLazyGetter(this, "GP", function() { + let obj = {}; + Cu.import("resource://gre/modules/gp_consts.js", obj); + return obj; +}); + +var DEBUG = SE.DEBUG_ACE; +function debug(msg) { + if (DEBUG) { + dump("-*- GPAccessRulesManager " + msg); + } +} + +/** + * Based on [1] - "GlobalPlatform Device Technology + * Secure Element Access Control Version 1.0". + * GPAccessRulesManager reads and parses access rules from SE file system + * as defined in section #7 of [1]: "Structure of Access Rule Files (ARF)". + * Rules retrieval from ARA-M applet is not implmented due to lack of + * commercial implemenations of ARA-M. + * @todo Bug 1137537: Implement ARA-M support according to section #4 of [1] + */ +function GPAccessRulesManager() {} + +GPAccessRulesManager.prototype = { + // source [1] section 7.1.3 PKCS#15 Selection + PKCS_AID: "a000000063504b43532d3135", + + // APDUs (ISO 7816-4) for accessing rules on SE file system + // see for more details: http://www.cardwerk.com/smartcards/ + // smartcard_standard_ISO7816-4_6_basic_interindustry_commands.aspx + READ_BINARY: [GP.CLA_SM, GP.INS_RB, GP.P1_RB, GP.P2_RB], + GET_RESPONSE: [GP.CLA_SM, GP.INS_GR, GP.P1_GR, GP.P2_GR], + SELECT_BY_DF: [GP.CLA_SM, GP.INS_SF, GP.P1_SF_DF, GP.P2_SF_FCP], + + // Non-null if there is a channel open + channel: null, + + // Refresh tag path in the acMain file as described in GPD spec, + // sections 7.1.5 and C.1. + REFRESH_TAG_PATH: [GP.TAG_SEQUENCE, GP.TAG_OCTETSTRING], + refreshTag: null, + + // Contains rules as read from the SE + rules: [], + + // Returns the latest rules. Results are cached. + getAccessRules: function getAccessRules() { + debug("getAccessRules"); + + return new Promise((resolve, reject) => { + this._readAccessRules(() => resolve(this.rules)); + }); + }, + + _readAccessRules: Task.async(function*(done) { + try { + yield this._openChannel(this.PKCS_AID); + + let odf = yield this._readODF(); + let dodf = yield this._readDODF(odf); + + let acmf = yield this._readACMF(dodf); + let refreshTag = acmf[this.REFRESH_TAG_PATH[0]] + [this.REFRESH_TAG_PATH[1]]; + + // Update cached rules based on refreshTag. + if (SEUtils.arraysEqual(this.refreshTag, refreshTag)) { + debug("_readAccessRules: refresh tag equals to the one saved."); + yield this._closeChannel(); + return done(); + } + + this.refreshTag = refreshTag; + debug("_readAccessRules: refresh tag saved: " + this.refreshTag); + + let acrf = yield this._readACRules(acmf); + let accf = yield this._readACConditions(acrf); + this.rules = yield this._parseRules(acrf, accf); + + DEBUG && debug("_readAccessRules: " + JSON.stringify(this.rules, 0, 2)); + + yield this._closeChannel(); + done(); + } catch (error) { + debug("_readAccessRules: " + error); + this.rules = []; + yield this._closeChannel(); + done(); + } + }), + + _openChannel: function _openChannel(aid) { + if (this.channel !== null) { + debug("_openChannel: Channel already opened, rejecting."); + return Promise.reject(); + } + + return new Promise((resolve, reject) => { + UiccConnector.openChannel(aid, { + notifyOpenChannelSuccess: (channel, openResponse) => { + debug("_openChannel/notifyOpenChannelSuccess: Channel " + channel + + " opened, open response: " + openResponse); + this.channel = channel; + resolve(); + }, + notifyError: (error) => { + debug("_openChannel/notifyError: failed to open channel, error: " + + error); + reject(error); + } + }); + }); + }, + + _closeChannel: function _closeChannel() { + if (this.channel === null) { + debug("_closeChannel: Channel not opened, rejecting."); + return Promise.reject(); + } + + return new Promise((resolve, reject) => { + UiccConnector.closeChannel(this.channel, { + notifyCloseChannelSuccess: () => { + debug("_closeChannel/notifyCloseChannelSuccess: chanel " + + this.channel + " closed"); + this.channel = null; + resolve(); + }, + notifyError: (error) => { + debug("_closeChannel/notifyError: error closing channel, error" + + error); + reject(error); + } + }); + }); + }, + + _exchangeAPDU: function _exchangeAPDU(bytes) { + DEBUG && debug("apdu " + JSON.stringify(bytes)); + + let apdu = this._bytesToAPDU(bytes); + return new Promise((resolve, reject) => { + UiccConnector.exchangeAPDU(this.channel, apdu.cla, + apdu.ins, apdu.p1, apdu.p2, apdu.data, apdu.le, + { + notifyExchangeAPDUResponse: (sw1, sw2, data) => { + debug("APDU response is " + sw1.toString(16) + sw2.toString(16) + + " data: " + data); + + // 90 00 is "success" + if (sw1 !== 0x90 && sw2 !== 0x00) { + debug("rejecting APDU response"); + reject(new Error("Response " + sw1 + "," + sw2)); + return; + } + + resolve(this._parseTLV(data)); + }, + + notifyError: (error) => { + debug("_exchangeAPDU/notifyError " + error); + reject(error); + } + } + ); + }); + }, + + _readBinaryFile: function _readBinaryFile(selectResponse) { + DEBUG && debug("Select response: " + JSON.stringify(selectResponse)); + // 0x80 tag parameter - get the elementary file (EF) length + // without structural information. + let fileLength = selectResponse[GP.TAG_FCP][0x80]; + + // If file is empty, no need to attempt to read it. + if (fileLength[0] === 0 && fileLength[1] === 0) { + return Promise.resolve(null); + } + + // TODO READ BINARY with filelength not supported + // let readApdu = this.READ_BINARY.concat(fileLength); + return this._exchangeAPDU(this.READ_BINARY); + }, + + _selectAndRead: function _selectAndRead(df) { + return this._exchangeAPDU(this.SELECT_BY_DF.concat(df.length & 0xFF, df)) + .then((resp) => this._readBinaryFile(resp)); + }, + + _readODF: function _readODF() { + debug("_readODF"); + return this._selectAndRead(GP.ODF_DF); + }, + + _readDODF: function _readDODF(odfFile) { + debug("_readDODF, ODF file: " + odfFile); + + // Data Object Directory File (DODF) is used as an entry point to the + // Access Control data. It is specified in PKCS#15 section 6.7.6. + // DODF is referenced by the ODF file, which looks as follows: + // A7 06 + // 30 04 + // 04 02 XY WZ + // where [0xXY, 0xWZ] is a DF of DODF file. + let DODF_DF = odfFile[GP.TAG_EF_ODF][GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; + return this._selectAndRead(DODF_DF); + }, + + _readACMF: function _readACMF(dodfFile) { + debug("_readACMF, DODF file: " + dodfFile); + + // ACMF file DF is referenced in DODF file, which looks like this: + // + // A1 29 + // 30 00 + // 30 0F + // 0C 0D 47 50 20 53 45 20 41 63 63 20 43 74 6C + // A1 14 + // 30 12 + // 06 0A 2A 86 48 86 FC 6B 81 48 01 01 <-- GPD registered OID + // 30 04 + // 04 02 AB CD <-- ACMF DF + // A1 2B + // 30 00 + // 30 0F + // 0C 0D 53 41 54 53 41 20 47 54 4F 20 31 2E 31 + // A1 16 + // 30 14 + // 06 0C 2B 06 01 04 01 2A 02 6E 03 01 01 01 <-- some other OID + // 30 04 + // 04 02 XY WZ <-- some other file's DF + // + // DODF file consists of DataTypes with oidDO entries. Entry with OID + // equal to "1.2.840.114283.200.1.1" ("2A 86 48 86 FC 6B 81 48 01 01") + // contains DF of the ACMF. In the file above, it means that ACMF DF + // equals to [0xAB, 0xCD], and not [0xXY, 0xWZ]. + // + // Algorithm used to encode OID to an byte array: + // http://www.snmpsharpnet.com/?p=153 + + let gpdOid = [0x2A, // 1.2 + 0x86, 0x48, // 840 + 0x86, 0xFC, 0x6B, // 114283 + 0x81, 0x48, // 129 + 0x01, // 1 + 0x01]; // 1 + + let records = SEUtils.ensureIsArray(dodfFile[GP.TAG_EXTERNALDO]); + + // Look for the OID registered for GPD SE. + let gpdRecords = records.filter((record) => { + let oid = record[GP.TAG_EXTERNALDO][GP.TAG_SEQUENCE][GP.TAG_OID]; + return SEUtils.arraysEqual(oid, gpdOid); + }); + + // [1] 7.1.5: "There shall be only one ACMF file per Secure Element. + // If a Secure Element contains several ACMF files, then the security shall + // be considered compromised and the Access Control enforcer shall forbid + // access to all (...) apps." + if (gpdRecords.length !== 1) { + return Promise.reject(new Error(gpdRecords.length + " ACMF files found")); + } + + let ACMain_DF = gpdRecords[0][GP.TAG_EXTERNALDO][GP.TAG_SEQUENCE] + [GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; + return this._selectAndRead(ACMain_DF); + }, + + _readACRules: function _readACRules(acMainFile) { + debug("_readACRules, ACMain file: " + acMainFile); + + // ACMF looks like this: + // + // 30 10 + // 04 08 XX XX XX XX XX XX XX XX + // 30 04 + // 04 02 XY WZ + // + // where [XY, WZ] is a DF of ACRF, and XX XX XX XX XX XX XX XX is a refresh + // tag. + + let ACRules_DF = acMainFile[GP.TAG_SEQUENCE][GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; + return this._selectAndRead(ACRules_DF); + }, + + _readACConditions: function _readACConditions(acRulesFile) { + debug("_readACCondition, ACRules file: " + acRulesFile); + + let acRules = SEUtils.ensureIsArray(acRulesFile[GP.TAG_SEQUENCE]); + if (acRules.length === 0) { + debug("No rules found in ACRules file."); + return Promise.reject(new Error("No rules found in ACRules file")); + } + + // We first read all the condition files referenced in the ACRules file, + // because ACRules file might reference one ACCondition file more than + // once. Since reading it isn't exactly fast, we optimize here. + let acReadQueue = Promise.resolve({}); + + acRules.forEach((ruleEntry) => { + let df = ruleEntry[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; + + // Promise chain read condition entries: + let readAcCondition = (acConditionFiles) => { + if (acConditionFiles[df] !== undefined) { + debug("Skipping previously read acCondition df: " + df); + return acConditionFiles; + } + + return this._selectAndRead(df) + .then((acConditionFileContents) => { + acConditionFiles[df] = acConditionFileContents; + return acConditionFiles; + }); + } + + acReadQueue = acReadQueue.then(readAcCondition); + }); + + return acReadQueue; + }, + + _parseRules: function _parseRules(acRulesFile, acConditionFiles) { + DEBUG && debug("_parseRules: acConditionFiles " + JSON.stringify(acConditionFiles)); + let rules = []; + + let acRules = SEUtils.ensureIsArray(acRulesFile[GP.TAG_SEQUENCE]); + acRules.forEach((ruleEntry) => { + DEBUG && debug("Parsing one rule: " + JSON.stringify(ruleEntry)); + let rule = {}; + + // 0xA0 and 0x82 tags as per GPD spec sections C.1 - C.3. 0xA0 means + // that rule describes access to one SE applet only (and its AID is + // given). 0x82 means that rule describes acccess to all SE applets. + let oneApplet = ruleEntry[GP.TAG_GPD_AID]; + let allApplets = ruleEntry[GP.TAG_GPD_ALL]; + + if (oneApplet) { + rule.applet = oneApplet[GP.TAG_OCTETSTRING]; + } else if (allApplets) { + rule.applet = Ci.nsIAccessRulesManager.ALL_APPLET; + } else { + throw Error("Unknown applet definition"); + } + + let df = ruleEntry[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]; + let condition = acConditionFiles[df]; + if (condition === null) { + rule.application = Ci.nsIAccessRulesManager.DENY_ALL; + } else if (condition[GP.TAG_SEQUENCE]) { + if (!Array.isArray(condition[GP.TAG_SEQUENCE]) && + !condition[GP.TAG_SEQUENCE][GP.TAG_OCTETSTRING]) { + rule.application = Ci.nsIAccessRulesManager.ALLOW_ALL; + } else { + rule.application = SEUtils.ensureIsArray(condition[GP.TAG_SEQUENCE]) + .map((conditionEntry) => { + return conditionEntry[GP.TAG_OCTETSTRING]; + }); + } + } else { + throw Error("Unknown application definition"); + } + + DEBUG && debug("Rule parsed, adding to the list: " + JSON.stringify(rule)); + rules.push(rule); + }); + + DEBUG && debug("All rules parsed, we have those in total: " + JSON.stringify(rules)); + return rules; + }, + + _parseTLV: function _parseTLV(bytes) { + let containerTags = [ + GP.TAG_SEQUENCE, + GP.TAG_FCP, + GP.TAG_GPD_AID, + GP.TAG_EXTERNALDO, + GP.TAG_INDIRECT, + GP.TAG_EF_ODF + ]; + return SEUtils.parseTLV(bytes, containerTags); + }, + + // TODO consider removing if better format for storing + // APDU consts will be introduced + _bytesToAPDU: function _bytesToAPDU(arr) { + let apdu = { + cla: arr[0] & 0xFF, + ins: arr[1] & 0xFF, + p1: arr[2] & 0xFF, + p2: arr[3] & 0xFF, + p3: arr[4] & 0xFF, + le: 0 + }; + + let data = (apdu.p3 > 0) ? (arr.slice(5)) : []; + apdu.data = (data.length) ? SEUtils.byteArrayToHexString(data) : null; + return apdu; + }, + + classID: Components.ID("{3e046b4b-9e66-439a-97e0-98a69f39f55f}"), + contractID: "@mozilla.org/secureelement/access-control/rules-manager;1", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessRulesManager]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([GPAccessRulesManager]); |