devise_fido_usf 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: b7c08c46417abf8bb8ee7e86fe62ca680de00e00
4
+ data.tar.gz: 8e7156d2787de596959d0c1c058011184117a153
5
+ SHA512:
6
+ metadata.gz: 7d7eba4cbae06efeafbfa510055ece1f1caa2ef29a3357e7e67f120f3b7a64cb7da940b9daa986692c3521293b8eb2024ac325df45240077fa802322596cab98
7
+ data.tar.gz: a0bac4db40ad751bde9c9ddcf88201831c38cb192ebb387e9abd890ae85767eabc4c4fa5008dc21ca0f9cde2e3e26f2c1a14a211dd58c06fa5ba2351f2e72e1e
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 H. Gregor Molter
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # FIDO U2F Authentication for Rails Devise
2
+
3
+ [![Dependency Status](https://gemnasium.com/badges/github.com/CyberDeck/devise-fido-u2f.svg)](https://gemnasium.com/github.com/CyberDeck/devise-fido-u2f)
4
+ [![Security](https://hakiri.io/github/CyberDeck/devise-fido-u2f/master.svg)](https://hakiri.io/github/CyberDeck/devise-fido-u2f/master)
5
+
6
+ [![Build Status](https://travis-ci.org/CyberDeck/devise-fido-u2f.svg?branch=master)](https://travis-ci.org/CyberDeck/devise-fido-u2f)
7
+ [![Code Climate](https://codeclimate.com/github/CyberDeck/devise-fido-u2f/badges/gpa.svg)](https://codeclimate.com/github/CyberDeck/devise-fido-u2f)
8
+ [![Coverage Status](https://coveralls.io/repos/github/CyberDeck/devise-fido-u2f/badge.svg)](https://coveralls.io/github/CyberDeck/devise-fido-u2f)
9
+
10
+ A gem which allows Rails Devise users to authenticate against a second factor.
11
+
12
+ ## Installation
13
+ Add this line to your application's Gemfile:
14
+
15
+ ```ruby
16
+ gem 'devise_fido_usf'
17
+ ```
18
+
19
+ And then execute:
20
+ ```bash
21
+ $ bundle
22
+ ```
23
+
24
+ Or install it yourself as:
25
+ ```bash
26
+ $ gem install devise_fido_usf
27
+ ```
28
+
29
+ ## Contributing
30
+ This is my first developed and published gem. If you find something unusual or uncommon within my code, please drop me a note how to fix it or make it better. Thank you!
31
+
32
+ ## License
33
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'DeviseFidoUsf'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+
33
+ task default: :test
@@ -0,0 +1,748 @@
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
+ /**
8
+ * @fileoverview The U2F api.
9
+ */
10
+ 'use strict';
11
+
12
+
13
+ /**
14
+ * Namespace for the U2F api.
15
+ * @type {Object}
16
+ */
17
+ var u2f = u2f || {};
18
+
19
+ /**
20
+ * FIDO U2F Javascript API Version
21
+ * @number
22
+ */
23
+ var js_api_version;
24
+
25
+ /**
26
+ * The U2F extension id
27
+ * @const {string}
28
+ */
29
+ // The Chrome packaged app extension ID.
30
+ // Uncomment this if you want to deploy a server instance that uses
31
+ // the package Chrome app and does not require installing the U2F Chrome extension.
32
+ u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd';
33
+ // The U2F Chrome extension ID.
34
+ // Uncomment this if you want to deploy a server instance that uses
35
+ // the U2F Chrome extension to authenticate.
36
+ // u2f.EXTENSION_ID = 'pfboblefjcgdjicmnffhdgionmgcdmne';
37
+
38
+
39
+ /**
40
+ * Message types for messsages to/from the extension
41
+ * @const
42
+ * @enum {string}
43
+ */
44
+ u2f.MessageTypes = {
45
+ 'U2F_REGISTER_REQUEST': 'u2f_register_request',
46
+ 'U2F_REGISTER_RESPONSE': 'u2f_register_response',
47
+ 'U2F_SIGN_REQUEST': 'u2f_sign_request',
48
+ 'U2F_SIGN_RESPONSE': 'u2f_sign_response',
49
+ 'U2F_GET_API_VERSION_REQUEST': 'u2f_get_api_version_request',
50
+ 'U2F_GET_API_VERSION_RESPONSE': 'u2f_get_api_version_response'
51
+ };
52
+
53
+
54
+ /**
55
+ * Response status codes
56
+ * @const
57
+ * @enum {number}
58
+ */
59
+ u2f.ErrorCodes = {
60
+ 'OK': 0,
61
+ 'OTHER_ERROR': 1,
62
+ 'BAD_REQUEST': 2,
63
+ 'CONFIGURATION_UNSUPPORTED': 3,
64
+ 'DEVICE_INELIGIBLE': 4,
65
+ 'TIMEOUT': 5
66
+ };
67
+
68
+
69
+ /**
70
+ * A message for registration requests
71
+ * @typedef {{
72
+ * type: u2f.MessageTypes,
73
+ * appId: ?string,
74
+ * timeoutSeconds: ?number,
75
+ * requestId: ?number
76
+ * }}
77
+ */
78
+ u2f.U2fRequest;
79
+
80
+
81
+ /**
82
+ * A message for registration responses
83
+ * @typedef {{
84
+ * type: u2f.MessageTypes,
85
+ * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse),
86
+ * requestId: ?number
87
+ * }}
88
+ */
89
+ u2f.U2fResponse;
90
+
91
+
92
+ /**
93
+ * An error object for responses
94
+ * @typedef {{
95
+ * errorCode: u2f.ErrorCodes,
96
+ * errorMessage: ?string
97
+ * }}
98
+ */
99
+ u2f.Error;
100
+
101
+ /**
102
+ * Data object for a single sign request.
103
+ * @typedef {enum {BLUETOOTH_RADIO, BLUETOOTH_LOW_ENERGY, USB, NFC}}
104
+ */
105
+ u2f.Transport;
106
+
107
+
108
+ /**
109
+ * Data object for a single sign request.
110
+ * @typedef {Array<u2f.Transport>}
111
+ */
112
+ u2f.Transports;
113
+
114
+ /**
115
+ * Data object for a single sign request.
116
+ * @typedef {{
117
+ * version: string,
118
+ * challenge: string,
119
+ * keyHandle: string,
120
+ * appId: string
121
+ * }}
122
+ */
123
+ u2f.SignRequest;
124
+
125
+
126
+ /**
127
+ * Data object for a sign response.
128
+ * @typedef {{
129
+ * keyHandle: string,
130
+ * signatureData: string,
131
+ * clientData: string
132
+ * }}
133
+ */
134
+ u2f.SignResponse;
135
+
136
+
137
+ /**
138
+ * Data object for a registration request.
139
+ * @typedef {{
140
+ * version: string,
141
+ * challenge: string
142
+ * }}
143
+ */
144
+ u2f.RegisterRequest;
145
+
146
+
147
+ /**
148
+ * Data object for a registration response.
149
+ * @typedef {{
150
+ * version: string,
151
+ * keyHandle: string,
152
+ * transports: Transports,
153
+ * appId: string
154
+ * }}
155
+ */
156
+ u2f.RegisterResponse;
157
+
158
+
159
+ /**
160
+ * Data object for a registered key.
161
+ * @typedef {{
162
+ * version: string,
163
+ * keyHandle: string,
164
+ * transports: ?Transports,
165
+ * appId: ?string
166
+ * }}
167
+ */
168
+ u2f.RegisteredKey;
169
+
170
+
171
+ /**
172
+ * Data object for a get API register response.
173
+ * @typedef {{
174
+ * js_api_version: number
175
+ * }}
176
+ */
177
+ u2f.GetJsApiVersionResponse;
178
+
179
+
180
+ //Low level MessagePort API support
181
+
182
+ /**
183
+ * Sets up a MessagePort to the U2F extension using the
184
+ * available mechanisms.
185
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
186
+ */
187
+ u2f.getMessagePort = function(callback) {
188
+ if (typeof chrome != 'undefined' && chrome.runtime) {
189
+ // The actual message here does not matter, but we need to get a reply
190
+ // for the callback to run. Thus, send an empty signature request
191
+ // in order to get a failure response.
192
+ var msg = {
193
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
194
+ signRequests: []
195
+ };
196
+ chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() {
197
+ if (!chrome.runtime.lastError) {
198
+ // We are on a whitelisted origin and can talk directly
199
+ // with the extension.
200
+ u2f.getChromeRuntimePort_(callback);
201
+ } else {
202
+ // chrome.runtime was available, but we couldn't message
203
+ // the extension directly, use iframe
204
+ u2f.getIframePort_(callback);
205
+ }
206
+ });
207
+ } else if (u2f.isAndroidChrome_()) {
208
+ u2f.getAuthenticatorPort_(callback);
209
+ } else if (u2f.isIosChrome_()) {
210
+ u2f.getIosPort_(callback);
211
+ } else {
212
+ // chrome.runtime was not available at all, which is normal
213
+ // when this origin doesn't have access to any extensions.
214
+ u2f.getIframePort_(callback);
215
+ }
216
+ };
217
+
218
+ /**
219
+ * Detect chrome running on android based on the browser's useragent.
220
+ * @private
221
+ */
222
+ u2f.isAndroidChrome_ = function() {
223
+ var userAgent = navigator.userAgent;
224
+ return userAgent.indexOf('Chrome') != -1 &&
225
+ userAgent.indexOf('Android') != -1;
226
+ };
227
+
228
+ /**
229
+ * Detect chrome running on iOS based on the browser's platform.
230
+ * @private
231
+ */
232
+ u2f.isIosChrome_ = function() {
233
+ return ["iPhone", "iPad", "iPod"].indexOf(navigator.platform) > -1;
234
+ };
235
+
236
+ /**
237
+ * Connects directly to the extension via chrome.runtime.connect.
238
+ * @param {function(u2f.WrappedChromeRuntimePort_)} callback
239
+ * @private
240
+ */
241
+ u2f.getChromeRuntimePort_ = function(callback) {
242
+ var port = chrome.runtime.connect(u2f.EXTENSION_ID,
243
+ {'includeTlsChannelId': true});
244
+ setTimeout(function() {
245
+ callback(new u2f.WrappedChromeRuntimePort_(port));
246
+ }, 0);
247
+ };
248
+
249
+ /**
250
+ * Return a 'port' abstraction to the Authenticator app.
251
+ * @param {function(u2f.WrappedAuthenticatorPort_)} callback
252
+ * @private
253
+ */
254
+ u2f.getAuthenticatorPort_ = function(callback) {
255
+ setTimeout(function() {
256
+ callback(new u2f.WrappedAuthenticatorPort_());
257
+ }, 0);
258
+ };
259
+
260
+ /**
261
+ * Return a 'port' abstraction to the iOS client app.
262
+ * @param {function(u2f.WrappedIosPort_)} callback
263
+ * @private
264
+ */
265
+ u2f.getIosPort_ = function(callback) {
266
+ setTimeout(function() {
267
+ callback(new u2f.WrappedIosPort_());
268
+ }, 0);
269
+ };
270
+
271
+ /**
272
+ * A wrapper for chrome.runtime.Port that is compatible with MessagePort.
273
+ * @param {Port} port
274
+ * @constructor
275
+ * @private
276
+ */
277
+ u2f.WrappedChromeRuntimePort_ = function(port) {
278
+ this.port_ = port;
279
+ };
280
+
281
+ /**
282
+ * Format and return a sign request compliant with the JS API version supported by the extension.
283
+ * @param {Array<u2f.SignRequest>} signRequests
284
+ * @param {number} timeoutSeconds
285
+ * @param {number} reqId
286
+ * @return {Object}
287
+ */
288
+ u2f.formatSignRequest_ =
289
+ function(appId, challenge, registeredKeys, timeoutSeconds, reqId) {
290
+ if (js_api_version === undefined || js_api_version < 1.1) {
291
+ // Adapt request to the 1.0 JS API
292
+ var signRequests = [];
293
+ for (var i = 0; i < registeredKeys.length; i++) {
294
+ signRequests[i] = {
295
+ version: registeredKeys[i].version,
296
+ challenge: challenge,
297
+ keyHandle: registeredKeys[i].keyHandle,
298
+ appId: appId
299
+ };
300
+ }
301
+ return {
302
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
303
+ signRequests: signRequests,
304
+ timeoutSeconds: timeoutSeconds,
305
+ requestId: reqId
306
+ };
307
+ }
308
+ // JS 1.1 API
309
+ return {
310
+ type: u2f.MessageTypes.U2F_SIGN_REQUEST,
311
+ appId: appId,
312
+ challenge: challenge,
313
+ registeredKeys: registeredKeys,
314
+ timeoutSeconds: timeoutSeconds,
315
+ requestId: reqId
316
+ };
317
+ };
318
+
319
+ /**
320
+ * Format and return a register request compliant with the JS API version supported by the extension..
321
+ * @param {Array<u2f.SignRequest>} signRequests
322
+ * @param {Array<u2f.RegisterRequest>} signRequests
323
+ * @param {number} timeoutSeconds
324
+ * @param {number} reqId
325
+ * @return {Object}
326
+ */
327
+ u2f.formatRegisterRequest_ =
328
+ function(appId, registeredKeys, registerRequests, timeoutSeconds, reqId) {
329
+ if (js_api_version === undefined || js_api_version < 1.1) {
330
+ // Adapt request to the 1.0 JS API
331
+ for (var i = 0; i < registerRequests.length; i++) {
332
+ registerRequests[i].appId = appId;
333
+ }
334
+ var signRequests = [];
335
+ for (var i = 0; i < registeredKeys.length; i++) {
336
+ signRequests[i] = {
337
+ version: registeredKeys[i].version,
338
+ challenge: registerRequests[0],
339
+ keyHandle: registeredKeys[i].keyHandle,
340
+ appId: appId
341
+ };
342
+ }
343
+ return {
344
+ type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
345
+ signRequests: signRequests,
346
+ registerRequests: registerRequests,
347
+ timeoutSeconds: timeoutSeconds,
348
+ requestId: reqId
349
+ };
350
+ }
351
+ // JS 1.1 API
352
+ return {
353
+ type: u2f.MessageTypes.U2F_REGISTER_REQUEST,
354
+ appId: appId,
355
+ registerRequests: registerRequests,
356
+ registeredKeys: registeredKeys,
357
+ timeoutSeconds: timeoutSeconds,
358
+ requestId: reqId
359
+ };
360
+ };
361
+
362
+
363
+ /**
364
+ * Posts a message on the underlying channel.
365
+ * @param {Object} message
366
+ */
367
+ u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) {
368
+ this.port_.postMessage(message);
369
+ };
370
+
371
+
372
+ /**
373
+ * Emulates the HTML 5 addEventListener interface. Works only for the
374
+ * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage.
375
+ * @param {string} eventName
376
+ * @param {function({data: Object})} handler
377
+ */
378
+ u2f.WrappedChromeRuntimePort_.prototype.addEventListener =
379
+ function(eventName, handler) {
380
+ var name = eventName.toLowerCase();
381
+ if (name == 'message' || name == 'onmessage') {
382
+ this.port_.onMessage.addListener(function(message) {
383
+ // Emulate a minimal MessageEvent object
384
+ handler({'data': message});
385
+ });
386
+ } else {
387
+ console.error('WrappedChromeRuntimePort only supports onMessage');
388
+ }
389
+ };
390
+
391
+ /**
392
+ * Wrap the Authenticator app with a MessagePort interface.
393
+ * @constructor
394
+ * @private
395
+ */
396
+ u2f.WrappedAuthenticatorPort_ = function() {
397
+ this.requestId_ = -1;
398
+ this.requestObject_ = null;
399
+ }
400
+
401
+ /**
402
+ * Launch the Authenticator intent.
403
+ * @param {Object} message
404
+ */
405
+ u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) {
406
+ var intentUrl =
407
+ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ +
408
+ ';S.request=' + encodeURIComponent(JSON.stringify(message)) +
409
+ ';end';
410
+ document.location = intentUrl;
411
+ };
412
+
413
+ /**
414
+ * Tells what type of port this is.
415
+ * @return {String} port type
416
+ */
417
+ u2f.WrappedAuthenticatorPort_.prototype.getPortType = function() {
418
+ return "WrappedAuthenticatorPort_";
419
+ };
420
+
421
+
422
+ /**
423
+ * Emulates the HTML 5 addEventListener interface.
424
+ * @param {string} eventName
425
+ * @param {function({data: Object})} handler
426
+ */
427
+ u2f.WrappedAuthenticatorPort_.prototype.addEventListener = function(eventName, handler) {
428
+ var name = eventName.toLowerCase();
429
+ if (name == 'message') {
430
+ var self = this;
431
+ /* Register a callback to that executes when
432
+ * chrome injects the response. */
433
+ window.addEventListener(
434
+ 'message', self.onRequestUpdate_.bind(self, handler), false);
435
+ } else {
436
+ console.error('WrappedAuthenticatorPort only supports message');
437
+ }
438
+ };
439
+
440
+ /**
441
+ * Callback invoked when a response is received from the Authenticator.
442
+ * @param function({data: Object}) callback
443
+ * @param {Object} message message Object
444
+ */
445
+ u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ =
446
+ function(callback, message) {
447
+ var messageObject = JSON.parse(message.data);
448
+ var intentUrl = messageObject['intentURL'];
449
+
450
+ var errorCode = messageObject['errorCode'];
451
+ var responseObject = null;
452
+ if (messageObject.hasOwnProperty('data')) {
453
+ responseObject = /** @type {Object} */ (
454
+ JSON.parse(messageObject['data']));
455
+ }
456
+
457
+ callback({'data': responseObject});
458
+ };
459
+
460
+ /**
461
+ * Base URL for intents to Authenticator.
462
+ * @const
463
+ * @private
464
+ */
465
+ u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ =
466
+ 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE';
467
+
468
+ /**
469
+ * Wrap the iOS client app with a MessagePort interface.
470
+ * @constructor
471
+ * @private
472
+ */
473
+ u2f.WrappedIosPort_ = function() {};
474
+
475
+ /**
476
+ * Launch the iOS client app request
477
+ * @param {Object} message
478
+ */
479
+ u2f.WrappedIosPort_.prototype.postMessage = function(message) {
480
+ var str = JSON.stringify(message);
481
+ var url = "u2f://auth?" + encodeURI(str);
482
+ location.replace(url);
483
+ };
484
+
485
+ /**
486
+ * Tells what type of port this is.
487
+ * @return {String} port type
488
+ */
489
+ u2f.WrappedIosPort_.prototype.getPortType = function() {
490
+ return "WrappedIosPort_";
491
+ };
492
+
493
+ /**
494
+ * Emulates the HTML 5 addEventListener interface.
495
+ * @param {string} eventName
496
+ * @param {function({data: Object})} handler
497
+ */
498
+ u2f.WrappedIosPort_.prototype.addEventListener = function(eventName, handler) {
499
+ var name = eventName.toLowerCase();
500
+ if (name !== 'message') {
501
+ console.error('WrappedIosPort only supports message');
502
+ }
503
+ };
504
+
505
+ /**
506
+ * Sets up an embedded trampoline iframe, sourced from the extension.
507
+ * @param {function(MessagePort)} callback
508
+ * @private
509
+ */
510
+ u2f.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
+ */
543
+ u2f.EXTENSION_TIMEOUT_SEC = 30;
544
+
545
+ /**
546
+ * A singleton instance for a MessagePort to the extension.
547
+ * @type {MessagePort|u2f.WrappedChromeRuntimePort_}
548
+ * @private
549
+ */
550
+ u2f.port_ = null;
551
+
552
+ /**
553
+ * Callbacks waiting for a port
554
+ * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>}
555
+ * @private
556
+ */
557
+ u2f.waitingForPort_ = [];
558
+
559
+ /**
560
+ * A counter for requestIds.
561
+ * @type {number}
562
+ * @private
563
+ */
564
+ u2f.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
+ */
572
+ u2f.callbackMap_ = {};
573
+
574
+ /**
575
+ * Creates or retrieves the MessagePort singleton to use.
576
+ * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback
577
+ * @private
578
+ */
579
+ u2f.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
+ */
603
+ u2f.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
+ * If the JS API version supported by the extension is unknown, it first sends a
618
+ * message to the extension to find out the supported API version and then it sends
619
+ * the sign request.
620
+ * @param {string=} appId
621
+ * @param {string=} challenge
622
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
623
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
624
+ * @param {number=} opt_timeoutSeconds
625
+ */
626
+ u2f.sign = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
627
+ if (js_api_version === undefined) {
628
+ // Send a message to get the extension to JS API version, then send the actual sign request.
629
+ u2f.getApiVersion(
630
+ function (response) {
631
+ js_api_version = response['js_api_version'] === undefined ? 0 : response['js_api_version'];
632
+ console.log("Extension JS API Version: ", js_api_version);
633
+ u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
634
+ });
635
+ } else {
636
+ // We know the JS API version. Send the actual sign request in the supported API version.
637
+ u2f.sendSignRequest(appId, challenge, registeredKeys, callback, opt_timeoutSeconds);
638
+ }
639
+ };
640
+
641
+ /**
642
+ * Dispatches an array of sign requests to available U2F tokens.
643
+ * @param {string=} appId
644
+ * @param {string=} challenge
645
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
646
+ * @param {function((u2f.Error|u2f.SignResponse))} callback
647
+ * @param {number=} opt_timeoutSeconds
648
+ */
649
+ u2f.sendSignRequest = function(appId, challenge, registeredKeys, callback, opt_timeoutSeconds) {
650
+ u2f.getPortSingleton_(function(port) {
651
+ var reqId = ++u2f.reqCounter_;
652
+ u2f.callbackMap_[reqId] = callback;
653
+ var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
654
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
655
+ var req = u2f.formatSignRequest_(appId, challenge, registeredKeys, timeoutSeconds, reqId);
656
+ port.postMessage(req);
657
+ });
658
+ };
659
+
660
+ /**
661
+ * Dispatches register requests to available U2F tokens. An array of sign
662
+ * requests identifies already registered tokens.
663
+ * If the JS API version supported by the extension is unknown, it first sends a
664
+ * message to the extension to find out the supported API version and then it sends
665
+ * the register request.
666
+ * @param {string=} appId
667
+ * @param {Array<u2f.RegisterRequest>} registerRequests
668
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
669
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
670
+ * @param {number=} opt_timeoutSeconds
671
+ */
672
+ u2f.register = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
673
+ if (js_api_version === undefined) {
674
+ // Send a message to get the extension to JS API version, then send the actual register request.
675
+ u2f.getApiVersion(
676
+ function (response) {
677
+ js_api_version = response['js_api_version'] === undefined ? 0: response['js_api_version'];
678
+ console.log("Extension JS API Version: ", js_api_version);
679
+ u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
680
+ callback, opt_timeoutSeconds);
681
+ });
682
+ } else {
683
+ // We know the JS API version. Send the actual register request in the supported API version.
684
+ u2f.sendRegisterRequest(appId, registerRequests, registeredKeys,
685
+ callback, opt_timeoutSeconds);
686
+ }
687
+ };
688
+
689
+ /**
690
+ * Dispatches register requests to available U2F tokens. An array of sign
691
+ * requests identifies already registered tokens.
692
+ * @param {string=} appId
693
+ * @param {Array<u2f.RegisterRequest>} registerRequests
694
+ * @param {Array<u2f.RegisteredKey>} registeredKeys
695
+ * @param {function((u2f.Error|u2f.RegisterResponse))} callback
696
+ * @param {number=} opt_timeoutSeconds
697
+ */
698
+ u2f.sendRegisterRequest = function(appId, registerRequests, registeredKeys, callback, opt_timeoutSeconds) {
699
+ u2f.getPortSingleton_(function(port) {
700
+ var reqId = ++u2f.reqCounter_;
701
+ u2f.callbackMap_[reqId] = callback;
702
+ var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ?
703
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC);
704
+ var req = u2f.formatRegisterRequest_(
705
+ appId, registeredKeys, registerRequests, timeoutSeconds, reqId);
706
+ port.postMessage(req);
707
+ });
708
+ };
709
+
710
+
711
+ /**
712
+ * Dispatches a message to the extension to find out the supported
713
+ * JS API version.
714
+ * If the user is on a mobile phone and is thus using Google Authenticator instead
715
+ * of the Chrome extension, don't send the request and simply return 0.
716
+ * @param {function((u2f.Error|u2f.GetJsApiVersionResponse))} callback
717
+ * @param {number=} opt_timeoutSeconds
718
+ */
719
+ u2f.getApiVersion = function(callback, opt_timeoutSeconds) {
720
+ u2f.getPortSingleton_(function(port) {
721
+ // If we are using Android Google Authenticator or iOS client app,
722
+ // do not fire an intent to ask which JS API version to use.
723
+ if (port.getPortType) {
724
+ var apiVersion;
725
+ switch (port.getPortType()) {
726
+ case 'WrappedIosPort_':
727
+ case 'WrappedAuthenticatorPort_':
728
+ apiVersion = 1.1;
729
+ break;
730
+
731
+ default:
732
+ apiVersion = 0;
733
+ break;
734
+ }
735
+ callback({ 'js_api_version': apiVersion });
736
+ return;
737
+ }
738
+ var reqId = ++u2f.reqCounter_;
739
+ u2f.callbackMap_[reqId] = callback;
740
+ var req = {
741
+ type: u2f.MessageTypes.U2F_GET_API_VERSION_REQUEST,
742
+ timeoutSeconds: (typeof opt_timeoutSeconds !== 'undefined' ?
743
+ opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC),
744
+ requestId: reqId
745
+ };
746
+ port.postMessage(req);
747
+ });
748
+ };