DOMSecureElement.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613
  1. /* This Source Code Form is subject to the terms of the Mozilla Public
  2. * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  3. * You can obtain one at http://mozilla.org/MPL/2.0/. */
  4. /* Copyright © 2014, Deutsche Telekom, Inc. */
  5. "use strict";
  6. /* globals dump, Components, XPCOMUtils, DOMRequestIpcHelper, cpmm, SE */
  7. const DEBUG = false;
  8. function debug(s) {
  9. if (DEBUG) {
  10. dump("-*- SecureElement DOM: " + s + "\n");
  11. }
  12. }
  13. const Ci = Components.interfaces;
  14. const Cu = Components.utils;
  15. Cu.import("resource://gre/modules/XPCOMUtils.jsm");
  16. Cu.import("resource://gre/modules/Services.jsm");
  17. Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
  18. XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
  19. "@mozilla.org/childprocessmessagemanager;1",
  20. "nsISyncMessageSender");
  21. XPCOMUtils.defineLazyGetter(this, "SE", function() {
  22. let obj = {};
  23. Cu.import("resource://gre/modules/se_consts.js", obj);
  24. return obj;
  25. });
  26. // Extend / Inherit from Error object
  27. function SEError(name, message) {
  28. this.name = name || SE.ERROR_GENERIC;
  29. this.message = message || "";
  30. }
  31. SEError.prototype = {
  32. __proto__: Error.prototype,
  33. };
  34. function PromiseHelpersSubclass(win) {
  35. this._window = win;
  36. }
  37. PromiseHelpersSubclass.prototype = {
  38. __proto__: DOMRequestIpcHelper.prototype,
  39. _window: null,
  40. _context: [],
  41. createSEPromise: function createSEPromise(callback, /* optional */ ctx) {
  42. let ctxCallback = (resolverId) => {
  43. if (ctx) {
  44. this._context[resolverId] = ctx;
  45. }
  46. callback(resolverId);
  47. };
  48. return this.createPromiseWithId((aResolverId) => {
  49. ctxCallback(aResolverId);
  50. });
  51. },
  52. takePromise: function takePromise(resolverId) {
  53. let resolver = this.takePromiseResolver(resolverId);
  54. if (!resolver) {
  55. return;
  56. }
  57. // Get the context associated with this resolverId
  58. let context = this._context[resolverId];
  59. delete this._context[resolverId];
  60. return {resolver: resolver, context: context};
  61. },
  62. rejectWithSEError: function rejectWithSEError(name, message) {
  63. let error = new SEError(name, message);
  64. debug("rejectWithSEError - " + error.toString());
  65. return this._window.Promise.reject(Cu.cloneInto(error, this._window));
  66. }
  67. };
  68. // Helper wrapper class to do promises related chores
  69. var PromiseHelpers;
  70. /**
  71. * Instance of 'SEReaderImpl' class is the connector to a secure element.
  72. * A reader may or may not have a secure element present, since some
  73. * secure elements are removable in nature (eg:- 'uicc'). These
  74. * Readers can be physical devices or virtual devices.
  75. */
  76. function SEReaderImpl() {}
  77. SEReaderImpl.prototype = {
  78. _window: null,
  79. _sessions: [],
  80. type: null,
  81. _isSEPresent: false,
  82. classID: Components.ID("{1c7bdba3-cd35-4f8b-a546-55b3232457d5}"),
  83. contractID: "@mozilla.org/secureelement/reader;1",
  84. QueryInterface: XPCOMUtils.generateQI([]),
  85. // Chrome-only function
  86. onSessionClose: function onSessionClose(sessionCtx) {
  87. let index = this._sessions.indexOf(sessionCtx);
  88. if (index != -1) {
  89. this._sessions.splice(index, 1);
  90. }
  91. },
  92. initialize: function initialize(win, type, isPresent) {
  93. this._window = win;
  94. this.type = type;
  95. this._isSEPresent = isPresent;
  96. },
  97. _checkPresence: function _checkPresence() {
  98. if (!this._isSEPresent) {
  99. throw new Error(SE.ERROR_NOTPRESENT);
  100. }
  101. },
  102. openSession: function openSession() {
  103. this._checkPresence();
  104. return PromiseHelpers.createSEPromise((resolverId) => {
  105. let sessionImpl = new SESessionImpl();
  106. sessionImpl.initialize(this._window, this);
  107. this._window.SESession._create(this._window, sessionImpl);
  108. this._sessions.push(sessionImpl);
  109. PromiseHelpers.takePromiseResolver(resolverId)
  110. .resolve(sessionImpl.__DOM_IMPL__);
  111. });
  112. },
  113. closeAll: function closeAll() {
  114. this._checkPresence();
  115. return PromiseHelpers.createSEPromise((resolverId) => {
  116. let promises = [];
  117. for (let session of this._sessions) {
  118. if (!session.isClosed) {
  119. promises.push(session.closeAll());
  120. }
  121. }
  122. let resolver = PromiseHelpers.takePromiseResolver(resolverId);
  123. // Wait till all the promises are resolved
  124. Promise.all(promises).then(() => {
  125. this._sessions = [];
  126. resolver.resolve();
  127. }, (reason) => {
  128. let error = new SEError(SE.ERROR_BADSTATE,
  129. "Unable to close all channels associated with this reader");
  130. resolver.reject(Cu.cloneInto(error, this._window));
  131. });
  132. });
  133. },
  134. updateSEPresence: function updateSEPresence(isSEPresent) {
  135. if (!isSEPresent) {
  136. this.invalidate();
  137. return;
  138. }
  139. this._isSEPresent = isSEPresent;
  140. },
  141. invalidate: function invalidate() {
  142. debug("Invalidating SE reader: " + this.type);
  143. this._isSEPresent = false;
  144. this._sessions.forEach(s => s.invalidate());
  145. this._sessions = [];
  146. },
  147. get isSEPresent() {
  148. return this._isSEPresent;
  149. }
  150. };
  151. /**
  152. * Instance of 'SESessionImpl' object represent a connection session
  153. * to one of the secure elements available on the device.
  154. * These objects can be used to get a communication channel with an application
  155. * hosted by the Secure Element.
  156. */
  157. function SESessionImpl() {}
  158. SESessionImpl.prototype = {
  159. _window: null,
  160. _channels: [],
  161. _isClosed: false,
  162. _reader: null,
  163. classID: Components.ID("{2b1809f8-17bd-4947-abd7-bdef1498561c}"),
  164. contractID: "@mozilla.org/secureelement/session;1",
  165. QueryInterface: XPCOMUtils.generateQI([]),
  166. // Chrome-only function
  167. onChannelOpen: function onChannelOpen(channelCtx) {
  168. this._channels.push(channelCtx);
  169. },
  170. // Chrome-only function
  171. onChannelClose: function onChannelClose(channelCtx) {
  172. let index = this._channels.indexOf(channelCtx);
  173. if (index != -1) {
  174. this._channels.splice(index, 1);
  175. }
  176. },
  177. initialize: function initialize(win, readerCtx) {
  178. this._window = win;
  179. this._reader = readerCtx;
  180. },
  181. openLogicalChannel: function openLogicalChannel(aid) {
  182. if (this._isClosed) {
  183. return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE,
  184. "Session Already Closed!");
  185. }
  186. let aidLen = aid ? aid.length : 0;
  187. if (aidLen < SE.MIN_AID_LEN || aidLen > SE.MAX_AID_LEN) {
  188. return PromiseHelpers.rejectWithSEError(SE.ERROR_ILLEGALPARAMETER,
  189. "Invalid AID length - " + aidLen);
  190. }
  191. return PromiseHelpers.createSEPromise((resolverId) => {
  192. /**
  193. * @params for 'SE:OpenChannel'
  194. *
  195. * resolverId : ID that identifies this IPC request.
  196. * aid : AID that identifies the applet on SecureElement
  197. * type : Reader type ('uicc' / 'eSE')
  198. * appId : Current appId obtained from 'Principal' obj
  199. */
  200. cpmm.sendAsyncMessage("SE:OpenChannel", {
  201. resolverId: resolverId,
  202. aid: aid,
  203. type: this.reader.type,
  204. appId: this._window.document.nodePrincipal.appId
  205. });
  206. }, this);
  207. },
  208. closeAll: function closeAll() {
  209. if (this._isClosed) {
  210. return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE,
  211. "Session Already Closed!");
  212. }
  213. return PromiseHelpers.createSEPromise((resolverId) => {
  214. let promises = [];
  215. for (let channel of this._channels) {
  216. if (!channel.isClosed) {
  217. promises.push(channel.close());
  218. }
  219. }
  220. let resolver = PromiseHelpers.takePromiseResolver(resolverId);
  221. Promise.all(promises).then(() => {
  222. this._isClosed = true;
  223. this._channels = [];
  224. // Notify parent of this session instance's closure, so that its
  225. // instance entry can be removed from the parent as well.
  226. this._reader.onSessionClose(this.__DOM_IMPL__);
  227. resolver.resolve();
  228. }, (reason) => {
  229. resolver.reject(new Error(SE.ERROR_BADSTATE +
  230. "Unable to close all channels associated with this session"));
  231. });
  232. });
  233. },
  234. invalidate: function invlidate() {
  235. this._isClosed = true;
  236. this._channels.forEach(ch => ch.invalidate());
  237. this._channels = [];
  238. },
  239. get reader() {
  240. return this._reader.__DOM_IMPL__;
  241. },
  242. get isClosed() {
  243. return this._isClosed;
  244. },
  245. };
  246. /**
  247. * Instance of 'SEChannelImpl' object represent an ISO/IEC 7816-4 specification
  248. * channel opened to a secure element. It can be either a logical channel
  249. * or basic channel.
  250. */
  251. function SEChannelImpl() {}
  252. SEChannelImpl.prototype = {
  253. _window: null,
  254. _channelToken: null,
  255. _isClosed: false,
  256. _session: null,
  257. openResponse: [],
  258. type: null,
  259. classID: Components.ID("{181ebcf4-5164-4e28-99f2-877ec6fa83b9}"),
  260. contractID: "@mozilla.org/secureelement/channel;1",
  261. QueryInterface: XPCOMUtils.generateQI([]),
  262. // Chrome-only function
  263. onClose: function onClose() {
  264. this._isClosed = true;
  265. // Notify the parent
  266. this._session.onChannelClose(this.__DOM_IMPL__);
  267. },
  268. initialize: function initialize(win, channelToken, isBasicChannel,
  269. openResponse, sessionCtx) {
  270. this._window = win;
  271. // Update the 'channel token' that identifies and represents this
  272. // instance of the object
  273. this._channelToken = channelToken;
  274. // Update 'session' obj
  275. this._session = sessionCtx;
  276. this.openResponse = Cu.cloneInto(new Uint8Array(openResponse), win);
  277. this.type = isBasicChannel ? "basic" : "logical";
  278. },
  279. transmit: function transmit(command) {
  280. // TODO remove this once it will be possible to have a non-optional dict
  281. // in the WebIDL
  282. if (!command) {
  283. return PromiseHelpers.rejectWithSEError(SE.ERROR_ILLEGALPARAMETER,
  284. "SECommand dict must be defined");
  285. }
  286. if (this._isClosed) {
  287. return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE,
  288. "Channel Already Closed!");
  289. }
  290. let dataLen = command.data ? command.data.length : 0;
  291. if (dataLen > SE.MAX_APDU_LEN) {
  292. return PromiseHelpers.rejectWithSEError(SE.ERROR_ILLEGALPARAMETER,
  293. " Command data length exceeds max limit - 255. " +
  294. " Extended APDU is not supported!");
  295. }
  296. if ((command.cla & 0x80 === 0) && ((command.cla & 0x60) !== 0x20)) {
  297. if (command.ins === SE.INS_MANAGE_CHANNEL) {
  298. return PromiseHelpers.rejectWithSEError(SE.ERROR_SECURITY,
  299. "MANAGE CHANNEL command not permitted");
  300. }
  301. if ((command.ins === SE.INS_SELECT) && (command.p1 == 0x04)) {
  302. // SELECT by DF Name (p1=04) is not allowed
  303. return PromiseHelpers.rejectWithSEError(SE.ERROR_SECURITY,
  304. "SELECT command not permitted");
  305. }
  306. debug("Attempting to transmit an ISO command");
  307. } else {
  308. debug("Attempting to transmit GlobalPlatform command");
  309. }
  310. return PromiseHelpers.createSEPromise((resolverId) => {
  311. /**
  312. * @params for 'SE:TransmitAPDU'
  313. *
  314. * resolverId : Id that identifies this IPC request.
  315. * apdu : Object containing APDU data
  316. * channelToken: Token that identifies the current channel over which
  317. 'c-apdu' is being sent.
  318. * appId : Current appId obtained from 'Principal' obj
  319. */
  320. cpmm.sendAsyncMessage("SE:TransmitAPDU", {
  321. resolverId: resolverId,
  322. apdu: command,
  323. channelToken: this._channelToken,
  324. appId: this._window.document.nodePrincipal.appId
  325. });
  326. }, this);
  327. },
  328. close: function close() {
  329. if (this._isClosed) {
  330. return PromiseHelpers.rejectWithSEError(SE.ERROR_BADSTATE,
  331. "Channel Already Closed!");
  332. }
  333. return PromiseHelpers.createSEPromise((resolverId) => {
  334. /**
  335. * @params for 'SE:CloseChannel'
  336. *
  337. * resolverId : Id that identifies this IPC request.
  338. * channelToken: Token that identifies the current channel over which
  339. 'c-apdu' is being sent.
  340. * appId : Current appId obtained from 'Principal' obj
  341. */
  342. cpmm.sendAsyncMessage("SE:CloseChannel", {
  343. resolverId: resolverId,
  344. channelToken: this._channelToken,
  345. appId: this._window.document.nodePrincipal.appId
  346. });
  347. }, this);
  348. },
  349. invalidate: function invalidate() {
  350. this._isClosed = true;
  351. },
  352. get session() {
  353. return this._session.__DOM_IMPL__;
  354. },
  355. get isClosed() {
  356. return this._isClosed;
  357. },
  358. };
  359. function SEResponseImpl() {}
  360. SEResponseImpl.prototype = {
  361. sw1: 0x00,
  362. sw2: 0x00,
  363. data: null,
  364. _channel: null,
  365. classID: Components.ID("{58bc6c7b-686c-47cc-8867-578a6ed23f4e}"),
  366. contractID: "@mozilla.org/secureelement/response;1",
  367. QueryInterface: XPCOMUtils.generateQI([]),
  368. initialize: function initialize(sw1, sw2, response, channelCtx) {
  369. // Update the status bytes
  370. this.sw1 = sw1;
  371. this.sw2 = sw2;
  372. this.data = response ? response.slice(0) : null;
  373. // Update the channel obj
  374. this._channel = channelCtx;
  375. },
  376. get channel() {
  377. return this._channel.__DOM_IMPL__;
  378. }
  379. };
  380. /**
  381. * SEManagerImpl
  382. */
  383. function SEManagerImpl() {}
  384. SEManagerImpl.prototype = {
  385. __proto__: DOMRequestIpcHelper.prototype,
  386. _window: null,
  387. classID: Components.ID("{4a8b6ec0-4674-11e4-916c-0800200c9a66}"),
  388. contractID: "@mozilla.org/secureelement/manager;1",
  389. QueryInterface: XPCOMUtils.generateQI([
  390. Ci.nsIDOMGlobalPropertyInitializer,
  391. Ci.nsISupportsWeakReference,
  392. Ci.nsIObserver
  393. ]),
  394. _readers: [],
  395. init: function init(win) {
  396. this._window = win;
  397. PromiseHelpers = new PromiseHelpersSubclass(this._window);
  398. // Add the messages to be listened to.
  399. const messages = ["SE:GetSEReadersResolved",
  400. "SE:OpenChannelResolved",
  401. "SE:CloseChannelResolved",
  402. "SE:TransmitAPDUResolved",
  403. "SE:GetSEReadersRejected",
  404. "SE:OpenChannelRejected",
  405. "SE:CloseChannelRejected",
  406. "SE:TransmitAPDURejected",
  407. "SE:ReaderPresenceChanged"];
  408. this.initDOMRequestHelper(win, messages);
  409. },
  410. // This function will be called from DOMRequestIPCHelper.
  411. uninit: function uninit() {
  412. // All requests that are still pending need to be invalidated
  413. // because the context is no longer valid.
  414. this.forEachPromiseResolver((k) => {
  415. this.takePromiseResolver(k).reject("Window Context got destroyed!");
  416. });
  417. PromiseHelpers = null;
  418. this._window = null;
  419. },
  420. getSEReaders: function getSEReaders() {
  421. // invalidate previous readers on new request
  422. if (this._readers.length) {
  423. this._readers.forEach(r => r.invalidate());
  424. this._readers = [];
  425. }
  426. return PromiseHelpers.createSEPromise((resolverId) => {
  427. cpmm.sendAsyncMessage("SE:GetSEReaders", {
  428. resolverId: resolverId,
  429. appId: this._window.document.nodePrincipal.appId
  430. });
  431. });
  432. },
  433. receiveMessage: function receiveMessage(message) {
  434. DEBUG && debug("Message received: " + JSON.stringify(message));
  435. let result = message.data.result;
  436. let resolver = null;
  437. let context = null;
  438. let promiseResolver = PromiseHelpers.takePromise(result.resolverId);
  439. if (promiseResolver) {
  440. resolver = promiseResolver.resolver;
  441. // This 'context' is the instance that originated this IPC message.
  442. context = promiseResolver.context;
  443. }
  444. switch (message.name) {
  445. case "SE:GetSEReadersResolved":
  446. let readers = new this._window.Array();
  447. result.readers.forEach(reader => {
  448. let readerImpl = new SEReaderImpl();
  449. readerImpl.initialize(this._window, reader.type, reader.isPresent);
  450. this._window.SEReader._create(this._window, readerImpl);
  451. this._readers.push(readerImpl);
  452. readers.push(readerImpl.__DOM_IMPL__);
  453. });
  454. resolver.resolve(readers);
  455. break;
  456. case "SE:OpenChannelResolved":
  457. let channelImpl = new SEChannelImpl();
  458. channelImpl.initialize(this._window,
  459. result.channelToken,
  460. result.isBasicChannel,
  461. result.openResponse,
  462. context);
  463. this._window.SEChannel._create(this._window, channelImpl);
  464. if (context) {
  465. // Notify context's handler with SEChannel instance
  466. context.onChannelOpen(channelImpl);
  467. }
  468. resolver.resolve(channelImpl.__DOM_IMPL__);
  469. break;
  470. case "SE:TransmitAPDUResolved":
  471. let responseImpl = new SEResponseImpl();
  472. responseImpl.initialize(result.sw1,
  473. result.sw2,
  474. result.response,
  475. context);
  476. this._window.SEResponse._create(this._window, responseImpl);
  477. resolver.resolve(responseImpl.__DOM_IMPL__);
  478. break;
  479. case "SE:CloseChannelResolved":
  480. if (context) {
  481. // Notify context's onClose handler
  482. context.onClose();
  483. }
  484. resolver.resolve();
  485. break;
  486. case "SE:GetSEReadersRejected":
  487. case "SE:OpenChannelRejected":
  488. case "SE:CloseChannelRejected":
  489. case "SE:TransmitAPDURejected":
  490. let error = new SEError(result.error, result.reason);
  491. resolver.reject(Cu.cloneInto(error, this._window));
  492. break;
  493. case "SE:ReaderPresenceChanged":
  494. debug("Reader " + result.type + " present: " + result.isPresent);
  495. let reader = this._readers.find(r => r.type === result.type);
  496. if (reader) {
  497. reader.updateSEPresence(result.isPresent);
  498. }
  499. break;
  500. default:
  501. debug("Could not find a handler for " + message.name);
  502. resolver.reject(Cu.cloneInto(new SEError(), this._window));
  503. break;
  504. }
  505. }
  506. };
  507. this.NSGetFactory = XPCOMUtils.generateNSGetFactory([
  508. SEResponseImpl, SEChannelImpl, SESessionImpl, SEReaderImpl, SEManagerImpl
  509. ]);