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