diff options
Diffstat (limited to 'dom/secureelement')
20 files changed, 2848 insertions, 0 deletions
diff --git a/dom/secureelement/DOMSecureElement.js b/dom/secureelement/DOMSecureElement.js new file mode 100644 index 000000000..48f7053b5 --- /dev/null +++ b/dom/secureelement/DOMSecureElement.js @@ -0,0 +1,612 @@ +/* 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 © 2014, Deutsche Telekom, Inc. */ + +"use strict"; + +/* globals dump, Components, XPCOMUtils, DOMRequestIpcHelper, cpmm, SE */ + +const DEBUG = false; +function debug(s) { + if (DEBUG) { + dump("-*- SecureElement DOM: " + s + "\n"); + } +} + +const Ci = Components.interfaces; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/DOMRequestHelper.jsm"); + +XPCOMUtils.defineLazyServiceGetter(this, "cpmm", + "@mozilla.org/childprocessmessagemanager;1", + "nsISyncMessageSender"); + +XPCOMUtils.defineLazyGetter(this, "SE", function() { + let obj = {}; + Cu.import("resource://gre/modules/se_consts.js", obj); + return obj; +}); + +// Extend / Inherit from Error object +function SEError(name, message) { + this.name = name || SE.ERROR_GENERIC; + this.message = message || ""; +} + +SEError.prototype = { + __proto__: Error.prototype, +}; + +function PromiseHelpersSubclass(win) { + this._window = win; +} + +PromiseHelpersSubclass.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + _window: null, + + _context: [], + + createSEPromise: function createSEPromise(callback, /* optional */ ctx) { + let ctxCallback = (resolverId) => { + if (ctx) { + this._context[resolverId] = ctx; + } + + callback(resolverId); + }; + + return this.createPromiseWithId((aResolverId) => { + ctxCallback(aResolverId); + }); + }, + + takePromise: function takePromise(resolverId) { + let resolver = this.takePromiseResolver(resolverId); + if (!resolver) { + return; + } + + // Get the context associated with this resolverId + let context = this._context[resolverId]; + delete this._context[resolverId]; + + return {resolver: resolver, context: context}; + }, + + rejectWithSEError: function rejectWithSEError(name, message) { + let error = new SEError(name, message); + debug("rejectWithSEError - " + error.toString()); + + return this._window.Promise.reject(Cu.cloneInto(error, this._window)); + } +}; + +// Helper wrapper class to do promises related chores +var PromiseHelpers; + +/** + * Instance of 'SEReaderImpl' class is the connector to a secure element. + * A reader may or may not have a secure element present, since some + * secure elements are removable in nature (eg:- 'uicc'). These + * Readers can be physical devices or virtual devices. + */ +function SEReaderImpl() {} + +SEReaderImpl.prototype = { + _window: null, + + _sessions: [], + + type: null, + _isSEPresent: false, + + classID: Components.ID("{1c7bdba3-cd35-4f8b-a546-55b3232457d5}"), + contractID: "@mozilla.org/secureelement/reader;1", + QueryInterface: XPCOMUtils.generateQI([]), + + // Chrome-only function + onSessionClose: function onSessionClose(sessionCtx) { + let index = this._sessions.indexOf(sessionCtx); + if (index != -1) { + this._sessions.splice(index, 1); + } + }, + + initialize: function initialize(win, type, isPresent) { + this._window = win; + this.type = type; + this._isSEPresent = isPresent; + }, + + _checkPresence: function _checkPresence() { + if (!this._isSEPresent) { + throw new Error(SE.ERROR_NOTPRESENT); + } + }, + + openSession: function openSession() { + this._checkPresence(); + + return PromiseHelpers.createSEPromise((resolverId) => { + let sessionImpl = new SESessionImpl(); + sessionImpl.initialize(this._window, this); + this._window.SESession._create(this._window, sessionImpl); + this._sessions.push(sessionImpl); + PromiseHelpers.takePromiseResolver(resolverId) + .resolve(sessionImpl.__DOM_IMPL__); + }); + }, + + closeAll: function closeAll() { + this._checkPresence(); + + return PromiseHelpers.createSEPromise((resolverId) => { + let promises = []; + for (let session of this._sessions) { + if (!session.isClosed) { + promises.push(session.closeAll()); + } + } + + let resolver = PromiseHelpers.takePromiseResolver(resolverId); + // Wait till all the promises are resolved + Promise.all(promises).then(() => { + this._sessions = []; + resolver.resolve(); + }, (reason) => { + let error = new SEError(SE.ERROR_BADSTATE, + "Unable to close all channels associated with this reader"); + resolver.reject(Cu.cloneInto(error, this._window)); + }); + }); + }, + + updateSEPresence: function updateSEPresence(isSEPresent) { + if (!isSEPresent) { + this.invalidate(); + return; + } + + this._isSEPresent = isSEPresent; + }, + + invalidate: function invalidate() { + debug("Invalidating SE reader: " + this.type); + this._isSEPresent = false; + this._sessions.forEach(s => s.invalidate()); + this._sessions = []; + }, + + get isSEPresent() { + return this._isSEPresent; + } +}; + +/** + * Instance of 'SESessionImpl' object represent a connection session + * to one of the secure elements available on the device. + * These objects can be used to get a communication channel with an application + * hosted by the Secure Element. + */ +function SESessionImpl() {} + +SESessionImpl.prototype = { + _window: null, + + _channels: [], + + _isClosed: false, + + _reader: null, + + classID: Components.ID("{2b1809f8-17bd-4947-abd7-bdef1498561c}"), + contractID: "@mozilla.org/secureelement/session;1", + QueryInterface: XPCOMUtils.generateQI([]), + + // Chrome-only function + onChannelOpen: function onChannelOpen(channelCtx) { + this._channels.push(channelCtx); + }, + + // Chrome-only function + onChannelClose: function onChannelClose(channelCtx) { + let index = this._channels.indexOf(channelCtx); + if (index != -1) { + this._channels.splice(index, 1); + } + }, + + initialize: function initialize(win, readerCtx) { + this._window = win; + this._reader = readerCtx; + }, + + openLogicalChannel: function openLogicalChannel(aid) { + if (this._isClosed) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE, + "Session Already Closed!"); + } + + let aidLen = aid ? aid.length : 0; + if (aidLen < SE.MIN_AID_LEN || aidLen > SE.MAX_AID_LEN) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_ILLEGALPARAMETER, + "Invalid AID length - " + aidLen); + } + + return PromiseHelpers.createSEPromise((resolverId) => { + /** + * @params for 'SE:OpenChannel' + * + * resolverId : ID that identifies this IPC request. + * aid : AID that identifies the applet on SecureElement + * type : Reader type ('uicc' / 'eSE') + * appId : Current appId obtained from 'Principal' obj + */ + cpmm.sendAsyncMessage("SE:OpenChannel", { + resolverId: resolverId, + aid: aid, + type: this.reader.type, + appId: this._window.document.nodePrincipal.appId + }); + }, this); + }, + + closeAll: function closeAll() { + if (this._isClosed) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE, + "Session Already Closed!"); + } + + return PromiseHelpers.createSEPromise((resolverId) => { + let promises = []; + for (let channel of this._channels) { + if (!channel.isClosed) { + promises.push(channel.close()); + } + } + + let resolver = PromiseHelpers.takePromiseResolver(resolverId); + Promise.all(promises).then(() => { + this._isClosed = true; + this._channels = []; + // Notify parent of this session instance's closure, so that its + // instance entry can be removed from the parent as well. + this._reader.onSessionClose(this.__DOM_IMPL__); + resolver.resolve(); + }, (reason) => { + resolver.reject(new Error(SE.ERROR_BADSTATE + + "Unable to close all channels associated with this session")); + }); + }); + }, + + invalidate: function invlidate() { + this._isClosed = true; + this._channels.forEach(ch => ch.invalidate()); + this._channels = []; + }, + + get reader() { + return this._reader.__DOM_IMPL__; + }, + + get isClosed() { + return this._isClosed; + }, +}; + +/** + * Instance of 'SEChannelImpl' object represent an ISO/IEC 7816-4 specification + * channel opened to a secure element. It can be either a logical channel + * or basic channel. + */ +function SEChannelImpl() {} + +SEChannelImpl.prototype = { + _window: null, + + _channelToken: null, + + _isClosed: false, + + _session: null, + + openResponse: [], + + type: null, + + classID: Components.ID("{181ebcf4-5164-4e28-99f2-877ec6fa83b9}"), + contractID: "@mozilla.org/secureelement/channel;1", + QueryInterface: XPCOMUtils.generateQI([]), + + // Chrome-only function + onClose: function onClose() { + this._isClosed = true; + // Notify the parent + this._session.onChannelClose(this.__DOM_IMPL__); + }, + + initialize: function initialize(win, channelToken, isBasicChannel, + openResponse, sessionCtx) { + this._window = win; + // Update the 'channel token' that identifies and represents this + // instance of the object + this._channelToken = channelToken; + // Update 'session' obj + this._session = sessionCtx; + this.openResponse = Cu.cloneInto(new Uint8Array(openResponse), win); + this.type = isBasicChannel ? "basic" : "logical"; + }, + + transmit: function transmit(command) { + // TODO remove this once it will be possible to have a non-optional dict + // in the WebIDL + if (!command) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_ILLEGALPARAMETER, + "SECommand dict must be defined"); + } + + if (this._isClosed) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE, + "Channel Already Closed!"); + } + + let dataLen = command.data ? command.data.length : 0; + if (dataLen > SE.MAX_APDU_LEN) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_ILLEGALPARAMETER, + " Command data length exceeds max limit - 255. " + + " Extended APDU is not supported!"); + } + + if ((command.cla & 0x80 === 0) && ((command.cla & 0x60) !== 0x20)) { + if (command.ins === SE.INS_MANAGE_CHANNEL) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_SECURITY, + "MANAGE CHANNEL command not permitted"); + } + if ((command.ins === SE.INS_SELECT) && (command.p1 == 0x04)) { + // SELECT by DF Name (p1=04) is not allowed + return PromiseHelpers.rejectWithSEError(SE.ERROR_SECURITY, + "SELECT command not permitted"); + } + debug("Attempting to transmit an ISO command"); + } else { + debug("Attempting to transmit GlobalPlatform command"); + } + + return PromiseHelpers.createSEPromise((resolverId) => { + /** + * @params for 'SE:TransmitAPDU' + * + * resolverId : Id that identifies this IPC request. + * apdu : Object containing APDU data + * channelToken: Token that identifies the current channel over which + 'c-apdu' is being sent. + * appId : Current appId obtained from 'Principal' obj + */ + cpmm.sendAsyncMessage("SE:TransmitAPDU", { + resolverId: resolverId, + apdu: command, + channelToken: this._channelToken, + appId: this._window.document.nodePrincipal.appId + }); + }, this); + }, + + close: function close() { + if (this._isClosed) { + return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE, + "Channel Already Closed!"); + } + + return PromiseHelpers.createSEPromise((resolverId) => { + /** + * @params for 'SE:CloseChannel' + * + * resolverId : Id that identifies this IPC request. + * channelToken: Token that identifies the current channel over which + 'c-apdu' is being sent. + * appId : Current appId obtained from 'Principal' obj + */ + cpmm.sendAsyncMessage("SE:CloseChannel", { + resolverId: resolverId, + channelToken: this._channelToken, + appId: this._window.document.nodePrincipal.appId + }); + }, this); + }, + + invalidate: function invalidate() { + this._isClosed = true; + }, + + get session() { + return this._session.__DOM_IMPL__; + }, + + get isClosed() { + return this._isClosed; + }, +}; + +function SEResponseImpl() {} + +SEResponseImpl.prototype = { + sw1: 0x00, + + sw2: 0x00, + + data: null, + + _channel: null, + + classID: Components.ID("{58bc6c7b-686c-47cc-8867-578a6ed23f4e}"), + contractID: "@mozilla.org/secureelement/response;1", + QueryInterface: XPCOMUtils.generateQI([]), + + initialize: function initialize(sw1, sw2, response, channelCtx) { + // Update the status bytes + this.sw1 = sw1; + this.sw2 = sw2; + this.data = response ? response.slice(0) : null; + // Update the channel obj + this._channel = channelCtx; + }, + + get channel() { + return this._channel.__DOM_IMPL__; + } +}; + +/** + * SEManagerImpl + */ +function SEManagerImpl() {} + +SEManagerImpl.prototype = { + __proto__: DOMRequestIpcHelper.prototype, + + _window: null, + + classID: Components.ID("{4a8b6ec0-4674-11e4-916c-0800200c9a66}"), + contractID: "@mozilla.org/secureelement/manager;1", + QueryInterface: XPCOMUtils.generateQI([ + Ci.nsIDOMGlobalPropertyInitializer, + Ci.nsISupportsWeakReference, + Ci.nsIObserver + ]), + + _readers: [], + + init: function init(win) { + this._window = win; + PromiseHelpers = new PromiseHelpersSubclass(this._window); + + // Add the messages to be listened to. + const messages = ["SE:GetSEReadersResolved", + "SE:OpenChannelResolved", + "SE:CloseChannelResolved", + "SE:TransmitAPDUResolved", + "SE:GetSEReadersRejected", + "SE:OpenChannelRejected", + "SE:CloseChannelRejected", + "SE:TransmitAPDURejected", + "SE:ReaderPresenceChanged"]; + + this.initDOMRequestHelper(win, messages); + }, + + // This function will be called from DOMRequestIPCHelper. + uninit: function uninit() { + // All requests that are still pending need to be invalidated + // because the context is no longer valid. + this.forEachPromiseResolver((k) => { + this.takePromiseResolver(k).reject("Window Context got destroyed!"); + }); + PromiseHelpers = null; + this._window = null; + }, + + getSEReaders: function getSEReaders() { + // invalidate previous readers on new request + if (this._readers.length) { + this._readers.forEach(r => r.invalidate()); + this._readers = []; + } + + return PromiseHelpers.createSEPromise((resolverId) => { + cpmm.sendAsyncMessage("SE:GetSEReaders", { + resolverId: resolverId, + appId: this._window.document.nodePrincipal.appId + }); + }); + }, + + receiveMessage: function receiveMessage(message) { + DEBUG && debug("Message received: " + JSON.stringify(message)); + + let result = message.data.result; + let resolver = null; + let context = null; + + let promiseResolver = PromiseHelpers.takePromise(result.resolverId); + if (promiseResolver) { + resolver = promiseResolver.resolver; + // This 'context' is the instance that originated this IPC message. + context = promiseResolver.context; + } + + switch (message.name) { + case "SE:GetSEReadersResolved": + let readers = new this._window.Array(); + result.readers.forEach(reader => { + let readerImpl = new SEReaderImpl(); + readerImpl.initialize(this._window, reader.type, reader.isPresent); + this._window.SEReader._create(this._window, readerImpl); + + this._readers.push(readerImpl); + readers.push(readerImpl.__DOM_IMPL__); + }); + resolver.resolve(readers); + break; + case "SE:OpenChannelResolved": + let channelImpl = new SEChannelImpl(); + channelImpl.initialize(this._window, + result.channelToken, + result.isBasicChannel, + result.openResponse, + context); + this._window.SEChannel._create(this._window, channelImpl); + if (context) { + // Notify context's handler with SEChannel instance + context.onChannelOpen(channelImpl); + } + resolver.resolve(channelImpl.__DOM_IMPL__); + break; + case "SE:TransmitAPDUResolved": + let responseImpl = new SEResponseImpl(); + responseImpl.initialize(result.sw1, + result.sw2, + result.response, + context); + this._window.SEResponse._create(this._window, responseImpl); + resolver.resolve(responseImpl.__DOM_IMPL__); + break; + case "SE:CloseChannelResolved": + if (context) { + // Notify context's onClose handler + context.onClose(); + } + resolver.resolve(); + break; + case "SE:GetSEReadersRejected": + case "SE:OpenChannelRejected": + case "SE:CloseChannelRejected": + case "SE:TransmitAPDURejected": + let error = new SEError(result.error, result.reason); + resolver.reject(Cu.cloneInto(error, this._window)); + break; + case "SE:ReaderPresenceChanged": + debug("Reader " + result.type + " present: " + result.isPresent); + let reader = this._readers.find(r => r.type === result.type); + if (reader) { + reader.updateSEPresence(result.isPresent); + } + break; + default: + debug("Could not find a handler for " + message.name); + resolver.reject(Cu.cloneInto(new SEError(), this._window)); + break; + } + } +}; + +this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ + SEResponseImpl, SEChannelImpl, SESessionImpl, SEReaderImpl, SEManagerImpl +]); diff --git a/dom/secureelement/DOMSecureElement.manifest b/dom/secureelement/DOMSecureElement.manifest new file mode 100644 index 000000000..858227496 --- /dev/null +++ b/dom/secureelement/DOMSecureElement.manifest @@ -0,0 +1,14 @@ +component {4a8b6ec0-4674-11e4-916c-0800200c9a66} DOMSecureElement.js +contract @mozilla.org/secureelement/manager;1 {4a8b6ec0-4674-11e4-916c-0800200c9a66} + +component {1c7bdba3-cd35-4f8b-a546-55b3232457d5} DOMSecureElement.js +contract @mozilla.org/secureelement/reader;1 {1c7bdba3-cd35-4f8b-a546-55b3232457d5} + +component {2b1809f8-17bd-4947-abd7-bdef1498561c} DOMSecureElement.js +contract @mozilla.org/secureelement/session;1 {2b1809f8-17bd-4947-abd7-bdef1498561c} + +component {181ebcf4-5164-4e28-99f2-877ec6fa83b9} DOMSecureElement.js +contract @mozilla.org/secureelement/channel;1 {181ebcf4-5164-4e28-99f2-877ec6fa83b9} + +component {58bc6c7b-686c-47cc-8867-578a6ed23f4e} DOMSecureElement.js +contract @mozilla.org/secureelement/response;1 {58bc6c7b-686c-47cc-8867-578a6ed23f4e} diff --git a/dom/secureelement/SEUtils.jsm b/dom/secureelement/SEUtils.jsm new file mode 100644 index 000000000..d5980b19c --- /dev/null +++ b/dom/secureelement/SEUtils.jsm @@ -0,0 +1,116 @@ +/* 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"; + +this.SEUtils = { + byteArrayToHexString: function byteArrayToHexString(array) { + let hexStr = ""; + + let len = array ? array.length : 0; + for (let i = 0; i < len; i++) { + let hex = (array[i] & 0xff).toString(16); + hex = (hex.length === 1) ? "0" + hex : hex; + hexStr += hex; + } + + return hexStr.toUpperCase(); + }, + + hexStringToByteArray: function hexStringToByteArray(hexStr) { + if (typeof hexStr !== "string" || hexStr.length % 2 !== 0) { + return []; + } + + let array = []; + for (let i = 0, len = hexStr.length; i < len; i += 2) { + array.push(parseInt(hexStr.substr(i, 2), 16)); + } + + return array; + }, + + arraysEqual: function arraysEqual(a1, a2) { + if (!a1 || !a2) { + return false; + } + + if (a1.length !== a2.length) { + return false; + } + + for (let i = 0, len = a1.length; i < len; i++) { + if (a1[i] !== a2[i]) { + return false; + } + } + + return true; + }, + + ensureIsArray: function ensureIsArray(obj) { + return Array.isArray(obj) ? obj : [obj]; + }, + + /** + * parseTLV is intended primarily to be used to parse Global Platform Device + * Technology secure element access control data. + * + * The parsed result value is an internal format only. + * + * All tags will be treated as simple Tag Length Values (TLV), (i.e. with a + * plain value, not subject to further unpacking), unless those tags are + * listed in the containerTags array. + * + * @param bytes - byte array + * @param containerTags - byte array of tags + */ + parseTLV: function parseTLV(bytes, containerTags) { + let result = {}; + + if (typeof bytes === "string") { + bytes = this.hexStringToByteArray(bytes); + } + + if (!Array.isArray(bytes)) { + debug("Passed value is not an array nor a string."); + return null; + } + + for (let pos = 0; pos < bytes.length; ) { + let tag = bytes[pos], + length = bytes[pos + 1], + value = bytes.slice(pos + 2, pos + 2 + length), + parsed = null; + + // Support for 0xFF padded files (GPD 7.1.2) + if (tag === 0xFF) { + break; + } + + if (containerTags.indexOf(tag) >= 0) { + parsed = this.parseTLV(value, containerTags); + } else { + parsed = value; + } + + // Internal parsed format. + if (!result[tag]) { + result[tag] = parsed; + } else if (Array.isArray(result[tag])) { + result[tag].push(parsed); + } else { + result[tag] = [result[tag], parsed]; + } + + pos = pos + 2 + length; + } + + return result; + } +}; + +this.EXPORTED_SYMBOLS = ["SEUtils"]; 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); diff --git a/dom/secureelement/moz.build b/dom/secureelement/moz.build new file mode 100644 index 000000000..a2c87b014 --- /dev/null +++ b/dom/secureelement/moz.build @@ -0,0 +1,40 @@ +# vim: set filetype=python: +# 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 © 2014 Deutsche Telekom, Inc. + +if CONFIG['MOZ_SECUREELEMENT']: + EXTRA_COMPONENTS += [ + 'DOMSecureElement.js', + 'DOMSecureElement.manifest', + ] + +if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk' and CONFIG['MOZ_SECUREELEMENT']: + EXTRA_COMPONENTS += [ + 'gonk/ACEService.js', + 'gonk/ACEService.manifest', + 'gonk/GPAccessRulesManager.js', + 'gonk/GPAccessRulesManager.manifest', + 'gonk/SecureElement.js', + 'gonk/SecureElement.manifest', + ] + XPIDL_MODULE = 'dom_secureelement' + XPIDL_SOURCES += [ + 'gonk/nsIAccessControlEnforcer.idl', + 'gonk/nsIAccessRulesManager.idl', + 'gonk/nsISecureElementConnector.idl', + ] + EXTRA_JS_MODULES += [ + 'gonk/gp_consts.js', + 'gonk/se_consts.js', + 'SEUtils.jsm' + ] + XPCSHELL_TESTS_MANIFESTS += [ + 'tests/unit/xpcshell.ini' + ] + +include('/ipc/chromium/chromium-config.mozbuild') + +FINAL_LIBRARY = 'xul' diff --git a/dom/secureelement/tests/unit/header_helper.js b/dom/secureelement/tests/unit/header_helper.js new file mode 100644 index 000000000..ff275e858 --- /dev/null +++ b/dom/secureelement/tests/unit/header_helper.js @@ -0,0 +1,9 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Strips spaces, and returns a byte array. + */ +function formatHexAndCreateByteArray(hexStr) { + return SEUtils.hexStringToByteArray(hexStr.replace(/\s+/g, "")); +} diff --git a/dom/secureelement/tests/unit/test_SEUtils.js b/dom/secureelement/tests/unit/test_SEUtils.js new file mode 100644 index 000000000..c06eca0e1 --- /dev/null +++ b/dom/secureelement/tests/unit/test_SEUtils.js @@ -0,0 +1,228 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* globals run_next_test, add_test, ok, Components, SEUtils */ +/* exported run_test */ + +Components.utils.import("resource://gre/modules/SEUtils.jsm"); +var GP = {}; +Components.utils.import("resource://gre/modules/gp_consts.js", GP); + +const VALID_HEX_STR = "0123456789ABCDEF"; +const VALID_BYTE_ARR = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF]; + +// This set should be what the actual ACE uses. +var containerTags = [ + GP.TAG_SEQUENCE, + GP.TAG_FCP, + GP.TAG_GPD_AID, + GP.TAG_EXTERNALDO, + GP.TAG_INDIRECT, + GP.TAG_EF_ODF +]; + +function run_test() { + ok(!!SEUtils, "SEUtils should be available"); + run_next_test(); +} + +add_test(function test_byteArrayToHexString() { + let hexStr = SEUtils.byteArrayToHexString(VALID_BYTE_ARR); + ok(hexStr === VALID_HEX_STR, + "should convert byte Array to uppercased hex string"); + + [[], null, undefined].forEach((input) => { + hexStr = SEUtils.byteArrayToHexString(input); + ok(hexStr === "", "invalid arg:" + input + " should return empty string"); + }); + + run_next_test(); +}); + +add_test(function test_hexStringToByteArray() { + let byteArr = SEUtils.hexStringToByteArray(VALID_HEX_STR); + ok(SEUtils.arraysEqual(byteArr, VALID_BYTE_ARR), + "should convert uppercased string to byte Array"); + + byteArr = SEUtils.hexStringToByteArray(VALID_HEX_STR.toLowerCase()); + ok(SEUtils.arraysEqual(byteArr, VALID_BYTE_ARR), + "should convert lowercased string to byte Array"); + + ["", null, undefined, "123"].forEach((input) => { + byteArr = SEUtils.hexStringToByteArray(input); + ok(Array.isArray(byteArr) && byteArr.length === 0, + "invalid arg: " + input + " should be empty Array"); + }); + + run_next_test(); +}); + +add_test(function test_arraysEqual() { + ok(SEUtils.arraysEqual([1, 2, 3], [1, 2, 3]), + "should return true on equal Arrays"); + + [[1], [1, 2, 4], [3, 2, 1]].forEach((input) => { + ok(!SEUtils.arraysEqual([1, 2, 3], input), + "should return false when Arrays not equal"); + }); + + [null, undefined].forEach((input) => { + ok(!SEUtils.arraysEqual([1, 2, 3], input), + "should return false when comparing Array with invalid argument"); + + ok(!SEUtils.arraysEqual(input, input), + "should return false when both args are invalid"); + }); + + run_next_test(); +}); + +add_test(function test_ensureIsArray() { + let obj = {a: "a"}; + let targetArray = [obj]; + let result = null; + + result = SEUtils.ensureIsArray(obj); + ok(targetArray[0].a === result[0].a, + "should return true if array element contains the same value"); + deepEqual(result, targetArray, + "result should be deeply equal to targetArray"); + + result = SEUtils.ensureIsArray(targetArray); + deepEqual(result, targetArray, + "ensureIsAray with an array should return same array value."); + + run_next_test(); +}); + +add_test(function test_parseTLV_empty() { + let containerTags = []; + let result = null; + + // Base: + result = SEUtils.parseTLV([], []); + deepEqual({}, result, + "empty parse input should result in an " + + "empty object (internal SEUtils format only)."); + run_next_test(); +}); + +add_test(function test_parseTLV_selectResponse() { + let result = null; + let hexStr = "62 27 82 02 78 21 83 02 7F 50 A5 06 83 04 00 04 C1 DC 8A" + + "01 05 8B 06 2F 06 01 16 00 14 C6 06 90 01 00 83 01 01 81" + + "02 FF FF"; + + let expected = { + 0x62: { + 0x82: [0x78, 0x21], + 0x83: [0x7F, 0x50], + 0xA5: { + 0x83: [0x00, 0x04, 0xC1, 0xDC] + }, + 0x8A: [0x05], + 0x8B: [0x2F, 0x06, 0x01, 0x16, 0x00, 0x14], + 0xC6: [0x90, 0x01, 0x00, 0x83, 0x01, 0x01], + 0x81: [0xFF, 0xFF] + } + }; + + result = SEUtils.parseTLV(formatHexAndCreateByteArray(hexStr), containerTags); + deepEqual(result, expected, + "parsed real selectResponse should equal the expected rules"); + run_next_test(); +}); + +add_test(function test_parseTLV_DODF() { + let result = null; + let hexStr = "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 30" + + "04 04 02 43 00 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 30 04 04 02 45 31 FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF"; + + let expected = { + 0xA1: [ + { + 0x30: [ + {}, + { + 0x0C: [0x47, 0x50, 0x20, 0x53, 0x45, 0x20, 0x41, 0x63, 0x63, + 0x20, 0x43, 0x74, 0x6C] + } + ], + 0xA1: { + 0x30: { + 0x06: [0x2A, 0x86, 0x48, 0x86, 0xFC, 0x6B, 0x81, 0x48, 0x01, + 0x01], + 0x30: { + 0x04: [0x43, 0x00] + } + } + } + }, + { + 0x30: [ + {}, + { + 0x0C: [0x53, 0x41, 0x54, 0x53, 0x41, 0x20, 0x47, 0x54, 0x4F, + 0x20, 0x31, 0x2E, 0x31] + } + ], + 0xA1: { + 0x30: { + 0x06: [0x2B, 0x06, 0x01, 0x04, 0x01, 0x2A, 0x02, 0x6E, 0x03, + 0x01, 0x01, 0x01], + 0x30: { + 0x04: [0x45, 0x31] + } + } + } + } + ] + }; + + result = SEUtils.parseTLV(formatHexAndCreateByteArray(hexStr), containerTags); + deepEqual(result, expected, + "Real Access Control Enforcer DODF file, with 0xFF padding. " + + "Should equal expected rules."); + run_next_test(); +}); + +add_test(function test_parseTLV_acRules() { + let result = null; + let hexStr = "30 08 82 00 30 04 04 02 43 11 FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF FF" + + "FF FF FF FF FF FF FF FF FF"; + + let expected = { + 0x30: { + 0x82: [], + 0x30: { + 0x04: [0x43, 0x11] + } + } + }; + + result = SEUtils.parseTLV(formatHexAndCreateByteArray(hexStr), containerTags); + deepEqual(result, expected, + "Parsed Access Control Rules should equal the expected rules"); + run_next_test(); +}); diff --git a/dom/secureelement/tests/unit/xpcshell.ini b/dom/secureelement/tests/unit/xpcshell.ini new file mode 100644 index 000000000..924fd581f --- /dev/null +++ b/dom/secureelement/tests/unit/xpcshell.ini @@ -0,0 +1,5 @@ +[DEFAULT] +head = header_helper.js +tail = + +[test_SEUtils.js]
\ No newline at end of file |