devise_fido_usf 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ };