/* 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]);