2
0

u2f-api.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. // Copyright 2014-2015 Google Inc. All rights reserved.
  2. //
  3. // Use of this source code is governed by a BSD-style
  4. // license that can be found in the LICENSE file or at
  5. // https://developers.google.com/open-source/licenses/bsd
  6. /**
  7. * @fileoverview The U2F api.
  8. */
  9. 'use strict';
  10. /** Namespace for the U2F api.
  11. * @type {Object}
  12. */
  13. var u2f = u2f || {};
  14. /**
  15. * The U2F extension id
  16. * @type {string}
  17. * @const
  18. */
  19. u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
  20. /**
  21. * Message types for messsages to/from the extension
  22. * @const
  23. * @enum {string}
  24. */
  25. u2f.MessageTypes = {
  26. 'U2F_REGISTER_REQUEST': 'u2f_register_request',
  27. 'U2F_SIGN_REQUEST': 'u2f_sign_request',
  28. 'U2F_REGISTER_RESPONSE': 'u2f_register_response',
  29. 'U2F_SIGN_RESPONSE': 'u2f_sign_response'
  30. };
  31. /**
  32. * Response status codes
  33. * @const
  34. * @enum {number}
  35. */
  36. u2f.ErrorCodes = {
  37. 'OK': 0,
  38. 'OTHER_ERROR': 1,
  39. 'BAD_REQUEST': 2,
  40. 'CONFIGURATION_UNSUPPORTED': 3,
  41. 'DEVICE_INELIGIBLE': 4,
  42. 'TIMEOUT': 5
  43. };
  44. /**
  45. * A message type for registration requests
  46. * @typedef {{
  47. * type: u2f.MessageTypes,
  48. * signRequests: Array<u2f.SignRequest>,
  49. * registerRequests: ?Array<u2f.RegisterRequest>,
  50. * timeoutSeconds: ?number,
  51. * requestId: ?number
  52. * }}
  53. */
  54. u2f.Request;
  55. /**
  56. * A message for registration responses
  57. * @typedef {{
  58. * type: u2f.MessageTypes,
  59. * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
  60. * requestId: ?number
  61. * }}
  62. */
  63. u2f.Response;
  64. /**
  65. * An error object for responses
  66. * @typedef {{
  67. * errorCode: u2f.ErrorCodes,
  68. * errorMessage: ?string
  69. * }}
  70. */
  71. u2f.Error;
  72. /**
  73. * Data object for a single sign request.
  74. * @typedef {{
  75. * version: string,
  76. * challenge: string,
  77. * keyHandle: string,
  78. * appId: string
  79. * }}
  80. */
  81. u2f.SignRequest;
  82. /**
  83. * Data object for a sign response.
  84. * @typedef {{
  85. * keyHandle: string,
  86. * signatureData: string,
  87. * clientData: string
  88. * }}
  89. */
  90. u2f.SignResponse;
  91. /**
  92. * Data object for a registration request.
  93. * @typedef {{
  94. * version: string,
  95. * challenge: string,
  96. * appId: string
  97. * }}
  98. */
  99. u2f.RegisterRequest;
  100. /**
  101. * Data object for a registration response.
  102. * @typedef {{
  103. * registrationData: string,
  104. * clientData: string
  105. * }}
  106. */
  107. u2f.RegisterResponse;
  108. // Low level MessagePort API support
  109. /**
  110. * Sets up a MessagePort to the U2F extension using the
  111. * available mechanisms.
  112. * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
  113. */
  114. u2f.getMessagePort = function(callback) {
  115. if (typeof chrome != 'undefined' && chrome.runtime) {
  116. // The actual message here does not matter, but we need to get a reply
  117. // for the callback to run. Thus, send an empty signature request
  118. // in order to get a failure response.
  119. var msg = {
  120. type: u2f.MessageTypes.U2F_SIGN_REQUEST,
  121. signRequests: []
  122. };
  123. chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
  124. if (!chrome.runtime.lastError) {
  125. // We are on a whitelisted origin and can talk directly
  126. // with the extension.
  127. u2f.getChromeRuntimePort_(callback);
  128. } else {
  129. // chrome.runtime was available, but we couldn't message
  130. // the extension directly, use iframe
  131. u2f.getIframePort_(callback);
  132. }
  133. });
  134. } else if (u2f.isAndroidChrome_()) {
  135. u2f.getAuthenticatorPort_(callback);
  136. } else {
  137. // chrome.runtime was not available at all, which is normal
  138. // when this origin doesn't have access to any extensions.
  139. u2f.getIframePort_(callback);
  140. }
  141. };
  142. /**
  143. * Detect chrome running on android based on the browser's useragent.
  144. * @private
  145. */
  146. u2f.isAndroidChrome_ = function() {
  147. var userAgent = navigator.userAgent;
  148. return userAgent.indexOf('Chrome') != -1 &&
  149. userAgent.indexOf('Android') != -1;
  150. };
  151. /**
  152. * Connects directly to the extension via chrome.runtime.connect
  153. * @param {function(u2f.WrappedChromeRuntimePort_)} callback
  154. * @private
  155. */
  156. u2f.getChromeRuntimePort_ = function(callback) {
  157. var port = chrome.runtime.connect(u2f.EXTENSION_ID,
  158. {'includeTlsChannelId': true});
  159. setTimeout(function() {
  160. callback(new u2f.WrappedChromeRuntimePort_(port));
  161. }, 0);
  162. };
  163. /**
  164. * Return a 'port' abstraction to the Authenticator app.
  165. * @param {function(u2f.WrappedAuthenticatorPort_)} callback
  166. * @private
  167. */
  168. u2f.getAuthenticatorPort_ = function(callback) {
  169. setTimeout(function() {
  170. callback(new u2f.WrappedAuthenticatorPort_());
  171. }, 0);
  172. };
  173. /**
  174. * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
  175. * @param {Port} port
  176. * @constructor
  177. * @private
  178. */
  179. u2f.WrappedChromeRuntimePort_ = function(port) {
  180. this.port_ = port;
  181. };
  182. /**
  183. * Format a return a sign request.
  184. * @param {Array<u2f.SignRequest>} signRequests
  185. * @param {number} timeoutSeconds
  186. * @param {number} reqId
  187. * @return {Object}
  188. */
  189. u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ =
  190. function(signRequests, timeoutSeconds, reqId) {
  191. return {
  192. type: u2f.MessageTypes.U2F_SIGN_REQUEST,
  193. signRequests: signRequests,
  194. timeoutSeconds: timeoutSeconds,
  195. requestId: reqId
  196. };
  197. };
  198. /**
  199. * Format a return a register request.
  200. * @param {Array<u2f.SignRequest>} signRequests
  201. * @param {Array<u2f.RegisterRequest>} signRequests
  202. * @param {number} timeoutSeconds
  203. * @param {number} reqId
  204. * @return {Object}
  205. */
  206. u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ =
  207. function(signRequests, registerRequests, timeoutSeconds, reqId) {
  208. return {
  209. type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
  210. signRequests: signRequests,
  211. registerRequests: registerRequests,
  212. timeoutSeconds: timeoutSeconds,
  213. requestId: reqId
  214. };
  215. };
  216. /**
  217. * Posts a message on the underlying channel.
  218. * @param {Object} message
  219. */
  220. u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
  221. this.port_.postMessage(message);
  222. };
  223. /**
  224. * Emulates the HTML 5 addEventListener interface. Works only for the
  225. * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
  226. * @param {string} eventName
  227. * @param {function({data: Object})} handler
  228. */
  229. u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
  230. function(eventName, handler) {
  231. var name = eventName.toLowerCase();
  232. if (name == 'message' || name == 'onmessage') {
  233. this.port_.onMessage.addListener(function(message) {
  234. // Emulate a minimal MessageEvent object
  235. handler({'data': message});
  236. });
  237. } else {
  238. console.error('WrappedChromeRuntimePort only supports onMessage');
  239. }
  240. };
  241. /**
  242. * Wrap the Authenticator app with a MessagePort interface.
  243. * @constructor
  244. * @private
  245. */
  246. u2f.WrappedAuthenticatorPort_ = function() {
  247. this.requestId_ = -1;
  248. this.requestObject_ = null;
  249. }
  250. /**
  251. * Launch the Authenticator intent.
  252. * @param {Object} message
  253. */
  254. u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
  255. var intentLocation = /** @type {string} */ (message);
  256. document.location = intentLocation;
  257. };
  258. /**
  259. * Emulates the HTML 5 addEventListener interface.
  260. * @param {string} eventName
  261. * @param {function({data: Object})} handler
  262. */
  263. u2f.WrappedAuthenticatorPort_.prototype.addEventListener =
  264. function(eventName, handler) {
  265. var name = eventName.toLowerCase();
  266. if (name == 'message') {
  267. var self = this;
  268. /* Register a callback to that executes when
  269. * chrome injects the response. */
  270. window.addEventListener(
  271. 'message', self.onRequestUpdate_.bind(self, handler), false);
  272. } else {
  273. console.error('WrappedAuthenticatorPort only supports message');
  274. }
  275. };
  276. /**
  277. * Callback invoked when a response is received from the Authenticator.
  278. * @param function({data: Object}) callback
  279. * @param {Object} message message Object
  280. */
  281. u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
  282. function(callback, message) {
  283. var messageObject = JSON.parse(message.data);
  284. var intentUrl = messageObject['intentURL'];
  285. var errorCode = messageObject['errorCode'];
  286. var responseObject = null;
  287. if (messageObject.hasOwnProperty('data')) {
  288. responseObject = /** @type {Object} */ (
  289. JSON.parse(messageObject['data']));
  290. responseObject['requestId'] = this.requestId_;
  291. }
  292. /* Sign responses from the authenticator do not conform to U2F,
  293. * convert to U2F here. */
  294. responseObject = this.doResponseFixups_(responseObject);
  295. callback({'data': responseObject});
  296. };
  297. /**
  298. * Fixup the response provided by the Authenticator to conform with
  299. * the U2F spec.
  300. * @param {Object} responseData
  301. * @return {Object} the U2F compliant response object
  302. */
  303. u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ =
  304. function(responseObject) {
  305. if (responseObject.hasOwnProperty('responseData')) {
  306. return responseObject;
  307. } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) {
  308. // Only sign responses require fixups. If this is not a response
  309. // to a sign request, then an internal error has occurred.
  310. return {
  311. 'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE,
  312. 'responseData': {
  313. 'errorCode': u2f.ErrorCodes.OTHER_ERROR,
  314. 'errorMessage': 'Internal error: invalid response from Authenticator'
  315. }
  316. };
  317. }
  318. /* Non-conformant sign response, do fixups. */
  319. var encodedChallengeObject = responseObject['challenge'];
  320. if (typeof encodedChallengeObject !== 'undefined') {
  321. var challengeObject = JSON.parse(atob(encodedChallengeObject));
  322. var serverChallenge = challengeObject['challenge'];
  323. var challengesList = this.requestObject_['signData'];
  324. var requestChallengeObject = null;
  325. for (var i = 0; i < challengesList.length; i++) {
  326. var challengeObject = challengesList[i];
  327. if (challengeObject['keyHandle'] == responseObject['keyHandle']) {
  328. requestChallengeObject = challengeObject;
  329. break;
  330. }
  331. }
  332. }
  333. var responseData = {
  334. 'errorCode': responseObject['resultCode'],
  335. 'keyHandle': responseObject['keyHandle'],
  336. 'signatureData': responseObject['signature'],
  337. 'clientData': encodedChallengeObject
  338. };
  339. return {
  340. 'type': u2f.MessageTypes.U2F_SIGN_RESPONSE,
  341. 'responseData': responseData,
  342. 'requestId': responseObject['requestId']
  343. }
  344. };
  345. /**
  346. * Base URL for intents to Authenticator.
  347. * @const
  348. * @private
  349. */
  350. u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
  351. 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
  352. /**
  353. * Format a return a sign request.
  354. * @param {Array<u2f.SignRequest>} signRequests
  355. * @param {number} timeoutSeconds (ignored for now)
  356. * @param {number} reqId
  357. * @return {string}
  358. */
  359. u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ =
  360. function(signRequests, timeoutSeconds, reqId) {
  361. if (!signRequests || signRequests.length == 0) {
  362. return null;
  363. }
  364. /* TODO(fixme): stash away requestId, as the authenticator app does
  365. * not return it for sign responses. */
  366. this.requestId_ = reqId;
  367. /* TODO(fixme): stash away the signRequests, to deal with the legacy
  368. * response format returned by the Authenticator app. */
  369. this.requestObject_ = {
  370. 'type': u2f.MessageTypes.U2F_SIGN_REQUEST,
  371. 'signData': signRequests,
  372. 'requestId': reqId,
  373. 'timeout': timeoutSeconds
  374. };
  375. var appId = signRequests[0]['appId'];
  376. var intentUrl =
  377. u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
  378. ';S.appId=' + encodeURIComponent(appId) +
  379. ';S.eventId=' + reqId +
  380. ';S.challenges=' +
  381. encodeURIComponent(
  382. JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end';
  383. return intentUrl;
  384. };
  385. /**
  386. * Get the browser data objects from the challenge list
  387. * @param {Array} challenges list of challenges
  388. * @return {Array} list of browser data objects
  389. * @private
  390. */
  391. u2f.WrappedAuthenticatorPort_
  392. .prototype.getBrowserDataList_ = function(challenges) {
  393. return challenges
  394. .map(function(challenge) {
  395. var browserData = {
  396. 'typ': 'navigator.id.getAssertion',
  397. 'challenge': challenge['challenge']
  398. };
  399. var challengeObject = {
  400. 'challenge' : browserData,
  401. 'keyHandle' : challenge['keyHandle']
  402. };
  403. return challengeObject;
  404. });
  405. };
  406. /**
  407. * Format a return a register request.
  408. * @param {Array<u2f.SignRequest>} signRequests
  409. * @param {Array<u2f.RegisterRequest>} enrollChallenges
  410. * @param {number} timeoutSeconds (ignored for now)
  411. * @param {number} reqId
  412. * @return {Object}
  413. */
  414. u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ =
  415. function(signRequests, enrollChallenges, timeoutSeconds, reqId) {
  416. if (!enrollChallenges || enrollChallenges.length == 0) {
  417. return null;
  418. }
  419. // Assume the appId is the same for all enroll challenges.
  420. var appId = enrollChallenges[0]['appId'];
  421. var registerRequests = [];
  422. for (var i = 0; i < enrollChallenges.length; i++) {
  423. var registerRequest = {
  424. 'challenge': enrollChallenges[i]['challenge'],
  425. 'version': enrollChallenges[i]['version']
  426. };
  427. if (enrollChallenges[i]['appId'] != appId) {
  428. // Only include the appId when it differs from the first appId.
  429. registerRequest['appId'] = enrollChallenges[i]['appId'];
  430. }
  431. registerRequests.push(registerRequest);
  432. }
  433. var registeredKeys = [];
  434. if (signRequests) {
  435. for (i = 0; i < signRequests.length; i++) {
  436. var key = {
  437. 'keyHandle': signRequests[i]['keyHandle'],
  438. 'version': signRequests[i]['version']
  439. };
  440. // Only include the appId when it differs from the appId that's
  441. // being registered now.
  442. if (signRequests[i]['appId'] != appId) {
  443. key['appId'] = signRequests[i]['appId'];
  444. }
  445. registeredKeys.push(key);
  446. }
  447. }
  448. var request = {
  449. 'type': u2f.MessageTypes.U2F_REGISTER_REQUEST,
  450. 'appId': appId,
  451. 'registerRequests': registerRequests,
  452. 'registeredKeys': registeredKeys,
  453. 'requestId': reqId,
  454. 'timeoutSeconds': timeoutSeconds
  455. };
  456. var intentUrl =
  457. u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
  458. ';S.request=' + encodeURIComponent(JSON.stringify(request)) +
  459. ';end';
  460. /* TODO(fixme): stash away requestId, this is is not necessary for
  461. * register requests, but here to keep parity with sign.
  462. */
  463. this.requestId_ = reqId;
  464. return intentUrl;
  465. };
  466. /**
  467. * Sets up an embedded trampoline iframe, sourced from the extension.
  468. * @param {function(MessagePort)} callback
  469. * @private
  470. */
  471. u2f.getIframePort_ = function(callback) {
  472. // Create the iframe
  473. var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID;
  474. var iframe = document.createElement('iframe');
  475. iframe.src = iframeOrigin + '/u2f-comms.html';
  476. iframe.setAttribute('style', 'display:none');
  477. document.body.appendChild(iframe);
  478. var channel = new MessageChannel();
  479. var ready = function(message) {
  480. if (message.data == 'ready') {
  481. channel.port1.removeEventListener('message', ready);
  482. callback(channel.port1);
  483. } else {
  484. console.error('First event on iframe port was not "ready"');
  485. }
  486. };
  487. channel.port1.addEventListener('message', ready);
  488. channel.port1.start();
  489. iframe.addEventListener('load', function() {
  490. // Deliver the port to the iframe and initialize
  491. iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]);
  492. });
  493. };
  494. // High-level JS API
  495. /**
  496. * Default extension response timeout in seconds.
  497. * @const
  498. */
  499. u2f.EXTENSION_TIMEOUT_SEC = 30;
  500. /**
  501. * A singleton instance for a MessagePort to the extension.
  502. * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
  503. * @private
  504. */
  505. u2f.port_ = null;
  506. /**
  507. * Callbacks waiting for a port
  508. * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
  509. * @private
  510. */
  511. u2f.waitingForPort_ = [];
  512. /**
  513. * A counter for requestIds.
  514. * @type {number}
  515. * @private
  516. */
  517. u2f.reqCounter_ = 0;
  518. /**
  519. * A map from requestIds to client callbacks
  520. * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse))
  521. * |function((u2f.Error|u2f.SignResponse)))>}
  522. * @private
  523. */
  524. u2f.callbackMap_ = {};
  525. /**
  526. * Creates or retrieves the MessagePort singleton to use.
  527. * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
  528. * @private
  529. */
  530. u2f.getPortSingleton_ = function(callback) {
  531. if (u2f.port_) {
  532. callback(u2f.port_);
  533. } else {
  534. if (u2f.waitingForPort_.length == 0) {
  535. u2f.getMessagePort(function(port) {
  536. u2f.port_ = port;
  537. u2f.port_.addEventListener('message',
  538. /** @type {function(Event)} */ (u2f.responseHandler_));
  539. // Careful, here be async callbacks. Maybe.
  540. while (u2f.waitingForPort_.length)
  541. u2f.waitingForPort_.shift()(u2f.port_);
  542. });
  543. }
  544. u2f.waitingForPort_.push(callback);
  545. }
  546. };
  547. /**
  548. * Handles response messages from the extension.
  549. * @param {MessageEvent.<u2f.Response>} message
  550. * @private
  551. */
  552. u2f.responseHandler_ = function(message) {
  553. var response = message.data;
  554. var reqId = response['requestId'];
  555. if (!reqId || !u2f.callbackMap_[reqId]) {
  556. console.error('Unknown or missing requestId in response.');
  557. return;
  558. }
  559. var cb = u2f.callbackMap_[reqId];
  560. delete u2f.callbackMap_[reqId];
  561. cb(response['responseData']);
  562. };
  563. /**
  564. * Dispatches an array of sign requests to available U2F tokens.
  565. * @param {Array<u2f.SignRequest>} signRequests
  566. * @param {function((u2f.Error|u2f.SignResponse))} callback
  567. * @param {number=} opt_timeoutSeconds
  568. */
  569. u2f.sign = function(signRequests, callback, opt_timeoutSeconds) {
  570. u2f.getPortSingleton_(function(port) {
  571. var reqId = ++u2f.reqCounter_;
  572. u2f.callbackMap_[reqId] = callback;
  573. var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
  574. opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
  575. var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId);
  576. port.postMessage(req);
  577. });
  578. };
  579. /**
  580. * Dispatches register requests to available U2F tokens. An array of sign
  581. * requests identifies already registered tokens.
  582. * @param {Array<u2f.RegisterRequest>} registerRequests
  583. * @param {Array<u2f.SignRequest>} signRequests
  584. * @param {function((u2f.Error|u2f.RegisterResponse))} callback
  585. * @param {number=} opt_timeoutSeconds
  586. */
  587. u2f.register = function(registerRequests, signRequests,
  588. callback, opt_timeoutSeconds) {
  589. u2f.getPortSingleton_(function(port) {
  590. var reqId = ++u2f.reqCounter_;
  591. u2f.callbackMap_[reqId] = callback;
  592. var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
  593. opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
  594. var req = port.formatRegisterRequest_(
  595. signRequests, registerRequests, timeoutSeconds, reqId);
  596. port.postMessage(req);
  597. });
  598. };