clarion 0.3.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,50 +1,76 @@
1
1
  require 'base64'
2
- require 'u2f'
2
+ require 'webauthn'
3
+ require 'securerandom'
4
+ require 'base64'
3
5
 
4
6
  module Clarion
5
7
  class Authenticator
6
8
  class Error < StandardError; end
7
9
  class InvalidKey < Error; end
10
+ class InvalidAssertion < Error; end
8
11
 
9
- def initialize(authn, u2f, counter, store)
12
+ def initialize(authn, counter, store, rp_id: nil, legacy_app_id: nil)
10
13
  @authn = authn
11
- @u2f = u2f
12
14
  @counter = counter
13
15
  @store = store
16
+ @rp_id = rp_id
17
+ @legacy_app_id = legacy_app_id
14
18
  end
15
19
 
16
- attr_reader :authn, :u2f, :counter, :store
20
+ attr_reader :authn, :counter, :store, :rp_id, :legacy_app_id
17
21
 
18
- def request
19
- [u2f.app_id, u2f.authentication_requests(authn.keys.map(&:handle)), u2f.challenge]
22
+ def challenge
23
+ @challenge ||= SecureRandom.random_bytes(32)
20
24
  end
21
25
 
22
- def verify!(challenge, response_json)
23
- response = U2F::SignResponse.load_from_json(response_json)
24
- key = authn.key_for_handle(response.key_handle)
25
- unless key
26
- raise InvalidKey, "#{response.key_handle.inspect} is invalid token for authn #{authn.id}"
26
+ def webauthn_request_extensions
27
+ {}.tap do |e|
28
+ e[:appid] = legacy_app_id if legacy_app_id
27
29
  end
28
- count = counter ? counter.get(key) : 0
30
+ end
29
31
 
