clarion 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +21 -0
- data/README.md +66 -0
- data/Rakefile +6 -0
- data/app/public/register.js +82 -0
- data/app/public/sign.js +67 -0
- data/app/public/test.js +81 -0
- data/app/public/u2f-api.js +748 -0
- data/app/views/authn.erb +51 -0
- data/app/views/layout.erb +128 -0
- data/app/views/register.erb +55 -0
- data/app/views/test.erb +35 -0
- data/app/views/test_callback.erb +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/bin/test-authn +11 -0
- data/clarion.gemspec +32 -0
- data/config.ru +63 -0
- data/dev.rb +30 -0
- data/docs/api.md +113 -0
- data/docs/counters.md +14 -0
- data/docs/stores.md +13 -0
- data/lib/clarion.rb +3 -0
- data/lib/clarion/app.rb +271 -0
- data/lib/clarion/authenticator.rb +51 -0
- data/lib/clarion/authn.rb +106 -0
- data/lib/clarion/config.rb +58 -0
- data/lib/clarion/const_finder.rb +24 -0
- data/lib/clarion/counters.rb +9 -0
- data/lib/clarion/counters/base.rb +17 -0
- data/lib/clarion/counters/dynamodb.rb +45 -0
- data/lib/clarion/counters/memory.rb +29 -0
- data/lib/clarion/key.rb +76 -0
- data/lib/clarion/registrator.rb +26 -0
- data/lib/clarion/stores.rb +9 -0
- data/lib/clarion/stores/base.rb +17 -0
- data/lib/clarion/stores/memory.rb +30 -0
- data/lib/clarion/stores/s3.rb +54 -0
- data/lib/clarion/version.rb +3 -0
- metadata +199 -0
checksums.yaml
ADDED
@@ -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
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -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
|
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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).
|
data/Rakefile
ADDED
@@ -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
|
+
});
|
data/app/public/sign.js
ADDED
@@ -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
|
+
});
|
data/app/public/test.js
ADDED
@@ -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
|
+
};
|