diff options
Diffstat (limited to 'dom/secureelement/DOMSecureElement.js')
-rw-r--r-- | dom/secureelement/DOMSecureElement.js | 612 |
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 +]); |