Oppaitime's version of Gazelle
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

u2f.js 18KB

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