summaryrefslogtreecommitdiffstats
path: root/dom/secureelement/DOMSecureElement.js
diff options
context:
space:
mode:
Diffstat (limited to 'dom/secureelement/DOMSecureElement.js')
-rw-r--r--dom/secureelement/DOMSecureElement.js612
1 files changed, 612 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
+]);