diff options
Diffstat (limited to 'dom/secureelement/gonk')
-rw-r--r-- | dom/secureelement/gonk/ACEService.js | 139 | ||||
-rw-r--r-- | dom/secureelement/gonk/ACEService.manifest | 2 | ||||
-rw-r--r-- | dom/secureelement/gonk/GPAccessRulesManager.js | 436 | ||||
-rw-r--r-- | dom/secureelement/gonk/GPAccessRulesManager.manifest | 2 | ||||
-rw-r--r-- | dom/secureelement/gonk/SecureElement.js | 514 | ||||
-rw-r--r-- | dom/secureelement/gonk/SecureElement.manifest | 18 | ||||
-rw-r--r-- | dom/secureelement/gonk/UiccConnector.js | 360 | ||||
-rw-r--r-- | dom/secureelement/gonk/UiccConnector.manifest | 17 | ||||
-rw-r--r-- | dom/secureelement/gonk/gp_consts.js | 62 | ||||
-rw-r--r-- | dom/secureelement/gonk/nsIAccessControlEnforcer.idl | 32 | ||||
-rw-r--r-- | dom/secureelement/gonk/nsIAccessRulesManager.idl | 50 | ||||
-rw-r--r-- | dom/secureelement/gonk/nsISecureElementConnector.idl | 124 | ||||
-rw-r--r-- | dom/secureelement/gonk/se_consts.js | 68 |
13 files changed, 1824 insertions, 0 deletions
diff --git a/dom/secureelement/gonk/ACEService.js b/dom/secureelement/gonk/ACEService.js new file mode 100644 index 000000000..b52ba5fab --- /dev/null +++ b/dom/secureelement/gonk/ACEService.js @@ -0,0 +1,139 @@ +/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components; +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +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; +}); + +var DEBUG = SE.DEBUG_ACE; +function debug(msg) { + if (DEBUG) { + dump("ACEservice: " + msg + "\n"); + } +} + +/** + * Implements decision making algorithm as described in GPD specification, + * mostly in 3.1, 3.2 and 4.2.3. + * + * TODO: Bug 1137533: Implement GPAccessRulesManager APDU filters + */ +function GPAccessDecision(rules, certHash, aid) { + this.rules = rules; + this.certHash = certHash; + this.aid = aid; +} + +GPAccessDecision.prototype = { + isAccessAllowed: function isAccessAllowed() { + // GPD SE Access Control v1.1, 3.4.1, Table 3-2: (Conflict resolution) + // If a specific rule allows, all other non-specific access is denied. + // Conflicting specific rules will resolve to the first Allowed == "true" + // match. Given no specific rule, the global "All" rules will determine + // access. "Some", skips further processing if access Allowed == "true". + // + // Access must be decided before the SE connector openChannel, and the + // exchangeAPDU call. + // + // NOTE: This implementation may change with the introduction of APDU + // filters. + let decision = this.rules.some(this._decideAppAccess.bind(this)); + return decision; + }, + + _decideAppAccess: function _decideAppAccess(rule) { + let appMatched, appletMatched; + + // GPD SE AC 4.2.3: Algorithm for Applying Rules + // Specific rule overrides global rule. + // + // DeviceAppID is the application hash, and the AID is SE Applet ID: + // + // GPD SE AC 4.2.3 A: + // SearchRuleFor(DeviceAppID, AID) + // GPD SE AC 4.2.3 B: If no rule fits A: + // SearchRuleFor(<AllDeviceApplications>, AID) + // GPD SE AC 4.2.3 C: If no rule fits A or B: + // SearchRuleFor(DeviceAppID, <AllSEApplications>) + // GPD SE AC 4.2.3 D: If no rule fits A, B, or C: + // SearchRuleFor(<AllDeviceApplications>, <AllSEApplications>) + + // Device App + appMatched = Array.isArray(rule.application) ? + // GPD SE AC 4.2.3 A and 4.2.3 C (DeviceAppID rule) + this._appCertHashMatches(rule.application) : + // GPD SE AC 4.2.3 B and 4.2.3 D (All Device Applications) + rule.application === Ci.nsIAccessRulesManager.ALLOW_ALL; + + if (!appMatched) { + return false; // bail out early. + } + + // SE Applet + appletMatched = Array.isArray(rule.applet) ? + // GPD SE AC 4.2.3 A and 4.2.3 B (AID rule) + SEUtils.arraysEqual(rule.applet, this.aid) : + // GPD SE AC 4.2.3 C and 4.2.3 D (All AID) + rule.applet === Ci.nsIAccessRulesManager.ALL_APPLET; + + return appletMatched; + }, + + _appCertHashMatches: function _appCertHashMatches(hashArray) { + if (!Array.isArray(hashArray)) { + return false; + } + + return !!(hashArray.find((hash) => { + return SEUtils.arraysEqual(hash, this.certHash); + })); + } +}; + +function ACEService() { + this._rulesManagers = new Map(); + + this._rulesManagers.set( + SE.TYPE_UICC, + Cc["@mozilla.org/secureelement/access-control/rules-manager;1"] + .createInstance(Ci.nsIAccessRulesManager)); +} + +ACEService.prototype = { + _rulesManagers: null, + + isAccessAllowed: function isAccessAllowed(localId, seType, aid) { + if(!Services.prefs.getBoolPref("devtools.debugger.forbid-certified-apps")) { + debug("Certified apps debug enabled, allowing access"); + return Promise.resolve(true); + } + + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + _getDevCertHashForApp: function getDevCertHashForApp(manifestURL) { + throw Components.results.NS_ERROR_NOT_IMPLEMENTED; + }, + + classID: Components.ID("{882a7463-2ca7-4d61-a89a-10eb6fd70478}"), + contractID: "@mozilla.org/secureelement/access-control/ace;1", + QueryInterface: XPCOMUtils.generateQI([Ci.nsIAccessControlEnforcer]) +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ACEService]); + diff --git a/dom/secureelement/gonk/ACEService.manifest b/dom/secureelement/gonk/ACEService.manifest new file mode 100644 index 000000000..40949c83d --- /dev/null +++ b/dom/secureelement/gonk/ACEService.manifest @@ -0,0 +1,2 @@ +component {882a7463-2ca7-4d61-a89a-10eb6fd70478} ACEService.js +contract @mozilla.org/secureelement/access-control/ace;1 {882a7463-2ca7-4d61-a89a-10eb6fd70478}
\ No newline at end of file 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]); diff --git a/dom/secureelement/gonk/GPAccessRulesManager.manifest b/dom/secureelement/gonk/GPAccessRulesManager.manifest new file mode 100644 index 000000000..2d7ea038b --- /dev/null +++ b/dom/secureelement/gonk/GPAccessRulesManager.manifest @@ -0,0 +1,2 @@ +component {3e046b4b-9e66-439a-97e0-98a69f39f55f} GPAccessRulesManager.js +contract @mozilla.org/secureelement/access-control/rules-manager;1 {3e046b4b-9e66-439a-97e0-98a69f39f55f} diff --git a/dom/secureelement/gonk/SecureElement.js b/dom/secureelement/gonk/SecureElement.js new file mode 100644 index 000000000..144c6d8d6 --- /dev/null +++ b/dom/secureelement/gonk/SecureElement.js @@ -0,0 +1,514 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Copyright © 2014, Deutsche Telekom, Inc. */ + +"use strict"; + +/* globals dump, Components, XPCOMUtils, SE, Services, UiccConnector, + SEUtils, ppmm, gMap, UUIDGenerator */ + +const { classes: Cc, interfaces: Ci, utils: Cu } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); + +XPCOMUtils.defineLazyGetter(this, "SE", () => { + let obj = {}; + Cu.import("resource://gre/modules/se_consts.js", obj); + return obj; +}); + +// set to true in se_consts.js to see debug messages +var DEBUG = SE.DEBUG_SE; +function debug(s) { + if (DEBUG) { + dump("-*- SecureElement: " + s + "\n"); + } +} + +const SE_IPC_SECUREELEMENT_MSG_NAMES = [ + "SE:GetSEReaders", + "SE:OpenChannel", + "SE:CloseChannel", + "SE:TransmitAPDU" +]; + +const SECUREELEMENTMANAGER_CONTRACTID = + "@mozilla.org/secureelement/parent-manager;1"; +const SECUREELEMENTMANAGER_CID = + Components.ID("{48f4e650-28d2-11e4-8c21-0800200c9a66}"); +const NS_XPCOM_SHUTDOWN_OBSERVER_ID = "xpcom-shutdown"; + +XPCOMUtils.defineLazyServiceGetter(this, "ppmm", + "@mozilla.org/parentprocessmessagemanager;1", + "nsIMessageBroadcaster"); + +XPCOMUtils.defineLazyServiceGetter(this, "UUIDGenerator", + "@mozilla.org/uuid-generator;1", + "nsIUUIDGenerator"); + +XPCOMUtils.defineLazyModuleGetter(this, "SEUtils", + "resource://gre/modules/SEUtils.jsm"); + +XPCOMUtils.defineLazyGetter(this, "UiccConnector", () => { + let uiccClass = Cc["@mozilla.org/secureelement/connector/uicc;1"]; + return uiccClass ? uiccClass.getService(Ci.nsISecureElementConnector) : null; +}); + +function getConnector(type) { + switch (type) { + case SE.TYPE_UICC: + return UiccConnector; + case SE.TYPE_ESE: + default: + debug("Unsupported SEConnector : " + type); + return null; + } +} + +/** + * 'gMap' is a nested dictionary object that manages all the information + * pertaining to channels for a given application (appId). It manages the + * relationship between given application and its opened channels. + */ +XPCOMUtils.defineLazyGetter(this, "gMap", function() { + return { + // example structure of AppInfoMap + // { + // "appId1": { + // target: target1, + // channels: { + // "channelToken1": { + // seType: "uicc", + // aid: "aid1", + // channelNumber: 1 + // }, + // "channelToken2": { ... } + // } + // }, + // "appId2": { ... } + // } + appInfoMap: {}, + + registerSecureElementTarget: function(appId, target) { + if (this.isAppIdRegistered(appId)) { + debug("AppId: " + appId + "already registered"); + return; + } + + this.appInfoMap[appId] = { + target: target, + channels: {} + }; + + debug("Registered a new SE target " + appId); + }, + + unregisterSecureElementTarget: function(target) { + let appId = Object.keys(this.appInfoMap).find((id) => { + return this.appInfoMap[id].target === target; + }); + + if (!appId) { + return; + } + + debug("Unregistered SE Target for AppId: " + appId); + delete this.appInfoMap[appId]; + }, + + isAppIdRegistered: function(appId) { + return this.appInfoMap[appId] !== undefined; + }, + + getChannelCountByAppIdType: function(appId, type) { + return Object.keys(this.appInfoMap[appId].channels) + .reduce((cnt, ch) => ch.type === type ? ++cnt : cnt, 0); + }, + + // Add channel to the appId. Upon successfully adding the entry + // this function will return the 'token' + addChannel: function(appId, type, aid, channelNumber) { + let token = UUIDGenerator.generateUUID().toString(); + this.appInfoMap[appId].channels[token] = { + seType: type, + aid: aid, + channelNumber: channelNumber + }; + return token; + }, + + removeChannel: function(appId, channelToken) { + if (this.appInfoMap[appId].channels[channelToken]) { + debug("Deleting channel with token : " + channelToken); + delete this.appInfoMap[appId].channels[channelToken]; + } + }, + + getChannel: function(appId, channelToken) { + if (!this.appInfoMap[appId].channels[channelToken]) { + return null; + } + + return this.appInfoMap[appId].channels[channelToken]; + }, + + getChannelsByTarget: function(target) { + let appId = Object.keys(this.appInfoMap).find((id) => { + return this.appInfoMap[id].target === target; + }); + + if (!appId) { + return []; + } + + return Object.keys(this.appInfoMap[appId].channels) + .map(token => this.appInfoMap[appId].channels[token]); + }, + + getTargets: function() { + return Object.keys(this.appInfoMap) + .map(appId => this.appInfoMap[appId].target); + }, + }; +}); + +/** + * 'SecureElementManager' is the main object that handles IPC messages from + * child process. It interacts with other objects such as 'gMap' & 'Connector + * instances (UiccConnector, eSEConnector)' to perform various + * SE-related (open, close, transmit) operations. + * @TODO: Bug 1118097 Support slot based SE/reader names + * @TODO: Bug 1118101 Introduce SE type specific permissions + */ +function SecureElementManager() { + this._registerMessageListeners(); + this._registerSEListeners(); + Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + this._acEnforcer = + Cc["@mozilla.org/secureelement/access-control/ace;1"] + .getService(Ci.nsIAccessControlEnforcer); +} + +SecureElementManager.prototype = { + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIMessageListener, + Ci.nsISEListener, + Ci.nsIObserver]), + classID: SECUREELEMENTMANAGER_CID, + classInfo: XPCOMUtils.generateCI({ + classID: SECUREELEMENTMANAGER_CID, + classDescription: "SecureElementManager", + interfaces: [Ci.nsIMessageListener, + Ci.nsISEListener, + Ci.nsIObserver] + }), + + // Stores information about supported SE types and their presence. + // key: secure element type, value: (Boolean) is present/accessible + _sePresence: {}, + + _acEnforcer: null, + + _shutdown: function() { + this._acEnforcer = null; + this.secureelement = null; + Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + this._unregisterMessageListeners(); + this._unregisterSEListeners(); + }, + + _registerMessageListeners: function() { + ppmm.addMessageListener("child-process-shutdown", this); + for (let msgname of SE_IPC_SECUREELEMENT_MSG_NAMES) { + ppmm.addMessageListener(msgname, this); + } + }, + + _unregisterMessageListeners: function() { + ppmm.removeMessageListener("child-process-shutdown", this); + for (let msgname of SE_IPC_SECUREELEMENT_MSG_NAMES) { + ppmm.removeMessageListener(msgname, this); + } + ppmm = null; + }, + + _registerSEListeners: function() { + let connector = getConnector(SE.TYPE_UICC); + if (!connector) { + return; + } + + this._sePresence[SE.TYPE_UICC] = false; + connector.registerListener(this); + }, + + _unregisterSEListeners: function() { + Object.keys(this._sePresence).forEach((type) => { + let connector = getConnector(type); + if (connector) { + connector.unregisterListener(this); + } + }); + + this._sePresence = {}; + }, + + notifySEPresenceChanged: function(type, isPresent) { + // we need to notify all targets, even those without open channels, + // app could've stored the reader without actually using it + debug("notifying DOM about SE state change"); + this._sePresence[type] = isPresent; + gMap.getTargets().forEach(target => { + let result = { type: type, isPresent: isPresent }; + target.sendAsyncMessage("SE:ReaderPresenceChanged", { result: result }); + }); + }, + + _canOpenChannel: function(appId, type) { + let opened = gMap.getChannelCountByAppIdType(appId, type); + let limit = SE.MAX_CHANNELS_ALLOWED_PER_SESSION; + // UICC basic channel is not accessible see comment in se_consts.js + limit = type === SE.TYPE_UICC ? limit - 1 : limit; + return opened < limit; + }, + + _handleOpenChannel: function(msg, callback) { + if (!this._canOpenChannel(msg.appId, msg.type)) { + debug("Max channels per session exceed"); + callback({ error: SE.ERROR_GENERIC }); + return; + } + + let connector = getConnector(msg.type); + if (!connector) { + debug("No SE connector available"); + callback({ error: SE.ERROR_NOTPRESENT }); + return; + } + + this._acEnforcer.isAccessAllowed(msg.appId, msg.type, msg.aid) + .then((allowed) => { + if (!allowed) { + callback({ error: SE.ERROR_SECURITY }); + return; + } + connector.openChannel(SEUtils.byteArrayToHexString(msg.aid), { + + notifyOpenChannelSuccess: (channelNumber, openResponse) => { + // Add the new 'channel' to the map upon success + let channelToken = + gMap.addChannel(msg.appId, msg.type, msg.aid, channelNumber); + if (channelToken) { + callback({ + error: SE.ERROR_NONE, + channelToken: channelToken, + isBasicChannel: (channelNumber === SE.BASIC_CHANNEL), + openResponse: SEUtils.hexStringToByteArray(openResponse) + }); + } else { + callback({ error: SE.ERROR_GENERIC }); + } + }, + + notifyError: (reason) => { + debug("Failed to open the channel to AID : " + + SEUtils.byteArrayToHexString(msg.aid) + + ", Rejected with Reason : " + reason); + callback({ error: SE.ERROR_GENERIC, reason: reason, response: [] }); + } + }); + }) + .catch((error) => { + debug("Failed to get info from accessControlEnforcer " + error); + callback({ error: SE.ERROR_SECURITY }); + }); + }, + + _handleTransmit: function(msg, callback) { + let channel = gMap.getChannel(msg.appId, msg.channelToken); + if (!channel) { + debug("Invalid token:" + msg.channelToken + ", appId: " + msg.appId); + callback({ error: SE.ERROR_GENERIC }); + return; + } + + let connector = getConnector(channel.seType); + if (!connector) { + debug("No SE connector available"); + callback({ error: SE.ERROR_NOTPRESENT }); + return; + } + + // Bug 1137533 - ACE GPAccessRulesManager APDU filters + connector.exchangeAPDU(channel.channelNumber, msg.apdu.cla, msg.apdu.ins, + msg.apdu.p1, msg.apdu.p2, + SEUtils.byteArrayToHexString(msg.apdu.data), + msg.apdu.le, { + notifyExchangeAPDUResponse: (sw1, sw2, response) => { + callback({ + error: SE.ERROR_NONE, + sw1: sw1, + sw2: sw2, + response: SEUtils.hexStringToByteArray(response) + }); + }, + + notifyError: (reason) => { + debug("Transmit failed, rejected with Reason : " + reason); + callback({ error: SE.ERROR_INVALIDAPPLICATION, reason: reason }); + } + }); + }, + + _handleCloseChannel: function(msg, callback) { + let channel = gMap.getChannel(msg.appId, msg.channelToken); + if (!channel) { + debug("Invalid token:" + msg.channelToken + ", appId:" + msg.appId); + callback({ error: SE.ERROR_GENERIC }); + return; + } + + let connector = getConnector(channel.seType); + if (!connector) { + debug("No SE connector available"); + callback({ error: SE.ERROR_NOTPRESENT }); + return; + } + + connector.closeChannel(channel.channelNumber, { + notifyCloseChannelSuccess: () => { + gMap.removeChannel(msg.appId, msg.channelToken); + callback({ error: SE.ERROR_NONE }); + }, + + notifyError: (reason) => { + debug("Failed to close channel with token: " + msg.channelToken + + ", reason: "+ reason); + callback({ error: SE.ERROR_BADSTATE, reason: reason }); + } + }); + }, + + _handleGetSEReadersRequest: function(msg, target, callback) { + gMap.registerSecureElementTarget(msg.appId, target); + let readers = Object.keys(this._sePresence).map(type => { + return { type: type, isPresent: this._sePresence[type] }; + }); + callback({ readers: readers, error: SE.ERROR_NONE }); + }, + + _handleChildProcessShutdown: function(target) { + let channels = gMap.getChannelsByTarget(target); + + let createCb = (seType, channelNumber) => { + return { + notifyCloseChannelSuccess: () => { + debug("closed " + seType + ", channel " + channelNumber); + }, + + notifyError: (reason) => { + debug("Failed to close " + seType + " channel " + + channelNumber + ", reason: " + reason); + } + }; + }; + + channels.forEach((channel) => { + let connector = getConnector(channel.seType); + if (!connector) { + return; + } + + connector.closeChannel(channel.channelNumber, + createCb(channel.seType, channel.channelNumber)); + }); + + gMap.unregisterSecureElementTarget(target); + }, + + _sendSEResponse: function(msg, result) { + let promiseStatus = (result.error === SE.ERROR_NONE) ? "Resolved" : "Rejected"; + result.resolverId = msg.data.resolverId; + msg.target.sendAsyncMessage(msg.name + promiseStatus, {result: result}); + }, + + _isValidMessage: function(msg) { + let appIdValid = gMap.isAppIdRegistered(msg.data.appId); + return msg.name === "SE:GetSEReaders" ? true : appIdValid; + }, + + /** + * nsIMessageListener interface methods. + */ + + receiveMessage: function(msg) { + DEBUG && debug("Received '" + msg.name + "' message from content process" + + ": " + JSON.stringify(msg.data)); + + if (msg.name === "child-process-shutdown") { + this._handleChildProcessShutdown(msg.target); + return null; + } + + if (SE_IPC_SECUREELEMENT_MSG_NAMES.indexOf(msg.name) !== -1) { + if (!msg.target.assertPermission("secureelement-manage")) { + debug("SecureElement message " + msg.name + " from a content process " + + "with no 'secureelement-manage' privileges."); + return null; + } + } else { + debug("Ignoring unknown message type: " + msg.name); + return null; + } + + let callback = (result) => this._sendSEResponse(msg, result); + if (!this._isValidMessage(msg)) { + debug("Message not valid"); + callback({ error: SE.ERROR_GENERIC }); + return null; + } + + switch (msg.name) { + case "SE:GetSEReaders": + this._handleGetSEReadersRequest(msg.data, msg.target, callback); + break; + case "SE:OpenChannel": + this._handleOpenChannel(msg.data, callback); + break; + case "SE:CloseChannel": + this._handleCloseChannel(msg.data, callback); + break; + case "SE:TransmitAPDU": + this._handleTransmit(msg.data, callback); + break; + } + return null; + }, + + /** + * nsIObserver interface methods. + */ + + observe: function(subject, topic, data) { + if (topic === NS_XPCOM_SHUTDOWN_OBSERVER_ID) { + this._shutdown(); + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SecureElementManager]); diff --git a/dom/secureelement/gonk/SecureElement.manifest b/dom/secureelement/gonk/SecureElement.manifest new file mode 100644 index 000000000..a76fcfc11 --- /dev/null +++ b/dom/secureelement/gonk/SecureElement.manifest @@ -0,0 +1,18 @@ +# Copyright 2012 Mozilla Foundation and Mozilla contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# SecureElementManager +component {48f4e650-28d2-11e4-8c21-0800200c9a66} SecureElement.js +contract @mozilla.org/secureelement/parent-manager;1 {48f4e650-28d2-11e4-8c21-0800200c9a66} +category profile-after-change SecureElementManager @mozilla.org/secureelement/parent-manager;1 diff --git a/dom/secureelement/gonk/UiccConnector.js b/dom/secureelement/gonk/UiccConnector.js new file mode 100644 index 000000000..517303de2 --- /dev/null +++ b/dom/secureelement/gonk/UiccConnector.js @@ -0,0 +1,360 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Copyright © 2014, Deutsche Telekom, Inc. */ + +"use strict"; + +/* globals Components, XPCOMUtils, SE, dump, libcutils, Services, + iccService, SEUtils */ + +const { interfaces: Ci, utils: Cu, results: Cr } = Components; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/systemlibs.js"); + +XPCOMUtils.defineLazyGetter(this, "SE", function() { + let obj = {}; + Cu.import("resource://gre/modules/se_consts.js", obj); + return obj; +}); + +// set to true in se_consts.js to see debug messages +var DEBUG = SE.DEBUG_CONNECTOR; +function debug(s) { + if (DEBUG) { + dump("-*- UiccConnector: " + s + "\n"); + } +} + +XPCOMUtils.defineLazyModuleGetter(this, "SEUtils", + "resource://gre/modules/SEUtils.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "iccService", + "@mozilla.org/icc/iccservice;1", + "nsIIccService"); + +const UICCCONNECTOR_CONTRACTID = + "@mozilla.org/secureelement/connector/uicc;1"; +const UICCCONNECTOR_CID = + Components.ID("{8e040e5d-c8c3-4c1b-ac82-c00d25d8c4a4}"); +const NS_XPCOM_SHUTDOWN_OBSERVER_ID = "xpcom-shutdown"; + +// TODO: Bug 1118099 - Add multi-sim support. +// In the Multi-sim, there is more than one client. +// For now, use default clientID as 0. Ideally, SE parent process would like to +// know which clients (uicc slot) are connected to CLF over SWP interface. +const PREFERRED_UICC_CLIENTID = + libcutils.property_get("ro.moz.se.def_client_id", "0"); + +/** + * 'UiccConnector' object is a wrapper over iccService's channel management + * related interfaces that implements nsISecureElementConnector interface. + */ +function UiccConnector() { + this._init(); +} + +UiccConnector.prototype = { + QueryInterface: XPCOMUtils.generateQI([Ci.nsISecureElementConnector, + Ci.nsIIccListener]), + classID: UICCCONNECTOR_CID, + classInfo: XPCOMUtils.generateCI({ + classID: UICCCONNECTOR_CID, + contractID: UICCCONNECTOR_CONTRACTID, + classDescription: "UiccConnector", + interfaces: [Ci.nsISecureElementConnector, + Ci.nsIIccListener, + Ci.nsIObserver] + }), + + _SEListeners: [], + _isPresent: false, + + _init: function() { + Services.obs.addObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false); + let icc = iccService.getIccByServiceId(PREFERRED_UICC_CLIENTID); + icc.registerListener(this); + + // Update the state in order to avoid race condition. + // By this time, 'notifyCardStateChanged (with proper card state)' + // may have occurred already before this module initialization. + this._updatePresenceState(); + }, + + _shutdown: function() { + Services.obs.removeObserver(this, NS_XPCOM_SHUTDOWN_OBSERVER_ID); + let icc = iccService.getIccByServiceId(PREFERRED_UICC_CLIENTID); + icc.unregisterListener(this); + }, + + _updatePresenceState: function() { + let uiccNotReadyStates = [ + Ci.nsIIcc.CARD_STATE_UNKNOWN, + Ci.nsIIcc.CARD_STATE_ILLEGAL, + Ci.nsIIcc.CARD_STATE_PERSONALIZATION_IN_PROGRESS, + Ci.nsIIcc.CARD_STATE_PERMANENT_BLOCKED, + Ci.nsIIcc.CARD_STATE_UNDETECTED + ]; + + let cardState = iccService.getIccByServiceId(PREFERRED_UICC_CLIENTID).cardState; + let uiccPresent = cardState !== null && + uiccNotReadyStates.indexOf(cardState) == -1; + + if (this._isPresent === uiccPresent) { + return; + } + + debug("Uicc presence changed " + this._isPresent + " -> " + uiccPresent); + this._isPresent = uiccPresent; + this._SEListeners.forEach((listener) => { + listener.notifySEPresenceChanged(SE.TYPE_UICC, this._isPresent); + }); + }, + + // See GP Spec, 11.1.4 Class Byte Coding + _setChannelToCLAByte: function(cla, channel) { + if (channel < SE.LOGICAL_CHANNEL_NUMBER_LIMIT) { + // b7 = 0 indicates the first interindustry class byte coding + cla = (cla & 0x9C) & 0xFF | channel; + } else if (channel < SE.SUPPLEMENTARY_LOGICAL_CHANNEL_NUMBER_LIMIT) { + // b7 = 1 indicates the further interindustry class byte coding + cla = (cla & 0xB0) & 0xFF | 0x40 | (channel - SE.LOGICAL_CHANNEL_NUMBER_LIMIT); + } else { + debug("Channel number must be within [0..19]"); + return SE.ERROR_GENERIC; + } + return cla; + }, + + _doGetOpenResponse: function(channel, length, callback) { + // Le value is set. It means that this is a request for all available + // response bytes. + let cla = this._setChannelToCLAByte(SE.CLA_GET_RESPONSE, channel); + this.exchangeAPDU(channel, cla, SE.INS_GET_RESPONSE, 0x00, 0x00, + null, length, { + notifyExchangeAPDUResponse: function(sw1, sw2, response) { + debug("GET Response : " + response); + if (callback) { + callback({ + error: SE.ERROR_NONE, + sw1: sw1, + sw2: sw2, + response: response + }); + } + }, + + notifyError: function(reason) { + debug("Failed to get open response: " + + ", Rejected with Reason : " + reason); + if (callback) { + callback({ error: SE.ERROR_INVALIDAPPLICATION, reason: reason }); + } + } + }); + }, + + _doIccExchangeAPDU: function(channel, cla, ins, p1, p2, p3, + data, appendResp, callback) { + let icc = iccService.getIccByServiceId(PREFERRED_UICC_CLIENTID); + icc.iccExchangeAPDU(channel, cla & 0xFC, ins, p1, p2, p3, data, { + notifyExchangeAPDUResponse: (sw1, sw2, response) => { + debug("sw1 : " + sw1 + ", sw2 : " + sw2 + ", response : " + response); + + // According to ETSI TS 102 221 , Section 7.2.2.3.1, + // Enforce 'Procedure bytes' checks before notifying the callback. + // Note that 'Procedure bytes'are special cases. + // There is no need to handle '0x60' procedure byte as it implies + // no-action from SE stack perspective. This procedure byte is not + // notified to application layer. + if (sw1 === 0x6C) { + // Use the previous command header with length as second procedure + // byte (SW2) as received and repeat the procedure. + + // Recursive! and Pass empty response '' as args, since '0x6C' + // procedure does not have to deal with appended responses. + this._doIccExchangeAPDU(channel, cla, ins, p1, p2, + sw2, data, "", callback); + } else if (sw1 === 0x61) { + // Since the terminal waited for a second procedure byte and + // received it (sw2), send a GET RESPONSE command header to the UICC + // with a maximum length of 'XX', where 'XX' is the value of the + // second procedure byte (SW2). + + let claWithChannel = this._setChannelToCLAByte(SE.CLA_GET_RESPONSE, + channel); + + // Recursive, with GET RESPONSE bytes and '0x61' procedure IS interested + // in appended responses. Pass appended response and note that p3=sw2. + this._doIccExchangeAPDU(channel, claWithChannel, SE.INS_GET_RESPONSE, + 0x00, 0x00, sw2, null, + (response ? response + appendResp : appendResp), + callback); + } else if (callback) { + callback.notifyExchangeAPDUResponse(sw1, sw2, response); + } + }, + + notifyError: (reason) => { + debug("Failed to trasmit C-APDU over the channel # : " + channel + + ", Rejected with Reason : " + reason); + if (callback) { + callback.notifyError(reason); + } + } + }); + }, + + /** + * nsISecureElementConnector interface methods. + */ + + /** + * Opens a channel on a default clientId + */ + openChannel: function(aid, callback) { + if (!this._isPresent) { + callback.notifyError(SE.ERROR_NOTPRESENT); + return; + } + + // TODO: Bug 1118106: Handle Resource management / leaks by persisting + // the newly opened channel in some persistent storage so that when this + // module gets restarted (say after opening a channel) in the event of + // some erroneous conditions such as gecko restart /, crash it can read + // the persistent storage to check if there are any held resources + // (opened channels) and close them. + let icc = iccService.getIccByServiceId(PREFERRED_UICC_CLIENTID); + icc.iccOpenChannel(aid, { + notifyOpenChannelSuccess: (channel) => { + this._doGetOpenResponse(channel, 0x00, function(result) { + if (callback) { + callback.notifyOpenChannelSuccess(channel, result.response); + } + }); + }, + + notifyError: (reason) => { + debug("Failed to open the channel to AID : " + aid + + ", Rejected with Reason : " + reason); + if (callback) { + callback.notifyError(reason); + } + } + }); + }, + + /** + * Transmit the C-APDU (command) on default clientId. + */ + exchangeAPDU: function(channel, cla, ins, p1, p2, data, le, callback) { + if (!this._isPresent) { + callback.notifyError(SE.ERROR_NOTPRESENT); + return; + } + + if (data && data.length % 2 !== 0) { + callback.notifyError("Data should be a hex string with length % 2 === 0"); + return; + } + + cla = this._setChannelToCLAByte(cla, channel); + let lc = data ? data.length / 2 : 0; + let p3 = lc || le; + + if (lc && (le !== -1)) { + data += SEUtils.byteArrayToHexString([le]); + } + + // Pass empty response '' as args as we are not interested in appended + // responses yet! + debug("exchangeAPDU on Channel # " + channel); + this._doIccExchangeAPDU(channel, cla, ins, p1, p2, p3, data, "", + callback); + }, + + /** + * Closes the channel on default clientId. + */ + closeChannel: function(channel, callback) { + if (!this._isPresent) { + callback.notifyError(SE.ERROR_NOTPRESENT); + return; + } + + let icc = iccService.getIccByServiceId(PREFERRED_UICC_CLIENTID); + icc.iccCloseChannel(channel, { + notifyCloseChannelSuccess: function() { + debug("closeChannel successfully closed the channel # : " + channel); + if (callback) { + callback.notifyCloseChannelSuccess(); + } + }, + + notifyError: function(reason) { + debug("Failed to close the channel # : " + channel + + ", Rejected with Reason : " + reason); + if (callback) { + callback.notifyError(reason); + } + } + }); + }, + + registerListener: function(listener) { + if (this._SEListeners.indexOf(listener) !== -1) { + throw Cr.NS_ERROR_UNEXPECTED; + } + + this._SEListeners.push(listener); + // immediately notify listener about the current state + listener.notifySEPresenceChanged(SE.TYPE_UICC, this._isPresent); + }, + + unregisterListener: function(listener) { + let idx = this._SEListeners.indexOf(listener); + if (idx !== -1) { + this._SEListeners.splice(idx, 1); + } + }, + + /** + * nsIIccListener interface methods. + */ + notifyStkCommand: function() {}, + + notifyStkSessionEnd: function() {}, + + notifyIccInfoChanged: function() {}, + + notifyCardStateChanged: function() { + debug("Card state changed, updating UICC presence."); + this._updatePresenceState(); + }, + + /** + * nsIObserver interface methods. + */ + + observe: function(subject, topic, data) { + if (topic === NS_XPCOM_SHUTDOWN_OBSERVER_ID) { + this._shutdown(); + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UiccConnector]); diff --git a/dom/secureelement/gonk/UiccConnector.manifest b/dom/secureelement/gonk/UiccConnector.manifest new file mode 100644 index 000000000..5ac8b3b7b --- /dev/null +++ b/dom/secureelement/gonk/UiccConnector.manifest @@ -0,0 +1,17 @@ +# Copyright 2012 Mozilla Foundation and Mozilla contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# UiccConnector +component {8e040e5d-c8c3-4c1b-ac82-c00d25d8c4a4} UiccConnector.js +contract @mozilla.org/secureelement/connector/uicc;1 {8e040e5d-c8c3-4c1b-ac82-c00d25d8c4a4} diff --git a/dom/secureelement/gonk/gp_consts.js b/dom/secureelement/gonk/gp_consts.js new file mode 100644 index 000000000..7c3bc7165 --- /dev/null +++ b/dom/secureelement/gonk/gp_consts.js @@ -0,0 +1,62 @@ +/* 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. */ + +/* Object Directory File (ODF) is an elementary file which contain + pointers to other EFs. It is specified in PKCS#15 section 6.7. */ +this.ODF_DF = [0x50, 0x31]; + +/* ISO 7816-4: secure messaging */ +this.CLA_SM = 0x00; + +/* ISO 7816-4, 5.4.1 table 11 */ +this.INS_SF = 0xA4; // select file +this.INS_GR = 0xC0; // get response +this.INS_RB = 0xB0; // read binary + +/* ISO 7816-4: select file, see 6.11.3, table 58 & 59 */ +this.P1_SF_DF = 0x00; // select DF +this.P2_SF_FCP = 0x04; // return FCP + +/* ISO 7816-4: read binary, 6.1.3. P1 and P2 describe offset of the first byte + to be read. We always read the whole files at the moment. */ +this.P1_RB = 0x00; +this.P2_RB = 0x00; + +/* ISO 7816-4: get response, 7.1.3 table 74, P1-P2 '0000' (other values RFU) */ +this.P1_GR = 0x00; +this.P2_GR = 0x00; + +/* ISO 7816-4: 5.1.5 File Control Information, Table 1. For FCP and FMD. */ +this.TAG_PROPRIETARY = 0x00; +this.TAG_NON_TLV = 0x53; +this.TAG_BER_TLV = 0x73; + +/* ASN.1 tags */ +this.TAG_SEQUENCE = 0x30; +this.TAG_OCTETSTRING = 0x04; +this.TAG_OID = 0x06; // Object Identifier + +/* ISO 7816-4: 5.1.5 File Control Information, Templates. */ +this.TAG_FCP = 0x62; // File control parameters template +this.TAG_FMD = 0x64; // File management data template +this.TAG_FCI = 0x6F; // File control information template + +/* EF_DIR tags */ +this.TAG_APPLTEMPLATE = 0x61; +this.TAG_APPLIDENTIFIER = 0x4F; +this.TAG_APPLLABEL = 0x50; +this.TAG_APPLPATH = 0x51; + +this.TAG_GPD_ALL = 0x82; // EF-ACRules - GPD spec. "all applets" + +/* Generic TLVs that are parsed */ +this.TAG_GPD_AID = 0xA0; // AID in the EF-ACRules - GPD spec, "one applet" +this.TAG_EXTERNALDO = 0xA1; // External data objects - PKCS#15 +this.TAG_INDIRECT = 0xA5; // Indirect value. +this.TAG_EF_ODF = 0xA7; // Elemenetary File Object Directory File + +// Allow this file to be imported via Components.utils.import(). +this.EXPORTED_SYMBOLS = Object.keys(this); diff --git a/dom/secureelement/gonk/nsIAccessControlEnforcer.idl b/dom/secureelement/gonk/nsIAccessControlEnforcer.idl new file mode 100644 index 000000000..7ad1a97f6 --- /dev/null +++ b/dom/secureelement/gonk/nsIAccessControlEnforcer.idl @@ -0,0 +1,32 @@ +/* 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. */ + +#include "nsISupports.idl" + +interface nsIVariant; + +[scriptable, uuid(4994a960-26d9-4d71-82dd-4505bd97bf2a)] +interface nsIAccessControlEnforcer : nsISupports +{ + /** + * Determines whether application identified by its ID should be allowed + * to access Secure Element's applet identified by its AID. Decision + * is made according to the GPD specification. + * + * @param localId + * ID of an application accessing SE + * @param seType + * Type of the SE. + * @param aid + * AID of a SE applet + * @return Promise which is resolved to true if access should be allowed, + * false otherwise, and rejected if the application contains + * no developer certificate. + */ + jsval isAccessAllowed(in unsigned long localId, + in DOMString seType, + in DOMString aid); +}; diff --git a/dom/secureelement/gonk/nsIAccessRulesManager.idl b/dom/secureelement/gonk/nsIAccessRulesManager.idl new file mode 100644 index 000000000..173f57c90 --- /dev/null +++ b/dom/secureelement/gonk/nsIAccessRulesManager.idl @@ -0,0 +1,50 @@ +/* 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. */ + +#include "nsISupports.idl" + +[scriptable, uuid(7baedd2a-3189-4b03-b2a3-34016043b5e2)] +interface nsIAccessRulesManager : nsISupports +{ + /* Wildcard: rule allows all applications to access an SE applet */ + const unsigned short ALLOW_ALL = 1; + /* Wildcard: rule denies all applications to access an SE applet */ + const unsigned short DENY_ALL = 2; + /* Wildcard: rule allows application(s) access to all SE applets */ + const unsigned short ALL_APPLET = 3; + + /** + * Initiates Access Rules Manager, this should perform the initial + * reading of rules from access rule source + * @return Promise which is resolved if init is successful or rejected + * otherwise + */ + jsval init(); + + /** + * Retrieves all access rules. + * + * Rules are stored in an array. Each rule contains the following properties: + * - applet - describes an SE applet referenced by this rule. Might equal + * to an applet AID (as a byte array), or to a wildcard "all" + * meaning all applets. + * - application - describes an application referenced by this rule. Might + * be an array of developer certificate hashes (each as + * a byte array) in which case it lists all applications + * allowed access. Alternatively, might equal to wildcard + * "allowed-all" or "denied-all". + * + * Example rule format: + * [{ applet: ALL_APPLET, + * application: [[0x01, 0x02, ..., 0x20], + * [0x20, 0x19, ...., 0x01]], + * { applet: [0x00, 0x01, ..., 0x05], + * application: ALLOW_ALL}}] + * + * @return Promise which resolves with Array containing parsed access rules + */ + jsval getAccessRules(); +}; diff --git a/dom/secureelement/gonk/nsISecureElementConnector.idl b/dom/secureelement/gonk/nsISecureElementConnector.idl new file mode 100644 index 000000000..92cc1eb2b --- /dev/null +++ b/dom/secureelement/gonk/nsISecureElementConnector.idl @@ -0,0 +1,124 @@ +/* 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/. */ + +#include "nsISupports.idl" + +[scriptable, uuid(1ff3f35a-1b6f-4e65-a89e-a363b8604cd7)] +interface nsISEChannelCallback : nsISupports +{ + /** + * Callback function to notify on successfully opening a logical channel. + * + * @param channel + * The Channel Number/Handle that is successfully opened. + * @param openResponse + * Response from SE for OpenChannel operation. + */ + void notifyOpenChannelSuccess(in long channel, in DOMString openResponse); + + /** + * Callback function to notify on successfully closing the logical channel. + * + */ + void notifyCloseChannelSuccess(); + + /** + * Callback function to notify the status of 'seExchangeAPDU' command. + * + * @param sw1 + * Response's First Status Byte + * @param sw2 + * Response's Second Status Byte + * @param data + * Response's data + */ + void notifyExchangeAPDUResponse(in octet sw1, + in octet sw2, + in DOMString data); + + /** + * Callback function to notify error + * + * @param error + * Error describing the reason for failure. + */ + void notifyError(in DOMString error); +}; + +[scriptable, uuid(417f59ee-f582-45b9-9a4e-e9dcefecb4f7)] +interface nsISEListener : nsISupports +{ + void notifySEPresenceChanged(in DOMString seType, in boolean isPresent); +}; + +[scriptable, uuid(3cef313a-1d01-432d-9cd2-6610a80911f3)] +interface nsISecureElementConnector : nsISupports +{ + /** + * Open a logical communication channel with the specific secure element type + * + * @param aid + * Application Identifier of the Card Applet on the secure element. + * @param callback + * callback to notify the result of the operation. + */ + void openChannel(in DOMString aid, + in nsISEChannelCallback callback); + + /** + * Exchanges APDU channel with the specific secure element type + * + * @param channel + * Channel on which C-APDU to be transmitted. + * @param cla + Class Byte. + * @param ins + Instruction Byte + * @param p1 + Reference parameter first byte + * @param p2 + Reference parameter second byte + * Refer to 3G TS 31.101 , 10.2 'Command APDU Structure' for all the cases. + * @param data + Sequence of C-APDU data octets + * @param le [optional] + * le is the length of expected response. If the response is not expected, + it should be explicitly set to -1. + * @param callback + * callback to notify the result of the operation. + */ + void exchangeAPDU(in long channel, + in octet cla, + in octet ins, + in octet p1, + in octet p2, + in DOMString data, + in short le, + in nsISEChannelCallback callback); + + /** + * Closes the logical communication channel to the specific secure element type + * + * @param channel + * Channel to be closed. + * @param callback + * callback to notify the result of the operation. + */ + void closeChannel(in long channel, + in nsISEChannelCallback callback); + + /** + * Register a Secure Element listener + * + * @param listener + */ + void registerListener(in nsISEListener listener); + + /** + * Unregister a Secure Element listener + * + * @param listener + */ + void unregisterListener(in nsISEListener listener); +}; diff --git a/dom/secureelement/gonk/se_consts.js b/dom/secureelement/gonk/se_consts.js new file mode 100644 index 000000000..13489b7ae --- /dev/null +++ b/dom/secureelement/gonk/se_consts.js @@ -0,0 +1,68 @@ +/* Copyright 2012 Mozilla Foundation and Mozilla contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* Copyright © 2014, Deutsche Telekom, Inc. */ + +// Set to true to debug SecureElement (SE) stack +this.DEBUG_ALL = false; + +// Set individually to debug specific layers +this.DEBUG_CONNECTOR = DEBUG_ALL || false; +this.DEBUG_ACE = DEBUG_ALL || false ; +this.DEBUG_SE = DEBUG_ALL || false ; + +// Maximun logical channels per session. +// For 'uicc' SE type this value is 3, as opening a basic channel' : 0 +// is not allowed for security reasons. In such scenarios, possible +// supplementary logical channels available are : [1, 2, or 3]. +// However,Other SE types may support upto max 4 (including '0'). +this.MAX_CHANNELS_ALLOWED_PER_SESSION = 4; + +this.BASIC_CHANNEL = 0; + +// According GPCardSpec 2.2 +this.MAX_APDU_LEN = 255; // including APDU header + +// CLA (1 byte) + INS (1 byte) + P1 (1 byte) + P2 (1 byte) +this.APDU_HEADER_LEN = 4; + +this.LOGICAL_CHANNEL_NUMBER_LIMIT = 4; +this.SUPPLEMENTARY_LOGICAL_CHANNEL_NUMBER_LIMIT = 20; + +this.MIN_AID_LEN = 5; +this.MAX_AID_LEN = 16; + +this.CLA_GET_RESPONSE = 0x00; + +this.INS_SELECT = 0xA4; +this.INS_MANAGE_CHANNEL = 0x70; +this.INS_GET_RESPONSE = 0xC0; + +// Match the following errors with SecureElement.webidl's SEError enum values +this.ERROR_NONE = ""; +this.ERROR_SECURITY = "SESecurityError"; +this.ERROR_IO = "SEIoError"; +this.ERROR_BADSTATE = "SEBadStateError"; +this.ERROR_INVALIDCHANNEL = "SEInvalidChannelError"; +this.ERROR_INVALIDAPPLICATION = "SEInvalidApplicationError"; +this.ERROR_GENERIC = "SEGenericError"; +this.ERROR_NOTPRESENT = "SENotPresentError"; +this.ERROR_ILLEGALPARAMETER = "SEIllegalParameterError"; + +this.TYPE_UICC = "uicc"; +this.TYPE_ESE = "eSE"; + +// Allow this file to be imported via Components.utils.import(). +this.EXPORTED_SYMBOLS = Object.keys(this); |