clarion 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 699d1c5cabc3e667559acac59425771c17c7364b655655e072149648c4e23a8e
4
+ data.tar.gz: db1dd4fe19189a5b8169c218f0d62c7a82661c98ca766c964949aeb86152acd2
5
+ SHA512:
6
+ metadata.gz: f1d33dfe907acbda086bb486db4ae472ee6ae024ce893d988cebd4388a83e9bf97a4c7f4b85bfd12a729ec7389689c9e1bd9a91659aeacd6b0f9b067745c9de9
7
+ data.tar.gz: 25e19c67f5c4ec5c7b47616f7f4ea580b4514f3944b0f90b3dbe965030c3a4c937ba33f7d42eee1f68511f7e1a9d0c9fb897ca7157a8e137e30aa3da61ae0952
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.5.0
5
+ before_install: gem install bundler -v 1.16.0
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in clarion.gemspec
6
+ gemspec
@@ -0,0 +1,69 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ clarion (0.1.0)
5
+ aws-sdk-dynamodb
6
+ aws-sdk-s3
7
+ erubis
8
+ sinatra
9
+ u2f
10
+
11
+ GEM
12
+ remote: https://rubygems.org/
13
+ specs:
14
+ aws-partitions (1.45.0)
15
+ aws-sdk-core (3.11.0)
16
+ aws-partitions (~> 1.0)
17
+ aws-sigv4 (~> 1.0)
18
+ jmespath (~> 1.0)
19
+ aws-sdk-dynamodb (1.3.0)
20
+ aws-sdk-core (~> 3)
21
+ aws-sigv4 (~> 1.0)
22
+ aws-sdk-kms (1.3.0)
23
+ aws-sdk-core (~> 3)
24
+ aws-sigv4 (~> 1.0)
25
+ aws-sdk-s3 (1.8.0)
26
+ aws-sdk-core (~> 3)
27
+ aws-sdk-kms (~> 1)
28
+ aws-sigv4 (~> 1.0)
29
+ aws-sigv4 (1.0.2)
30
+ diff-lcs (1.3)
31
+ erubis (2.7.0)
32
+ jmespath (1.3.1)
33
+ mustermann (1.0.1)
34
+ rack (2.0.3)
35
+ rack-protection (2.0.0)
36
+ rack
37
+ rake (12.3.0)
38
+ rspec (3.7.0)
39
+ rspec-core (~> 3.7.0)
40
+ rspec-expectations (~> 3.7.0)
41
+ rspec-mocks (~> 3.7.0)
42
+ rspec-core (3.7.0)
43
+ rspec-support (~> 3.7.0)
44
+ rspec-expectations (3.7.0)
45
+ diff-lcs (>= 1.2.0, < 2.0)
46
+ rspec-support (~> 3.7.0)
47
+ rspec-mocks (3.7.0)
48
+ diff-lcs (>= 1.2.0, < 2.0)
49
+ rspec-support (~> 3.7.0)
50
+ rspec-support (3.7.0)
51
+ sinatra (2.0.0)
52
+ mustermann (~> 1.0)
53
+ rack (~> 2.0)
54
+ rack-protection (= 2.0.0)
55
+ tilt (~> 2.0)
56
+ tilt (2.0.8)
57
+ u2f (1.0.0)
58
+
59
+ PLATFORMS
60
+ ruby
61
+
62
+ DEPENDENCIES
63
+ bundler
64
+ clarion!
65
+ rake
66
+ rspec (~> 3.0)
67
+
68
+ BUNDLED WITH
69
+ 1.16.0
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Sorah Fukumori
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,66 @@
1
+ # Clarion: Web-based FIDO U2F helper for CLI operations (e.g. SSH Log in)
2
+
3
+ Clarion is a web-based frontend to allow remote,non-browser operations (CLI) to perform 2FA on their users.
4
+
5
+ ## How it works
6
+
7
+ Any software/scripts want to perform 2FA _(app)_ creates _a request_ on Clarion. Then _app_ requests user to visit a request specific path on Clarion.
8
+ Clarion then performs 2FA on behalf of _app,_ and finally returns an authentication result to _app._
9
+
10
+ Clarion also provides a way to retrieve user's U2F keyhandle and public key.
11
+
12
+ Note that Clarion itself doesn't manage users' keyhandle and public key. User information should be provided every time when requesting authentication.
13
+
14
+ ## Set up
15
+
16
+ Clarion is a Rack application. Docker image is also available.
17
+
18
+ See [config.ru](./config.ru) for detailed configuration. The following environment variable is supported by the bundled config.ru.
19
+
20
+ - `SECRET_KEY_BASE` (required)
21
+ - `CLARION_REGISTRATION_ALLOWED_URL` (required): Regexp that matches against URLs. Only matched URLs are allowed for key registration callback.
22
+ - `CLARION_AUTHN_DEFAULT_EXPIRES_IN` (default: `300`): authn lifetime in seconds.
23
+ - `CLARION_STORE` (required, default: `s3`): See [docs/stores.md](./docs/stores.md)
24
+ - S3 store:
25
+ - `CLARION_STORE_S3_BUCKET`
26
+ - `CLARION_STORE_S3_REGION`
27
+ - `CLARION_STORE_S3_PREFIX` (optional, recommended to end with `/`)
28
+ - `CLARION_COUNTER` (optional, default: `dynamodb`): See [docs/counters.md](./docs/counters.md)
29
+ - `CLARION_COUNTER_DYNAMODB_TABLE`
30
+ - `CLARION_COUNTER_DYNAMODB_REGION`
31
+
32
+
33
+ ## Usage
34
+
35
+ ### Real world example: SSH log in
36
+
37
+ TBD
38
+
39
+ ### Test implementation
40
+
41
+ Visit `/test` exists in your application. This endpoint doesn't work for multi-process/multi-threaded deployment.
42
+
43
+ See [app/public/test.erb](./app/public/test.erb), [app/public/test_callback.erb](./app/public/test_callback.erb), [app/public/test.js](./app/public/test.erb) for implementation.
44
+
45
+ ### API
46
+
47
+ See [docs/api.md](./docs/api.md)
48
+
49
+ ## Development
50
+
51
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
52
+
53
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
54
+
55
+ ## To-dos
56
+
57
+ - [ ] Write an integration test
58
+ - [ ] Write a unit test
59
+
60
+ ## Contributing
61
+
62
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sorah/clarion.
63
+
64
+ ## License
65
+
66
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+
3
+ document.addEventListener("DOMContentLoaded", function() {
4
+ let processionElem = document.getElementById("procession");
5
+
6
+ let handleUnsupported = () => {
7
+ processionElem.className = 'procession_unsupported';
8
+ };
9
+ let unsupportedTimer = setTimeout(handleUnsupported, 3000);
10
+
11
+ window.u2f.getApiVersion((ver) => {
12
+ console.log(ver);
13
+ clearTimeout(unsupportedTimer);
14
+ let appId = processionElem.attributes['data-app-id'].value;
15
+ let regId = processionElem.attributes['data-reg-id'].value;
16
+ let requests = JSON.parse(processionElem.attributes['data-requests'].value);
17
+ let state = processionElem.attributes['data-state'].value;
18
+ let callbackUrl = processionElem.attributes['data-callback'].value;
19
+
20
+
21
+ let processCallback = (json) => {
22
+ processionElem.className = 'procession_ok';
23
+
24
+ if (callbackUrl.match(/^js:/)) {
25
+ if (!window.opener) {
26
+ console.log("window.opener is not truthy")
27
+ processionElem.className = 'procession_error';
28
+ return;
29
+ }
30
+ window.opener.postMessage({clarion_key: {state: state, data: json.encrypted_key}}, callbackUrl.slice(3));
31
+ window.close();
32
+ } else {
33
+ let form = document.getElementById("callback_form");
34
+ form.action = callbackUrl;
35
+ form.querySelector('[name=data]').value = json.encrypted_key;
36
+ form.submit();
37
+ }
38
+ }
39
+
40
+ let cb = (response) => {
41
+ console.log(response);
42
+
43
+ if (response.errorCode) {
44
+ processionElem.className = 'procession_error';
45
+ return;
46
+ }
47
+ processionElem.className = 'procession_contact';
48
+
49
+ let payload = JSON.stringify({
50
+ reg_id: regId,
51
+ response: JSON.stringify(response),
52
+ });
53
+
54
+ let handleError = (err) => {
55
+ console.log(err);
56
+ processionElem.className = 'procession_error';
57
+ };
58
+
59
+ fetch(`/ui/register`, {credentials: 'include', method: 'POST', body: payload}).then((resp) => {
60
+ console.log(resp);
61
+ if (!resp.ok) {
62
+ processionElem.className = 'procession_error';
63
+ return;
64
+ }
65
+ return resp.json().then((json) => {
66
+ console.log(json);
67
+ if (json.ok) {
68
+ processCallback(json);
69
+ } else {
70
+ processionElem.className = 'procession_error';
71
+ }
72
+ });
73
+ }).catch(handleError);
74
+ };
75
+
76
+ processionElem.className = 'procession_wait';
77
+ console.log(requests);
78
+ window.u2f.register(appId, requests, [], cb, 300000);
79
+ });
80
+
81
+
82
+ });
@@ -0,0 +1,67 @@
1
+ "use strict";
2
+
3
+ document.addEventListener("DOMContentLoaded", function() {
4
+ let processionElem = document.getElementById("procession");
5
+
6
+ let handleUnsupported = () => {
7
+ processionElem.className = 'procession_unsupported';
8
+ };
9
+ let unsupportedTimer = setTimeout(handleUnsupported, 3000);
10
+
11
+ window.u2f.getApiVersion((ver) => {
12
+ console.log(ver);
13
+ clearTimeout(unsupportedTimer);
14
+
15
+ let authnId = processionElem.attributes['data-authn-id'].value;
16
+ let appId = processionElem.attributes['data-app-id'].value;
17
+ let reqId = processionElem.attributes['data-req-id'].value;
18
+ let requests = JSON.parse(processionElem.attributes['data-requests'].value);
19
+ let challenge = JSON.parse(processionElem.attributes['data-challenge'].value);
20
+
21
+
22
+ let processCallback = (json) => {
23
+ processionElem.className = 'procession_ok';
24
+ }
25
+
26
+ let cb = (response) => {
27
+ console.log(response);
28
+
29
+ if (response.errorCode) {
30
+ processionElem.className = 'procession_error';
31
+ return;
32
+ }
33
+ processionElem.className = 'procession_contact';
34
+
35
+ let payload = JSON.stringify({
36
+ req_id: reqId,
37
+ response: JSON.stringify(response),
38
+ });
39
+
40
+ let handleError = (err) => {
41
+ console.log(err);
42
+ processionElem.className = 'procession_error';
43
+ };
44
+
45
+ fetch(`/ui/verify/${authnId}`, {credentials: 'include', method: 'POST', body: payload}).then((resp) => {
46
+ console.log(resp);
47
+ if (!resp.ok) {
48
+ processionElem.className = 'procession_error';
49
+ return;
50
+ }
51
+ return resp.json().then((json) => {
52
+ console.log(json);
53
+ if (json.ok) {
54
+ processCallback(json);
55
+ } else {
56
+ processionElem.className = 'procession_error';
57
+ }
58
+ });
59
+ }).catch(handleError);
60
+ };
61
+
62
+ processionElem.className = 'procession_wait';
63
+ window.u2f.sign(appId, challenge, requests, cb, 300000);
64
+ });
65
+
66
+
67
+ });
@@ -0,0 +1,81 @@
1
+ "use strict";
2
+
3
+ document.addEventListener("DOMContentLoaded", function() {
4
+ let elem = document.getElementById('authn_test');
5
+ let key = JSON.parse(elem.attributes['data-key'].value);
6
+ var authnId = null;
7
+
8
+ var status = {};
9
+ let updateView = function () {
10
+ elem.innerHTML = JSON.stringify(status, null, 2);
11
+ }
12
+
13
+ setInterval(function() {
14
+ if (!authnId) return;
15
+
16
+ let handleError = (err) => {
17
+ status.error = 'authn get error (fetch)';
18
+ updateView();
19
+ console.log(err);
20
+ };
21
+
22
+ fetch(status.authn.url, {credentials: 'include'}).then((resp) => {
23
+ console.log(resp);
24
+ if (!resp.ok) {
25
+ status.error = 'authn get error (!ok)';
26
+ updateView();
27
+ return;
28
+ }
29
+ return resp.json().then((json) => {
30
+ console.log(json);
31
+ if (json.authn) {
32
+ status.authn = json.authn;
33
+ if (status.authn.status == 'verified') {
34
+ authnId = null;
35
+ }
36
+ } else {
37
+ status.error = 'authn get error';
38
+ }
39
+ updateView();
40
+ });
41
+ }).catch(handleError);
42
+ }, 1000);
43
+
44
+ document.getElementById("open_authn_button").addEventListener("click", function() {
45
+ window.open(status.authn.html_url, '_blank');
46
+ });
47
+
48
+ document.getElementById("start_authn_button").addEventListener("click", function() {
49
+ let payload = JSON.stringify({
50
+ name: "testuser",
51
+ comment: "test authn",
52
+ keys: [key],
53
+ });
54
+
55
+ let handleError = (err) => {
56
+ status.error = 'authn create error (fetch)';
57
+ updateView();
58
+ console.log(err);
59
+ };
60
+
61
+ fetch(`/api/authn`, {credentials: 'include', method: 'POST', body: payload}).then((resp) => {
62
+ console.log(resp);
63
+ if (!resp.ok) {
64
+ status.error = 'authn create error (!ok)';
65
+ updateView();
66
+ return;
67
+ }
68
+ return resp.json().then((json) => {
69
+ console.log(json);
70
+ if (json.authn) {
71
+ status.authn = json.authn;
72
+ authnId = json.authn.id;
73
+ document.getElementById("open_authn_button").className = '';
74
+ } else {
75
+ status.error = 'authn create error';
76
+ }
77
+ updateView();
78
+ });
79
+ }).catch(handleError);
80
+ });
81
+ });
@@ -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, 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
+ };