mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-01 06:35:42 +00:00
515 lines
16 KiB
JavaScript
515 lines
16 KiB
JavaScript
/* 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]);
|