diff options
Diffstat (limited to 'dom/secureelement/gonk/SecureElement.js')
-rw-r--r-- | dom/secureelement/gonk/SecureElement.js | 514 |
1 files changed, 514 insertions, 0 deletions
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]); |