30
- u2f.authenticate!(
31
- challenge,
32
- response,
33
- Base64.decode64(key.public_key),
34
- count,
32
+ def credential_request_options
33
+ {
34
+ publicKey: {
35
+ timeout: 60000,
36
+ # Convert to ArrayBuffer in sign.js
37
+ challenge: challenge.each_byte.map(&:ord),
38
+ allowCredentials: authn.keys.map { |_| {type: 'public-key', id: Base64.urlsafe_decode64(_.handle).each_byte.map(&:ord)} },
39
+ extensions: webauthn_request_extensions,
40
+ }
41
+ }
42
+ end
43
+
44
+ def verify!(challenge: self.challenge(), origin:, extension_results: {}, credential_id:, authenticator_data:, client_data_json:, signature:)
45
+ assertion = WebAuthn::AuthenticatorAssertionResponse.new(
46
+ credential_id: credential_id,
47
+ authenticator_data: authenticator_data,
48
+ client_data_json: client_data_json,
49
+ signature: signature,
35
50
  )
36
51
 
37
- unless authn.verify(key)
52
+ key = authn.verify_by_handle(credential_id)
53
+ unless key
38
54
  raise Authenticator::InvalidKey
39
55
  end
40
56
 
41
- key.counter = response.counter
42
- if counter
43
- counter.store(key)
57
+ rp_id = extension_results&.fetch('appid', false) ? legacy_app_id : self.rp_id()
58
+ allowed_credentials = authn.keys.map { |_| {id: _.handle, public_key: _.public_key_bytes} }
59
+ unless assertion.valid?(challenge, origin, rp_id: rp_id, allowed_credentials: allowed_credentials)
60
+ raise Authenticator::InvalidAssertion, "invalid assertion"
44
61
  end
45
62
 
46
- store.store_authn(authn)
63
+ sign_count = assertion.authenticator_data.sign_count
64
+ last_sign_count = counter ? counter.get(key) : 0
65
+
66
+ if sign_count <= last_sign_count
67
+ raise Authenticator::InvalidAssertion, "sign_count is decreased"
68
+ end
47
69
 
70
+ key.counter = sign_count
71
+
72
+ counter.store(key) if counter
73
+ store.store_authn(authn)
48
74
  true
49
75
  end
50
76
  end
@@ -77,10 +77,16 @@ module Clarion
77
77
  keys.find { |_| _.handle == handle }
78
78
  end
79
79
 
80
- def verify(key, verified_at: Time.now)
81
- unless key_for_handle(key.handle)
82
- return false
80
+ def verify_by_handle(handle, verified_at: Time.now)
81
+ key = key_for_handle(handle)
82
+ unless key
83
+ return nil
83
84
  end
85
+ verify(key)
86
+ return key
87
+ end
88
+
89
+ def verify(key, verified_at: Time.now)
84
90
  @verified_at = verified_at
85
91
  @verified_key = key
86
92
  @status = :verified
@@ -36,6 +36,10 @@ module Clarion
36
36
  @options[:app_id]
37
37
  end
38
38
 
39
+ option def rp_id
40
+ @options[:rp_id]
41
+ end
42
+
39
43
  option def store
40
44
  @store ||= Clarion::Stores.find(@options.fetch(:store).fetch(:kind)).new(store_options)
41
45
  end
@@ -26,18 +26,21 @@ module Clarion
26
26
  new(**key)
27
27
  end
28
28
 
29
- def initialize(handle:, name: nil, public_key: nil, counter: nil)
29
+ def initialize(handle:, type: 'fido-legacy', name: nil, public_key: nil, counter: nil, user_handle: nil)
30
+ @type = type
30
31
  @handle = handle
32
+ @user_handle = user_handle
31
33
  @name = name
32
34
  @public_key = public_key
33
35
  @counter = counter
34
36
  end
35
37
 
36
- attr_reader :handle, :public_key
38
+ attr_reader :type, :handle, :public_key, :user_handle
37
39
  attr_accessor :counter, :name
38
40
 
39
41
  def to_h(all=false)
40
42
  {
43
+ type: type,
41
44
  handle: handle,
42
45
  }.tap do |h|
43
46
  h[:name] = name if name
@@ -48,6 +51,10 @@ module Clarion
48
51
  end
49
52
  end
50
53
 
54
+ def public_key_bytes
55
+ public_key.unpack('m*')[0]
56
+ end
57
+
51
58
  def to_json(*args)
52
59
  to_h(*args).to_json
53
60
  end
@@ -1,22 +1,66 @@
1
1
  require 'clarion/key'
2
+ require 'webauthn'
3
+ require 'securerandom'
4
+ require 'base64'
2
5
 
3
6
  module Clarion
4
7
  class Registrator
5
- def initialize(u2f, counter)
6
- @u2f = u2f
8
+ class Error < StandardError; end
9
+ class InvalidAttestation < Error; end
10
+
11
+ def initialize(counter, rp_name: 'clarion', rp_id:, user_handle: SecureRandom.base64(64), user_name: 'clarion user', display_name: user_name)
7
12
  @counter = counter
13
+ @rp_id = rp_id
14
+ @rp_name = rp_name
15
+ @user_handle = user_handle
16
+ @user_name = user_name
17
+ @display_name = display_name
8
18
  end
9
19
 
10
- attr_reader :u2f, :counter
20
+ attr_reader :counter, :rp_id, :rp_name, :user_handle, :user_name, :display_name
21
+
22
+ def challenge
23
+ @challenge ||= SecureRandom.random_bytes(32)
24
+ end
11
25
 
12
- def request
13
- [u2f.app_id, u2f.registration_requests]
26
+ def credential_creation_options
27
+ {
28
+ publicKey: {
29
+ timeout: 60000,
30
+ # Convert to ArrayBuffer in register.js
31
+ challenge: challenge.each_byte.map(&:ord),
32
+ attestation: 'none',
33
+ pubKeyCredParams: [WebAuthn::CRED_PARAM_ES256],
34
+ rp: {
35
+ name: rp_name,
36
+ },
37
+ user: {
38
+ id: Base64.decode64(user_handle).each_byte.map(&:ord),
39
+ displayName: display_name,
40
+ name: user_name,
41
+ },
42
+ },
43
+ }
14
44
  end
15
45
 
16
- def register!(challenges, response_json)
17
- response = U2F::RegisterResponse.load_from_json(response_json)
18
- reg = u2f.register!(challenges, response)
19
- key = Key.new(handle: reg.key_handle, public_key: reg.public_key, counter: reg.counter)
46
+
47
+ def register!(challenge: self.challenge(), origin:, attestation_object:, client_data_json:)
48
+ attestation = WebAuthn::AuthenticatorAttestationResponse.new(
49
+ attestation_object: attestation_object,
50
+ client_data_json: client_data_json
51
+ )
52
+
53
+ unless attestation.valid?(challenge, origin, rp_id: rp_id)
54
+ raise InvalidAttestation, "invalid attestation"
55
+ end
56
+
57
+ key = Key.new(
58
+ type: 'webauthn',
59
+ handle: Base64.urlsafe_encode64(attestation.credential.id).gsub(/\r?\n|=+/,''),
60
+ user_handle: user_handle,
61
+ public_key: Base64.encode64(attestation.credential.public_key).gsub(/\r?\n/,''),
62
+ counter: attestation.authenticator_data.sign_count,
63
+ )
20
64
  if counter
21
65
  counter.store(key)
22
66
  end
@@ -1,3 +1,3 @@
1
1
  module Clarion
2
- VERSION = "0.3.0"
2
+ VERSION = "1.0.0"
3
3
  end
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: clarion
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sorah Fukumori
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-12-07 00:00:00.000000000 Z
11
+ date: 2018-10-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: u2f
14
+ name: webauthn
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: 1.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: 1.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: sinatra
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -129,6 +129,7 @@ executables: []
129
129
  extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
+ - ".gitattributes"
132
133
  - ".gitignore"
133
134
  - ".rspec"
134
135
  - ".travis.yml"
@@ -140,7 +141,6 @@ files:
140
141
  - app/public/register.js
141
142
  - app/public/sign.js
142
143
  - app/public/test.js
143
- - app/public/u2f-api.js
144
144
  - app/views/authn.erb
145
145
  - app/views/layout.erb
146
146
  - app/views/register.erb
@@ -155,6 +155,8 @@ files:
155
155
  - docs/api.md
156
156
  - docs/counters.md
157
157
  - docs/stores.md
158
+ - examples/pam-u2f/README.md
159
+ - examples/pam-u2f/pam-u2f.rb
158
160
  - lib/clarion.rb
159
161
  - lib/clarion/app.rb
160
162
  - lib/clarion/authenticator.rb
@@ -192,8 +194,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
192
194
  version: '0'
193
195
  requirements: []
194
196
  rubyforge_project:
195
- rubygems_version: 2.7.3
197
+ rubygems_version: 2.7.7
196
198
  signing_key:
197
199
  specification_version: 4
198
- summary: Web-based FIDO U2F Helper for CLI operations (SSH login...)
200
+ summary: Web-based WebAuthn (U2F) Helper for CLI operations (SSH login...)
199
201
  test_files: []
@@ -1,748 +0,0 @@
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, USB_INTERNAL}}
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
- };