clarion 0.3.0 → 1.0.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.
@@ -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
- };