FIDO U2F (Universal 2nd factor) component extension
NOTRY=true
BUG=364678
TBR=miket
Review URL: https://codereview.chromium.org/249913002
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@266655 0039d316-1c4b-4281-b951-d872f2087c98
diff --git a/chrome/browser/browser_resources.grd b/chrome/browser/browser_resources.grd
index 24bc6d93..4e2fd43 100644
--- a/chrome/browser/browser_resources.grd
+++ b/chrome/browser/browser_resources.grd
@@ -288,6 +288,7 @@
<include name="IDR_WEBRTC_LOGS_JS" file="resources\media\webrtc_logs.js" type="BINDATA" />
</if>
<include name="IDR_WEBSTORE_MANIFEST" file="resources\webstore_app\manifest.json" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_MANIFEST" file="resources\cryptotoken\manifest.json" type="BINDATA" />
<include name="IDR_GAIA_AUTH_MANIFEST" file="resources\gaia_auth\manifest.json" type="BINDATA" />
<include name="IDR_GAIA_AUTH_KEYBOARD_MANIFEST" file="resources\gaia_auth\manifest_keyboard.json" type="BINDATA" />
<include name="IDR_GAIA_AUTH_SAML_MANIFEST" file="resources\gaia_auth\manifest_saml.json" type="BINDATA" />
diff --git a/chrome/browser/extensions/component_loader.cc b/chrome/browser/extensions/component_loader.cc
index 105118e..3b7647e 100644
--- a/chrome/browser/extensions/component_loader.cc
+++ b/chrome/browser/extensions/component_loader.cc
@@ -594,6 +594,9 @@
Add(IDR_PDF_MANIFEST, base::FilePath(FILE_PATH_LITERAL("pdf")));
}
#endif
+
+ Add(IDR_CRYPTOTOKEN_MANIFEST,
+ base::FilePath(FILE_PATH_LITERAL("cryptotoken")));
}
void ComponentLoader::UnloadComponent(ComponentExtensionInfo* component) {
diff --git a/chrome/browser/resources/component_extension_resources.grd b/chrome/browser/resources/component_extension_resources.grd
index dddfd184..09c237d 100644
--- a/chrome/browser/resources/component_extension_resources.grd
+++ b/chrome/browser/resources/component_extension_resources.grd
@@ -149,6 +149,33 @@
<include name="IDR_PDF_BUTTON_HIGH_5" file="pdf/html_office/elements/viewer-button/img/hiDPI/button_save.png" type="BINDATA" />
<include name="IDR_PDF_BUTTON_HIGH_6" file="pdf/html_office/elements/viewer-button/img/hiDPI/button_print.png" type="BINDATA" />
</if>
+ <include name="IDR_CRYPTOTOKEN_UTIL_JS" file="cryptotoken/util.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_B64_JS" file="cryptotoken/b64.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_CLOSEABLE_JS" file="cryptotoken/closeable.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_COUNTDOWN_JS" file="cryptotoken/countdown.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_SHA256_JS" file="cryptotoken/sha256.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_LLGNUBBY_JS" file="cryptotoken/llgnubby.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_LLHIDGNUBBY_JS" file="cryptotoken/llhidgnubby.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_LLUSBGNUBBY_JS" file="cryptotoken/llusbgnubby.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_GNUBBIES_JS" file="cryptotoken/gnubbies.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_GNUBBY_JS" file="cryptotoken/gnubby.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_GNUBBY_U2F_JS" file="cryptotoken/gnubby-u2f.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_GNUBBYCODETYPES_JS" file="cryptotoken/gnubbycodetypes.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_GNUBBYFACTORY_JS" file="cryptotoken/gnubbyfactory.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_GNUBBYMSGTYPES_JS" file="cryptotoken/gnubbymsgtypes.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_USBGNUBBYFACTORY_JS" file="cryptotoken/usbgnubbyfactory.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_DEVICESTATUSCODES_JS" file="cryptotoken/devicestatuscodes.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_ENROLLER_JS" file="cryptotoken/enroller.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_ENROLLHELPER_JS" file="cryptotoken/enrollhelper.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_USBENROLLHELPER_JS" file="cryptotoken/usbenrollhelper.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_REQUESTQUEUE_JS" file="cryptotoken/requestqueue.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_SIGNER_JS" file="cryptotoken/signer.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_SIGNHELPER_JS" file="cryptotoken/signhelper.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_SINGLESIGNER_JS" file="cryptotoken/singlesigner.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_MULTIPLESIGNER_JS" file="cryptotoken/multiplesigner.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_USBSIGNHELPER_JS" file="cryptotoken/usbsignhelper.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_WEBREQUEST_JS" file="cryptotoken/webrequest.js" type="BINDATA" />
+ <include name="IDR_CRYPTOTOKEN_BACKGROUND_JS" file="cryptotoken/background.js" type="BINDATA" />
</includes>
</release>
</grit>
diff --git a/chrome/browser/resources/cryptotoken/b64.js b/chrome/browser/resources/cryptotoken/b64.js
new file mode 100644
index 0000000..21140be
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/b64.js
@@ -0,0 +1,89 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// WebSafeBase64Escape and Unescape.
+// [email protected]
+function B64_encode(bytes, opt_length) {
+ if (!opt_length) opt_length = bytes.length;
+ var b64out =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_';
+ var result = '';
+ var shift = 0;
+ var accu = 0;
+ var input_index = 0;
+ while (opt_length--) {
+ accu <<= 8;
+ accu |= bytes[input_index++];
+ shift += 8;
+ while (shift >= 6) {
+ var i = (accu >> (shift - 6)) & 63;
+ result += b64out.charAt(i);
+ shift -= 6;
+ }
+ }
+ if (shift) {
+ accu <<= 8;
+ shift += 8;
+ var i = (accu >> (shift - 6)) & 63;
+ result += b64out.charAt(i);
+ }
+ return result;
+}
+
+// Normal base64 encode; not websafe, including padding.
+function base64_encode(bytes, opt_length) {
+ if (!opt_length) opt_length = bytes.length;
+ var b64out =
+ 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
+ var result = '';
+ var shift = 0;
+ var accu = 0;
+ var input_index = 0;
+ while (opt_length--) {
+ accu <<= 8;
+ accu |= bytes[input_index++];
+ shift += 8;
+ while (shift >= 6) {
+ var i = (accu >> (shift - 6)) & 63;
+ result += b64out.charAt(i);
+ shift -= 6;
+ }
+ }
+ if (shift) {
+ accu <<= 8;
+ shift += 8;
+ var i = (accu >> (shift - 6)) & 63;
+ result += b64out.charAt(i);
+ }
+ while (result.length % 4) result += '=';
+ return result;
+}
+
+var B64_inmap =
+[
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 63, 0, 0,
+ 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0,
+ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+ 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 0, 0, 0, 0, 64,
+ 0, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
+ 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 0, 0, 0, 0, 0
+];
+
+function B64_decode(string) {
+ var bytes = [];
+ var accu = 0;
+ var shift = 0;
+ for (var i = 0; i < string.length; ++i) {
+ var c = string.charCodeAt(i);
+ if (c < 32 || c > 127 || !B64_inmap[c - 32]) return [];
+ accu <<= 6;
+ accu |= (B64_inmap[c - 32] - 1);
+ shift += 6;
+ if (shift >= 8) {
+ bytes.push((accu >> (shift - 8)) & 255);
+ shift -= 8;
+ }
+ }
+ return bytes;
+}
diff --git a/chrome/browser/resources/cryptotoken/background.js b/chrome/browser/resources/cryptotoken/background.js
new file mode 100644
index 0000000..98496e0
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/background.js
@@ -0,0 +1,85 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview U2F gnubbyd background page
+ */
+
+'use strict';
+
+// Singleton tracking available devices.
+var gnubbies = new Gnubbies();
+llUsbGnubby.register(gnubbies);
+// Only include HID support if it's available in this browser.
+if (chrome.hid) {
+ llHidGnubby.register(gnubbies);
+}
+
+var GNUBBY_FACTORY = new UsbGnubbyFactory(gnubbies);
+var SIGN_HELPER_FACTORY = new UsbSignHelperFactory(GNUBBY_FACTORY);
+var ENROLL_HELPER_FACTORY = new UsbEnrollHelperFactory(GNUBBY_FACTORY);
+
+/**
+ * @param {boolean} toleratesMultipleResponses Whether the web page can handle
+ * multiple responses given to its sendResponse callback.
+ * @param {Object} request
+ * @param {MessageSender} sender
+ * @param {Function} sendResponse
+ * @return {Closeable}
+ */
+function handleWebPageRequest(toleratesMultipleResponses, request, sender,
+ sendResponse) {
+ var enforceAppIdValid = true;
+ switch (request.type) {
+ case GnubbyMsgTypes.ENROLL_WEB_REQUEST:
+ return handleEnrollRequest(ENROLL_HELPER_FACTORY, sender, request,
+ enforceAppIdValid, sendResponse, toleratesMultipleResponses);
+
+ case GnubbyMsgTypes.SIGN_WEB_REQUEST:
+ return handleSignRequest(SIGN_HELPER_FACTORY, sender, request,
+ enforceAppIdValid, sendResponse, toleratesMultipleResponses);
+
+ default:
+ var response = formatWebPageResponse(
+ GnubbyMsgTypes.ENROLL_WEB_REPLY, GnubbyCodeTypes.BAD_REQUEST);
+ sendResponse(response);
+ return null;
+ }
+}
+
+// Message handler for requests coming from web pages.
+function messageHandler(toleratesMultipleResponses, request, sender,
+ sendResponse) {
+ console.log(UTIL_fmt('onMessageExternal listener: ' + request.type));
+ console.log(UTIL_fmt('request'));
+ console.log(request);
+ console.log(UTIL_fmt('sender'));
+ console.log(sender);
+
+ handleWebPageRequest(toleratesMultipleResponses, request, sender,
+ sendResponse);
+ return true;
+}
+
+// Listen to web pages.
+chrome.runtime.onMessageExternal.addListener(messageHandler.bind(null, false));
+
+// List to connection events, and wire up a message handler on the port.
+chrome.runtime.onConnectExternal.addListener(function(port) {
+ var closeable;
+ port.onMessage.addListener(function(request) {
+ var toleratesMultipleResponses = true;
+ closeable = handleWebPageRequest(toleratesMultipleResponses, request,
+ port.sender,
+ function(response) {
+ response['requestId'] = request['requestId'];
+ port.postMessage(response);
+ });
+ });
+ port.onDisconnect.addListener(function() {
+ if (closeable) {
+ closeable.close();
+ }
+ });
+});
diff --git a/chrome/browser/resources/cryptotoken/closeable.js b/chrome/browser/resources/cryptotoken/closeable.js
new file mode 100644
index 0000000..dda2d6d
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/closeable.js
@@ -0,0 +1,18 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Defines a Closeable interface.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * A closeable interface.
+ * @interface
+ */
+function Closeable() {}
+
+/** Closes this object. */
+Closeable.prototype.close = function() {};
diff --git a/chrome/browser/resources/cryptotoken/countdown.js b/chrome/browser/resources/cryptotoken/countdown.js
new file mode 100644
index 0000000..b9e14eb1
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/countdown.js
@@ -0,0 +1,125 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Provides a countdown-based timer.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * A countdown timer.
+ * @interface
+ */
+function Countdown() {}
+
+/**
+ * Sets a new timeout for this timer.
+ * @param {number} timeoutMillis how long, in milliseconds, the countdown lasts.
+ * @param {Function=} cb called back when the countdown expires.
+ * @return {boolean} whether the timeout could be set.
+ */
+Countdown.prototype.setTimeout = function(timeoutMillis, cb) {};
+
+/** Clears this timer's timeout. */
+Countdown.prototype.clearTimeout = function() {};
+
+/**
+ * @return {number} how many milliseconds are remaining until the timer expires.
+ */
+Countdown.prototype.millisecondsUntilExpired = function() {};
+
+/** @return {boolean} whether the timer has expired. */
+Countdown.prototype.expired = function() {};
+
+/**
+ * Constructs a new clone of this timer, while overriding its callback.
+ * @param {Function=} cb callback for new timer.
+ * @return {!Countdown} new clone.
+ */
+Countdown.prototype.clone = function(cb) {};
+
+/**
+ * Constructs a new timer. The timer has a very limited resolution, and does
+ * not attempt to be millisecond accurate. Its intended use is as a
+ * low-precision timer that pauses while debugging.
+ * @param {number=} timeoutMillis how long, in milliseconds, the countdown
+ * lasts.
+ * @param {Function=} cb called back when the countdown expires.
+ * @constructor
+ * @implements {Countdown}
+ */
+function CountdownTimer(timeoutMillis, cb) {
+ this.remainingMillis = 0;
+ this.setTimeout(timeoutMillis || 0, cb);
+}
+
+CountdownTimer.TIMER_INTERVAL_MILLIS = 200;
+
+/**
+ * Sets a new timeout for this timer. Only possible if the timer is not
+ * currently active.
+ * @param {number} timeoutMillis how long, in milliseconds, the countdown lasts.
+ * @param {Function=} cb called back when the countdown expires.
+ * @return {boolean} whether the timeout could be set.
+ */
+CountdownTimer.prototype.setTimeout = function(timeoutMillis, cb) {
+ if (this.timeoutId)
+ return false;
+ if (!timeoutMillis || timeoutMillis < 0)
+ return false;
+ this.remainingMillis = timeoutMillis;
+ this.cb = cb;
+ if (this.remainingMillis > CountdownTimer.TIMER_INTERVAL_MILLIS) {
+ this.timeoutId =
+ window.setInterval(this.timerTick.bind(this),
+ CountdownTimer.TIMER_INTERVAL_MILLIS);
+ } else {
+ // Set a one-shot timer for the last interval.
+ this.timeoutId =
+ window.setTimeout(this.timerTick.bind(this), this.remainingMillis);
+ }
+ return true;
+};
+
+/** Clears this timer's timeout. */
+CountdownTimer.prototype.clearTimeout = function() {
+ if (this.timeoutId) {
+ window.clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+ }
+};
+
+/**
+ * @return {number} how many milliseconds are remaining until the timer expires.
+ */
+CountdownTimer.prototype.millisecondsUntilExpired = function() {
+ return this.remainingMillis > 0 ? this.remainingMillis : 0;
+};
+
+/** @return {boolean} whether the timer has expired. */
+CountdownTimer.prototype.expired = function() {
+ return this.remainingMillis <= 0;
+};
+
+/**
+ * Constructs a new clone of this timer, while overriding its callback.
+ * @param {Function=} cb callback for new timer.
+ * @return {!Countdown} new clone.
+ */
+CountdownTimer.prototype.clone = function(cb) {
+ return new CountdownTimer(this.remainingMillis, cb);
+};
+
+/** Timer callback. */
+CountdownTimer.prototype.timerTick = function() {
+ this.remainingMillis -= CountdownTimer.TIMER_INTERVAL_MILLIS;
+ if (this.expired()) {
+ window.clearTimeout(this.timeoutId);
+ this.timeoutId = undefined;
+ if (this.cb) {
+ this.cb();
+ }
+ }
+};
diff --git a/chrome/browser/resources/cryptotoken/devicestatuscodes.js b/chrome/browser/resources/cryptotoken/devicestatuscodes.js
new file mode 100644
index 0000000..2bc2a1e
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/devicestatuscodes.js
@@ -0,0 +1,51 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview This file defines the status codes returned by the device.
+ */
+
+var DeviceStatusCodes = {};
+
+/**
+ * Device operation succeeded.
+ * @const
+ */
+DeviceStatusCodes.OK_STATUS = 0;
+
+/**
+ * Device operation wait touch status.
+ * @const
+ */
+DeviceStatusCodes.WAIT_TOUCH_STATUS = 0x6985;
+
+/**
+ * Device operation invalid data status.
+ * @const
+ */
+DeviceStatusCodes.INVALID_DATA_STATUS = 0x6984;
+
+/**
+ * Device operation wrong data status.
+ * @const
+ */
+DeviceStatusCodes.WRONG_DATA_STATUS = 0x6a80;
+
+/**
+ * Device operation timeout status.
+ * @const
+ */
+DeviceStatusCodes.TIMEOUT_STATUS = -5;
+
+/**
+ * Device operation busy status.
+ * @const
+ */
+DeviceStatusCodes.BUSY_STATUS = -6;
+
+/**
+ * Device removed status.
+ * @const
+ */
+DeviceStatusCodes.GONE_STATUS = -8;
diff --git a/chrome/browser/resources/cryptotoken/enroller.js b/chrome/browser/resources/cryptotoken/enroller.js
new file mode 100644
index 0000000..71a8ff05
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/enroller.js
@@ -0,0 +1,570 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Handles web page requests for gnubby enrollment.
+ * @author [email protected] (Juan Lang)
+ */
+
+'use strict';
+
+/**
+ * Handles an enroll request.
+ * @param {!EnrollHelperFactory} factory Factory to create an enroll helper.
+ * @param {MessageSender} sender The sender of the message.
+ * @param {Object} request The web page's enroll request.
+ * @param {boolean} enforceAppIdValid Whether to enforce that the appId in the
+ * request matches the sender's origin.
+ * @param {Function} sendResponse Called back with the result of the enroll.
+ * @param {boolean} toleratesMultipleResponses Whether the sendResponse
+ * callback can be called more than once, e.g. for progress updates.
+ * @return {Closeable}
+ */
+function handleEnrollRequest(factory, sender, request, enforceAppIdValid,
+ sendResponse, toleratesMultipleResponses) {
+ var sentResponse = false;
+ function sendResponseOnce(r) {
+ if (enroller) {
+ enroller.close();
+ enroller = null;
+ }
+ if (!sentResponse) {
+ sentResponse = true;
+ try {
+ // If the page has gone away or the connection has otherwise gone,
+ // sendResponse fails.
+ sendResponse(r);
+ } catch (exception) {
+ console.warn('sendResponse failed: ' + exception);
+ }
+ } else {
+ console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
+ }
+ }
+
+ function sendErrorResponse(code) {
+ console.log(UTIL_fmt('code=' + code));
+ var response = formatWebPageResponse(GnubbyMsgTypes.ENROLL_WEB_REPLY, code);
+ if (request['requestId']) {
+ response['requestId'] = request['requestId'];
+ }
+ sendResponseOnce(response);
+ }
+
+ var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
+ if (!origin) {
+ sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
+ return null;
+ }
+
+ if (!isValidEnrollRequest(request)) {
+ sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
+ return null;
+ }
+
+ var signData = request['signData'];
+ var enrollChallenges = request['enrollChallenges'];
+ var logMsgUrl = request['logMsgUrl'];
+ var timeoutMillis = Enroller.DEFAULT_TIMEOUT_MILLIS;
+ if (request['timeout']) {
+ // Request timeout is in seconds.
+ timeoutMillis = request['timeout'] * 1000;
+ }
+
+ function findChallengeOfVersion(enrollChallenges, version) {
+ for (var i = 0; i < enrollChallenges.length; i++) {
+ if (enrollChallenges[i]['version'] == version) {
+ return enrollChallenges[i];
+ }
+ }
+ return null;
+ }
+
+ function sendSuccessResponse(u2fVersion, info, browserData) {
+ var enrollChallenge = findChallengeOfVersion(enrollChallenges, u2fVersion);
+ if (!enrollChallenge) {
+ sendErrorResponse(GnubbyCodeTypes.UNKNOWN_ERROR);
+ return;
+ }
+ var enrollUpdateData = {};
+ enrollUpdateData['enrollData'] = info;
+ // Echo the used challenge back in the reply.
+ for (var k in enrollChallenge) {
+ enrollUpdateData[k] = enrollChallenge[k];
+ }
+ if (u2fVersion == 'U2F_V2') {
+ // For U2F_V2, the challenge sent to the gnubby is modified to be the
+ // hash of the browser data. Include the browser data.
+ enrollUpdateData['browserData'] = browserData;
+ }
+ var response = formatWebPageResponse(
+ GnubbyMsgTypes.ENROLL_WEB_REPLY, GnubbyCodeTypes.OK, enrollUpdateData);
+ sendResponseOnce(response);
+ }
+
+ function sendNotification(code) {
+ console.log(UTIL_fmt('notification, code=' + code));
+ // Can the callback handle progress updates? If so, send one.
+ if (toleratesMultipleResponses) {
+ var response = formatWebPageResponse(
+ GnubbyMsgTypes.ENROLL_WEB_NOTIFICATION, code);
+ if (request['requestId']) {
+ response['requestId'] = request['requestId'];
+ }
+ sendResponse(response);
+ }
+ }
+
+ var timer = new CountdownTimer(timeoutMillis);
+ var enroller = new Enroller(factory, timer, origin, sendErrorResponse,
+ sendSuccessResponse, sendNotification, sender.tlsChannelId, logMsgUrl);
+ enroller.doEnroll(enrollChallenges, signData, enforceAppIdValid);
+ return /** @type {Closeable} */ (enroller);
+}
+
+/**
+ * Returns whether the request appears to be a valid enroll request.
+ * @param {Object} request the request.
+ * @return {boolean} whether the request appears valid.
+ */
+function isValidEnrollRequest(request) {
+ if (!request.hasOwnProperty('enrollChallenges'))
+ return false;
+ var enrollChallenges = request['enrollChallenges'];
+ if (!enrollChallenges.length)
+ return false;
+ var seenVersions = {};
+ for (var i = 0; i < enrollChallenges.length; i++) {
+ var enrollChallenge = enrollChallenges[i];
+ var version = enrollChallenge['version'];
+ if (!version) {
+ // Version is implicitly V1 if not specified.
+ version = 'U2F_V1';
+ }
+ if (version != 'U2F_V1' && version != 'U2F_V2') {
+ return false;
+ }
+ if (seenVersions[version]) {
+ // Each version can appear at most once.
+ return false;
+ }
+ seenVersions[version] = version;
+ if (!enrollChallenge['appId']) {
+ return false;
+ }
+ if (!enrollChallenge['challenge']) {
+ // The challenge is required.
+ return false;
+ }
+ }
+ var signData = request['signData'];
+ // An empty signData is ok, in the case the user is not already enrolled.
+ if (signData && !isValidSignData(signData))
+ return false;
+ return true;
+}
+
+/**
+ * Creates a new object to track enrolling with a gnubby.
+ * @param {!EnrollHelperFactory} helperFactory factory to create an enroll
+ * helper.
+ * @param {!Countdown} timer Timer for enroll request.
+ * @param {string} origin The origin making the request.
+ * @param {function(number)} errorCb Called upon enroll failure with an error
+ * code.
+ * @param {function(string, string, (string|undefined))} successCb Called upon
+ * enroll success with the version of the succeeding gnubby, the enroll
+ * data, and optionally the browser data associated with the enrollment.
+ * @param {(function(number)|undefined)} opt_progressCb Called with progress
+ * updates to the enroll request.
+ * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
+ * making the request.
+ * @param {string=} opt_logMsgUrl The url to post log messages to.
+ * @constructor
+ */
+function Enroller(helperFactory, timer, origin, errorCb, successCb,
+ opt_progressCb, opt_tlsChannelId, opt_logMsgUrl) {
+ /** @private {Countdown} */
+ this.timer_ = timer;
+ /** @private {string} */
+ this.origin_ = origin;
+ /** @private {function(number)} */
+ this.errorCb_ = errorCb;
+ /** @private {function(string, string, (string|undefined))} */
+ this.successCb_ = successCb;
+ /** @private {(function(number)|undefined)} */
+ this.progressCb_ = opt_progressCb;
+ /** @private {string|undefined} */
+ this.tlsChannelId_ = opt_tlsChannelId;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+
+ /** @private {boolean} */
+ this.done_ = false;
+ /** @private {number|undefined} */
+ this.lastProgressUpdate_ = undefined;
+
+ /** @private {Object.<string, string>} */
+ this.browserData_ = {};
+ /** @private {Array.<EnrollHelperChallenge>} */
+ this.encodedEnrollChallenges_ = [];
+ /** @private {Array.<SignHelperChallenge>} */
+ this.encodedSignChallenges_ = [];
+ // Allow http appIds for http origins. (Broken, but the caller deserves
+ // what they get.)
+ /** @private {boolean} */
+ this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
+
+ /** @private {EnrollHelper} */
+ this.helper_ = helperFactory.createHelper(timer,
+ this.helperError_.bind(this), this.helperSuccess_.bind(this),
+ this.helperProgress_.bind(this));
+}
+
+/**
+ * Default timeout value in case the caller never provides a valid timeout.
+ */
+Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
+
+/**
+ * Performs an enroll request with the given enroll and sign challenges.
+ * @param {Array.<Object>} enrollChallenges
+ * @param {Array.<Object>} signChallenges
+ * @param {boolean} enforceAppIdValid
+ */
+Enroller.prototype.doEnroll =
+ function(enrollChallenges, signChallenges, enforceAppIdValid) {
+ this.setEnrollChallenges_(enrollChallenges);
+ this.setSignChallenges_(signChallenges);
+
+ if (!enforceAppIdValid) {
+ // If not enforcing app id validity, begin enrolling right away.
+ this.helper_.doEnroll(this.encodedEnrollChallenges_,
+ this.encodedSignChallenges_);
+ }
+ // Whether or not enforcing app id validity, begin fetching/checking the
+ // app ids.
+ var enrollAppIds = [];
+ for (var i = 0; i < enrollChallenges.length; i++) {
+ enrollAppIds.push(enrollChallenges[i]['appId']);
+ }
+ var self = this;
+ this.checkAppIds_(enrollAppIds, signChallenges, function(result) {
+ if (!enforceAppIdValid) {
+ // Nothing to do, move along.
+ return;
+ }
+ if (result) {
+ self.helper_.doEnroll(self.encodedEnrollChallenges_,
+ self.encodedSignChallenges_);
+ } else {
+ self.notifyError_(GnubbyCodeTypes.BAD_APP_ID);
+ }
+ });
+};
+
+/**
+ * Encodes the enroll challenges for use by an enroll helper.
+ * @param {Array.<Object>} enrollChallenges
+ * @return {Array.<EnrollHelperChallenge>} the encoded challenges.
+ * @private
+ */
+Enroller.encodeEnrollChallenges_ = function(enrollChallenges) {
+ var encodedChallenges = [];
+ for (var i = 0; i < enrollChallenges.length; i++) {
+ var enrollChallenge = enrollChallenges[i];
+ var encodedChallenge = {};
+ var version;
+ if (enrollChallenge['version']) {
+ version = enrollChallenge['version'];
+ } else {
+ // Version is implicitly V1 if not specified.
+ version = 'U2F_V1';
+ }
+ encodedChallenge['version'] = version;
+ encodedChallenge['challenge'] = enrollChallenge['challenge'];
+ encodedChallenge['appIdHash'] =
+ B64_encode(sha256HashOfString(enrollChallenge['appId']));
+ encodedChallenges.push(encodedChallenge);
+ }
+ return encodedChallenges;
+};
+
+/**
+ * Sets this enroller's enroll challenges.
+ * @param {Array.<Object>} enrollChallenges The enroll challenges.
+ * @private
+ */
+Enroller.prototype.setEnrollChallenges_ = function(enrollChallenges) {
+ var challenges = [];
+ for (var i = 0; i < enrollChallenges.length; i++) {
+ var enrollChallenge = enrollChallenges[i];
+ var version = enrollChallenge.version;
+ if (!version) {
+ // Version is implicitly V1 if not specified.
+ version = 'U2F_V1';
+ }
+
+ if (version == 'U2F_V2') {
+ var modifiedChallenge = {};
+ for (var k in enrollChallenge) {
+ modifiedChallenge[k] = enrollChallenge[k];
+ }
+ // V2 enroll responses contain signatures over a browser data object,
+ // which we're constructing here. The browser data object contains, among
+ // other things, the server challenge.
+ var serverChallenge = enrollChallenge['challenge'];
+ var browserData = makeEnrollBrowserData(
+ serverChallenge, this.origin_, this.tlsChannelId_);
+ // Replace the challenge with the hash of the browser data.
+ modifiedChallenge['challenge'] =
+ B64_encode(sha256HashOfString(browserData));
+ this.browserData_[version] =
+ B64_encode(UTIL_StringToBytes(browserData));
+ challenges.push(modifiedChallenge);
+ } else {
+ challenges.push(enrollChallenge);
+ }
+ }
+ // Store the encoded challenges for use by the enroll helper.
+ this.encodedEnrollChallenges_ =
+ Enroller.encodeEnrollChallenges_(challenges);
+};
+
+/**
+ * Sets this enroller's sign data.
+ * @param {Array=} signData the sign challenges to add.
+ * @private
+ */
+Enroller.prototype.setSignChallenges_ = function(signData) {
+ this.encodedSignChallenges_ = [];
+ if (signData) {
+ for (var i = 0; i < signData.length; i++) {
+ var incomingChallenge = signData[i];
+ var serverChallenge = incomingChallenge['challenge'];
+ var appId = incomingChallenge['appId'];
+ var encodedKeyHandle = incomingChallenge['keyHandle'];
+
+ var challenge = makeChallenge(serverChallenge, appId, encodedKeyHandle,
+ incomingChallenge['version']);
+
+ this.encodedSignChallenges_.push(challenge);
+ }
+ }
+};
+
+/**
+ * Checks the app ids associated with this enroll request, and calls a callback
+ * with the result of the check.
+ * @param {!Array.<string>} enrollAppIds The app ids in the enroll challenge
+ * portion of the enroll request.
+ * @param {SignData} signData The sign data associated with the request.
+ * @param {function(boolean)} cb Called with the result of the check.
+ * @private
+ */
+Enroller.prototype.checkAppIds_ = function(enrollAppIds, signData, cb) {
+ if (!enrollAppIds || !enrollAppIds.length) {
+ // Defensive programming check: the enroll request is required to contain
+ // its own app ids, so if there aren't any, reject the request.
+ cb(false);
+ return;
+ }
+
+ /** @private {Array.<string>} */
+ this.distinctAppIds_ =
+ UTIL_unionArrays(enrollAppIds, getDistinctAppIds(signData));
+ /** @private {boolean} */
+ this.anyInvalidAppIds_ = false;
+ /** @private {boolean} */
+ this.appIdFailureReported_ = false;
+ /** @private {number} */
+ this.fetchedAppIds_ = 0;
+
+ for (var i = 0; i < this.distinctAppIds_.length; i++) {
+ var appId = this.distinctAppIds_[i];
+ if (appId == this.origin_) {
+ // Trivially allowed.
+ this.fetchedAppIds_++;
+ if (this.fetchedAppIds_ == this.distinctAppIds_.length &&
+ !this.anyInvalidAppIds_) {
+ // Last app id was fetched, and they were all valid: we're done.
+ // (Note that the case when anyInvalidAppIds_ is true doesn't need to
+ // be handled here: the callback was already called with false at that
+ // point, see fetchedAllowedOriginsForAppId_.)
+ cb(true);
+ }
+ } else {
+ var start = new Date();
+ fetchAllowedOriginsForAppId(appId, this.allowHttp_,
+ this.fetchedAllowedOriginsForAppId_.bind(this, appId, start, cb));
+ }
+ }
+};
+
+/**
+ * Called with the result of an app id fetch.
+ * @param {string} appId the app id that was fetched.
+ * @param {Date} start the time the fetch request started.
+ * @param {function(boolean)} cb Called with the result of the app id check.
+ * @param {number} rc The HTTP response code for the app id fetch.
+ * @param {!Array.<string>} allowedOrigins The origins allowed for this app id.
+ * @private
+ */
+Enroller.prototype.fetchedAllowedOriginsForAppId_ =
+ function(appId, start, cb, rc, allowedOrigins) {
+ var end = new Date();
+ this.fetchedAppIds_++;
+ logFetchAppIdResult(appId, end - start, allowedOrigins, this.logMsgUrl_);
+ if (rc != 200 && !(rc >= 400 && rc < 500)) {
+ if (this.timer_.expired()) {
+ // Act as though the helper timed out.
+ this.helperError_(DeviceStatusCodes.TIMEOUT_STATUS, false);
+ } else {
+ start = new Date();
+ fetchAllowedOriginsForAppId(appId, this.allowHttp_,
+ this.fetchedAllowedOriginsForAppId_.bind(this, appId, start, cb));
+ }
+ return;
+ }
+ if (!isValidAppIdForOrigin(appId, this.origin_, allowedOrigins)) {
+ logInvalidOriginForAppId(this.origin_, appId, this.logMsgUrl_);
+ this.anyInvalidAppIds_ = true;
+ if (!this.appIdFailureReported_) {
+ // Only the failure case can happen more than once, so only report
+ // it the first time.
+ this.appIdFailureReported_ = true;
+ cb(false);
+ }
+ }
+ if (this.fetchedAppIds_ == this.distinctAppIds_.length &&
+ !this.anyInvalidAppIds_) {
+ // Last app id was fetched, and they were all valid: we're done.
+ cb(true);
+ }
+};
+
+/** Closes this enroller. */
+Enroller.prototype.close = function() {
+ if (this.helper_) this.helper_.close();
+};
+
+/**
+ * Notifies the caller with the error code.
+ * @param {number} code
+ * @private
+ */
+Enroller.prototype.notifyError_ = function(code) {
+ if (this.done_)
+ return;
+ this.close();
+ this.done_ = true;
+ this.errorCb_(code);
+};
+
+/**
+ * Notifies the caller of success with the provided response data.
+ * @param {string} u2fVersion
+ * @param {string} info
+ * @param {string|undefined} opt_browserData
+ * @private
+ */
+Enroller.prototype.notifySuccess_ =
+ function(u2fVersion, info, opt_browserData) {
+ if (this.done_)
+ return;
+ this.close();
+ this.done_ = true;
+ this.successCb_(u2fVersion, info, opt_browserData);
+};
+
+/**
+ * Notifies the caller of progress with the error code.
+ * @param {number} code
+ * @private
+ */
+Enroller.prototype.notifyProgress_ = function(code) {
+ if (this.done_)
+ return;
+ if (code != this.lastProgressUpdate_) {
+ this.lastProgressUpdate_ = code;
+ // If there is no progress callback, treat it like an error and clean up.
+ if (this.progressCb_) {
+ this.progressCb_(code);
+ } else {
+ this.notifyError_(code);
+ }
+ }
+};
+
+/**
+ * Maps an enroll helper's error code namespace to the page's error code
+ * namespace.
+ * @param {number} code Error code from DeviceStatusCodes namespace.
+ * @param {boolean} anyGnubbies Whether any gnubbies were found.
+ * @return {number} A GnubbyCodeTypes error code.
+ * @private
+ */
+Enroller.mapError_ = function(code, anyGnubbies) {
+ var reportedError = GnubbyCodeTypes.UNKNOWN_ERROR;
+ switch (code) {
+ case DeviceStatusCodes.WRONG_DATA_STATUS:
+ reportedError = anyGnubbies ? GnubbyCodeTypes.ALREADY_ENROLLED :
+ GnubbyCodeTypes.NO_GNUBBIES;
+ break;
+
+ case DeviceStatusCodes.WAIT_TOUCH_STATUS:
+ reportedError = GnubbyCodeTypes.WAIT_TOUCH;
+ break;
+
+ case DeviceStatusCodes.BUSY_STATUS:
+ reportedError = GnubbyCodeTypes.BUSY;
+ break;
+ }
+ return reportedError;
+};
+
+/**
+ * Called by the helper upon error.
+ * @param {number} code
+ * @param {boolean} anyGnubbies
+ * @private
+ */
+Enroller.prototype.helperError_ = function(code, anyGnubbies) {
+ var reportedError = Enroller.mapError_(code, anyGnubbies);
+ console.log(UTIL_fmt('helper reported ' + code.toString(16) +
+ ', returning ' + reportedError));
+ this.notifyError_(reportedError);
+};
+
+/**
+ * Called by helper upon success.
+ * @param {string} u2fVersion gnubby version.
+ * @param {string} info enroll data.
+ * @private
+ */
+Enroller.prototype.helperSuccess_ = function(u2fVersion, info) {
+ console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
+
+ var browserData;
+ if (u2fVersion == 'U2F_V2') {
+ // For U2F_V2, the challenge sent to the gnubby is modified to be the hash
+ // of the browser data. Include the browser data.
+ browserData = this.browserData_[u2fVersion];
+ }
+
+ this.notifySuccess_(u2fVersion, info, browserData);
+};
+
+/**
+ * Called by helper to notify progress.
+ * @param {number} code
+ * @param {boolean} anyGnubbies
+ * @private
+ */
+Enroller.prototype.helperProgress_ = function(code, anyGnubbies) {
+ var reportedError = Enroller.mapError_(code, anyGnubbies);
+ console.log(UTIL_fmt('helper notified ' + code.toString(16) +
+ ', returning ' + reportedError));
+ this.notifyProgress_(reportedError);
+};
diff --git a/chrome/browser/resources/cryptotoken/enrollhelper.js b/chrome/browser/resources/cryptotoken/enrollhelper.js
new file mode 100644
index 0000000..1c82c39
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/enrollhelper.js
@@ -0,0 +1,53 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Provides a "bottom half" helper to assist with raw enroll
+ * requests.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * A helper for enroll requests.
+ * @extends {Closeable}
+ * @interface
+ */
+function EnrollHelper() {}
+
+/**
+ * Attempts to enroll using the provided data.
+ * @param {Array} enrollChallenges an array enroll challenges.
+ * @param {Array.<SignHelperChallenge>} signChallenges a list of sign
+ * challenges for already enrolled gnubbies, to prevent double-enrolling a
+ * device.
+ */
+EnrollHelper.prototype.doEnroll =
+ function(enrollChallenges, signChallenges) {};
+
+/** Closes this helper. */
+EnrollHelper.prototype.close = function() {};
+
+/**
+ * A factory for creating enroll helpers.
+ * @interface
+ */
+function EnrollHelperFactory() {}
+
+/**
+ * Creates a new enroll helper.
+ * @param {!Countdown} timer Timer after whose expiration the caller is no
+ * longer interested in the result of an enroll request.
+ * @param {function(number, boolean)} errorCb Called when an enroll request
+ * fails with an error code and whether any gnubbies were found.
+ * @param {function(string, string)} successCb Called with the result of a
+ * successful enroll request, along with the version of the gnubby that
+ * provided it.
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
+ * progress updates to the enroll request.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @return {EnrollHelper} the newly created helper.
+ */
+EnrollHelperFactory.prototype.createHelper =
+ function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) {};
diff --git a/chrome/browser/resources/cryptotoken/gnubbies.js b/chrome/browser/resources/cryptotoken/gnubbies.js
new file mode 100644
index 0000000..e541253
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/gnubbies.js
@@ -0,0 +1,313 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview A class for managing all enumerated gnubby devices.
+ */
+'use strict';
+
+/**
+ * @typedef {{
+ * namespace: string,
+ * device: number
+ * }}
+ */
+var llGnubbyDeviceId;
+
+/**
+ * @typedef {{
+ * enumerate: function(function(Array)),
+ * deviceToDeviceId: function(*): llGnubbyDeviceId,
+ * open: function(Gnubbies, number, *, function(number, llGnubby=))
+ * }}
+ */
+var GnubbyNamespaceImpl;
+
+/**
+ * Manager of opened devices.
+ * @constructor
+ */
+function Gnubbies() {
+ /** @private {Object.<string, Array>} */
+ this.devs_ = {};
+ this.pendingEnumerate = []; // clients awaiting an enumerate
+ /** @private {Object.<string, GnubbyNamespaceImpl>} */
+ this.impl_ = {};
+ /** @private {Object.<string, Object.<number, !llGnubby>>} */
+ this.openDevs_ = {};
+ /** @private {Object.<string, Object.<number, *>>} */
+ this.pendingOpens_ = {}; // clients awaiting an open
+}
+
+/**
+ * Registers a new gnubby namespace, i.e. an implementation of the
+ * enumerate/open functions for all devices within a namespace.
+ * @param {string} namespace The namespace of the numerator, e.g. 'usb'.
+ * @param {GnubbyNamespaceImpl} impl The implementation.
+ */
+Gnubbies.prototype.registerNamespace = function(namespace, impl) {
+ this.impl_[namespace] = impl;
+};
+
+/**
+ * @param {llGnubbyDeviceId} which The device to remove.
+ */
+Gnubbies.prototype.removeOpenDevice = function(which) {
+ if (this.openDevs_[which.namespace] &&
+ this.openDevs_[which.namespace].hasOwnProperty(which.device)) {
+ delete this.openDevs_[which.namespace][which.device];
+ }
+};
+
+/** Close all enumerated devices. */
+Gnubbies.prototype.closeAll = function() {
+ if (this.inactivityTimer) {
+ this.inactivityTimer.clearTimeout();
+ this.inactivityTimer = undefined;
+ }
+ // Close and stop talking to any gnubbies we have enumerated.
+ for (var namespace in this.openDevs_) {
+ for (var dev in this.openDevs_[namespace]) {
+ var deviceId = Number(dev);
+ this.openDevs_[namespace][deviceId].destroy();
+ }
+ }
+ this.devs_ = {};
+ this.openDevs_ = {};
+};
+
+/**
+ * @param {function(number, Array.<llGnubbyDeviceId>)} cb Called back with the
+ * result of enumerating.
+ */
+Gnubbies.prototype.enumerate = function(cb) {
+ var self = this;
+
+ /**
+ * @param {string} namespace The namespace that was enumerated.
+ * @param {boolean} lastNamespace Whether this was the last namespace.
+ * @param {Array.<llGnubbyDeviceId>} existingDeviceIds Previously enumerated
+ * device IDs (from other namespaces), if any.
+ * @param {Array} devs The devices in the namespace.
+ */
+ function enumerated(namespace, lastNamespace, existingDeviceIds, devs) {
+ if (chrome.runtime.lastError) {
+ console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
+ console.log(chrome.runtime.lastError);
+ devs = [];
+ }
+
+ console.log(UTIL_fmt('Enumerated ' + devs.length + ' gnubbies'));
+ console.log(devs);
+
+ var presentDevs = {};
+ var deviceIds = [];
+ var deviceToDeviceId = self.impl_[namespace].deviceToDeviceId;
+ for (var i = 0; i < devs.length; ++i) {
+ var deviceId = deviceToDeviceId(devs[i]);
+ deviceIds.push(deviceId);
+ presentDevs[deviceId.device] = devs[i];
+ }
+
+ var toRemove = [];
+ for (var dev in self.openDevs_[namespace]) {
+ if (!presentDevs.hasOwnProperty(dev)) {
+ toRemove.push(dev);
+ }
+ }
+
+ for (var i = 0; i < toRemove.length; i++) {
+ dev = toRemove[i];
+ if (self.openDevs_[namespace][dev]) {
+ self.openDevs_[namespace][dev].destroy();
+ delete self.openDevs_[namespace][dev];
+ }
+ }
+
+ self.devs_[namespace] = devs;
+ existingDeviceIds.push.apply(existingDeviceIds, deviceIds);
+ if (lastNamespace) {
+ while (self.pendingEnumerate.length != 0) {
+ var cb = self.pendingEnumerate.shift();
+ cb(-llGnubby.OK, existingDeviceIds);
+ }
+ }
+ }
+
+ this.pendingEnumerate.push(cb);
+ if (this.pendingEnumerate.length == 1) {
+ var namespaces = Object.keys(/** @type {!Object} */ (this.impl_));
+ if (!namespaces.length) {
+ cb(-llGnubby.OK, []);
+ return;
+ }
+ var deviceIds = [];
+ for (var i = 0; i < namespaces.length; i++) {
+ var namespace = namespaces[i];
+ var enumerator = this.impl_[namespace].enumerate;
+ enumerator(
+ enumerated.bind(null,
+ namespace,
+ i == namespaces.length - 1,
+ deviceIds));
+ }
+ }
+};
+
+/**
+ * Amount of time past last activity to set the inactivity timer to, in millis.
+ * @const
+ */
+Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS = 30000;
+
+/**
+ * @param {number|undefined} opt_timeoutMillis
+ */
+Gnubbies.prototype.resetInactivityTimer = function(opt_timeoutMillis) {
+ var millis = opt_timeoutMillis ?
+ opt_timeoutMillis + Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS :
+ Gnubbies.INACTIVITY_TIMEOUT_MARGIN_MILLIS;
+ if (!this.inactivityTimer) {
+ this.inactivityTimer =
+ new CountdownTimer(millis, this.inactivityTimeout_.bind(this));
+ } else if (millis > this.inactivityTimer.millisecondsUntilExpired()) {
+ this.inactivityTimer.clearTimeout();
+ this.inactivityTimer.setTimeout(millis, this.inactivityTimeout_.bind(this));
+ }
+};
+
+/**
+ * Called when the inactivity timeout expires.
+ * @private
+ */
+Gnubbies.prototype.inactivityTimeout_ = function() {
+ this.inactivityTimer = undefined;
+ for (var namespace in this.openDevs_) {
+ for (var dev in this.openDevs_[namespace]) {
+ var deviceId = Number(dev);
+ console.warn(namespace + ' device ' + deviceId +
+ ' still open after inactivity, closing');
+ this.openDevs_[namespace][deviceId].destroy();
+ this.removeOpenDevice({namespace: namespace, device: deviceId});
+ }
+ }
+};
+
+/**
+ * Opens and adds a new client of the specified device.
+ * @param {llGnubbyDeviceId} which Which device to open.
+ * @param {*} who Client of the device.
+ * @param {function(number, llGnubby=)} cb Called back with the result of
+ * opening the device.
+ */
+Gnubbies.prototype.addClient = function(which, who, cb) {
+ this.resetInactivityTimer();
+
+ var self = this;
+
+ function opened(gnubby, who, cb) {
+ if (gnubby.closing) {
+ // Device is closing or already closed.
+ self.removeClient(gnubby, who);
+ if (cb) { cb(-llGnubby.GONE); }
+ } else {
+ gnubby.registerClient(who);
+ if (cb) { cb(-llGnubby.OK, gnubby); }
+ }
+ }
+
+ function notifyOpenResult(rc) {
+ if (self.pendingOpens_[which.namespace]) {
+ while (self.pendingOpens_[which.namespace][which.device].length != 0) {
+ var client = self.pendingOpens_[which.namespace][which.device].shift();
+ client.cb(rc);
+ }
+ delete self.pendingOpens_[which.namespace][which.device];
+ }
+ }
+
+ var dev = null;
+ var deviceToDeviceId = this.impl_[which.namespace].deviceToDeviceId;
+ if (this.devs_[which.namespace]) {
+ for (var i = 0; i < this.devs_[which.namespace].length; i++) {
+ var device = this.devs_[which.namespace][i];
+ if (deviceToDeviceId(device).device == which.device) {
+ dev = device;
+ break;
+ }
+ }
+ }
+ if (!dev) {
+ // Index out of bounds. Device does not exist in current enumeration.
+ this.removeClient(null, who);
+ if (cb) { cb(-llGnubby.NODEVICE); }
+ return;
+ }
+
+ function openCb(rc, opt_gnubby) {
+ if (rc) {
+ notifyOpenResult(rc);
+ return;
+ }
+ if (!opt_gnubby) {
+ notifyOpenResult(-llGnubby.NODEVICE);
+ return;
+ }
+ var gnubby = /** @type {!llGnubby} */ (opt_gnubby);
+ if (!self.openDevs_[which.namespace]) {
+ self.openDevs_[which.namespace] = {};
+ }
+ self.openDevs_[which.namespace][which.device] = gnubby;
+ while (self.pendingOpens_[which.namespace][which.device].length != 0) {
+ var client = self.pendingOpens_[which.namespace][which.device].shift();
+ opened(gnubby, client.who, client.cb);
+ }
+ delete self.pendingOpens_[which.namespace][which.device];
+ }
+
+ if (this.openDevs_[which.namespace] &&
+ this.openDevs_[which.namespace].hasOwnProperty(which.device)) {
+ var gnubby = this.openDevs_[which.namespace][which.device];
+ opened(gnubby, who, cb);
+ } else {
+ var opener = {who: who, cb: cb};
+ if (!this.pendingOpens_.hasOwnProperty(which.namespace)) {
+ this.pendingOpens_[which.namespace] = {};
+ }
+ if (this.pendingOpens_[which.namespace].hasOwnProperty(which)) {
+ this.pendingOpens_[which.namespace][which.device].push(opener);
+ } else {
+ this.pendingOpens_[which.namespace][which.device] = [opener];
+ var openImpl = this.impl_[which.namespace].open;
+ openImpl(this, which.device, dev, openCb);
+ }
+ }
+};
+
+/**
+ * Removes a client from a low-level gnubby.
+ * @param {llGnubby} whichDev The gnubby.
+ * @param {*} who The client.
+ */
+Gnubbies.prototype.removeClient = function(whichDev, who) {
+ console.log(UTIL_fmt('Gnubbies.removeClient()'));
+
+ this.resetInactivityTimer();
+
+ // De-register client from all known devices.
+ for (var namespace in this.openDevs_) {
+ for (var devId in this.openDevs_[namespace]) {
+ var deviceId = Number(devId);
+ var dev = this.openDevs_[namespace][deviceId];
+ if (dev.hasClient(who)) {
+ if (whichDev && dev != whichDev) {
+ console.warn('usbGnubby attached to more than one device!?');
+ }
+ if (!dev.deregisterClient(who)) {
+ dev.destroy();
+ }
+ }
+ }
+ }
+};
diff --git a/chrome/browser/resources/cryptotoken/gnubby-u2f.js b/chrome/browser/resources/cryptotoken/gnubby-u2f.js
new file mode 100644
index 0000000..a8418d9
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/gnubby-u2f.js
@@ -0,0 +1,88 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview usbGnubby methods related to U2F support.
+ */
+'use strict';
+
+// Commands and flags of the Gnubby applet at
+// //depot/google3/security/tools/gnubby/applet/gnubby/src/pkgGnubby/Gnubby.java
+usbGnubby.U2F_ENROLL = 0x01;
+usbGnubby.U2F_SIGN = 0x02;
+usbGnubby.U2F_VERSION = 0x03;
+
+usbGnubby.APPLET_VERSION = 0x11; // First 3 bytes are applet version.
+
+// APDU.P1 flags
+usbGnubby.P1_TUP_REQUIRED = 0x01;
+usbGnubby.P1_TUP_CONSUME = 0x02;
+usbGnubby.P1_TUP_TESTONLY = 0x04;
+usbGnubby.P1_INDIVIDUAL_KEY = 0x80;
+
+usbGnubby.prototype.enroll = function(challenge, appIdHash, cb) {
+ var apdu = new Uint8Array(
+ [0x00,
+ usbGnubby.U2F_ENROLL,
+ usbGnubby.P1_TUP_REQUIRED | usbGnubby.P1_TUP_CONSUME |
+ usbGnubby.P1_INDIVIDUAL_KEY,
+ 0x00, 0x00, 0x00,
+ challenge.length + appIdHash.length]);
+ // TODO(mschilder): only use P1_INDIVIDUAL_KEY for corp appIdHashes.
+ var u8 = new Uint8Array(apdu.length + challenge.length +
+ appIdHash.length + 2);
+ for (var i = 0; i < apdu.length; ++i) u8[i] = apdu[i];
+ for (var i = 0; i < challenge.length; ++i) u8[i + apdu.length] =
+ challenge[i];
+ for (var i = 0; i < appIdHash.length; ++i) {
+ u8[i + apdu.length + challenge.length] = appIdHash[i];
+ }
+ this.apduReply_(u8.buffer, cb);
+};
+
+usbGnubby.prototype.sign = function(challengeHash, appIdHash, keyHandle, cb,
+ opt_nowink) {
+ var apdu = new Uint8Array(
+ [0x00,
+ usbGnubby.U2F_SIGN,
+ usbGnubby.P1_TUP_REQUIRED | usbGnubby.P1_TUP_CONSUME,
+ 0x00, 0x00, 0x00,
+ challengeHash.length + appIdHash.length + keyHandle.length]);
+ if (opt_nowink) {
+ // A signature request that does not want winking.
+ // These are used during enroll to figure out whether a gnubby was already
+ // enrolled.
+ // Tell applet to not actually produce a signature, even
+ // if already touched.
+ apdu[2] |= usbGnubby.P1_TUP_TESTONLY;
+ }
+ var u8 = new Uint8Array(apdu.length + challengeHash.length +
+ appIdHash.length + keyHandle.length + 2);
+ for (var i = 0; i < apdu.length; ++i) u8[i] = apdu[i];
+ for (var i = 0; i < challengeHash.length; ++i) u8[i + apdu.length] =
+ challengeHash[i];
+ for (var i = 0; i < appIdHash.length; ++i) {
+ u8[i + apdu.length + challengeHash.length] = appIdHash[i];
+ }
+ for (var i = 0; i < keyHandle.length; ++i) {
+ u8[i + apdu.length + challengeHash.length + appIdHash.length] =
+ keyHandle[i];
+ }
+ this.apduReply_(u8.buffer, cb, opt_nowink);
+};
+
+usbGnubby.prototype.version = function(cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ var apdu = new Uint8Array([0x00, usbGnubby.U2F_VERSION, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00]);
+ this.apduReply_(apdu.buffer, function(rc, data) {
+ if (rc == 0x6d00) {
+ // Command not implemented. Pretend this is v1.
+ var v1 = new Uint8Array(UTIL_StringToBytes('U2F_V1'));
+ cb(-llGnubby.OK, v1.buffer);
+ } else {
+ cb(rc, data);
+ }
+ });
+};
diff --git a/chrome/browser/resources/cryptotoken/gnubby.js b/chrome/browser/resources/cryptotoken/gnubby.js
new file mode 100644
index 0000000..f3363f0
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/gnubby.js
@@ -0,0 +1,627 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Low level usb cruft to talk gnubby.
+ * @author [email protected]
+ */
+
+'use strict';
+
+// Global Gnubby instance counter.
+var gnubby_id = 0;
+
+/**
+ * Creates a worker Gnubby instance.
+ * @constructor
+ * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result.
+ */
+function usbGnubby(opt_busySeconds) {
+ this.dev = null;
+ this.cid = (++gnubby_id) & 0x00ffffff; // Pick unique channel.
+ this.rxframes = [];
+ this.synccnt = 0;
+ this.rxcb = null;
+ this.closed = false;
+ this.commandPending = false;
+ this.notifyOnClose = [];
+ this.busyMillis = (opt_busySeconds ? opt_busySeconds * 1000 : 2500);
+}
+
+/**
+ * Sets usbGnubby's Gnubbies singleton.
+ * @param {Gnubbies} gnubbies
+ */
+usbGnubby.setGnubbies = function(gnubbies) {
+ /** @private {Gnubbies} */
+ usbGnubby.gnubbies_ = gnubbies;
+};
+
+/**
+ * @param {function(number, Array.<llGnubbyDeviceId>)} cb Called back with the
+ * result of enumerating.
+ */
+usbGnubby.prototype.enumerate = function(cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ if (this.closed) {
+ cb(-llGnubby.GONE);
+ return;
+ }
+ if (!usbGnubby.gnubbies_) {
+ cb(-llGnubby.NODEVICE);
+ return;
+ }
+
+ usbGnubby.gnubbies_.enumerate(cb);
+};
+
+/**
+ * Opens the gnubby with the given index, or the first found gnubby if no
+ * index is specified.
+ * @param {llGnubbyDeviceId|undefined} opt_which The device to open.
+ * @param {function(number)|undefined} opt_cb Called with result of opening the
+ * gnubby.
+ */
+usbGnubby.prototype.open = function(opt_which, opt_cb) {
+ var cb = opt_cb ? opt_cb : usbGnubby.defaultCallback;
+ if (this.closed) {
+ cb(-llGnubby.GONE);
+ return;
+ }
+ this.closingWhenIdle = false;
+
+ if (document.location.href.indexOf('_generated_') == -1) {
+ // Not background page.
+ // Pick more random cid to tell things apart on the usb bus.
+ var rnd = UTIL_getRandom(2);
+ this.cid ^= (rnd[0] << 16) | (rnd[1] << 8);
+ }
+
+ var self = this;
+ function addSelfAsClient(which) {
+ self.cid &= 0x00ffffff;
+ self.cid |= ((which.device + 1) << 24); // For debugging.
+
+ usbGnubby.gnubbies_.addClient(which, self, function(rc, device) {
+ self.dev = device;
+ cb(rc);
+ });
+ }
+
+ if (!usbGnubby.gnubbies_) {
+ cb(-llGnubby.NODEVICE);
+ return;
+ }
+ if (opt_which) {
+ addSelfAsClient(opt_which);
+ } else {
+ usbGnubby.gnubbies_.enumerate(function(rc, devs) {
+ if (rc || !devs.length) {
+ cb(-llGnubby.NODEVICE);
+ return;
+ }
+ addSelfAsClient(devs[0]);
+ });
+ }
+};
+
+/**
+ * @return {boolean} Whether this gnubby has any command outstanding.
+ * @private
+ */
+usbGnubby.prototype.inUse_ = function() {
+ return this.commandPending;
+};
+
+/** Closes this gnubby. */
+usbGnubby.prototype.close = function() {
+ this.closed = true;
+
+ if (this.dev) {
+ console.log(UTIL_fmt('usbGnubby.close()'));
+ this.rxframes = [];
+ this.rxcb = null;
+ var dev = this.dev;
+ this.dev = null;
+ var self = this;
+ // Wait a bit in case simpleton client tries open next gnubby.
+ // Without delay, gnubbies would drop all idle devices, before client
+ // gets to the next one.
+ window.setTimeout(
+ function() {
+ usbGnubby.gnubbies_.removeClient(dev, self);
+ }, 300);
+ }
+};
+
+/**
+ * Asks this gnubby to close when it gets a chance.
+ * @param {Function=} cb called back when closed.
+ */
+usbGnubby.prototype.closeWhenIdle = function(cb) {
+ if (!this.inUse_()) {
+ this.close();
+ if (cb) cb();
+ return;
+ }
+ this.closingWhenIdle = true;
+ if (cb) this.notifyOnClose.push(cb);
+};
+
+/**
+ * Close and notify every caller that it is now closed.
+ * @private
+ */
+usbGnubby.prototype.idleClose_ = function() {
+ this.close();
+ while (this.notifyOnClose.length != 0) {
+ var cb = this.notifyOnClose.shift();
+ cb();
+ }
+};
+
+/**
+ * Notify callback for every frame received.
+ * @private
+ */
+usbGnubby.prototype.notifyFrame_ = function(cb) {
+ if (this.rxframes.length != 0) {
+ // Already have frames; continue.
+ if (cb) window.setTimeout(cb, 0);
+ } else {
+ this.rxcb = cb;
+ }
+};
+
+/**
+ * Called by low level driver with a frame.
+ * @param {ArrayBuffer} frame
+ * @return {boolean} Whether this client is still interested in receiving
+ * frames from its device.
+ */
+usbGnubby.prototype.receivedFrame = function(frame) {
+ if (this.closed) return false; // No longer interested.
+
+ if (!this.checkCID_(frame)) {
+ // Not for me, ignore.
+ return true;
+ }
+
+ this.rxframes.push(frame);
+
+ // Callback self in case we were waiting. Once.
+ var cb = this.rxcb;
+ this.rxcb = null;
+ if (cb) window.setTimeout(cb, 0);
+
+ return true;
+};
+
+/**
+ * @return {ArrayBuffer} oldest received frame. Throw if none.
+ * @private
+ */
+usbGnubby.prototype.readFrame_ = function() {
+ if (this.rxframes.length == 0) throw 'rxframes empty!';
+
+ var frame = this.rxframes.shift();
+ return frame;
+};
+
+// Poll from rxframes[].
+// timeout in seconds.
+usbGnubby.prototype.read_ = function(cmd, timeout, cb) {
+ if (this.closed) { cb(-llGnubby.GONE); return; }
+ if (!this.dev) { cb(-llGnubby.NODEVICE); return; }
+
+ var tid = null; // timeout timer id.
+ var callback = cb;
+ var self = this;
+
+ var msg = null;
+ var seqno = 0;
+ var count = 0;
+
+ /**
+ * Schedule call to cb if not called yet.
+ * @param {number} a Return code.
+ * @param {Object=} b Optional data.
+ */
+ function schedule_cb(a, b) {
+ self.commandPending = false;
+ if (tid) {
+ // Cancel timeout timer.
+ window.clearTimeout(tid);
+ tid = null;
+ }
+ var c = callback;
+ if (c) {
+ callback = null;
+ window.setTimeout(function() { c(a, b); }, 0);
+ }
+ if (self.closingWhenIdle) self.idleClose_();
+ };
+
+ function read_timeout() {
+ if (!callback || !tid) return; // Already done.
+
+ console.error(UTIL_fmt(
+ '[' + self.cid.toString(16) + '] timeout!'));
+
+ if (self.dev) {
+ self.dev.destroy(); // Stop pretending this thing works.
+ }
+
+ tid = null;
+
+ schedule_cb(-llGnubby.TIMEOUT);
+ };
+
+ function cont_frame() {
+ if (!callback || !tid) return; // Already done.
+
+ var f = new Uint8Array(self.readFrame_());
+ var rcmd = f[4];
+ var total_len = (f[5] << 8) + f[6];
+
+ if (rcmd == llGnubby.CMD_ERROR && total_len == 1) {
+ // Error from device; forward.
+ console.log(UTIL_fmt(
+ '[' + self.cid.toString(16) + '] error frame ' +
+ UTIL_BytesToHex(f)));
+ if (f[7] == llGnubby.GONE) {
+ self.closed = true;
+ }
+ schedule_cb(-f[7]);
+ return;
+ }
+
+ if ((rcmd & 0x80)) {
+ // Not an CONT frame, ignore.
+ console.log(UTIL_fmt(
+ '[' + self.cid.toString(16) + '] ignoring non-cont frame ' +
+ UTIL_BytesToHex(f)));
+ self.notifyFrame_(cont_frame);
+ return;
+ }
+
+ var seq = (rcmd & 0x7f);
+ if (seq != seqno++) {
+ console.log(UTIL_fmt(
+ '[' + self.cid.toString(16) + '] bad cont frame ' +
+ UTIL_BytesToHex(f)));
+ schedule_cb(-llGnubby.INVALID_SEQ);
+ return;
+ }
+
+ // Copy payload.
+ for (var i = 5; i < f.length && count < msg.length; ++i) {
+ msg[count++] = f[i];
+ }
+
+ if (count == msg.length) {
+ // Done.
+ schedule_cb(-llGnubby.OK, msg.buffer);
+ } else {
+ // Need more CONT frame(s).
+ self.notifyFrame_(cont_frame);
+ }
+ }
+
+ function init_frame() {
+ if (!callback || !tid) return; // Already done.
+
+ var f = new Uint8Array(self.readFrame_());
+
+ var rcmd = f[4];
+ var total_len = (f[5] << 8) + f[6];
+
+ if (rcmd == llGnubby.CMD_ERROR && total_len == 1) {
+ // Error from device; forward.
+ // Don't log busy frames, they're "normal".
+ if (f[7] != llGnubby.BUSY) {
+ console.log(UTIL_fmt(
+ '[' + self.cid.toString(16) + '] error frame ' +
+ UTIL_BytesToHex(f)));
+ }
+ if (f[7] == llGnubby.GONE) {
+ self.closed = true;
+ }
+ schedule_cb(-f[7]);
+ return;
+ }
+
+ if (!(rcmd & 0x80)) {
+ // Not an init frame, ignore.
+ console.log(UTIL_fmt(
+ '[' + self.cid.toString(16) + '] ignoring non-init frame ' +
+ UTIL_BytesToHex(f)));
+ self.notifyFrame_(init_frame);
+ return;
+ }
+
+ if (rcmd != cmd) {
+ // Not expected ack, read more.
+ console.log(UTIL_fmt(
+ '[' + self.cid.toString(16) + '] ignoring non-ack frame ' +
+ UTIL_BytesToHex(f)));
+ self.notifyFrame_(init_frame);
+ return;
+ }
+
+ // Copy payload.
+ msg = new Uint8Array(total_len);
+ for (var i = 7; i < f.length && count < msg.length; ++i) {
+ msg[count++] = f[i];
+ }
+
+ if (count == msg.length) {
+ // Done.
+ schedule_cb(-llGnubby.OK, msg.buffer);
+ } else {
+ // Need more CONT frame(s).
+ self.notifyFrame_(cont_frame);
+ }
+ }
+
+ // Start timeout timer.
+ tid = window.setTimeout(read_timeout, 1000.0 * timeout);
+
+ // Schedule read of first frame.
+ self.notifyFrame_(init_frame);
+};
+
+/**
+ * @param {ArrayBuffer} frame
+ * @return {boolean} Whether frame is for my channel.
+ * @private
+ */
+usbGnubby.prototype.checkCID_ = function(frame) {
+ var f = new Uint8Array(frame);
+ var c = (f[0] << 24) |
+ (f[1] << 16) |
+ (f[2] << 8) |
+ (f[3]);
+ return c === this.cid ||
+ c === 0; // Generic notification.
+};
+
+/**
+ * Queue command for sending.
+ * @param {number} cmd The command to send.
+ * @param {ArrayBuffer} data
+ * @private
+ */
+usbGnubby.prototype.write_ = function(cmd, data) {
+ if (this.closed) return;
+ if (!this.dev) return;
+
+ this.commandPending = true;
+
+ this.dev.queueCommand(this.cid, cmd, data);
+};
+
+/**
+ * Writes the command, and calls back when the command's reply is received.
+ * @param {number} cmd The command to send.
+ * @param {ArrayBuffer} data
+ * @param {number} timeout Timeout in seconds.
+ * @param {function(number, ArrayBuffer=)} cb
+ * @private
+ */
+usbGnubby.prototype.exchange_ = function(cmd, data, timeout, cb) {
+ var busyWait = new CountdownTimer(this.busyMillis);
+ var self = this;
+
+ function retryBusy(rc, rc_data) {
+ if (rc == -llGnubby.BUSY && !busyWait.expired()) {
+ if (usbGnubby.gnubbies_) {
+ usbGnubby.gnubbies_.resetInactivityTimer(timeout * 1000);
+ }
+ self.write_(cmd, data);
+ self.read_(cmd, timeout, retryBusy);
+ } else {
+ busyWait.clearTimeout();
+ cb(rc, rc_data);
+ }
+ }
+
+ retryBusy(-llGnubby.BUSY, undefined); // Start work.
+};
+
+// For console interaction.
+usbGnubby.defaultCallback = function(rc, data) {
+ var msg = 'defaultCallback(' + rc;
+ if (data) {
+ if (typeof data == 'string') msg += ', ' + data;
+ else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data));
+ }
+ msg += ')';
+ console.log(UTIL_fmt(msg));
+};
+
+// Send nonce to device, flush read queue until match.
+usbGnubby.prototype.sync = function(cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ if (this.closed) {
+ cb(-llGnubby.GONE);
+ return;
+ }
+
+ var done = false;
+ var trycount = 6;
+ var tid = null;
+ var self = this;
+
+ function callback(rc) {
+ done = true;
+ self.commandPending = false;
+ if (tid) {
+ window.clearTimeout(tid);
+ tid = null;
+ }
+ if (rc) console.warn(UTIL_fmt('sync failed: ' + rc));
+ if (cb) cb(rc);
+ if (self.closingWhenIdle) self.idleClose_();
+ }
+
+ function sendSentinel() {
+ var data = new Uint8Array(1);
+ data[0] = ++self.synccnt;
+ self.write_(llGnubby.CMD_SYNC, data.buffer);
+ }
+
+ function checkSentinel() {
+ var f = new Uint8Array(self.readFrame_());
+
+ // Device disappeared on us.
+ if (f[4] == llGnubby.CMD_ERROR &&
+ f[5] == 0 && f[6] == 1 &&
+ f[7] == llGnubby.GONE) {
+ self.closed = true;
+ callback(-llGnubby.GONE);
+ return;
+ }
+
+ // Eat everything else but expected sync reply.
+ if (f[4] != llGnubby.CMD_SYNC ||
+ (f.length > 7 && /* fw pre-0.2.1 bug: does not echo sentinel */
+ f[7] != self.synccnt)) {
+ // Read more.
+ self.notifyFrame_(checkSentinel);
+ return;
+ }
+
+ // Done.
+ callback(-llGnubby.OK);
+ };
+
+ function timeoutLoop() {
+ if (done) return;
+
+ if (trycount == 0) {
+ // Failed.
+ callback(-llGnubby.TIMEOUT);
+ return;
+ }
+
+ --trycount; // Try another one.
+ sendSentinel();
+ self.notifyFrame_(checkSentinel);
+ tid = window.setTimeout(timeoutLoop, 500);
+ };
+
+ timeoutLoop();
+};
+
+// Communication timeout values in seconds.
+usbGnubby.SHORT_TIMEOUT = 1;
+usbGnubby.NORMAL_TIMEOUT = 3;
+// Max timeout usb firmware has for smartcard response is 30 seconds.
+// Make our application level tolerance a little longer.
+usbGnubby.MAX_TIMEOUT = 31;
+
+usbGnubby.prototype.blink = function(data, cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ if (typeof data == 'number') {
+ var d = new Uint8Array([data]);
+ data = d.buffer;
+ }
+ this.exchange_(llGnubby.CMD_PROMPT, data, usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.lock = function(data, cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ if (typeof data == 'number') {
+ var d = new Uint8Array([data]);
+ data = d.buffer;
+ }
+ this.exchange_(llGnubby.CMD_LOCK, data, usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.unlock = function(cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ var data = new Uint8Array([0]);
+ this.exchange_(llGnubby.CMD_LOCK, data.buffer,
+ usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.sysinfo = function(cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ this.exchange_(llGnubby.CMD_SYSINFO, new ArrayBuffer(0),
+ usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.wink = function(cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ this.exchange_(llGnubby.CMD_WINK, new ArrayBuffer(0),
+ usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.dfu = function(data, cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ this.exchange_(llGnubby.CMD_DFU, data, usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.ping = function(data, cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ if (typeof data == 'number') {
+ var d = new Uint8Array(data);
+ window.crypto.getRandomValues(d);
+ data = d.buffer;
+ }
+ this.exchange_(llGnubby.CMD_PING, data, usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.apdu = function(data, cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ this.exchange_(llGnubby.CMD_APDU, data, usbGnubby.MAX_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.reset = function(cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ this.exchange_(llGnubby.CMD_ATR, new ArrayBuffer(0),
+ usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+// byte args[3] = [delay-in-ms before disabling interrupts,
+// delay-in-ms before disabling usb (aka remove),
+// delay-in-ms before reboot (aka insert)]
+usbGnubby.prototype.usb_test = function(args, cb) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ var u8 = new Uint8Array(args);
+ this.exchange_(llGnubby.CMD_USB_TEST, u8.buffer,
+ usbGnubby.NORMAL_TIMEOUT, cb);
+};
+
+usbGnubby.prototype.apduReply_ = function(request, cb, opt_nowink) {
+ if (!cb) cb = usbGnubby.defaultCallback;
+ var self = this;
+
+ this.apdu(request, function(rc, data) {
+ if (rc == 0) {
+ var r8 = new Uint8Array(data);
+ if (r8[r8.length - 2] == 0x90 && r8[r8.length - 1] == 0x00) {
+ // strip trailing 9000
+ var buf = new Uint8Array(r8.subarray(0, r8.length - 2));
+ cb(-llGnubby.OK, buf.buffer);
+ return;
+ } else {
+ // return non-9000 as rc
+ rc = r8[r8.length - 2] * 256 + r8[r8.length - 1];
+ // wink gnubby at hand if it needs touching.
+ if (rc == 0x6985 && !opt_nowink) {
+ self.wink(function() { cb(rc); });
+ return;
+ }
+ }
+ }
+ // Warn on errors other than waiting for touch, wrong data, and
+ // unrecognized command.
+ if (rc != 0x6985 && rc != 0x6a80 && rc != 0x6d00) {
+ console.warn(UTIL_fmt('apduReply_ fail: ' + rc.toString(16)));
+ }
+ cb(rc);
+ });
+};
diff --git a/chrome/browser/resources/cryptotoken/gnubbycodetypes.js b/chrome/browser/resources/cryptotoken/gnubbycodetypes.js
new file mode 100644
index 0000000..d6c8436
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/gnubbycodetypes.js
@@ -0,0 +1,89 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview This provides the different code types for the gnubby
+ * operations.
+ */
+
+var GnubbyCodeTypes = {};
+
+/**
+ * Request succeeded.
+ * @const
+ */
+GnubbyCodeTypes.OK = 0;
+
+/**
+ * All plugged in devices are already enrolled.
+ * @const
+ */
+GnubbyCodeTypes.ALREADY_ENROLLED = 2;
+
+/**
+ * None of the plugged in devices are enrolled.
+ * @const
+ */
+GnubbyCodeTypes.NONE_PLUGGED_ENROLLED = 3;
+
+/**
+ * One or more devices are waiting for touch.
+ * @const
+ */
+GnubbyCodeTypes.WAIT_TOUCH = 4;
+
+/**
+ * No gnubbies found.
+ * @const
+ */
+GnubbyCodeTypes.NO_GNUBBIES = 5;
+
+/**
+ * Unknown error during enrollment.
+ * @const
+ */
+GnubbyCodeTypes.UNKNOWN_ERROR = 7;
+
+/**
+ * Extension not found.
+ * @const
+ */
+GnubbyCodeTypes.NO_EXTENSION = 8;
+
+// TODO(jayini): change to none_enrolled_for_account and none_enrolled_present
+/**
+ * No devices enrolled for this user.
+ * @const
+ */
+GnubbyCodeTypes.NO_DEVICES_ENROLLED = 9;
+
+/**
+ * gnubby errors due to chrome issues
+ * @const
+ */
+GnubbyCodeTypes.BROWSER_ERROR = 10;
+
+/**
+ * gnubbyd taking too long
+ * @const
+ */
+GnubbyCodeTypes.LONG_WAIT = 11;
+
+/**
+ * Bad request.
+ * @const
+ */
+GnubbyCodeTypes.BAD_REQUEST = 12;
+
+/**
+ * All gnubbies are too busy to handle your request.
+ * @const
+ */
+GnubbyCodeTypes.BUSY = 13;
+
+/**
+ * There is a bad app id in the request.
+ * @const
+ */
+GnubbyCodeTypes.BAD_APP_ID = 14;
diff --git a/chrome/browser/resources/cryptotoken/gnubbyfactory.js b/chrome/browser/resources/cryptotoken/gnubbyfactory.js
new file mode 100644
index 0000000..ecf8f35f
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/gnubbyfactory.js
@@ -0,0 +1,33 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Contains a factory interface for creating and opening gnubbies.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * A factory for creating and opening gnubbies.
+ * @interface
+ */
+function GnubbyFactory() {}
+
+/**
+ * Enumerates gnubbies.
+ * @param {function(number, Array.<llGnubbyDeviceId>)} cb
+ */
+GnubbyFactory.prototype.enumerate = function(cb) {
+};
+
+/**
+ * Creates a new gnubby object, and opens the gnubby with the given index.
+ * @param {llGnubbyDeviceId} which The device to open.
+ * @param {boolean} forEnroll Whether this gnubby is being opened for enrolling.
+ * @param {function(number, usbGnubby=)} cb Called with result of opening the
+ * gnubby.
+ * @param {string=} logMsgUrl the url to post log messages to
+ */
+GnubbyFactory.prototype.openGnubby = function(which, forEnroll, cb, logMsgUrl) {
+};
diff --git a/chrome/browser/resources/cryptotoken/gnubbymsgtypes.js b/chrome/browser/resources/cryptotoken/gnubbymsgtypes.js
new file mode 100644
index 0000000..8695d06
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/gnubbymsgtypes.js
@@ -0,0 +1,46 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview This provides the different message types for the gnubby
+ * operations.
+ */
+
+var GnubbyMsgTypes = {};
+
+/**
+ * Enroll request message type.
+ * @const
+ */
+GnubbyMsgTypes.ENROLL_WEB_REQUEST = 'enroll_web_request';
+
+/**
+ * Enroll reply message type.
+ * @const
+ */
+GnubbyMsgTypes.ENROLL_WEB_REPLY = 'enroll_web_reply';
+
+/**
+ * Enroll notification message type.
+ * @const
+ */
+GnubbyMsgTypes.ENROLL_WEB_NOTIFICATION = 'enroll_web_notification';
+
+/**
+ * Sign request message type.
+ * @const
+ */
+GnubbyMsgTypes.SIGN_WEB_REQUEST = 'sign_web_request';
+
+/**
+ * Sign reply message type.
+ * @const
+ */
+GnubbyMsgTypes.SIGN_WEB_REPLY = 'sign_web_reply';
+
+/**
+ * Sign notification message type.
+ * @const
+ */
+GnubbyMsgTypes.SIGN_WEB_NOTIFICATION = 'sign_web_notification';
diff --git a/chrome/browser/resources/cryptotoken/llgnubby.js b/chrome/browser/resources/cryptotoken/llgnubby.js
new file mode 100644
index 0000000..70551e93
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/llgnubby.js
@@ -0,0 +1,85 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Interface for representing a low-level gnubby device.
+ */
+'use strict';
+
+/**
+ * Low level gnubby 'driver'. One per physical USB device.
+ * @interface
+ */
+function llGnubby() {}
+
+// Commands of the USB interface.
+// //depot/google3/security/tools/gnubby/gnubbyd/gnubby_if.h
+llGnubby.CMD_PING = 0x81;
+llGnubby.CMD_ATR = 0x82;
+llGnubby.CMD_APDU = 0x83;
+llGnubby.CMD_LOCK = 0x84;
+llGnubby.CMD_SYSINFO = 0x85;
+llGnubby.CMD_PROMPT = 0x87;
+llGnubby.CMD_WINK = 0x88;
+llGnubby.CMD_USB_TEST = 0xb9;
+llGnubby.CMD_DFU = 0xba;
+llGnubby.CMD_SYNC = 0xbc;
+llGnubby.CMD_ERROR = 0xbf;
+
+// Low-level error codes.
+// //depot/google3/security/tools/gnubby/gnubbyd/gnubby_if.h
+// //depot/google3/security/tools/gnubby/client/gnubby_error_codes.h
+llGnubby.OK = 0;
+llGnubby.INVALID_CMD = 1;
+llGnubby.INVALID_PAR = 2;
+llGnubby.INVALID_LEN = 3;
+llGnubby.INVALID_SEQ = 4;
+llGnubby.TIMEOUT = 5;
+llGnubby.BUSY = 6;
+llGnubby.ACCESS_DENIED = 7;
+llGnubby.GONE = 8;
+llGnubby.VERIFY_ERROR = 9;
+llGnubby.LOCK_REQUIRED = 10;
+llGnubby.SYNC_FAIL = 11;
+llGnubby.OTHER = 127;
+
+// Remote helper errors.
+llGnubby.NOTREMOTE = 263;
+llGnubby.COULDNOTDIAL = 264;
+
+// chrome.usb-related errors.
+llGnubby.NODEVICE = 512;
+llGnubby.NOPERMISSION = 666;
+
+/** Destroys this low-level device instance. */
+llGnubby.prototype.destroy = function() {};
+
+/**
+ * Register a client for this gnubby.
+ * @param {*} who The client.
+ */
+llGnubby.prototype.registerClient = function(who) {};
+
+/**
+ * De-register a client.
+ * @param {*} who The client.
+ * @return {number} The number of remaining listeners for this device, or -1
+ * if this had no clients to start with.
+ */
+llGnubby.prototype.deregisterClient = function(who) {};
+
+/**
+ * @param {*} who The client.
+ * @return {boolean} Whether this device has who as a client.
+ */
+llGnubby.prototype.hasClient = function(who) {};
+
+/**
+ * Queue command to be sent.
+ * If queue was empty, initiate the write.
+ * @param {number} cid The client's channel ID.
+ * @param {number} cmd The command to send.
+ * @param {ArrayBuffer} data
+ */
+llGnubby.prototype.queueCommand = function(cid, cmd, data) {};
diff --git a/chrome/browser/resources/cryptotoken/llhidgnubby.js b/chrome/browser/resources/cryptotoken/llhidgnubby.js
new file mode 100644
index 0000000..b79c11d
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/llhidgnubby.js
@@ -0,0 +1,458 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Implements a low-level gnubby driver based on chrome.hid.
+ */
+'use strict';
+
+/**
+ * Low level gnubby 'driver'. One per physical USB device.
+ * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
+ * in.
+ * @param {!chrome.hid.ConnectionHandle} dev The device.
+ * @param {number} id The device's id.
+ * @constructor
+ * @implements {llGnubby}
+ */
+function llHidGnubby(gnubbies, dev, id) {
+ /** @private {Gnubbies} */
+ this.gnubbies_ = gnubbies;
+ this.dev = dev;
+ this.id = id;
+ this.txqueue = [];
+ this.clients = [];
+ this.lockCID = 0; // channel ID of client holding a lock, if != 0.
+ this.lockMillis = 0; // current lock period.
+ this.lockTID = null; // timer id of lock timeout.
+ this.closing = false; // device to be closed by receive loop.
+ this.updating = false; // device firmware is in final stage of updating.
+}
+
+/**
+ * Namespace for the llHidGnubby implementation.
+ * @const
+ */
+llHidGnubby.NAMESPACE = 'hid';
+
+/** Destroys this low-level device instance. */
+llHidGnubby.prototype.destroy = function() {
+ if (!this.dev) return; // Already dead.
+
+ this.closing = true;
+
+ console.log(UTIL_fmt('llHidGnubby.destroy()'));
+
+ // Synthesize a close error frame to alert all clients,
+ // some of which might be in read state.
+ //
+ // Use magic CID 0 to address all.
+ this.publishFrame_(new Uint8Array([
+ 0, 0, 0, 0, // broadcast CID
+ llGnubby.CMD_ERROR,
+ 0, 1, // length
+ llGnubby.GONE]).buffer);
+
+ // Set all clients to closed status and remove them.
+ while (this.clients.length != 0) {
+ var client = this.clients.shift();
+ if (client) client.closed = true;
+ }
+
+ if (this.lockTID) {
+ window.clearTimeout(this.lockTID);
+ this.lockTID = null;
+ }
+
+ var dev = this.dev;
+ this.dev = null;
+
+ var self = this;
+
+ function onClosed() {
+ console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
+ self.gnubbies_.removeOpenDevice(
+ {namespace: llHidGnubby.NAMESPACE, device: self.id});
+ }
+
+ chrome.hid.disconnect(dev.connectionId, onClosed);
+};
+
+/**
+ * Push frame to all clients.
+ * @param {ArrayBuffer} f
+ * @private
+ */
+llHidGnubby.prototype.publishFrame_ = function(f) {
+ var old = this.clients;
+
+ var remaining = [];
+ var changes = false;
+ for (var i = 0; i < old.length; ++i) {
+ var client = old[i];
+ if (client.receivedFrame(f)) {
+ // Client still alive; keep on list.
+ remaining.push(client);
+ } else {
+ changes = true;
+ console.log(UTIL_fmt(
+ '[' + client.cid.toString(16) + '] left?'));
+ }
+ }
+ if (changes) this.clients = remaining;
+};
+
+/**
+ * @return {boolean} whether this device is open and ready to use.
+ * @private
+ */
+llHidGnubby.prototype.readyToUse_ = function() {
+ if (this.closing) return false;
+ if (!this.dev) return false;
+
+ return true;
+};
+
+/**
+ * Register a client for this gnubby.
+ * @param {*} who The client.
+ */
+llHidGnubby.prototype.registerClient = function(who) {
+ for (var i = 0; i < this.clients.length; ++i) {
+ if (this.clients[i] === who) return; // Already registered.
+ }
+ this.clients.push(who);
+ if (this.clients.length == 1) {
+ // First client? Kick off read loop.
+ this.readLoop_();
+ }
+};
+
+/**
+ * De-register a client.
+ * @param {*} who The client.
+ * @return {number} The number of remaining listeners for this device, or -1
+ * Returns number of remaining listeners for this device.
+ * if this had no clients to start with.
+ */
+llHidGnubby.prototype.deregisterClient = function(who) {
+ var current = this.clients;
+ if (current.length == 0) return -1;
+ this.clients = [];
+ for (var i = 0; i < current.length; ++i) {
+ var client = current[i];
+ if (client !== who) this.clients.push(client);
+ }
+ return this.clients.length;
+};
+
+/**
+ * @param {*} who The client.
+ * @return {boolean} Whether this device has who as a client.
+ */
+llHidGnubby.prototype.hasClient = function(who) {
+ if (this.clients.length == 0) return false;
+ for (var i = 0; i < this.clients.length; ++i) {
+ if (who === this.clients[i])
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Reads all incoming frames and notifies clients of their receipt.
+ * @private
+ */
+llHidGnubby.prototype.readLoop_ = function() {
+ //console.log(UTIL_fmt('entering readLoop'));
+ if (!this.dev) return;
+
+ if (this.closing) {
+ this.destroy();
+ return;
+ }
+
+ // No interested listeners, yet we hit readLoop().
+ // Must be clean-up. We do this here to make sure no transfer is pending.
+ if (!this.clients.length) {
+ this.closing = true;
+ this.destroy();
+ return;
+ }
+
+ // firmwareUpdate() sets this.updating when writing the last block before
+ // the signature. We process that reply with the already pending
+ // read transfer but we do not want to start another read transfer for the
+ // signature block, since that request will have no reply.
+ // Instead we will see the device drop and re-appear on the bus.
+ // Current libusb on some platforms gets unhappy when transfer are pending
+ // when that happens.
+ // TODO(mschilder): revisit once Chrome stabilizes its behavior.
+ if (this.updating) {
+ console.log(UTIL_fmt('device updating. Ending readLoop()'));
+ return;
+ }
+
+ var self = this;
+ chrome.hid.receive(
+ this.dev.connectionId,
+ 64,
+ function(x) {
+ if (chrome.runtime.lastError || !x) {
+ console.log(UTIL_fmt('got lastError'));
+ console.log(chrome.runtime.lastError);
+ window.setTimeout(function() { self.destroy(); }, 0);
+ return;
+ }
+ var u8 = new Uint8Array(x);
+ //console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
+
+ self.publishFrame_(x);
+
+ // Read more.
+ window.setTimeout(function() { self.readLoop_(); }, 0);
+ }
+ );
+};
+
+/**
+ * Check whether channel is locked for this request or not.
+ * @param {number} cid
+ * @param {number} cmd
+ * @return {boolean} true if not locked for this request.
+ * @private
+ */
+llHidGnubby.prototype.checkLock_ = function(cid, cmd) {
+ if (this.lockCID) {
+ // We have an active lock.
+ if (this.lockCID != cid) {
+ // Some other channel has active lock.
+
+ if (cmd != llGnubby.CMD_SYNC) {
+ // Anything but SYNC gets an immediate busy.
+ var busy = new Uint8Array(
+ [(cid >> 24) & 255,
+ (cid >> 16) & 255,
+ (cid >> 8) & 255,
+ cid & 255,
+ llGnubby.CMD_ERROR,
+ 0, 1, // length
+ llGnubby.BUSY]);
+ // Log the synthetic busy too.
+ console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
+ this.publishFrame_(busy.buffer);
+ return false;
+ }
+
+ // SYNC gets to go to the device to flush OS tx/rx queues.
+ // The usb firmware always responds to SYNC, regardless of lock status.
+ }
+ }
+ return true;
+};
+
+/**
+ * Update or grab lock.
+ * @param {number} cid
+ * @param {number} cmd
+ * @param {number} arg
+ * @private
+ */
+llHidGnubby.prototype.updateLock_ = function(cid, cmd, arg) {
+ if (this.lockCID == 0 || this.lockCID == cid) {
+ // It is this caller's or nobody's lock.
+ if (this.lockTID) {
+ window.clearTimeout(this.lockTID);
+ this.lockTID = null;
+ }
+
+ if (cmd == llGnubby.CMD_LOCK) {
+ var nseconds = arg;
+ if (nseconds != 0) {
+ this.lockCID = cid;
+ // Set tracking time to be .1 seconds longer than usb device does.
+ this.lockMillis = nseconds * 1000 + 100;
+ } else {
+ // Releasing lock voluntarily.
+ this.lockCID = 0;
+ }
+ }
+
+ // (re)set the lock timeout if we still hold it.
+ if (this.lockCID) {
+ var self = this;
+ this.lockTID = window.setTimeout(
+ function() {
+ console.warn(UTIL_fmt(
+ 'lock for CID ' + cid.toString(16) + ' expired!'));
+ self.lockTID = null;
+ self.lockCID = 0;
+ },
+ this.lockMillis);
+ }
+ }
+};
+
+/**
+ * Queue command to be sent.
+ * If queue was empty, initiate the write.
+ * @param {number} cid The client's channel ID.
+ * @param {number} cmd The command to send.
+ * @param {ArrayBuffer} data
+ */
+llHidGnubby.prototype.queueCommand = function(cid, cmd, data) {
+ if (!this.dev) return;
+ if (!this.checkLock_(cid, cmd)) return;
+
+ var u8 = new Uint8Array(data);
+ var f = new Uint8Array(64);
+
+ llHidGnubby.setCid_(f, cid);
+ f[4] = cmd;
+ f[5] = (u8.length >> 8);
+ f[6] = (u8.length & 255);
+
+ var lockArg = (u8.length > 0) ? u8[0] : 0;
+
+ // Fragment over our 64 byte frames.
+ var n = 7;
+ var seq = 0;
+ for (var i = 0; i < u8.length; ++i) {
+ f[n++] = u8[i];
+ if (n == f.length) {
+ this.queueFrame_(f.buffer, cid, cmd, lockArg);
+
+ f = new Uint8Array(64);
+ llHidGnubby.setCid_(f, cid);
+ cmd = f[4] = seq++;
+ n = 5;
+ }
+ }
+ if (n != 5) {
+ this.queueFrame_(f.buffer, cid, cmd, lockArg);
+ }
+};
+
+/**
+ * Sets the channel id in the frame.
+ * @param {Uint8Array} frame
+ * @param {number} cid The client's channel ID.
+ * @private
+ */
+llHidGnubby.setCid_ = function(frame, cid) {
+ frame[0] = cid >>> 24;
+ frame[1] = cid >>> 16;
+ frame[2] = cid >>> 8;
+ frame[3] = cid;
+};
+
+/**
+ * Updates the lock, and queues the frame for sending. Also begins sending if
+ * no other writes are outstanding.
+ * @param {ArrayBuffer} frame
+ * @param {number} cid The client's channel ID.
+ * @param {number} cmd The command to send.
+ * @param {number} arg
+ * @private
+ */
+llHidGnubby.prototype.queueFrame_ = function(frame, cid, cmd, arg) {
+ this.updateLock_(cid, cmd, arg);
+ var wasEmpty = (this.txqueue.length == 0);
+ this.txqueue.push(frame);
+ if (wasEmpty) this.writePump_();
+};
+
+/**
+ * Stuff queued frames from txqueue[] to device, one by one.
+ * @private
+ */
+llHidGnubby.prototype.writePump_ = function() {
+ if (!this.dev) return; // Ignore.
+
+ if (this.txqueue.length == 0) return; // Done with current queue.
+
+ var frame = this.txqueue[0];
+
+ var self = this;
+ function transferComplete(x) {
+ if (chrome.runtime.lastError) {
+ console.log(UTIL_fmt('got lastError'));
+ console.log(chrome.runtime.lastError);
+ window.setTimeout(function() { self.destroy(); }, 0);
+ return;
+ }
+ self.txqueue.shift(); // drop sent frame from queue.
+ if (self.txqueue.length != 0) {
+ window.setTimeout(function() { self.writePump_(); }, 0);
+ }
+ };
+
+ var u8 = new Uint8Array(frame);
+ //console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
+
+ var u8f = new Uint8Array(64);
+ for (var i = 0; i < u8.length; ++i) {
+ u8f[i] = u8[i];
+ }
+
+ chrome.hid.send(
+ this.dev.connectionId,
+ 0, // report Id
+ u8f.buffer,
+ transferComplete
+ );
+};
+/**
+ * @param {function(Array)} cb
+ */
+llHidGnubby.enumerate = function(cb) {
+ chrome.hid.getDevices({'vendorId': 4176, 'productId': 512}, cb);
+};
+
+/**
+ * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
+ * in.
+ * @param {number} which The index of the device to open.
+ * @param {!chrome.hid.HidDeviceInfo} dev The device to open.
+ * @param {function(number, llGnubby=)} cb Called back with the
+ * result of opening the device.
+ */
+llHidGnubby.open = function(gnubbies, which, dev, cb) {
+ chrome.hid.connect(dev.deviceId, function(handle) {
+ if (chrome.runtime.lastError) {
+ console.log(chrome.runtime.lastError);
+ }
+ if (!handle) {
+ console.warn(UTIL_fmt('failed to connect device. permissions issue?'));
+ cb(-llGnubby.NODEVICE);
+ return;
+ }
+ var nonNullHandle = /** @type {!chrome.hid.HidConnection} */ (handle);
+ var gnubby = new llHidGnubby(gnubbies, nonNullHandle, which);
+ cb(-llGnubby.OK, gnubby);
+ });
+};
+
+/**
+ * @param {*} dev
+ * @return {llGnubbyDeviceId} A device identifier for the device.
+ */
+llHidGnubby.deviceToDeviceId = function(dev) {
+ var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev);
+ var deviceId = { namespace: llHidGnubby.NAMESPACE, device: hidDev.deviceId };
+ return deviceId;
+};
+
+/**
+ * Registers this implementation with gnubbies.
+ * @param {Gnubbies} gnubbies
+ */
+llHidGnubby.register = function(gnubbies) {
+ var HID_GNUBBY_IMPL = {
+ enumerate: llHidGnubby.enumerate,
+ deviceToDeviceId: llHidGnubby.deviceToDeviceId,
+ open: llHidGnubby.open
+ };
+ gnubbies.registerNamespace(llHidGnubby.NAMESPACE, HID_GNUBBY_IMPL);
+};
diff --git a/chrome/browser/resources/cryptotoken/llusbgnubby.js b/chrome/browser/resources/cryptotoken/llusbgnubby.js
new file mode 100644
index 0000000..6dbaf7f
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/llusbgnubby.js
@@ -0,0 +1,480 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Implements a low-level gnubby driver based on chrome.usb.
+ */
+'use strict';
+
+/**
+ * Low level gnubby 'driver'. One per physical USB device.
+ * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
+ * in.
+ * @param {!chrome.usb.ConnectionHandle} dev The device.
+ * @param {number} id The device's id.
+ * @param {number} inEndpoint The device's in endpoint.
+ * @param {number} outEndpoint The device's out endpoint.
+ * @constructor
+ * @implements {llGnubby}
+ */
+function llUsbGnubby(gnubbies, dev, id, inEndpoint, outEndpoint) {
+ /** @private {Gnubbies} */
+ this.gnubbies_ = gnubbies;
+ this.dev = dev;
+ this.id = id;
+ this.inEndpoint = inEndpoint;
+ this.outEndpoint = outEndpoint;
+ this.txqueue = [];
+ this.clients = [];
+ this.lockCID = 0; // channel ID of client holding a lock, if != 0.
+ this.lockMillis = 0; // current lock period.
+ this.lockTID = null; // timer id of lock timeout.
+ this.closing = false; // device to be closed by receive loop.
+ this.updating = false; // device firmware is in final stage of updating.
+ this.inTransferPending = false;
+ this.outTransferPending = false;
+}
+
+/**
+ * Namespace for the llUsbGnubby implementation.
+ * @const
+ */
+llUsbGnubby.NAMESPACE = 'usb';
+
+/** Destroys this low-level device instance. */
+llUsbGnubby.prototype.destroy = function() {
+ if (!this.dev) return; // Already dead.
+
+ this.closing = true;
+
+ console.log(UTIL_fmt('llUsbGnubby.destroy()'));
+
+ // Synthesize a close error frame to alert all clients,
+ // some of which might be in read state.
+ //
+ // Use magic CID 0 to address all.
+ this.publishFrame_(new Uint8Array([
+ 0, 0, 0, 0, // broadcast CID
+ llGnubby.CMD_ERROR,
+ 0, 1, // length
+ llGnubby.GONE]).buffer);
+
+ // Set all clients to closed status and remove them.
+ while (this.clients.length != 0) {
+ var client = this.clients.shift();
+ if (client) client.closed = true;
+ }
+
+ if (this.lockTID) {
+ window.clearTimeout(this.lockTID);
+ this.lockTID = null;
+ }
+
+ var dev = this.dev;
+ this.dev = null;
+
+ var self = this;
+
+ function onClosed() {
+ console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
+ self.gnubbies_.removeOpenDevice(
+ {namespace: llUsbGnubby.NAMESPACE, device: self.id});
+ }
+
+ // Release first.
+ chrome.usb.releaseInterface(dev, 0, function() {
+ console.log(UTIL_fmt('Device ' + dev.handle + ' released'));
+ chrome.usb.closeDevice(dev, onClosed);
+ });
+};
+
+/**
+ * Push frame to all clients.
+ * @param {ArrayBuffer} f
+ * @private
+ */
+llUsbGnubby.prototype.publishFrame_ = function(f) {
+ var old = this.clients;
+
+ var remaining = [];
+ var changes = false;
+ for (var i = 0; i < old.length; ++i) {
+ var client = old[i];
+ if (client.receivedFrame(f)) {
+ // Client still alive; keep on list.
+ remaining.push(client);
+ } else {
+ changes = true;
+ console.log(UTIL_fmt(
+ '[' + client.cid.toString(16) + '] left?'));
+ }
+ }
+ if (changes) this.clients = remaining;
+};
+
+/**
+ * @return {boolean} whether this device is open and ready to use.
+ * @private
+ */
+llUsbGnubby.prototype.readyToUse_ = function() {
+ if (this.closing) return false;
+ if (!this.dev) return false;
+
+ return true;
+};
+
+/**
+ * Reads one reply from the low-level device.
+ * @private
+ */
+llUsbGnubby.prototype.readOneReply_ = function() {
+ if (!this.readyToUse_()) return; // No point in continuing.
+ if (this.updating) return; // Do not bother waiting for final update reply.
+
+ var self = this;
+
+ function inTransferComplete(x) {
+ self.inTransferPending = false;
+
+ if (!self.readyToUse_()) return; // No point in continuing.
+
+ if (chrome.runtime.lastError) {
+ console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
+ console.log(chrome.runtime.lastError);
+ window.setTimeout(function() { self.destroy(); }, 0);
+ return;
+ }
+
+ if (x.data) {
+ var u8 = new Uint8Array(x.data);
+ console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
+
+ self.publishFrame_(x.data);
+
+ // Write another pending request, if any.
+ window.setTimeout(
+ function() {
+ self.txqueue.shift(); // Drop sent frame from queue.
+ self.writeOneRequest_();
+ },
+ 0);
+ } else {
+ console.log(UTIL_fmt('no x.data!'));
+ console.log(x);
+ window.setTimeout(function() { self.destroy(); }, 0);
+ }
+ }
+
+ if (this.inTransferPending == false) {
+ this.inTransferPending = true;
+ chrome.usb.bulkTransfer(
+ /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
+ { direction: 'in', endpoint: this.inEndpoint, length: 2048 },
+ inTransferComplete);
+ } else {
+ throw 'inTransferPending!';
+ }
+};
+
+/**
+ * Register a client for this gnubby.
+ * @param {*} who The client.
+ */
+llUsbGnubby.prototype.registerClient = function(who) {
+ for (var i = 0; i < this.clients.length; ++i) {
+ if (this.clients[i] === who) return; // Already registered.
+ }
+ this.clients.push(who);
+};
+
+/**
+ * De-register a client.
+ * @param {*} who The client.
+ * @return {number} The number of remaining listeners for this device, or -1
+ * Returns number of remaining listeners for this device.
+ * if this had no clients to start with.
+ */
+llUsbGnubby.prototype.deregisterClient = function(who) {
+ var current = this.clients;
+ if (current.length == 0) return -1;
+ this.clients = [];
+ for (var i = 0; i < current.length; ++i) {
+ var client = current[i];
+ if (client !== who) this.clients.push(client);
+ }
+ return this.clients.length;
+};
+
+/**
+ * @param {*} who The client.
+ * @return {boolean} Whether this device has who as a client.
+ */
+llUsbGnubby.prototype.hasClient = function(who) {
+ if (this.clients.length == 0) return false;
+ for (var i = 0; i < this.clients.length; ++i) {
+ if (who === this.clients[i])
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Stuff queued frames from txqueue[] to device, one by one.
+ * @private
+ */
+llUsbGnubby.prototype.writeOneRequest_ = function() {
+ if (!this.readyToUse_()) return; // No point in continuing.
+
+ if (this.txqueue.length == 0) return; // Nothing to send.
+
+ var frame = this.txqueue[0];
+
+ var self = this;
+ function OutTransferComplete(x) {
+ self.outTransferPending = false;
+
+ if (!self.readyToUse_()) return; // No point in continuing.
+
+ if (chrome.runtime.lastError) {
+ console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
+ console.log(chrome.runtime.lastError);
+ window.setTimeout(function() { self.destroy(); }, 0);
+ return;
+ }
+
+ window.setTimeout(function() { self.readOneReply_(); }, 0);
+ };
+
+ var u8 = new Uint8Array(frame);
+ console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
+
+ if (this.outTransferPending == false) {
+ this.outTransferPending = true;
+ chrome.usb.bulkTransfer(
+ /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
+ { direction: 'out', endpoint: this.outEndpoint, data: frame },
+ OutTransferComplete);
+ } else {
+ throw 'outTransferPending!';
+ }
+};
+
+/**
+ * Check whether channel is locked for this request or not.
+ * @param {number} cid
+ * @param {number} cmd
+ * @return {boolean} true if not locked for this request.
+ * @private
+ */
+llUsbGnubby.prototype.checkLock_ = function(cid, cmd) {
+ if (this.lockCID) {
+ // We have an active lock.
+ if (this.lockCID != cid) {
+ // Some other channel has active lock.
+
+ if (cmd != llGnubby.CMD_SYNC) {
+ // Anything but SYNC gets an immediate busy.
+ var busy = new Uint8Array(
+ [(cid >> 24) & 255,
+ (cid >> 16) & 255,
+ (cid >> 8) & 255,
+ cid & 255,
+ llGnubby.CMD_ERROR,
+ 0, 1, // length
+ llGnubby.BUSY]);
+ // Log the synthetic busy too.
+ console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
+ this.publishFrame_(busy.buffer);
+ return false;
+ }
+
+ // SYNC gets to go to the device to flush OS tx/rx queues.
+ // The usb firmware always responds to SYNC, regardless of lock status.
+ }
+ }
+ return true;
+};
+
+/**
+ * Update or grab lock.
+ * @param {number} cid
+ * @param {number} cmd
+ * @param {number} arg
+ * @private
+ */
+llUsbGnubby.prototype.updateLock_ = function(cid, cmd, arg) {
+ if (this.lockCID == 0 || this.lockCID == cid) {
+ // It is this caller's or nobody's lock.
+ if (this.lockTID) {
+ window.clearTimeout(this.lockTID);
+ this.lockTID = null;
+ }
+
+ if (cmd == llGnubby.CMD_LOCK) {
+ var nseconds = arg;
+ if (nseconds != 0) {
+ this.lockCID = cid;
+ // Set tracking time to be .1 seconds longer than usb device does.
+ this.lockMillis = nseconds * 1000 + 100;
+ } else {
+ // Releasing lock voluntarily.
+ this.lockCID = 0;
+ }
+ }
+
+ // (re)set the lock timeout if we still hold it.
+ if (this.lockCID) {
+ var self = this;
+ this.lockTID = window.setTimeout(
+ function() {
+ console.warn(UTIL_fmt(
+ 'lock for CID ' + cid.toString(16) + ' expired!'));
+ self.lockTID = null;
+ self.lockCID = 0;
+ },
+ this.lockMillis);
+ }
+ }
+};
+
+/**
+ * Queue command to be sent.
+ * If queue was empty, initiate the write.
+ * @param {number} cid The client's channel ID.
+ * @param {number} cmd The command to send.
+ * @param {ArrayBuffer} data
+ */
+llUsbGnubby.prototype.queueCommand = function(cid, cmd, data) {
+ if (!this.dev) return;
+ if (!this.checkLock_(cid, cmd)) return;
+
+ var u8 = new Uint8Array(data);
+ var frame = new Uint8Array(u8.length + 7);
+
+ frame[0] = cid >>> 24;
+ frame[1] = cid >>> 16;
+ frame[2] = cid >>> 8;
+ frame[3] = cid;
+ frame[4] = cmd;
+ frame[5] = (u8.length >> 8);
+ frame[6] = (u8.length & 255);
+
+ frame.set(u8, 7);
+
+ var lockArg = (u8.length > 0) ? u8[0] : 0;
+ this.updateLock_(cid, cmd, lockArg);
+
+ var wasEmpty = (this.txqueue.length == 0);
+ this.txqueue.push(frame.buffer);
+ if (wasEmpty) this.writeOneRequest_();
+};
+
+/**
+ * @param {function(Array)} cb
+ */
+llUsbGnubby.enumerate = function(cb) {
+ chrome.usb.getDevices({'vendorId': 4176, 'productId': 529}, cb);
+};
+
+/**
+ * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
+ * in.
+ * @param {number} which The index of the device to open.
+ * @param {!chrome.usb.Device} dev The device to open.
+ * @param {function(number, llGnubby=)} cb Called back with the
+ * result of opening the device.
+ */
+llUsbGnubby.open = function(gnubbies, which, dev, cb) {
+ /** @param {chrome.usb.ConnectionHandle=} handle */
+ function deviceOpened(handle) {
+ if (!handle) {
+ console.warn(UTIL_fmt('failed to open device. permissions issue?'));
+ cb(-llGnubby.NODEVICE);
+ return;
+ }
+ var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle);
+ chrome.usb.listInterfaces(nonNullHandle, function(descriptors) {
+ var inEndpoint, outEndpoint;
+ for (var i = 0; i < descriptors.length; i++) {
+ var descriptor = descriptors[i];
+ for (var j = 0; j < descriptor.endpoints.length; j++) {
+ var endpoint = descriptor.endpoints[j];
+ if (inEndpoint == undefined && endpoint.type == 'bulk' &&
+ endpoint.direction == 'in') {
+ inEndpoint = endpoint.address;
+ }
+ if (outEndpoint == undefined && endpoint.type == 'bulk' &&
+ endpoint.direction == 'out') {
+ outEndpoint = endpoint.address;
+ }
+ }
+ }
+ if (inEndpoint == undefined || outEndpoint == undefined) {
+ console.warn(UTIL_fmt('device lacking an endpoint (broken?)'));
+ chrome.usb.closeDevice(nonNullHandle);
+ cb(-llGnubby.NODEVICE);
+ return;
+ }
+ // Try getting it claimed now.
+ chrome.usb.claimInterface(nonNullHandle, 0, function() {
+ if (chrome.runtime.lastError) {
+ console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
+ console.log(chrome.runtime.lastError);
+ }
+ var claimed = !chrome.runtime.lastError;
+ if (!claimed) {
+ console.warn(UTIL_fmt('failed to claim interface. busy?'));
+ // Claim failed? Let the callers know and bail out.
+ chrome.usb.closeDevice(nonNullHandle);
+ cb(-llGnubby.BUSY);
+ return;
+ }
+ var gnubby = new llUsbGnubby(gnubbies, nonNullHandle, which, inEndpoint,
+ outEndpoint);
+ cb(-llGnubby.OK, gnubby);
+ });
+ });
+ }
+
+ if (llUsbGnubby.runningOnCrOS === undefined) {
+ llUsbGnubby.runningOnCrOS =
+ (window.navigator.appVersion.indexOf('; CrOS ') != -1);
+ }
+ if (llUsbGnubby.runningOnCrOS) {
+ chrome.usb.requestAccess(dev, 0, function(success) {
+ // Even though the argument to requestAccess is a chrome.usb.Device, the
+ // access request is for access to all devices with the same vid/pid.
+ // Curiously, if the first chrome.usb.requestAccess succeeds, a second
+ // call with a separate device with the same vid/pid fails. Since
+ // chrome.usb.openDevice will fail if a previous access request really
+ // failed, just ignore the outcome of the access request and move along.
+ chrome.usb.openDevice(dev, deviceOpened);
+ });
+ } else {
+ chrome.usb.openDevice(dev, deviceOpened);
+ }
+};
+
+/**
+ * @param {*} dev
+ * @return {llGnubbyDeviceId} A device identifier for the device.
+ */
+llUsbGnubby.deviceToDeviceId = function(dev) {
+ var usbDev = /** @type {!chrome.usb.Device} */ (dev);
+ var deviceId = { namespace: llUsbGnubby.NAMESPACE, device: usbDev.device };
+ return deviceId;
+};
+
+/**
+ * Registers this implementation with gnubbies.
+ * @param {Gnubbies} gnubbies
+ */
+llUsbGnubby.register = function(gnubbies) {
+ var USB_GNUBBY_IMPL = {
+ enumerate: llUsbGnubby.enumerate,
+ deviceToDeviceId: llUsbGnubby.deviceToDeviceId,
+ open: llUsbGnubby.open
+ };
+ gnubbies.registerNamespace(llUsbGnubby.NAMESPACE, USB_GNUBBY_IMPL);
+};
diff --git a/chrome/browser/resources/cryptotoken/manifest.json b/chrome/browser/resources/cryptotoken/manifest.json
new file mode 100644
index 0000000..0ae3b0b
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/manifest.json
@@ -0,0 +1,58 @@
+{
+ "name": "CryptoTokenExtension",
+ "version": "0.0.1",
+ "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7zRobvA+AVlvNqkHSSVhh1sEWsHSqz4oR/XptkDe/Cz3+gW9ZGumZ20NCHjaac8j1iiesdigp8B1LJsd/2WWv2Dbnto4f8GrQ5MVphKyQ9WJHwejEHN2K4vzrTcwaXqv5BSTXwxlxS/mXCmXskTfryKTLuYrcHEWK8fCHb+0gvr8b/kvsi75A1aMmb6nUnFJvETmCkOCPNX5CHTdy634Ts/x0fLhRuPlahk63rdf7agxQv5viVjQFk+tbgv6aa9kdSd11Js/RZ9yZjrFgHOBWgP4jTBqud4+HUglrzu8qynFipyNRLCZsaxhm+NItTyNgesxLdxZcwOz56KD1Q4IQIDAQAB",
+ "manifest_version": 2,
+ "description": "CryptoToken Component Extension",
+ "permissions": [
+ "hid",
+ "usb",
+ {
+ "usbDevices": [
+ {"vendorId": 4176, "productId": 512},
+ {"vendorId": 4176, "productId": 529}
+ ]
+ },
+ "https://www.gstatic.com/"
+ ],
+ "externally_connectable": {
+ "matches": [
+ "https://accounts.google.com/*",
+ "https://security.google.com/*"
+ ],
+ "accepts_tls_channel_id": true
+ },
+ "background": {
+ "scripts": [
+ "util.js",
+ "b64.js",
+ "closeable.js",
+ "countdown.js",
+ "sha256.js",
+ "llgnubby.js",
+ "llhidgnubby.js",
+ "llusbgnubby.js",
+ "gnubbies.js",
+ "gnubby.js",
+ "gnubby-u2f.js",
+ "gnubbycodetypes.js",
+ "gnubbyfactory.js",
+ "gnubbymsgtypes.js",
+ "usbgnubbyfactory.js",
+ "devicestatuscodes.js",
+ "enroller.js",
+ "enrollhelper.js",
+ "usbenrollhelper.js",
+ "requestqueue.js",
+ "signer.js",
+ "signhelper.js",
+ "singlesigner.js",
+ "multiplesigner.js",
+ "usbsignhelper.js",
+ "webrequest.js",
+ "background.js"
+ ],
+ "persistent": false
+ }
+}
+
diff --git a/chrome/browser/resources/cryptotoken/multiplesigner.js b/chrome/browser/resources/cryptotoken/multiplesigner.js
new file mode 100644
index 0000000..e132a2a
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/multiplesigner.js
@@ -0,0 +1,276 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview A multiple gnubby signer wraps the process of opening a number
+ * of gnubbies, signing each challenge in an array of challenges until a
+ * success condition is satisfied, and yielding each succeeding gnubby.
+ *
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * Creates a new sign handler with an array of gnubby indexes.
+ * @param {!GnubbyFactory} factory Used to create and open the gnubbies.
+ * @param {Array.<llGnubbyDeviceId>} gnubbyIndexes Which gnubbies to open.
+ * @param {boolean} forEnroll Whether this signer is signing for an attempted
+ * enroll operation.
+ * @param {function(boolean, (number|undefined))} completedCb Called when this
+ * signer completes sign attempts, i.e. no further results should be
+ * expected.
+ * @param {function(number, MultipleSignerResult)} gnubbyFoundCb Called with
+ * each gnubby/challenge that yields a successful result.
+ * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the
+ * signer will not attempt any new operations, assuming the caller is no
+ * longer interested in the outcome.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @constructor
+ */
+function MultipleGnubbySigner(factory, gnubbyIndexes, forEnroll, completedCb,
+ gnubbyFoundCb, opt_timer, opt_logMsgUrl) {
+ /** @private {!GnubbyFactory} */
+ this.factory_ = factory;
+ /** @private {Array.<llGnubbyDeviceId>} */
+ this.gnubbyIndexes_ = gnubbyIndexes;
+ /** @private {boolean} */
+ this.forEnroll_ = forEnroll;
+ /** @private {function(boolean, (number|undefined))} */
+ this.completedCb_ = completedCb;
+ /** @private {function(number, MultipleSignerResult)} */
+ this.gnubbyFoundCb_ = gnubbyFoundCb;
+ /** @private {Countdown|undefined} */
+ this.timer_ = opt_timer;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+
+ /** @private {Array.<SignHelperChallenge>} */
+ this.challenges_ = [];
+ /** @private {boolean} */
+ this.challengesFinal_ = false;
+
+ // Create a signer for each gnubby.
+ /** @private {boolean} */
+ this.anySucceeded_ = false;
+ /** @private {number} */
+ this.numComplete_ = 0;
+ /** @private {Array.<SingleGnubbySigner>} */
+ this.signers_ = [];
+ /** @private {Array.<boolean>} */
+ this.stillGoing_ = [];
+ /** @private {Array.<number>} */
+ this.errorStatus_ = [];
+ for (var i = 0; i < gnubbyIndexes.length; i++) {
+ this.addGnubby(gnubbyIndexes[i]);
+ }
+}
+
+/**
+ * Attempts to open this signer's gnubbies, if they're not already open.
+ * (This is implicitly done by addChallenges.)
+ */
+MultipleGnubbySigner.prototype.open = function() {
+ for (var i = 0; i < this.signers_.length; i++) {
+ this.signers_[i].open();
+ }
+};
+
+/**
+ * Closes this signer's gnubbies, if any are open.
+ */
+MultipleGnubbySigner.prototype.close = function() {
+ for (var i = 0; i < this.signers_.length; i++) {
+ this.signers_[i].close();
+ }
+};
+
+/**
+ * Adds challenges to the set of challenges being tried by this signer.
+ * The challenges are an array of challenge objects, where each challenge
+ * object's values are base64-encoded.
+ * If the signer is currently idle, begins signing the new challenges.
+ *
+ * @param {Array} challenges
+ * @param {boolean} finalChallenges
+ * @return {boolean} whether the challenges were successfully added.
+ * @public
+ */
+MultipleGnubbySigner.prototype.addEncodedChallenges =
+ function(challenges, finalChallenges) {
+ var decodedChallenges = [];
+ if (challenges) {
+ for (var i = 0; i < challenges.length; i++) {
+ var decodedChallenge = {};
+ var challenge = challenges[i];
+ decodedChallenge['challengeHash'] =
+ B64_decode(challenge['challengeHash']);
+ decodedChallenge['appIdHash'] = B64_decode(challenge['appIdHash']);
+ decodedChallenge['keyHandle'] = B64_decode(challenge['keyHandle']);
+ if (challenge['version']) {
+ decodedChallenge['version'] = challenge['version'];
+ }
+ decodedChallenges.push(decodedChallenge);
+ }
+ }
+ return this.addChallenges(decodedChallenges, finalChallenges);
+};
+
+/**
+ * Adds challenges to the set of challenges being tried by this signer.
+ * If the signer is currently idle, begins signing the new challenges.
+ *
+ * @param {Array.<SignHelperChallenge>} challenges
+ * @param {boolean} finalChallenges
+ * @return {boolean} whether the challenges were successfully added.
+ * @public
+ */
+MultipleGnubbySigner.prototype.addChallenges =
+ function(challenges, finalChallenges) {
+ if (this.challengesFinal_) {
+ // Can't add new challenges once they're finalized.
+ return false;
+ }
+
+ if (challenges) {
+ for (var i = 0; i < challenges.length; i++) {
+ this.challenges_.push(challenges[i]);
+ }
+ }
+ this.challengesFinal_ = finalChallenges;
+
+ for (var i = 0; i < this.signers_.length; i++) {
+ this.stillGoing_[i] =
+ this.signers_[i].addChallenges(challenges, finalChallenges);
+ this.errorStatus_[i] = 0;
+ }
+ return true;
+};
+
+/**
+ * Adds a new gnubby to this signer's list of gnubbies. (Only possible while
+ * this signer is still signing: without this restriction, the morePossible
+ * indication in the callbacks could become violated.) If this signer has
+ * challenges to sign, begins signing on the new gnubby with them.
+ * @param {llGnubbyDeviceId} gnubbyIndex The index of the gnubby to add.
+ * @return {boolean} Whether the gnubby was added successfully.
+ * @public
+ */
+MultipleGnubbySigner.prototype.addGnubby = function(gnubbyIndex) {
+ if (this.numComplete_ && this.numComplete_ == this.signers_.length)
+ return false;
+
+ var index = this.signers_.length;
+ this.signers_.push(
+ new SingleGnubbySigner(
+ this.factory_,
+ gnubbyIndex,
+ this.forEnroll_,
+ this.signFailedCallback_.bind(this, index),
+ this.signSucceededCallback_.bind(this, index),
+ this.timer_ ? this.timer_.clone() : null,
+ this.logMsgUrl_));
+ this.stillGoing_.push(false);
+
+ if (this.challenges_.length) {
+ this.stillGoing_[index] =
+ this.signers_[index].addChallenges(this.challenges_,
+ this.challengesFinal_);
+ }
+ return true;
+};
+
+/**
+ * Called by a SingleGnubbySigner upon failure, i.e. unsuccessful completion of
+ * all its sign operations.
+ * @param {number} index the index of the gnubby whose result this is
+ * @param {number} code the result code of the sign operation
+ * @private
+ */
+MultipleGnubbySigner.prototype.signFailedCallback_ = function(index, code) {
+ console.log(
+ UTIL_fmt('failure. gnubby ' + index + ' got code ' + code.toString(16)));
+ if (!this.stillGoing_[index]) {
+ console.log(UTIL_fmt('gnubby ' + index + ' no longer running!'));
+ // Shouldn't ever happen? Disregard.
+ return;
+ }
+ this.stillGoing_[index] = false;
+ this.errorStatus_[index] = code;
+ this.numComplete_++;
+ var morePossible = this.numComplete_ < this.signers_.length;
+ if (!morePossible)
+ this.notifyComplete_();
+};
+
+/**
+ * Called by a SingleGnubbySigner upon success.
+ * @param {number} index the index of the gnubby whose result this is
+ * @param {usbGnubby} gnubby the underlying gnubby that succeded.
+ * @param {number} code the result code of the sign operation
+ * @param {SingleSignerResult=} signResult
+ * @private
+ */
+MultipleGnubbySigner.prototype.signSucceededCallback_ =
+ function(index, gnubby, code, signResult) {
+ console.log(UTIL_fmt('success! gnubby ' + index + ' got code ' +
+ code.toString(16)));
+ if (!this.stillGoing_[index]) {
+ console.log(UTIL_fmt('gnubby ' + index + ' no longer running!'));
+ // Shouldn't ever happen? Disregard.
+ return;
+ }
+ this.anySucceeded_ = true;
+ this.stillGoing_[index] = false;
+ this.notifySuccess_(code, gnubby, index, signResult);
+ this.numComplete_++;
+ var morePossible = this.numComplete_ < this.signers_.length;
+ if (!morePossible)
+ this.notifyComplete_();
+};
+
+/**
+ * @private
+ */
+MultipleGnubbySigner.prototype.notifyComplete_ = function() {
+ // See if any of the signers failed with a strange error. If so, report a
+ // single error to the caller, partly as a diagnostic aid and partly to
+ // distinguish real failures from wrong data.
+ var funnyBusiness;
+ for (var i = 0; i < this.errorStatus_.length; i++) {
+ if (this.errorStatus_[i] &&
+ this.errorStatus_[i] != DeviceStatusCodes.WRONG_DATA_STATUS &&
+ this.errorStatus_[i] != DeviceStatusCodes.WAIT_TOUCH_STATUS) {
+ funnyBusiness = this.errorStatus_[i];
+ break;
+ }
+ }
+ if (funnyBusiness) {
+ console.warn(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ', ' +
+ 'funny error = ' + funnyBusiness + ')'));
+ } else {
+ console.log(UTIL_fmt('all done (success: ' + this.anySucceeded_ + ')'));
+ }
+ this.completedCb_(this.anySucceeded_, funnyBusiness);
+};
+
+/**
+ * @param {number} code
+ * @param {usbGnubby} gnubby
+ * @param {number} gnubbyIndex
+ * @param {SingleSignerResult=} singleSignerResult
+ * @private
+ */
+MultipleGnubbySigner.prototype.notifySuccess_ =
+ function(code, gnubby, gnubbyIndex, singleSignerResult) {
+ console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
+ var signResult = {
+ 'gnubby': gnubby,
+ 'gnubbyIndex': gnubbyIndex
+ };
+ if (singleSignerResult && singleSignerResult['challenge'])
+ signResult['challenge'] = singleSignerResult['challenge'];
+ if (singleSignerResult && singleSignerResult['info'])
+ signResult['info'] = singleSignerResult['info'];
+ this.gnubbyFoundCb_(code, signResult);
+};
diff --git a/chrome/browser/resources/cryptotoken/requestqueue.js b/chrome/browser/resources/cryptotoken/requestqueue.js
new file mode 100644
index 0000000..4628554
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/requestqueue.js
@@ -0,0 +1,181 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Queue of pending requests from an origin.
+ *
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * Represents a queued request. Once given a token, call complete() once the
+ * request is processed (or dropped.)
+ * @interface
+ */
+function QueuedRequestToken() {}
+
+/** Completes (or cancels) this queued request. */
+QueuedRequestToken.prototype.complete = function() {};
+
+/**
+ * @param {!RequestQueue} queue The queue for this request.
+ * @param {function(QueuedRequestToken)} beginCb Called when work may begin on
+ * this request.
+ * @param {RequestToken} opt_prev Previous request in the same queue.
+ * @param {RequestToken} opt_next Next request in the same queue.
+ * @constructor
+ * @implements {QueuedRequestToken}
+ */
+function RequestToken(queue, beginCb, opt_prev, opt_next) {
+ /** @private {!RequestQueue} */
+ this.queue_ = queue;
+ /** @type {function(QueuedRequestToken)} */
+ this.beginCb = beginCb;
+ /** @type {RequestToken} */
+ this.prev = null;
+ /** @type {RequestToken} */
+ this.next = null;
+ /** @private {boolean} */
+ this.completed_ = false;
+}
+
+/** Completes (or cancels) this queued request. */
+RequestToken.prototype.complete = function() {
+ if (this.completed_) {
+ // Either the caller called us more than once, or the timer is firing.
+ // Either way, nothing more to do here.
+ return;
+ }
+ this.completed_ = true;
+ this.queue_.complete(this);
+};
+
+/** @return {boolean} Whether this token has already completed. */
+RequestToken.prototype.completed = function() {
+ return this.completed_;
+};
+
+/**
+ * @constructor
+ */
+function RequestQueue() {
+ /** @private {RequestToken} */
+ this.head_ = null;
+ /** @private {RequestToken} */
+ this.tail_ = null;
+}
+
+/**
+ * Inserts this token into the queue.
+ * @param {RequestToken} token
+ * @private
+ */
+RequestQueue.prototype.insertToken_ = function(token) {
+ if (this.head_ === null) {
+ this.head_ = token;
+ this.tail_ = token;
+ } else {
+ if (!this.tail_) throw 'Non-empty list missing tail';
+ this.tail_.next = token;
+ token.prev = this.tail_;
+ this.tail_ = token;
+ }
+};
+
+/**
+ * Removes this token from the queue.
+ * @param {RequestToken} token
+ * @private
+ */
+RequestQueue.prototype.removeToken_ = function(token) {
+ if (token.next) {
+ token.next.prev = token.prev;
+ }
+ if (token.prev) {
+ token.prev.next = token.next;
+ }
+ if (this.head_ === token && this.tail_ === token) {
+ this.head_ = this.tail_ = null;
+ } else {
+ if (this.head_ === token) {
+ this.head_ = token.next;
+ this.head_.prev = null;
+ }
+ if (this.tail_ === token) {
+ this.tail_ = token.prev;
+ this.tail_.next = null;
+ }
+ }
+ token.prev = token.next = null;
+};
+
+/**
+ * Completes this token's request, and begins the next queued request, if one
+ * exists.
+ * @param {RequestToken} token
+ */
+RequestQueue.prototype.complete = function(token) {
+ var next = token.next;
+ this.removeToken_(token);
+ if (next) {
+ next.beginCb(next);
+ }
+};
+
+/** @return {boolean} Whether this queue is empty. */
+RequestQueue.prototype.empty = function() {
+ return this.head_ === null;
+};
+
+/**
+ * Queues this request, and, if it's the first request, begins work on it.
+ * @param {function(QueuedRequestToken)} beginCb Called when work begins on this
+ * request.
+ * @param {Countdown} timer
+ * @return {QueuedRequestToken} A token for the request.
+ */
+RequestQueue.prototype.queueRequest = function(beginCb, timer) {
+ var startNow = this.empty();
+ var token = new RequestToken(this, beginCb);
+ // Clone the timer to set a callback on it, which will ensure complete() is
+ // eventually called, even if the caller never gets around to it.
+ timer.clone(token.complete.bind(token));
+ this.insertToken_(token);
+ if (startNow) {
+ window.setTimeout(function() {
+ if (!token.completed()) {
+ token.beginCb(token);
+ }
+ }, 0);
+ }
+ return token;
+};
+
+/**
+ * @constructor
+ */
+function OriginKeyedRequestQueue() {
+ /** @private {Object.<string, !RequestQueue>} */
+ this.requests_ = {};
+}
+
+/**
+ * Queues this request, and, if it's the first request, begins work on it.
+ * @param {string} appId
+ * @param {string} origin
+ * @param {function(QueuedRequestToken)} beginCb Called when work begins on this
+ * request.
+ * @param {Countdown} timer
+ * @return {QueuedRequestToken} A token for the request.
+ */
+OriginKeyedRequestQueue.prototype.queueRequest =
+ function(appId, origin, beginCb, timer) {
+ var key = appId + origin;
+ if (!this.requests_.hasOwnProperty(key)) {
+ this.requests_[key] = new RequestQueue();
+ }
+ var queue = this.requests_[key];
+ return queue.queueRequest(beginCb, timer);
+};
diff --git a/chrome/browser/resources/cryptotoken/sha256.js b/chrome/browser/resources/cryptotoken/sha256.js
new file mode 100644
index 0000000..b41cb92
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/sha256.js
@@ -0,0 +1,156 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// SHA256 in javascript by mschilder.
+//
+// SHA256 {
+// SHA256();
+// void reset();
+// void update(byte[] data, opt_length);
+// byte[32] digest();
+// }
+
+/** @constructor */
+function SHA256() {
+ this._buf = new Array(64);
+ this._W = new Array(64);
+ this._pad = new Array(64);
+ this._k = [
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
+ 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
+ 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
+ 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
+ 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
+ 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
+ 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
+ 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
+
+ this._pad[0] = 0x80;
+ for (var i = 1; i < 64; ++i) this._pad[i] = 0;
+
+ this.reset();
+}
+
+SHA256.prototype.reset = function() {
+ this._chain = [
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
+
+ this._inbuf = 0;
+ this._total = 0;
+};
+
+SHA256.prototype._compress = function(buf) {
+ var W = this._W;
+ var k = this._k;
+
+ function _rotr(w, r) { return ((w << (32 - r)) | (w >>> r)); };
+
+ // get 16 big endian words
+ for (var i = 0; i < 64; i += 4) {
+ var w = (buf[i] << 24) | (buf[i + 1] << 16) | (buf[i + 2] << 8) | (buf[i + 3]);
+ W[i / 4] = w;
+ }
+
+ // expand to 64 words
+ for (var i = 16; i < 64; ++i) {
+ var s0 = _rotr(W[i - 15], 7) ^ _rotr(W[i - 15], 18) ^ (W[i - 15] >>> 3);
+ var s1 = _rotr(W[i - 2], 17) ^ _rotr(W[i - 2], 19) ^ (W[i - 2] >>> 10);
+ W[i] = (W[i - 16] + s0 + W[i - 7] + s1) & 0xffffffff;
+ }
+
+ var A = this._chain[0];
+ var B = this._chain[1];
+ var C = this._chain[2];
+ var D = this._chain[3];
+ var E = this._chain[4];
+ var F = this._chain[5];
+ var G = this._chain[6];
+ var H = this._chain[7];
+
+ for (var i = 0; i < 64; ++i) {
+ var S0 = _rotr(A, 2) ^ _rotr(A, 13) ^ _rotr(A, 22);
+ var maj = (A & B) ^ (A & C) ^ (B & C);
+ var t2 = (S0 + maj) & 0xffffffff;
+ var S1 = _rotr(E, 6) ^ _rotr(E, 11) ^ _rotr(E, 25);
+ var ch = (E & F) ^ ((~E) & G);
+ var t1 = (H + S1 + ch + k[i] + W[i]) & 0xffffffff;
+
+ H = G;
+ G = F;
+ F = E;
+ E = (D + t1) & 0xffffffff;
+ D = C;
+ C = B;
+ B = A;
+ A = (t1 + t2) & 0xffffffff;
+ }
+
+ this._chain[0] += A;
+ this._chain[1] += B;
+ this._chain[2] += C;
+ this._chain[3] += D;
+ this._chain[4] += E;
+ this._chain[5] += F;
+ this._chain[6] += G;
+ this._chain[7] += H;
+};
+
+SHA256.prototype.update = function(bytes, opt_length) {
+ if (!opt_length) opt_length = bytes.length;
+
+ this._total += opt_length;
+ for (var n = 0; n < opt_length; ++n) {
+ this._buf[this._inbuf++] = bytes[n];
+ if (this._inbuf == 64) {
+ this._compress(this._buf);
+ this._inbuf = 0;
+ }
+ }
+};
+
+SHA256.prototype.updateRange = function(bytes, start, end) {
+ this._total += (end - start);
+ for (var n = start; n < end; ++n) {
+ this._buf[this._inbuf++] = bytes[n];
+ if (this._inbuf == 64) {
+ this._compress(this._buf);
+ this._inbuf = 0;
+ }
+ }
+};
+
+/**
+ * Optionally update the hash with additional arguments, and return the
+ * resulting hash value.
+ * @param {...*} var_args
+ * @return the SHA256 hash value.
+ */
+SHA256.prototype.digest = function(var_args) {
+ for (var i = 0; i < arguments.length; ++i)
+ this.update(arguments[i]);
+
+ var digest = new Array(32);
+ var totalBits = this._total * 8;
+
+ // add pad 0x80 0x00*
+ if (this._inbuf < 56)
+ this.update(this._pad, 56 - this._inbuf);
+ else
+ this.update(this._pad, 64 - (this._inbuf - 56));
+
+ // add # bits, big endian
+ for (var i = 63; i >= 56; --i) {
+ this._buf[i] = totalBits & 255;
+ totalBits >>>= 8;
+ }
+
+ this._compress(this._buf);
+
+ var n = 0;
+ for (var i = 0; i < 8; ++i)
+ for (var j = 24; j >= 0; j -= 8)
+ digest[n++] = (this._chain[i] >> j) & 255;
+
+ return digest;
+};
diff --git a/chrome/browser/resources/cryptotoken/signer.js b/chrome/browser/resources/cryptotoken/signer.js
new file mode 100644
index 0000000..e17142e
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/signer.js
@@ -0,0 +1,626 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Handles web page requests for gnubby sign requests.
+ *
+ * @author [email protected] (Juan Lang)
+ */
+
+'use strict';
+
+var signRequestQueue = new OriginKeyedRequestQueue();
+
+/**
+ * Handles a sign request.
+ * @param {!SignHelperFactory} factory Factory to create a sign helper.
+ * @param {MessageSender} sender The sender of the message.
+ * @param {Object} request The web page's sign request.
+ * @param {boolean} enforceAppIdValid Whether to enforce that the app id in the
+ * request matches the sender's origin.
+ * @param {Function} sendResponse Called back with the result of the sign.
+ * @param {boolean} toleratesMultipleResponses Whether the sendResponse
+ * callback can be called more than once, e.g. for progress updates.
+ * @return {Closeable}
+ */
+function handleSignRequest(factory, sender, request, enforceAppIdValid,
+ sendResponse, toleratesMultipleResponses) {
+ var sentResponse = false;
+ function sendResponseOnce(r) {
+ if (queuedSignRequest) {
+ queuedSignRequest.close();
+ queuedSignRequest = null;
+ }
+ if (!sentResponse) {
+ sentResponse = true;
+ try {
+ // If the page has gone away or the connection has otherwise gone,
+ // sendResponse fails.
+ sendResponse(r);
+ } catch (exception) {
+ console.warn('sendResponse failed: ' + exception);
+ }
+ } else {
+ console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
+ }
+ }
+
+ function sendErrorResponse(code) {
+ var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY, code);
+ sendResponseOnce(response);
+ }
+
+ function sendSuccessResponse(challenge, info, browserData) {
+ var responseData = {};
+ for (var k in challenge) {
+ responseData[k] = challenge[k];
+ }
+ responseData['browserData'] = B64_encode(UTIL_StringToBytes(browserData));
+ responseData['signatureData'] = info;
+ var response = formatWebPageResponse(GnubbyMsgTypes.SIGN_WEB_REPLY,
+ GnubbyCodeTypes.OK, responseData);
+ sendResponseOnce(response);
+ }
+
+ function sendNotification(code) {
+ console.log(UTIL_fmt('notification, code=' + code));
+ // Can the callback handle progress updates? If so, send one.
+ if (toleratesMultipleResponses) {
+ var response = formatWebPageResponse(
+ GnubbyMsgTypes.SIGN_WEB_NOTIFICATION, code);
+ if (request['requestId']) {
+ response['requestId'] = request['requestId'];
+ }
+ sendResponse(response);
+ }
+ }
+
+ var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
+ if (!origin) {
+ sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
+ return null;
+ }
+ // More closure type inference fail.
+ var nonNullOrigin = /** @type {string} */ (origin);
+
+ if (!isValidSignRequest(request)) {
+ sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
+ return null;
+ }
+
+ var signData = request['signData'];
+ // A valid sign data has at least one challenge, so get the first appId from
+ // the first challenge.
+ var firstAppId = signData[0]['appId'];
+ var timeoutMillis = Signer.DEFAULT_TIMEOUT_MILLIS;
+ if (request['timeout']) {
+ // Request timeout is in seconds.
+ timeoutMillis = request['timeout'] * 1000;
+ }
+ var timer = new CountdownTimer(timeoutMillis);
+ var logMsgUrl = request['logMsgUrl'];
+
+ // Queue sign requests from the same origin, to protect against simultaneous
+ // sign-out on many tabs resulting in repeated sign-in requests.
+ var queuedSignRequest = new QueuedSignRequest(signData, factory, timer,
+ nonNullOrigin, enforceAppIdValid, sendErrorResponse, sendSuccessResponse,
+ sendNotification, sender.tlsChannelId, logMsgUrl);
+ var requestToken = signRequestQueue.queueRequest(firstAppId, nonNullOrigin,
+ queuedSignRequest.begin.bind(queuedSignRequest), timer);
+ queuedSignRequest.setToken(requestToken);
+ return queuedSignRequest;
+}
+
+/**
+ * Returns whether the request appears to be a valid sign request.
+ * @param {Object} request the request.
+ * @return {boolean} whether the request appears valid.
+ */
+function isValidSignRequest(request) {
+ if (!request.hasOwnProperty('signData'))
+ return false;
+ var signData = request['signData'];
+ // If a sign request contains an empty array of challenges, it could never
+ // be fulfilled. Fail.
+ if (!signData.length)
+ return false;
+ return isValidSignData(signData);
+}
+
+/**
+ * Adapter class representing a queued sign request.
+ * @param {!SignData} signData
+ * @param {!SignHelperFactory} factory
+ * @param {Countdown} timer
+ * @param {string} origin
+ * @param {boolean} enforceAppIdValid
+ * @param {function(number)} errorCb
+ * @param {function(SignChallenge, string, string)} successCb
+ * @param {(function(number)|undefined)} opt_progressCb
+ * @param {string|undefined} opt_tlsChannelId
+ * @param {string|undefined} opt_logMsgUrl
+ * @constructor
+ * @implements {Closeable}
+ */
+function QueuedSignRequest(signData, factory, timer, origin, enforceAppIdValid,
+ errorCb, successCb, opt_progressCb, opt_tlsChannelId, opt_logMsgUrl) {
+ /** @private {!SignData} */
+ this.signData_ = signData;
+ /** @private {!SignHelperFactory} */
+ this.factory_ = factory;
+ /** @private {Countdown} */
+ this.timer_ = timer;
+ /** @private {string} */
+ this.origin_ = origin;
+ /** @private {boolean} */
+ this.enforceAppIdValid_ = enforceAppIdValid;
+ /** @private {function(number)} */
+ this.errorCb_ = errorCb;
+ /** @private {function(SignChallenge, string, string)} */
+ this.successCb_ = successCb;
+ /** @private {(function(number)|undefined)} */
+ this.progressCb_ = opt_progressCb;
+ /** @private {string|undefined} */
+ this.tlsChannelId_ = opt_tlsChannelId;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+ /** @private {boolean} */
+ this.begun_ = false;
+ /** @private {boolean} */
+ this.closed_ = false;
+}
+
+/** Closes this sign request. */
+QueuedSignRequest.prototype.close = function() {
+ if (this.closed_) return;
+ if (this.begun_ && this.signer_) {
+ this.signer_.close();
+ }
+ if (this.token_) {
+ this.token_.complete();
+ }
+ this.closed_ = true;
+};
+
+/**
+ * @param {QueuedRequestToken} token Token for this sign request.
+ */
+QueuedSignRequest.prototype.setToken = function(token) {
+ /** @private {QueuedRequestToken} */
+ this.token_ = token;
+};
+
+/**
+ * Called when this sign request may begin work.
+ * @param {QueuedRequestToken} token Token for this sign request.
+ */
+QueuedSignRequest.prototype.begin = function(token) {
+ this.begun_ = true;
+ this.setToken(token);
+ this.signer_ = new Signer(this.factory_, this.timer_, this.origin_,
+ this.enforceAppIdValid_, this.signerFailed_.bind(this),
+ this.signerSucceeded_.bind(this), this.progressCb_,
+ this.tlsChannelId_, this.logMsgUrl_);
+ if (!this.signer_.setChallenges(this.signData_)) {
+ token.complete();
+ this.errorCb_(GnubbyCodeTypes.BAD_REQUEST);
+ }
+};
+
+/**
+ * Called when this request's signer fails.
+ * @param {number} code The failure code reported by the signer.
+ * @private
+ */
+QueuedSignRequest.prototype.signerFailed_ = function(code) {
+ this.token_.complete();
+ this.errorCb_(code);
+};
+
+/**
+ * Called when this request's signer succeeds.
+ * @param {SignChallenge} challenge The challenge that was signed.
+ * @param {string} info The sign result.
+ * @param {string} browserData
+ * @private
+ */
+QueuedSignRequest.prototype.signerSucceeded_ =
+ function(challenge, info, browserData) {
+ this.token_.complete();
+ this.successCb_(challenge, info, browserData);
+};
+
+/**
+ * Creates an object to track signing with a gnubby.
+ * @param {!SignHelperFactory} helperFactory Factory to create a sign helper.
+ * @param {Countdown} timer Timer for sign request.
+ * @param {string} origin The origin making the request.
+ * @param {boolean} enforceAppIdValid Whether to enforce that the appId in the
+ * request matches the sender's origin.
+ * @param {function(number)} errorCb Called when the sign operation fails.
+ * @param {function(SignChallenge, string, string)} successCb Called when the
+ * sign operation succeeds.
+ * @param {(function(number)|undefined)} opt_progressCb Called with progress
+ * updates to the sign request.
+ * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
+ * making the request.
+ * @param {string=} opt_logMsgUrl The url to post log messages to.
+ * @constructor
+ */
+function Signer(helperFactory, timer, origin, enforceAppIdValid,
+ errorCb, successCb, opt_progressCb, opt_tlsChannelId, opt_logMsgUrl) {
+ /** @private {Countdown} */
+ this.timer_ = timer;
+ /** @private {string} */
+ this.origin_ = origin;
+ /** @private {boolean} */
+ this.enforceAppIdValid_ = enforceAppIdValid;
+ /** @private {function(number)} */
+ this.errorCb_ = errorCb;
+ /** @private {function(SignChallenge, string, string)} */
+ this.successCb_ = successCb;
+ /** @private {(function(number)|undefined)} */
+ this.progressCb_ = opt_progressCb;
+ /** @private {string|undefined} */
+ this.tlsChannelId_ = opt_tlsChannelId;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+
+ /** @private {boolean} */
+ this.challengesSet_ = false;
+ /** @private {Array.<SignHelperChallenge>} */
+ this.pendingChallenges_ = [];
+ /** @private {boolean} */
+ this.done_ = false;
+
+ /** @private {Object.<string, string>} */
+ this.browserData_ = {};
+ /** @private {Object.<string, SignChallenge>} */
+ this.serverChallenges_ = {};
+ // Allow http appIds for http origins. (Broken, but the caller deserves
+ // what they get.)
+ /** @private {boolean} */
+ this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
+
+ // Protect against helper failure with a watchdog.
+ this.createWatchdog_(timer);
+ /** @private {SignHelper} */
+ this.helper_ = helperFactory.createHelper(
+ timer, this.helperError_.bind(this), this.helperSuccess_.bind(this),
+ this.helperProgress_.bind(this), this.logMsgUrl_);
+}
+
+/**
+ * Creates a timer with an expiry greater than the expiration time of the given
+ * timer.
+ * @param {Countdown} timer
+ * @private
+ */
+Signer.prototype.createWatchdog_ = function(timer) {
+ var millis = timer.millisecondsUntilExpired();
+ millis += CountdownTimer.TIMER_INTERVAL_MILLIS;
+ /** @private {Countdown|undefined} */
+ this.watchdogTimer_ = new CountdownTimer(millis, this.timeout_.bind(this));
+};
+
+/**
+ * Default timeout value in case the caller never provides a valid timeout.
+ */
+Signer.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
+
+/**
+ * Sets the challenges to be signed.
+ * @param {SignData} signData The challenges to set.
+ * @return {boolean} Whether the challenges could be set.
+ */
+Signer.prototype.setChallenges = function(signData) {
+ if (this.challengesSet_ || this.done_)
+ return false;
+ /** @private {SignData} */
+ this.signData_ = signData;
+ /** @private {boolean} */
+ this.challengesSet_ = true;
+
+ // If app id enforcing isn't in effect, go ahead and start the helper with
+ // all of the incoming challenges.
+ var success = true;
+ if (!this.enforceAppIdValid_) {
+ success = this.addChallenges(signData, true /* finalChallenges */);
+ }
+
+ this.checkAppIds_();
+ return success;
+};
+
+/**
+ * Adds new challenges to the challenges being signed.
+ * @param {SignData} signData Challenges to add.
+ * @param {boolean} finalChallenges Whether these are the final challenges.
+ * @return {boolean} Whether the challenge could be added.
+ */
+Signer.prototype.addChallenges = function(signData, finalChallenges) {
+ var newChallenges = this.encodeSignChallenges_(signData);
+ for (var i = 0; i < newChallenges.length; i++) {
+ this.pendingChallenges_.push(newChallenges[i]);
+ }
+ if (!finalChallenges) {
+ return true;
+ }
+ return this.helper_.doSign(this.pendingChallenges_);
+};
+
+/**
+ * Creates challenges for helper from challenges.
+ * @param {Array.<SignChallenge>} challenges Challenges to add.
+ * @return {Array.<SignHelperChallenge>}
+ * @private
+ */
+Signer.prototype.encodeSignChallenges_ = function(challenges) {
+ var newChallenges = [];
+ for (var i = 0; i < challenges.length; i++) {
+ var incomingChallenge = challenges[i];
+ var serverChallenge = incomingChallenge['challenge'];
+ var appId = incomingChallenge['appId'];
+ var encodedKeyHandle = incomingChallenge['keyHandle'];
+ var version = incomingChallenge['version'];
+
+ var browserData =
+ makeSignBrowserData(serverChallenge, this.origin_, this.tlsChannelId_);
+ var encodedChallenge = makeChallenge(browserData, appId, encodedKeyHandle,
+ version);
+
+ var key = encodedKeyHandle + encodedChallenge['challengeHash'];
+ this.browserData_[key] = browserData;
+ this.serverChallenges_[key] = incomingChallenge;
+
+ newChallenges.push(encodedChallenge);
+ }
+ return newChallenges;
+};
+
+/**
+ * Checks the app ids of incoming requests, and, when this signer is enforcing
+ * that app ids are valid, adds successful challenges to those being signed.
+ * @private
+ */
+Signer.prototype.checkAppIds_ = function() {
+ // Check the incoming challenges' app ids.
+ /** @private {Array.<[string, Array.<Request>]>} */
+ this.orderedRequests_ = requestsByAppId(this.signData_);
+ if (!this.orderedRequests_.length) {
+ // Safety check: if the challenges are somehow empty, the helper will never
+ // be fed any data, so the request could never be satisfied. You lose.
+ this.notifyError_(GnubbyCodeTypes.BAD_REQUEST);
+ return;
+ }
+ /** @private {number} */
+ this.fetchedAppIds_ = 0;
+ /** @private {number} */
+ this.validAppIds_ = 0;
+ for (var i = 0, appIdRequestsPair; i < this.orderedRequests_.length; i++) {
+ var appIdRequestsPair = this.orderedRequests_[i];
+ var appId = appIdRequestsPair[0];
+ var requests = appIdRequestsPair[1];
+ if (appId == this.origin_) {
+ // Trivially allowed.
+ this.fetchedAppIds_++;
+ this.validAppIds_++;
+ // Only add challenges if in enforcing mode, i.e. they weren't added
+ // earlier.
+ if (this.enforceAppIdValid_) {
+ this.addChallenges(requests,
+ this.fetchedAppIds_ == this.orderedRequests_.length);
+ }
+ } else {
+ var start = new Date();
+ fetchAllowedOriginsForAppId(appId, this.allowHttp_,
+ this.fetchedAllowedOriginsForAppId_.bind(this, appId, start,
+ requests));
+ }
+ }
+};
+
+/**
+ * Called with the result of an app id fetch.
+ * @param {string} appId the app id that was fetched.
+ * @param {Date} start the time the fetch request started.
+ * @param {Array.<SignChallenge>} challenges Challenges for this app id.
+ * @param {number} rc The HTTP response code for the app id fetch.
+ * @param {!Array.<string>} allowedOrigins The origins allowed for this app id.
+ * @private
+ */
+Signer.prototype.fetchedAllowedOriginsForAppId_ = function(appId, start,
+ challenges, rc, allowedOrigins) {
+ var end = new Date();
+ logFetchAppIdResult(appId, end - start, allowedOrigins, this.logMsgUrl_);
+ if (rc != 200 && !(rc >= 400 && rc < 500)) {
+ if (this.timer_.expired()) {
+ // Act as though the helper timed out.
+ this.helperError_(DeviceStatusCodes.TIMEOUT_STATUS, false);
+ } else {
+ start = new Date();
+ fetchAllowedOriginsForAppId(appId, this.allowHttp_,
+ this.fetchedAllowedOriginsForAppId_.bind(this, appId, start,
+ challenges));
+ }
+ return;
+ }
+ this.fetchedAppIds_++;
+ var finalChallenges = (this.fetchedAppIds_ == this.orderedRequests_.length);
+ if (isValidAppIdForOrigin(appId, this.origin_, allowedOrigins)) {
+ this.validAppIds_++;
+ // Only add challenges if in enforcing mode, i.e. they weren't added
+ // earlier.
+ if (this.enforceAppIdValid_) {
+ this.addChallenges(challenges, finalChallenges);
+ }
+ } else {
+ logInvalidOriginForAppId(this.origin_, appId, this.logMsgUrl_);
+ // If in enforcing mode and this is the final request, sign the valid
+ // challenges.
+ if (this.enforceAppIdValid_ && finalChallenges) {
+ if (!this.helper_.doSign(this.pendingChallenges_)) {
+ this.notifyError_(GnubbyCodeTypes.BAD_REQUEST);
+ return;
+ }
+ }
+ }
+ if (this.enforceAppIdValid_ && finalChallenges && !this.validAppIds_) {
+ // If all app ids are invalid, notify the caller, otherwise implicitly
+ // allow the helper to report whether any of the valid challenges succeeded.
+ this.notifyError_(GnubbyCodeTypes.BAD_APP_ID);
+ }
+};
+
+/**
+ * Called when the timeout expires on this signer.
+ * @private
+ */
+Signer.prototype.timeout_ = function() {
+ this.watchdogTimer_ = undefined;
+ // The web page gets grumpy if it doesn't get WAIT_TOUCH within a reasonable
+ // time.
+ this.notifyError_(GnubbyCodeTypes.WAIT_TOUCH);
+};
+
+/** Closes this signer. */
+Signer.prototype.close = function() {
+ if (this.helper_) this.helper_.close();
+};
+
+/**
+ * Notifies the caller of error with the given error code.
+ * @param {number} code
+ * @private
+ */
+Signer.prototype.notifyError_ = function(code) {
+ if (this.done_)
+ return;
+ this.close();
+ this.done_ = true;
+ this.errorCb_(code);
+};
+
+/**
+ * Notifies the caller of success.
+ * @param {SignChallenge} challenge The challenge that was signed.
+ * @param {string} info The sign result.
+ * @param {string} browserData
+ * @private
+ */
+Signer.prototype.notifySuccess_ = function(challenge, info, browserData) {
+ if (this.done_)
+ return;
+ this.close();
+ this.done_ = true;
+ this.successCb_(challenge, info, browserData);
+};
+
+/**
+ * Notifies the caller of progress with the error code.
+ * @param {number} code
+ * @private
+ */
+Signer.prototype.notifyProgress_ = function(code) {
+ if (this.done_)
+ return;
+ if (code != this.lastProgressUpdate_) {
+ this.lastProgressUpdate_ = code;
+ // If there is no progress callback, treat it like an error and clean up.
+ if (this.progressCb_) {
+ this.progressCb_(code);
+ } else {
+ this.notifyError_(code);
+ }
+ }
+};
+
+/**
+ * Maps a sign helper's error code namespace to the page's error code namespace.
+ * @param {number} code Error code from DeviceStatusCodes namespace.
+ * @param {boolean} anyGnubbies Whether any gnubbies were found.
+ * @return {number} A GnubbyCodeTypes error code.
+ * @private
+ */
+Signer.mapError_ = function(code, anyGnubbies) {
+ var reportedError;
+ switch (code) {
+ case DeviceStatusCodes.WRONG_DATA_STATUS:
+ reportedError = anyGnubbies ? GnubbyCodeTypes.NONE_PLUGGED_ENROLLED :
+ GnubbyCodeTypes.NO_GNUBBIES;
+ break;
+
+ case DeviceStatusCodes.OK_STATUS:
+ // If the error callback is called with OK, it means the signature was
+ // empty, which we treat the same as...
+ case DeviceStatusCodes.WAIT_TOUCH_STATUS:
+ reportedError = GnubbyCodeTypes.WAIT_TOUCH;
+ break;
+
+ case DeviceStatusCodes.BUSY_STATUS:
+ reportedError = GnubbyCodeTypes.BUSY;
+ break;
+
+ default:
+ reportedError = GnubbyCodeTypes.UNKNOWN_ERROR;
+ break;
+ }
+ return reportedError;
+};
+
+/**
+ * Called by the helper upon error.
+ * @param {number} code
+ * @param {boolean} anyGnubbies
+ * @private
+ */
+Signer.prototype.helperError_ = function(code, anyGnubbies) {
+ this.clearTimeout_();
+ var reportedError = Signer.mapError_(code, anyGnubbies);
+ console.log(UTIL_fmt('helper reported ' + code.toString(16) +
+ ', returning ' + reportedError));
+ this.notifyError_(reportedError);
+};
+
+/**
+ * Called by helper upon success.
+ * @param {SignHelperChallenge} challenge The challenge that was signed.
+ * @param {string} info The sign result.
+ * @private
+ */
+Signer.prototype.helperSuccess_ = function(challenge, info) {
+ // Got a good reply, kill timer.
+ this.clearTimeout_();
+
+ var key = challenge['keyHandle'] + challenge['challengeHash'];
+ var browserData = this.browserData_[key];
+ // Notify with server-provided challenge, not the encoded one: the
+ // server-provided challenge contains additional fields it relies on.
+ var serverChallenge = this.serverChallenges_[key];
+ this.notifySuccess_(serverChallenge, info, browserData);
+};
+
+/**
+ * Called by helper to notify progress.
+ * @param {number} code
+ * @param {boolean} anyGnubbies
+ * @private
+ */
+Signer.prototype.helperProgress_ = function(code, anyGnubbies) {
+ var reportedError = Signer.mapError_(code, anyGnubbies);
+ console.log(UTIL_fmt('helper notified ' + code.toString(16) +
+ ', returning ' + reportedError));
+ this.notifyProgress_(reportedError);
+};
+
+/**
+ * Clears the timeout for this signer.
+ * @private
+ */
+Signer.prototype.clearTimeout_ = function() {
+ if (this.watchdogTimer_) {
+ this.watchdogTimer_.clearTimeout();
+ this.watchdogTimer_ = undefined;
+ }
+};
diff --git a/chrome/browser/resources/cryptotoken/signhelper.js b/chrome/browser/resources/cryptotoken/signhelper.js
new file mode 100644
index 0000000..17db212
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/signhelper.js
@@ -0,0 +1,49 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Provides a "bottom half" helper to assist with raw sign
+ * requests.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * A helper for sign requests.
+ * @extends {Closeable}
+ * @interface
+ */
+function SignHelper() {}
+
+/**
+ * Attempts to sign the provided challenges.
+ * @param {Array.<SignHelperChallenge>} challenges the new challenges to sign.
+ * @return {boolean} whether the challenges were successfully added.
+ */
+SignHelper.prototype.doSign = function(challenges) {};
+
+/** Closes this helper. */
+SignHelper.prototype.close = function() {};
+
+/**
+ * A factory for creating sign helpers.
+ * @interface
+ */
+function SignHelperFactory() {}
+
+/**
+ * Creates a new sign helper.
+ * @param {Countdown} timer Timer after whose expiration the caller is no longer
+ * interested in the result of a sign request.
+ * @param {function(number, boolean)} errorCb Called when a sign request fails
+ * with an error code and whether any gnubbies were found.
+ * @param {function(SignHelperChallenge, string)} successCb Called with the
+ * signature produced by a successful sign request.
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
+ * progress updates to the sign request.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @return {SignHelper} The newly created helper.
+ */
+SignHelperFactory.prototype.createHelper =
+ function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) {};
diff --git a/chrome/browser/resources/cryptotoken/singlesigner.js b/chrome/browser/resources/cryptotoken/singlesigner.js
new file mode 100644
index 0000000..a30d68b
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/singlesigner.js
@@ -0,0 +1,434 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview A single gnubby signer wraps the process of opening a gnubby,
+ * signing each challenge in an array of challenges until a success condition
+ * is satisfied, and finally yielding the gnubby upon success.
+ *
+ * @author [email protected] (Juan Lang)
+ */
+
+'use strict';
+
+/**
+ * Creates a new sign handler with a gnubby. This handler will perform a sign
+ * operation using each challenge in an array of challenges until its success
+ * condition is satisified, or an error or timeout occurs. The success condition
+ * is defined differently depending whether this signer is used for enrolling
+ * or for signing:
+ *
+ * For enroll, success is defined as each challenge yielding wrong data. This
+ * means this gnubby is not currently enrolled for any of the appIds in any
+ * challenge.
+ *
+ * For sign, success is defined as any challenge yielding ok or waiting for
+ * touch.
+ *
+ * At most one of the success or failure callbacks will be called, and it will
+ * be called at most once. Neither callback is guaranteed to be called: if
+ * a final set of challenges is never given to this gnubby, or if the gnubby
+ * stays busy, the signer has no way to know whether the set of challenges it's
+ * been given has succeeded or failed.
+ * The callback is called only when the signer reaches success or failure, i.e.
+ * when there is no need for this signer to continue trying new challenges.
+ *
+ * @param {!GnubbyFactory} factory Used to create and open the gnubby.
+ * @param {llGnubbyDeviceId} gnubbyIndex Which gnubby to open.
+ * @param {boolean} forEnroll Whether this signer is signing for an attempted
+ * enroll operation.
+ * @param {function(number)} errorCb Called when this signer fails, i.e. no
+ * further attempts can succeed.
+ * @param {function(usbGnubby, number, (SingleSignerResult|undefined))}
+ * successCb Called when this signer succeeds.
+ * @param {Countdown=} opt_timer An advisory timer, beyond whose expiration the
+ * signer will not attempt any new operations, assuming the caller is no
+ * longer interested in the outcome.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @constructor
+ */
+function SingleGnubbySigner(factory, gnubbyIndex, forEnroll, errorCb, successCb,
+ opt_timer, opt_logMsgUrl) {
+ /** @private {GnubbyFactory} */
+ this.factory_ = factory;
+ /** @private {llGnubbyDeviceId} */
+ this.gnubbyIndex_ = gnubbyIndex;
+ /** @private {SingleGnubbySigner.State} */
+ this.state_ = SingleGnubbySigner.State.INIT;
+ /** @private {boolean} */
+ this.forEnroll_ = forEnroll;
+ /** @private {function(number)} */
+ this.errorCb_ = errorCb;
+ /** @private {function(usbGnubby, number, (SingleSignerResult|undefined))} */
+ this.successCb_ = successCb;
+ /** @private {Countdown|undefined} */
+ this.timer_ = opt_timer;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+
+ /** @private {!Array.<!SignHelperChallenge>} */
+ this.challenges_ = [];
+ /** @private {number} */
+ this.challengeIndex_ = 0;
+ /** @private {boolean} */
+ this.challengesFinal_ = false;
+
+ /** @private {!Array.<string>} */
+ this.notForMe_ = [];
+}
+
+/** @enum {number} */
+SingleGnubbySigner.State = {
+ /** Initial state. */
+ INIT: 0,
+ /** The signer is attempting to open a gnubby. */
+ OPENING: 1,
+ /** The signer's gnubby opened, but is busy. */
+ BUSY: 2,
+ /** The signer has an open gnubby, but no challenges to sign. */
+ IDLE: 3,
+ /** The signer is currently signing a challenge. */
+ SIGNING: 4,
+ /** The signer encountered an error. */
+ ERROR: 5,
+ /** The signer got a successful outcome. */
+ SUCCESS: 6,
+ /** The signer is closing its gnubby. */
+ CLOSING: 7,
+ /** The signer is closed. */
+ CLOSED: 8
+};
+
+/**
+ * Attempts to open this signer's gnubby, if it's not already open.
+ * (This is implicitly done by addChallenges.)
+ */
+SingleGnubbySigner.prototype.open = function() {
+ if (this.state_ == SingleGnubbySigner.State.INIT) {
+ this.state_ = SingleGnubbySigner.State.OPENING;
+ this.factory_.openGnubby(this.gnubbyIndex_,
+ this.forEnroll_,
+ this.openCallback_.bind(this),
+ this.logMsgUrl_);
+ }
+};
+
+/**
+ * Closes this signer's gnubby, if it's held.
+ */
+SingleGnubbySigner.prototype.close = function() {
+ if (!this.gnubby_) return;
+ this.state_ = SingleGnubbySigner.State.CLOSING;
+ this.gnubby_.closeWhenIdle(this.closed_.bind(this));
+};
+
+/**
+ * Called when this signer's gnubby is closed.
+ * @private
+ */
+SingleGnubbySigner.prototype.closed_ = function() {
+ this.gnubby_ = null;
+ this.state_ = SingleGnubbySigner.State.CLOSED;
+};
+
+/**
+ * Adds challenges to the set of challenges being tried by this signer.
+ * If the signer is currently idle, begins signing the new challenges.
+ *
+ * @param {Array.<SignHelperChallenge>} challenges
+ * @param {boolean} finalChallenges
+ * @return {boolean} Whether the challenges were accepted.
+ */
+SingleGnubbySigner.prototype.addChallenges =
+ function(challenges, finalChallenges) {
+ if (this.challengesFinal_) {
+ // Can't add new challenges once they're finalized.
+ return false;
+ }
+
+ if (challenges) {
+ console.log(this.gnubby_);
+ console.log(UTIL_fmt('adding ' + challenges.length + ' challenges'));
+ for (var i = 0; i < challenges.length; i++) {
+ this.challenges_.push(challenges[i]);
+ }
+ }
+ this.challengesFinal_ = finalChallenges;
+
+ switch (this.state_) {
+ case SingleGnubbySigner.State.INIT:
+ this.open();
+ break;
+ case SingleGnubbySigner.State.OPENING:
+ // The open has already commenced, so accept the added challenges, but
+ // don't do anything.
+ break;
+ case SingleGnubbySigner.State.IDLE:
+ if (this.challengeIndex_ < challenges.length) {
+ // New challenges added: restart signing.
+ this.doSign_(this.challengeIndex_);
+ } else if (finalChallenges) {
+ // Finalized with no new challenges can happen when the caller rejects
+ // the appId for some challenge.
+ // If this signer is for enroll, the request must be rejected: this
+ // signer can't determine whether the gnubby is or is not enrolled for
+ // the origin.
+ // If this signer is for sign, the request must also be rejected: there
+ // are no new challenges to sign, and all previous ones did not yield
+ // success.
+ var self = this;
+ window.setTimeout(function() {
+ self.goToError_(DeviceStatusCodes.WRONG_DATA_STATUS);
+ }, 0);
+ }
+ break;
+ case SingleGnubbySigner.State.SIGNING:
+ // Already signing, so don't kick off a new sign, but accept the added
+ // challenges.
+ break;
+ default:
+ return false;
+ }
+ return true;
+};
+
+/**
+ * How long to delay retrying a failed open.
+ */
+SingleGnubbySigner.OPEN_DELAY_MILLIS = 200;
+
+/**
+ * @param {number} rc The result of the open operation.
+ * @param {usbGnubby=} gnubby The opened gnubby, if open was successful (or
+ * busy).
+ * @private
+ */
+SingleGnubbySigner.prototype.openCallback_ = function(rc, gnubby) {
+ if (this.state_ != SingleGnubbySigner.State.OPENING &&
+ this.state_ != SingleGnubbySigner.State.BUSY) {
+ // Open completed after close, perhaps? Ignore.
+ return;
+ }
+
+ switch (rc) {
+ case DeviceStatusCodes.OK_STATUS:
+ if (!gnubby) {
+ console.warn(UTIL_fmt('open succeeded but gnubby is null, WTF?'));
+ } else {
+ this.gnubby_ = gnubby;
+ this.gnubby_.version(this.versionCallback_.bind(this));
+ }
+ break;
+ case DeviceStatusCodes.BUSY_STATUS:
+ this.gnubby_ = gnubby;
+ this.openedBusy_ = true;
+ this.state_ = SingleGnubbySigner.State.BUSY;
+ // If there's still time, retry the open.
+ if (!this.timer_ || !this.timer_.expired()) {
+ var self = this;
+ window.setTimeout(function() {
+ if (self.gnubby_) {
+ self.factory_.openGnubby(self.gnubbyIndex_,
+ self.forEnroll_,
+ self.openCallback_.bind(self),
+ self.logMsgUrl_);
+ }
+ }, SingleGnubbySigner.OPEN_DELAY_MILLIS);
+ } else {
+ this.goToError_(DeviceStatusCodes.BUSY_STATUS);
+ }
+ break;
+ default:
+ // TODO(juanlang): This won't be confused with success, but should it be
+ // part of the same namespace as the other error codes, which are
+ // always in DeviceStatusCodes.*?
+ this.goToError_(rc);
+ }
+};
+
+/**
+ * Called with the result of a version command.
+ * @param {number} rc Result of version command.
+ * @param {ArrayBuffer=} opt_data Version.
+ * @private
+ */
+SingleGnubbySigner.prototype.versionCallback_ = function(rc, opt_data) {
+ if (rc) {
+ this.goToError_(rc);
+ return;
+ }
+ this.state_ = SingleGnubbySigner.State.IDLE;
+ this.version_ = UTIL_BytesToString(new Uint8Array(opt_data || []));
+ this.doSign_(this.challengeIndex_);
+};
+
+/**
+ * @param {number} challengeIndex
+ * @private
+ */
+SingleGnubbySigner.prototype.doSign_ = function(challengeIndex) {
+ if (this.timer_ && this.timer_.expired()) {
+ // If the timer is expired, that means we never got a success or a touch
+ // required response: either always implies completion of this signer's
+ // state machine (see signCallback's cases for OK_STATUS and
+ // WAIT_TOUCH_STATUS.) We could have gotten wrong data on a partial set of
+ // challenges, but this means we don't yet know the final outcome. In any
+ // event, we don't yet know the final outcome: return busy.
+ this.goToError_(DeviceStatusCodes.BUSY_STATUS);
+ return;
+ }
+
+ this.state_ = SingleGnubbySigner.State.SIGNING;
+
+ if (challengeIndex >= this.challenges_.length) {
+ this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
+ return;
+ }
+
+ var challenge = this.challenges_[challengeIndex];
+ var challengeHash = challenge.challengeHash;
+ var appIdHash = challenge.appIdHash;
+ var keyHandle = challenge.keyHandle;
+ if (this.notForMe_.indexOf(keyHandle) != -1) {
+ // Cache hit: return wrong data again.
+ this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
+ } else if (challenge.version && challenge.version != this.version_) {
+ // Sign challenge for a different version of gnubby: return wrong data.
+ this.signCallback_(challengeIndex, DeviceStatusCodes.WRONG_DATA_STATUS);
+ } else {
+ var opt_nowink = this.forEnroll_;
+ this.gnubby_.sign(challengeHash, appIdHash, keyHandle,
+ this.signCallback_.bind(this, challengeIndex),
+ opt_nowink);
+ }
+};
+
+/**
+ * Called with the result of a single sign operation.
+ * @param {number} challengeIndex the index of the challenge just attempted
+ * @param {number} code the result of the sign operation
+ * @param {ArrayBuffer=} opt_info
+ * @private
+ */
+SingleGnubbySigner.prototype.signCallback_ =
+ function(challengeIndex, code, opt_info) {
+ console.log(UTIL_fmt('gnubby ' + JSON.stringify(this.gnubbyIndex_) +
+ ', challenge ' + challengeIndex + ' yielded ' + code.toString(16)));
+ if (this.state_ != SingleGnubbySigner.State.SIGNING) {
+ console.log(UTIL_fmt('already done!'));
+ // We're done, the caller's no longer interested.
+ return;
+ }
+
+ // Cache wrong data result, re-asking the gnubby to sign it won't produce
+ // different results.
+ if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
+ if (challengeIndex < this.challenges_.length) {
+ var challenge = this.challenges_[challengeIndex];
+ if (this.notForMe_.indexOf(challenge.keyHandle) == -1) {
+ this.notForMe_.push(challenge.keyHandle);
+ }
+ }
+ }
+
+ switch (code) {
+ case DeviceStatusCodes.GONE_STATUS:
+ this.goToError_(code);
+ break;
+
+ case DeviceStatusCodes.TIMEOUT_STATUS:
+ // TODO(juanlang): On a TIMEOUT_STATUS, sync first, then retry.
+ case DeviceStatusCodes.BUSY_STATUS:
+ this.doSign_(this.challengeIndex_);
+ break;
+
+ case DeviceStatusCodes.OK_STATUS:
+ if (this.forEnroll_) {
+ this.goToError_(code);
+ } else {
+ this.goToSuccess_(code, this.challenges_[challengeIndex], opt_info);
+ }
+ break;
+
+ case DeviceStatusCodes.WAIT_TOUCH_STATUS:
+ if (this.forEnroll_) {
+ this.goToError_(code);
+ } else {
+ this.goToSuccess_(code, this.challenges_[challengeIndex]);
+ }
+ break;
+
+ case DeviceStatusCodes.WRONG_DATA_STATUS:
+ if (this.challengeIndex_ < this.challenges_.length - 1) {
+ this.doSign_(++this.challengeIndex_);
+ } else if (!this.challengesFinal_) {
+ this.state_ = SingleGnubbySigner.State.IDLE;
+ } else if (this.forEnroll_) {
+ // Signal the caller whether the open was busy, because it may take
+ // an unusually long time when opened for enroll. Use an empty
+ // "challenge" as the signal for a busy open.
+ var challenge = undefined;
+ if (this.openedBusy) {
+ challenge = { appIdHash: '', challengeHash: '', keyHandle: '' };
+ }
+ this.goToSuccess_(code, challenge);
+ } else {
+ this.goToError_(code);
+ }
+ break;
+
+ default:
+ if (this.forEnroll_) {
+ this.goToError_(code);
+ } else if (this.challengeIndex_ < this.challenges_.length - 1) {
+ this.doSign_(++this.challengeIndex_);
+ } else if (!this.challengesFinal_) {
+ // Increment the challenge index, as this one isn't useful any longer,
+ // but a subsequent challenge may appear, and it might be useful.
+ this.challengeIndex_++;
+ this.state_ = SingleGnubbySigner.State.IDLE;
+ } else {
+ this.goToError_(code);
+ }
+ }
+};
+
+/**
+ * Switches to the error state, and notifies caller.
+ * @param {number} code
+ * @private
+ */
+SingleGnubbySigner.prototype.goToError_ = function(code) {
+ this.state_ = SingleGnubbySigner.State.ERROR;
+ console.log(UTIL_fmt('failed (' + code.toString(16) + ')'));
+ this.errorCb_(code);
+ // Since this gnubby can no longer produce a useful result, go ahead and
+ // close it.
+ this.close();
+};
+
+/**
+ * Switches to the success state, and notifies caller.
+ * @param {number} code
+ * @param {SignHelperChallenge=} opt_challenge
+ * @param {ArrayBuffer=} opt_info
+ * @private
+ */
+SingleGnubbySigner.prototype.goToSuccess_ =
+ function(code, opt_challenge, opt_info) {
+ this.state_ = SingleGnubbySigner.State.SUCCESS;
+ console.log(UTIL_fmt('success (' + code.toString(16) + ')'));
+ if (opt_challenge || opt_info) {
+ var singleSignerResult = {};
+ if (opt_challenge) {
+ singleSignerResult['challenge'] = opt_challenge;
+ }
+ if (opt_info) {
+ singleSignerResult['info'] = opt_info;
+ }
+ }
+ this.successCb_(this.gnubby_, code, singleSignerResult);
+ // this.gnubby_ is now owned by successCb.
+ this.gnubby_ = null;
+};
diff --git a/chrome/browser/resources/cryptotoken/usbenrollhelper.js b/chrome/browser/resources/cryptotoken/usbenrollhelper.js
new file mode 100644
index 0000000..65838a1
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/usbenrollhelper.js
@@ -0,0 +1,442 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Implements an enroll helper using USB gnubbies.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * @param {!GnubbyFactory} factory
+ * @param {!Countdown} timer
+ * @param {function(number, boolean)} errorCb Called when an enroll request
+ * fails with an error code and whether any gnubbies were found.
+ * @param {function(string, string)} successCb Called with the result of a
+ * successful enroll request, along with the version of the gnubby that
+ * provided it.
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
+ * progress updates to the enroll request.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @constructor
+ * @implements {EnrollHelper}
+ */
+function UsbEnrollHelper(factory, timer, errorCb, successCb, opt_progressCb,
+ opt_logMsgUrl) {
+ /** @private {!GnubbyFactory} */
+ this.factory_ = factory;
+ /** @private {!Countdown} */
+ this.timer_ = timer;
+ /** @private {function(number, boolean)} */
+ this.errorCb_ = errorCb;
+ /** @private {function(string, string)} */
+ this.successCb_ = successCb;
+ /** @private {(function(number, boolean)|undefined)} */
+ this.progressCb_ = opt_progressCb;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+
+ /** @private {Array.<SignHelperChallenge>} */
+ this.signChallenges_ = [];
+ /** @private {boolean} */
+ this.signChallengesFinal_ = false;
+ /** @private {Array.<usbGnubby>} */
+ this.waitingForTouchGnubbies_ = [];
+
+ /** @private {boolean} */
+ this.closed_ = false;
+ /** @private {boolean} */
+ this.notified_ = false;
+ /** @private {number|undefined} */
+ this.lastProgressUpdate_ = undefined;
+ /** @private {boolean} */
+ this.signerComplete_ = false;
+ this.getSomeGnubbies_();
+}
+
+/**
+ * Attempts to enroll using the provided data.
+ * @param {Object} enrollChallenges a map of version string to enroll
+ * challenges.
+ * @param {Array.<SignHelperChallenge>} signChallenges a list of sign
+ * challenges for already enrolled gnubbies, to prevent double-enrolling a
+ * device.
+ */
+UsbEnrollHelper.prototype.doEnroll =
+ function(enrollChallenges, signChallenges) {
+ this.enrollChallenges = enrollChallenges;
+ this.signChallengesFinal_ = true;
+ if (this.signer_) {
+ this.signer_.addEncodedChallenges(
+ signChallenges, this.signChallengesFinal_);
+ } else {
+ this.signChallenges_ = signChallenges;
+ }
+};
+
+/** Closes this helper. */
+UsbEnrollHelper.prototype.close = function() {
+ this.closed_ = true;
+ for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) {
+ this.waitingForTouchGnubbies_[i].closeWhenIdle();
+ }
+ this.waitingForTouchGnubbies_ = [];
+ if (this.signer_) {
+ this.signer_.close();
+ this.signer_ = null;
+ }
+};
+
+/**
+ * Enumerates gnubbies, and begins processing challenges upon enumeration if
+ * any gnubbies are found.
+ * @private
+ */
+UsbEnrollHelper.prototype.getSomeGnubbies_ = function() {
+ this.factory_.enumerate(this.enumerateCallback_.bind(this));
+};
+
+/**
+ * Called with the result of enumerating gnubbies.
+ * @param {number} rc the result of the enumerate.
+ * @param {Array.<llGnubbyDeviceId>} indexes
+ * @private
+ */
+UsbEnrollHelper.prototype.enumerateCallback_ = function(rc, indexes) {
+ if (rc) {
+ // Enumerate failure is rare enough that it might be worth reporting
+ // directly, rather than trying again.
+ this.errorCb_(rc, false);
+ return;
+ }
+ if (!indexes.length) {
+ this.maybeReEnumerateGnubbies_();
+ return;
+ }
+ if (this.timer_.expired()) {
+ this.errorCb_(DeviceStatusCodes.TIMEOUT_STATUS, true);
+ return;
+ }
+ this.gotSomeGnubbies_(indexes);
+};
+
+/**
+ * If there's still time, re-enumerates devices and try with them. Otherwise
+ * reports an error and, implicitly, stops the enroll operation.
+ * @private
+ */
+UsbEnrollHelper.prototype.maybeReEnumerateGnubbies_ = function() {
+ var errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
+ var anyGnubbies = false;
+ // If there's still time and we're still going, retry enumerating.
+ if (!this.closed_ && !this.timer_.expired()) {
+ this.notifyProgress_(errorCode, anyGnubbies);
+ var self = this;
+ // Use a delayed re-enumerate to prevent hammering the system unnecessarily.
+ window.setTimeout(function() {
+ if (self.timer_.expired()) {
+ self.notifyError_(errorCode, anyGnubbies);
+ } else {
+ self.getSomeGnubbies_();
+ }
+ }, 200);
+ } else {
+ this.notifyError_(errorCode, anyGnubbies);
+ }
+};
+
+/**
+ * Called with the result of enumerating gnubby indexes.
+ * @param {Array.<llGnubbyDeviceId>} indexes
+ * @private
+ */
+UsbEnrollHelper.prototype.gotSomeGnubbies_ = function(indexes) {
+ this.signer_ = new MultipleGnubbySigner(
+ this.factory_,
+ indexes,
+ true /* forEnroll */,
+ this.signerCompleted_.bind(this),
+ this.signerFoundGnubby_.bind(this),
+ this.timer_,
+ this.logMsgUrl_);
+ if (this.signChallengesFinal_) {
+ this.signer_.addEncodedChallenges(
+ this.signChallenges_, this.signChallengesFinal_);
+ this.pendingSignChallenges_ = [];
+ }
+};
+
+/**
+ * Called when a MultipleGnubbySigner completes its sign request.
+ * @param {boolean} anySucceeded whether any sign attempt completed
+ * successfully.
+ * @param {number=} errorCode an error code from a failing gnubby, if one was
+ * found.
+ * @private
+ */
+UsbEnrollHelper.prototype.signerCompleted_ = function(anySucceeded, errorCode) {
+ this.signerComplete_ = true;
+ // The signer is not created unless some gnubbies were enumerated, so
+ // anyGnubbies is mostly always true. The exception is when the last gnubby is
+ // removed, handled shortly.
+ var anyGnubbies = true;
+ if (!anySucceeded) {
+ if (errorCode == -llGnubby.GONE) {
+ // If the last gnubby was removed, report as though no gnubbies were
+ // found.
+ this.maybeReEnumerateGnubbies_();
+ } else {
+ if (!errorCode) errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
+ this.notifyError_(errorCode, anyGnubbies);
+ }
+ } else if (this.anyTimeout) {
+ // Some previously succeeding gnubby timed out: return its error code.
+ this.notifyError_(this.timeoutError, anyGnubbies);
+ } else {
+ // Do nothing: signerFoundGnubby will have been called with each succeeding
+ // gnubby.
+ }
+};
+
+/**
+ * Called when a MultipleGnubbySigner finds a gnubby that can enroll.
+ * @param {number} code
+ * @param {MultipleSignerResult} signResult
+ * @private
+ */
+UsbEnrollHelper.prototype.signerFoundGnubby_ = function(code, signResult) {
+ var gnubby = signResult['gnubby'];
+ this.waitingForTouchGnubbies_.push(gnubby);
+ this.notifyProgress_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true);
+ if (code == DeviceStatusCodes.WRONG_DATA_STATUS) {
+ if (signResult['challenge']) {
+ // If the signer yielded a busy open, indicate waiting for touch
+ // immediately, rather than attempting enroll. This allows the UI to
+ // update, since a busy open is a potentially long operation.
+ this.notifyError_(DeviceStatusCodes.WAIT_TOUCH_STATUS, true);
+ } else {
+ this.matchEnrollVersionToGnubby_(gnubby);
+ }
+ }
+};
+
+/**
+ * Attempts to match the gnubby's U2F version with an appropriate enroll
+ * challenge.
+ * @param {usbGnubby} gnubby
+ * @private
+ */
+UsbEnrollHelper.prototype.matchEnrollVersionToGnubby_ = function(gnubby) {
+ if (!gnubby) {
+ console.warn(UTIL_fmt('no gnubby, WTF?'));
+ }
+ gnubby.version(this.gnubbyVersioned_.bind(this, gnubby));
+};
+
+/**
+ * Called with the result of a version command.
+ * @param {usbGnubby} gnubby
+ * @param {number} rc result of version command.
+ * @param {ArrayBuffer=} data version.
+ * @private
+ */
+UsbEnrollHelper.prototype.gnubbyVersioned_ = function(gnubby, rc, data) {
+ if (rc) {
+ this.removeWrongVersionGnubby_(gnubby);
+ return;
+ }
+ var version = UTIL_BytesToString(new Uint8Array(data || null));
+ this.tryEnroll_(gnubby, version);
+};
+
+/**
+ * Drops the gnubby from the list of eligible gnubbies.
+ * @param {usbGnubby} gnubby
+ * @private
+ */
+UsbEnrollHelper.prototype.removeWaitingGnubby_ = function(gnubby) {
+ gnubby.closeWhenIdle();
+ var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
+ if (index >= 0) {
+ this.waitingForTouchGnubbies_.splice(index, 1);
+ }
+};
+
+/**
+ * Drops the gnubby from the list of eligible gnubbies, as it has the wrong
+ * version.
+ * @param {usbGnubby} gnubby
+ * @private
+ */
+UsbEnrollHelper.prototype.removeWrongVersionGnubby_ = function(gnubby) {
+ this.removeWaitingGnubby_(gnubby);
+ if (!this.waitingForTouchGnubbies_.length && this.signerComplete_) {
+ // Whoops, this was the last gnubby: indicate there are none.
+ this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false);
+ }
+};
+
+/**
+ * Attempts enrolling a particular gnubby with a challenge of the appropriate
+ * version.
+ * @param {usbGnubby} gnubby
+ * @param {string} version
+ * @private
+ */
+UsbEnrollHelper.prototype.tryEnroll_ = function(gnubby, version) {
+ var challenge = this.getChallengeOfVersion_(version);
+ if (!challenge) {
+ this.removeWrongVersionGnubby_(gnubby);
+ return;
+ }
+ var challengeChallenge = B64_decode(challenge['challenge']);
+ var appIdHash = B64_decode(challenge['appIdHash']);
+ gnubby.enroll(challengeChallenge, appIdHash,
+ this.enrollCallback_.bind(this, gnubby, version));
+};
+
+/**
+ * Finds the (first) challenge of the given version in this helper's challenges.
+ * @param {string} version
+ * @return {Object} challenge, if found, or null if not.
+ * @private
+ */
+UsbEnrollHelper.prototype.getChallengeOfVersion_ = function(version) {
+ for (var i = 0; i < this.enrollChallenges.length; i++) {
+ if (this.enrollChallenges[i]['version'] == version) {
+ return this.enrollChallenges[i];
+ }
+ }
+ return null;
+};
+
+/**
+ * Called with the result of an enroll request to a gnubby.
+ * @param {usbGnubby} gnubby
+ * @param {string} version
+ * @param {number} code
+ * @param {ArrayBuffer=} infoArray
+ * @private
+ */
+UsbEnrollHelper.prototype.enrollCallback_ =
+ function(gnubby, version, code, infoArray) {
+ if (this.notified_) {
+ // Enroll completed after previous success or failure. Disregard.
+ return;
+ }
+ switch (code) {
+ case -llGnubby.GONE:
+ // Close this gnubby.
+ this.removeWaitingGnubby_(gnubby);
+ if (!this.waitingForTouchGnubbies_.length) {
+ // Last enroll attempt is complete and last gnubby is gone: retry if
+ // possible.
+ this.maybeReEnumerateGnubbies_();
+ }
+ break;
+
+ case DeviceStatusCodes.WAIT_TOUCH_STATUS:
+ case DeviceStatusCodes.BUSY_STATUS:
+ case DeviceStatusCodes.TIMEOUT_STATUS:
+ if (this.timer_.expired()) {
+ // Store any timeout error code, to be returned from the complete
+ // callback if no other eligible gnubbies are found.
+ this.anyTimeout = true;
+ this.timeoutError = code;
+ // Close this gnubby.
+ this.removeWaitingGnubby_(gnubby);
+ if (!this.waitingForTouchGnubbies_.length && !this.notified_) {
+ // Last enroll attempt is complete: return this error.
+ console.log(UTIL_fmt('timeout (' + code.toString(16) +
+ ') enrolling'));
+ this.notifyError_(code, true);
+ }
+ } else {
+ // Notify caller of waiting for touch events.
+ if (code == DeviceStatusCodes.WAIT_TOUCH_STATUS) {
+ this.notifyProgress_(code, true);
+ }
+ window.setTimeout(this.tryEnroll_.bind(this, gnubby, version), 200);
+ }
+ break;
+
+ case DeviceStatusCodes.OK_STATUS:
+ var info = B64_encode(new Uint8Array(infoArray || []));
+ this.notifySuccess_(version, info);
+ break;
+
+ default:
+ console.log(UTIL_fmt('Failed to enroll gnubby: ' + code));
+ this.notifyError_(code, true);
+ break;
+ }
+};
+
+/**
+ * @param {number} code
+ * @param {boolean} anyGnubbies
+ * @private
+ */
+UsbEnrollHelper.prototype.notifyError_ = function(code, anyGnubbies) {
+ if (this.notified_ || this.closed_)
+ return;
+ this.notified_ = true;
+ this.close();
+ this.errorCb_(code, anyGnubbies);
+};
+
+/**
+ * @param {string} version
+ * @param {string} info
+ * @private
+ */
+UsbEnrollHelper.prototype.notifySuccess_ = function(version, info) {
+ if (this.notified_ || this.closed_)
+ return;
+ this.notified_ = true;
+ this.close();
+ this.successCb_(version, info);
+};
+
+/**
+ * @param {number} code
+ * @param {boolean} anyGnubbies
+ * @private
+ */
+UsbEnrollHelper.prototype.notifyProgress_ = function(code, anyGnubbies) {
+ if (this.lastProgressUpdate_ == code || this.notified_ || this.closed_)
+ return;
+ this.lastProgressUpdate_ = code;
+ if (this.progressCb_) this.progressCb_(code, anyGnubbies);
+};
+
+/**
+ * @param {!GnubbyFactory} gnubbyFactory factory to create gnubbies.
+ * @constructor
+ * @implements {EnrollHelperFactory}
+ */
+function UsbEnrollHelperFactory(gnubbyFactory) {
+ /** @private {!GnubbyFactory} */
+ this.gnubbyFactory_ = gnubbyFactory;
+}
+
+/**
+ * @param {!Countdown} timer
+ * @param {function(number, boolean)} errorCb Called when an enroll request
+ * fails with an error code and whether any gnubbies were found.
+ * @param {function(string, string)} successCb Called with the result of a
+ * successful enroll request, along with the version of the gnubby that
+ * provided it.
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
+ * progress updates to the enroll request.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @return {UsbEnrollHelper} the newly created helper.
+ */
+UsbEnrollHelperFactory.prototype.createHelper =
+ function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) {
+ var helper =
+ new UsbEnrollHelper(this.gnubbyFactory_, timer, errorCb, successCb,
+ opt_progressCb, opt_logMsgUrl);
+ return helper;
+};
diff --git a/chrome/browser/resources/cryptotoken/usbgnubbyfactory.js b/chrome/browser/resources/cryptotoken/usbgnubbyfactory.js
new file mode 100644
index 0000000..d9c54c95
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/usbgnubbyfactory.js
@@ -0,0 +1,46 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Contains a simple factory for creating and opening usbGnubby
+ * instances.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+/**
+ * @param {Gnubbies} gnubbies
+ * @constructor
+ * @implements {GnubbyFactory}
+ */
+function UsbGnubbyFactory(gnubbies) {
+ /** @private {Gnubbies} */
+ this.gnubbies_ = gnubbies;
+ usbGnubby.setGnubbies(gnubbies);
+}
+
+/**
+ * Creates a new gnubby object, and opens the gnubby with the given index.
+ * @param {llGnubbyDeviceId} which The device to open.
+ * @param {boolean} forEnroll Whether this gnubby is being opened for enrolling.
+ * @param {function(number, usbGnubby=)} cb Called with result of opening the
+ * gnubby.
+ * @param {string=} logMsgUrl the url to post log messages to
+ * @override
+ */
+UsbGnubbyFactory.prototype.openGnubby =
+ function(which, forEnroll, cb, logMsgUrl) {
+ var gnubby = new usbGnubby();
+ gnubby.open(which, function(rc) {
+ cb(rc, gnubby);
+ });
+};
+
+/**
+ * Enumerates gnubbies.
+ * @param {function(number, Array.<llGnubbyDeviceId>)} cb
+ */
+UsbGnubbyFactory.prototype.enumerate = function(cb) {
+ this.gnubbies_.enumerate(cb);
+};
diff --git a/chrome/browser/resources/cryptotoken/usbsignhelper.js b/chrome/browser/resources/cryptotoken/usbsignhelper.js
new file mode 100644
index 0000000..2c5b643
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/usbsignhelper.js
@@ -0,0 +1,344 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Implements a sign helper using USB gnubbies.
+ * @author [email protected] (Juan Lang)
+ */
+'use strict';
+
+var CORRUPT_sign = false;
+
+/**
+ * @param {!GnubbyFactory} factory
+ * @param {Countdown} timer Timer after whose expiration the caller is no longer
+ * interested in the result of a sign request.
+ * @param {function(number, boolean)} errorCb Called when a sign request fails
+ * with an error code and whether any gnubbies were found.
+ * @param {function(SignHelperChallenge, string)} successCb Called with the
+ * signature produced by a successful sign request.
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
+ * progress updates to the sign request.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @constructor
+ * @implements {SignHelper}
+ */
+function UsbSignHelper(factory, timer, errorCb, successCb, opt_progressCb,
+ opt_logMsgUrl) {
+ /** @private {!GnubbyFactory} */
+ this.factory_ = factory;
+ /** @private {Countdown} */
+ this.timer_ = timer;
+ /** @private {function(number, boolean)} */
+ this.errorCb_ = errorCb;
+ /** @private {function(SignHelperChallenge, string)} */
+ this.successCb_ = successCb;
+ /** @private {string|undefined} */
+ this.logMsgUrl_ = opt_logMsgUrl;
+
+ /** @private {Array.<SignHelperChallenge>} */
+ this.pendingChallenges_ = [];
+ /** @private {Array.<usbGnubby>} */
+ this.waitingForTouchGnubbies_ = [];
+
+ /** @private {boolean} */
+ this.notified_ = false;
+ /** @private {boolean} */
+ this.signerComplete_ = false;
+}
+
+/**
+ * Attempts to sign the provided challenges.
+ * @param {Array.<SignHelperChallenge>} challenges
+ * @return {boolean} whether this set of challenges was accepted.
+ */
+UsbSignHelper.prototype.doSign = function(challenges) {
+ if (!challenges.length) {
+ // Fail a sign request with an empty set of challenges, and pretend to have
+ // alerted the caller in case the enumerate is still pending.
+ this.notified_ = true;
+ return false;
+ } else {
+ this.pendingChallenges_ = challenges;
+ this.getSomeGnubbies_();
+ return true;
+ }
+};
+
+/**
+ * Enumerates gnubbies, and begins processing challenges upon enumeration if
+ * any gnubbies are found.
+ * @private
+ */
+UsbSignHelper.prototype.getSomeGnubbies_ = function() {
+ this.factory_.enumerate(this.enumerateCallback.bind(this));
+};
+
+/**
+ * Called with the result of enumerating gnubbies.
+ * @param {number} rc the result of the enumerate.
+ * @param {Array.<llGnubbyDeviceId>} indexes
+ */
+UsbSignHelper.prototype.enumerateCallback = function(rc, indexes) {
+ if (rc) {
+ this.notifyError_(rc, false);
+ return;
+ }
+ if (!indexes.length) {
+ this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false);
+ return;
+ }
+ if (this.timer_.expired()) {
+ this.notifyError_(DeviceStatusCodes.TIMEOUT_STATUS, true);
+ return;
+ }
+ this.gotSomeGnubbies_(indexes);
+};
+
+/**
+ * Called with the result of enumerating gnubby indexes.
+ * @param {Array.<llGnubbyDeviceId>} indexes
+ * @private
+ */
+UsbSignHelper.prototype.gotSomeGnubbies_ = function(indexes) {
+ /** @private {MultipleGnubbySigner} */
+ this.signer_ = new MultipleGnubbySigner(
+ this.factory_,
+ indexes,
+ false /* forEnroll */,
+ this.signerCompleted_.bind(this),
+ this.signerFoundGnubby_.bind(this),
+ this.timer_,
+ this.logMsgUrl_);
+ this.signer_.addEncodedChallenges(this.pendingChallenges_, true);
+};
+
+/**
+ * Called when a MultipleGnubbySigner completes its sign request.
+ * @param {boolean} anySucceeded whether any sign attempt completed
+ * successfully.
+ * @param {number=} errorCode an error code from a failing gnubby, if one was
+ * found.
+ * @private
+ */
+UsbSignHelper.prototype.signerCompleted_ = function(anySucceeded, errorCode) {
+ this.signerComplete_ = true;
+ // The signer is not created unless some gnubbies were enumerated, so
+ // anyGnubbies is mostly always true. The exception is when the last gnubby is
+ // removed, handled shortly.
+ var anyGnubbies = true;
+ if (!anySucceeded) {
+ if (!errorCode) {
+ errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
+ } else if (errorCode == -llGnubby.GONE) {
+ // If the last gnubby was removed, report as though no gnubbies were
+ // found.
+ errorCode = DeviceStatusCodes.WRONG_DATA_STATUS;
+ anyGnubbies = false;
+ }
+ this.notifyError_(errorCode, anyGnubbies);
+ } else if (this.anyTimeout_) {
+ // Some previously succeeding gnubby timed out: return its error code.
+ this.notifyError_(this.timeoutError_, anyGnubbies);
+ } else {
+ // Do nothing: signerFoundGnubby_ will have been called with each
+ // succeeding gnubby.
+ }
+};
+
+/**
+ * Called when a MultipleGnubbySigner finds a gnubby that has successfully
+ * signed, or can successfully sign, one of the challenges.
+ * @param {number} code
+ * @param {MultipleSignerResult} signResult
+ * @private
+ */
+UsbSignHelper.prototype.signerFoundGnubby_ = function(code, signResult) {
+ var gnubby = signResult['gnubby'];
+ var challenge = signResult['challenge'];
+ var info = new Uint8Array(signResult['info']);
+ if (code == DeviceStatusCodes.OK_STATUS && info.length > 0 && info[0]) {
+ this.notifySuccess_(gnubby, challenge, info);
+ } else {
+ this.waitingForTouchGnubbies_.push(gnubby);
+ this.retrySignIfNotTimedOut_(gnubby, challenge, code);
+ }
+};
+
+/**
+ * Reports the result of a successful sign operation.
+ * @param {usbGnubby} gnubby
+ * @param {SignHelperChallenge} challenge
+ * @param {Uint8Array} info
+ * @private
+ */
+UsbSignHelper.prototype.notifySuccess_ = function(gnubby, challenge, info) {
+ if (this.notified_)
+ return;
+ this.notified_ = true;
+
+ gnubby.closeWhenIdle();
+ this.close();
+
+ if (CORRUPT_sign) {
+ CORRUPT_sign = false;
+ info[info.length - 1] = info[info.length - 1] ^ 0xff;
+ }
+ var encodedChallenge = {};
+ encodedChallenge['challengeHash'] = B64_encode(challenge['challengeHash']);
+ encodedChallenge['appIdHash'] = B64_encode(challenge['appIdHash']);
+ encodedChallenge['keyHandle'] = B64_encode(challenge['keyHandle']);
+ this.successCb_(
+ /** @type {SignHelperChallenge} */ (encodedChallenge), B64_encode(info));
+};
+
+/**
+ * Reports error to the caller.
+ * @param {number} code error to report
+ * @param {boolean} anyGnubbies
+ * @private
+ */
+UsbSignHelper.prototype.notifyError_ = function(code, anyGnubbies) {
+ if (this.notified_)
+ return;
+ this.notified_ = true;
+ this.close();
+ this.errorCb_(code, anyGnubbies);
+};
+
+/**
+ * Retries signing a particular challenge on a gnubby.
+ * @param {usbGnubby} gnubby
+ * @param {SignHelperChallenge} challenge
+ * @private
+ */
+UsbSignHelper.prototype.retrySign_ = function(gnubby, challenge) {
+ var challengeHash = challenge['challengeHash'];
+ var appIdHash = challenge['appIdHash'];
+ var keyHandle = challenge['keyHandle'];
+ gnubby.sign(challengeHash, appIdHash, keyHandle,
+ this.signCallback_.bind(this, gnubby, challenge));
+};
+
+/**
+ * Called when a gnubby completes a sign request.
+ * @param {usbGnubby} gnubby
+ * @param {SignHelperChallenge} challenge
+ * @param {number} code
+ * @private
+ */
+UsbSignHelper.prototype.retrySignIfNotTimedOut_ =
+ function(gnubby, challenge, code) {
+ if (this.timer_.expired()) {
+ // Store any timeout error code, to be returned from the complete
+ // callback if no other eligible gnubbies are found.
+ /** @private {boolean} */
+ this.anyTimeout_ = true;
+ /** @private {number} */
+ this.timeoutError_ = code;
+ this.removePreviouslyEligibleGnubby_(gnubby, code);
+ } else {
+ window.setTimeout(this.retrySign_.bind(this, gnubby, challenge), 200);
+ }
+};
+
+/**
+ * Removes a gnubby that was waiting for touch from the list, with the given
+ * error code. If this is the last gnubby, notifies the caller of the error.
+ * @param {usbGnubby} gnubby
+ * @param {number} code
+ * @private
+ */
+UsbSignHelper.prototype.removePreviouslyEligibleGnubby_ =
+ function(gnubby, code) {
+ // Close this gnubby.
+ gnubby.closeWhenIdle();
+ var index = this.waitingForTouchGnubbies_.indexOf(gnubby);
+ if (index >= 0) {
+ this.waitingForTouchGnubbies_.splice(index, 1);
+ }
+ if (!this.waitingForTouchGnubbies_.length && this.signerComplete_ &&
+ !this.notified_) {
+ // Last sign attempt is complete: return this error.
+ console.log(UTIL_fmt('timeout or error (' + code.toString(16) +
+ ') signing'));
+ // If the last device is gone, report as if no gnubbies were found.
+ if (code == -llGnubby.GONE) {
+ this.notifyError_(DeviceStatusCodes.WRONG_DATA_STATUS, false);
+ return;
+ }
+ this.notifyError_(code, true);
+ }
+};
+
+/**
+ * Called when a gnubby completes a sign request.
+ * @param {usbGnubby} gnubby
+ * @param {SignHelperChallenge} challenge
+ * @param {number} code
+ * @param {ArrayBuffer=} infoArray
+ * @private
+ */
+UsbSignHelper.prototype.signCallback_ =
+ function(gnubby, challenge, code, infoArray) {
+ if (this.notified_) {
+ // Individual sign completed after previous success or failure. Disregard.
+ return;
+ }
+ var info = new Uint8Array(infoArray || []);
+ if (code == DeviceStatusCodes.OK_STATUS && info.length > 0 && info[0]) {
+ this.notifySuccess_(gnubby, challenge, info);
+ } else if (code == DeviceStatusCodes.OK_STATUS ||
+ code == DeviceStatusCodes.WAIT_TOUCH_STATUS ||
+ code == DeviceStatusCodes.BUSY_STATUS) {
+ this.retrySignIfNotTimedOut_(gnubby, challenge, code);
+ } else {
+ console.log(UTIL_fmt('got error ' + code.toString(16) + ' signing'));
+ this.removePreviouslyEligibleGnubby_(gnubby, code);
+ }
+};
+
+/**
+ * Closes the MultipleGnubbySigner, if any.
+ */
+UsbSignHelper.prototype.close = function() {
+ if (this.signer_) {
+ this.signer_.close();
+ this.signer_ = null;
+ }
+ for (var i = 0; i < this.waitingForTouchGnubbies_.length; i++) {
+ this.waitingForTouchGnubbies_[i].closeWhenIdle();
+ }
+ this.waitingForTouchGnubbies_ = [];
+};
+
+/**
+ * @param {!GnubbyFactory} gnubbyFactory Factory to create gnubbies.
+ * @constructor
+ * @implements {SignHelperFactory}
+ */
+function UsbSignHelperFactory(gnubbyFactory) {
+ /** @private {!GnubbyFactory} */
+ this.gnubbyFactory_ = gnubbyFactory;
+}
+
+/**
+ * @param {Countdown} timer Timer after whose expiration the caller is no longer
+ * interested in the result of a sign request.
+ * @param {function(number, boolean)} errorCb Called when a sign request fails
+ * with an error code and whether any gnubbies were found.
+ * @param {function(SignHelperChallenge, string)} successCb Called with the
+ * signature produced by a successful sign request.
+ * @param {(function(number, boolean)|undefined)} opt_progressCb Called with
+ * progress updates to the sign request.
+ * @param {string=} opt_logMsgUrl A URL to post log messages to.
+ * @return {UsbSignHelper} the newly created helper.
+ */
+UsbSignHelperFactory.prototype.createHelper =
+ function(timer, errorCb, successCb, opt_progressCb, opt_logMsgUrl) {
+ var helper =
+ new UsbSignHelper(this.gnubbyFactory_, timer, errorCb, successCb,
+ opt_progressCb, opt_logMsgUrl);
+ return helper;
+};
diff --git a/chrome/browser/resources/cryptotoken/util.js b/chrome/browser/resources/cryptotoken/util.js
new file mode 100644
index 0000000..0ce15a9
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/util.js
@@ -0,0 +1,157 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Various string utility functions by [email protected]
+'use strict';
+
+/**
+ * Converts a string to an array of bytes.
+ * @param {string} s The string to convert.
+ * @param {(Array|Uint8Array)=} bytes The Array-like object into which to store
+ * the bytes. A new Array will be created if not provided.
+ * @return {(Array|Uint8Array)} An array of bytes representing the string.
+ */
+function UTIL_StringToBytes(s, bytes) {
+ bytes = bytes || new Array(s.length);
+ for (var i = 0; i < s.length; ++i)
+ bytes[i] = s.charCodeAt(i);
+ return bytes;
+}
+
+function UTIL_BytesToString(b) {
+ return String.fromCharCode.apply(null, b);
+}
+
+function UTIL_BytesToHex(b) {
+ if (!b) return '(null)';
+ var hexchars = '0123456789ABCDEF';
+ var hexrep = new Array(b.length * 2);
+
+ for (var i = 0; i < b.length; ++i) {
+ hexrep[i * 2 + 0] = hexchars.charAt((b[i] >> 4) & 15);
+ hexrep[i * 2 + 1] = hexchars.charAt(b[i] & 15);
+ }
+ return hexrep.join('');
+}
+
+function UTIL_BytesToHexWithSeparator(b, sep) {
+ var hexchars = '0123456789ABCDEF';
+ var stride = 2 + (sep ? 1 : 0);
+ var hexrep = new Array(b.length * stride);
+
+ for (var i = 0; i < b.length; ++i) {
+ if (sep) hexrep[i * stride + 0] = sep;
+ hexrep[i * stride + stride - 2] = hexchars.charAt((b[i] >> 4) & 15);
+ hexrep[i * stride + stride - 1] = hexchars.charAt(b[i] & 15);
+ }
+ return (sep ? hexrep.slice(1) : hexrep).join('');
+}
+
+function UTIL_HexToBytes(h) {
+ var hexchars = '0123456789ABCDEFabcdef';
+ var res = new Uint8Array(h.length / 2);
+ for (var i = 0; i < h.length; i += 2) {
+ if (hexchars.indexOf(h.substring(i, i + 1)) == -1) break;
+ res[i / 2] = parseInt(h.substring(i, i + 2), 16);
+ }
+ return res;
+}
+
+function UTIL_equalArrays(a, b) {
+ if (!a || !b) return false;
+ if (a.length != b.length) return false;
+ var accu = 0;
+ for (var i = 0; i < a.length; ++i)
+ accu |= a[i] ^ b[i];
+ return accu === 0;
+}
+
+function UTIL_ltArrays(a, b) {
+ if (a.length < b.length) return true;
+ if (a.length > b.length) return false;
+ for (var i = 0; i < a.length; ++i) {
+ if (a[i] < b[i]) return true;
+ if (a[i] > b[i]) return false;
+ }
+ return false;
+}
+
+function UTIL_gtArrays(a, b) {
+ return UTIL_ltArrays(b, a);
+}
+
+function UTIL_geArrays(a, b) {
+ return !UTIL_ltArrays(a, b);
+}
+
+function UTIL_unionArrays(a, b) {
+ var obj = {};
+ for (var i = 0; i < a.length; i++) {
+ obj[a[i]] = a[i];
+ }
+ for (var i = 0; i < b.length; i++) {
+ obj[b[i]] = b[i];
+ }
+ var union = [];
+ for (var k in obj) {
+ union.push(obj[k]);
+ }
+ return union;
+}
+
+function UTIL_getRandom(a) {
+ var tmp = new Array(a);
+ var rnd = new Uint8Array(a);
+ window.crypto.getRandomValues(rnd); // Yay!
+ for (var i = 0; i < a; ++i) tmp[i] = rnd[i] & 255;
+ return tmp;
+}
+
+function UTIL_setFavicon(icon) {
+ // Construct a new favion link tag
+ var faviconLink = document.createElement('link');
+ faviconLink.rel = 'Shortcut Icon';
+ faviconLink.type = 'image/x-icon';
+ faviconLink.href = icon;
+
+ // Remove the old favion, if it exists
+ var head = document.getElementsByTagName('head')[0];
+ var links = head.getElementsByTagName('link');
+ for (var i = 0; i < links.length; i++) {
+ var link = links[i];
+ if (link.type == faviconLink.type && link.rel == faviconLink.rel) {
+ head.removeChild(link);
+ }
+ }
+
+ // Add in the new one
+ head.appendChild(faviconLink);
+}
+
+// Erase all entries in array
+function UTIL_clear(a) {
+ if (a instanceof Array) {
+ for (var i = 0; i < a.length; ++i)
+ a[i] = 0;
+ }
+}
+
+// hr:min:sec.milli string
+function UTIL_time() {
+ var d = new Date();
+ var m = '000' + d.getMilliseconds();
+ var s = d.toTimeString().substring(0, 8) + '.' + m.substring(m.length - 3);
+ return s;
+}
+var UTIL_events = [];
+var UTIL_max_events = 500;
+
+function UTIL_fmt(s) {
+ var line = UTIL_time() + ' ' + s;
+ if (UTIL_events.push(line) > UTIL_max_events) {
+ // Drop from head.
+ UTIL_events.splice(0, UTIL_events.length - UTIL_max_events);
+ }
+ return line;
+}
diff --git a/chrome/browser/resources/cryptotoken/webrequest.js b/chrome/browser/resources/cryptotoken/webrequest.js
new file mode 100644
index 0000000..3d7a6d5
--- /dev/null
+++ b/chrome/browser/resources/cryptotoken/webrequest.js
@@ -0,0 +1,385 @@
+// Copyright 2014 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ * @fileoverview Does common handling for requests coming from web pages and
+ * routes them to the provided handler.
+ */
+
+/**
+ * Gets the scheme + origin from a web url.
+ * @param {string} url
+ * @return {?string}
+ */
+function getOriginFromUrl(url) {
+ var re = new RegExp('^(https?://)[^/]*/?');
+ var originarray = re.exec(url);
+ if (originarray == null) return originarray;
+ var origin = originarray[0];
+ while (origin.charAt(origin.length - 1) == '/') {
+ origin = origin.substring(0, origin.length - 1);
+ }
+ if (origin == 'http:' || origin == 'https:')
+ return null;
+ return origin;
+}
+
+/**
+ * Parses the text as JSON and returns it as an array of strings.
+ * @param {string} text
+ * @return {Array.<string>}
+ */
+function getOriginsFromJson(text) {
+ try {
+ var urls = JSON.parse(text);
+ var origins = [];
+ for (var i = 0, url; url = urls[i]; i++) {
+ var origin = getOriginFromUrl(url);
+ if (origin)
+ origins.push(origin);
+ }
+ return origins;
+ } catch (e) {
+ console.log(UTIL_fmt('could not parse ' + text));
+ return [];
+ }
+}
+
+/**
+ * Fetches the app id, and calls a callback with list of allowed origins for it.
+ * @param {string} appId the app id to fetch.
+ * @param {Function} cb called with a list of allowed origins for the app id.
+ */
+function fetchAppId(appId, cb) {
+ var origin = getOriginFromUrl(appId);
+ if (!origin) {
+ cb(404, appId);
+ return;
+ }
+ var xhr = new XMLHttpRequest();
+ var origins = [];
+ xhr.open('GET', appId, true);
+ xhr.onloadend = function() {
+ if (xhr.status != 200) {
+ cb(xhr.status, appId);
+ return;
+ }
+ cb(xhr.status, appId, getOriginsFromJson(xhr.responseText));
+ };
+ xhr.send();
+}
+
+/**
+ * Retrieves a set of distinct app ids from the SignData.
+ * @param {SignData=} signData
+ * @return {Array.<string>} array of distinct app ids.
+ */
+function getDistinctAppIds(signData) {
+ var appIds = [];
+ if (!signData) {
+ return appIds;
+ }
+ for (var i = 0, request; request = signData[i]; i++) {
+ var appId = request['appId'];
+ if (appId && appIds.indexOf(appId) == -1) {
+ appIds.push(appId);
+ }
+ }
+ return appIds;
+}
+
+/**
+ * Reorganizes the requests from the SignData to an array of
+ * (appId, [Request]) tuples.
+ * @param {SignData} signData
+ * @return {Array.<[string, Array.<Request>]>} array of
+ * (appId, [Request]) tuples.
+ */
+function requestsByAppId(signData) {
+ var requests = {};
+ var appIdOrder = {};
+ var orderToAppId = {};
+ var lastOrder = 0;
+ for (var i = 0, request; request = signData[i]; i++) {
+ var appId = request['appId'];
+ if (appId) {
+ if (!appIdOrder.hasOwnProperty(appId)) {
+ appIdOrder[appId] = lastOrder;
+ orderToAppId[lastOrder] = appId;
+ lastOrder++;
+ }
+ if (requests[appId]) {
+ requests[appId].push(request);
+ } else {
+ requests[appId] = [request];
+ }
+ }
+ }
+ var orderedRequests = [];
+ for (var order = 0; order < lastOrder; order++) {
+ appId = orderToAppId[order];
+ orderedRequests.push([appId, requests[appId]]);
+ }
+ return orderedRequests;
+}
+
+/**
+ * Fetches the allowed origins for an appId.
+ * @param {string} appId
+ * @param {boolean} allowHttp Whether http is a valid scheme for an appId.
+ * (This should be false except on test domains.)
+ * @param {function(number, !Array.<string>)} cb Called back with an HTTP
+ * response code and a list of allowed origins for appId.
+ */
+function fetchAllowedOriginsForAppId(appId, allowHttp, cb) {
+ var allowedOrigins = [];
+ if (!appId) {
+ cb(200, allowedOrigins);
+ return;
+ }
+ if (appId.indexOf('http://') == 0 && !allowHttp) {
+ console.log(UTIL_fmt('http app ids disallowed, ' + appId + ' requested'));
+ cb(200, allowedOrigins);
+ return;
+ }
+ // TODO(juanlang): hack for old enrolled gnubbies, don't treat
+ // accounts.google.com/login.corp.google.com specially when cryptauth server
+ // stops reporting them as appId.
+ if (appId == 'https://accounts.google.com') {
+ allowedOrigins = ['https://login.corp.google.com'];
+ cb(200, allowedOrigins);
+ return;
+ }
+ if (appId == 'https://login.corp.google.com') {
+ allowedOrigins = ['https://accounts.google.com'];
+ cb(200, allowedOrigins);
+ return;
+ }
+ // Termination of this function relies in fetchAppId completing.
+ // (Not completing would be a bug in XMLHttpRequest.)
+ // TODO(juanlang): provide a termination guarantee, e.g. with a timer?
+ fetchAppId(appId, function(rc, fetchedAppId, origins) {
+ if (rc != 200) {
+ console.log(UTIL_fmt('fetching ' + fetchedAppId + ' failed: ' + rc));
+ allowedOrigins = [];
+ } else {
+ allowedOrigins = origins;
+ }
+ cb(rc, allowedOrigins);
+ });
+}
+
+/**
+ * Checks whether an appId is valid for a given origin.
+ * @param {!string} appId
+ * @param {!string} origin
+ * @param {!Array.<string>} allowedOrigins the list of allowed origins for each
+ * appId.
+ * @return {boolean} whether the appId is allowed for the origin.
+ */
+function isValidAppIdForOrigin(appId, origin, allowedOrigins) {
+ if (!appId)
+ return false;
+ if (appId == origin) {
+ // trivially allowed
+ return true;
+ }
+ if (!allowedOrigins)
+ return false;
+ return allowedOrigins.indexOf(origin) >= 0;
+}
+
+/**
+ * Returns whether the signData object appears to be valid.
+ * @param {Array.<Object>} signData the signData object.
+ * @return {boolean} whether the object appears valid.
+ */
+function isValidSignData(signData) {
+ for (var i = 0; i < signData.length; i++) {
+ var incomingChallenge = signData[i];
+ if (!incomingChallenge.hasOwnProperty('challenge'))
+ return false;
+ if (!incomingChallenge.hasOwnProperty('appId')) {
+ return false;
+ }
+ if (!incomingChallenge.hasOwnProperty('keyHandle'))
+ return false;
+ if (incomingChallenge['version']) {
+ if (incomingChallenge['version'] != 'U2F_V1' &&
+ incomingChallenge['version'] != 'U2F_V2') {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+/** Posts the log message to the log url.
+ * @param {string} logMsg the log message to post.
+ * @param {string=} opt_logMsgUrl the url to post log messages to.
+ */
+function logMessage(logMsg, opt_logMsgUrl) {
+ console.log(UTIL_fmt('logMessage("' + logMsg + '")'));
+
+ if (!opt_logMsgUrl) {
+ return;
+ }
+ // Image fetching is not allowed per packaged app CSP.
+ // But video and audio is.
+ var audio = new Audio();
+ audio.src = opt_logMsgUrl + logMsg;
+}
+
+/**
+ * Logs the result of fetching an appId.
+ * @param {!string} appId
+ * @param {number} millis elapsed time while fetching the appId.
+ * @param {Array.<string>} allowedOrigins the allowed origins retrieved.
+ * @param {string=} opt_logMsgUrl
+ */
+function logFetchAppIdResult(appId, millis, allowedOrigins, opt_logMsgUrl) {
+ var logMsg = 'log=fetchappid&appid=' + appId + '&millis=' + millis +
+ '&numorigins=' + allowedOrigins.length;
+ logMessage(logMsg, opt_logMsgUrl);
+}
+
+/**
+ * Logs a mismatch between an origin and an appId.
+ * @param {string} origin
+ * @param {!string} appId
+ * @param {string=} opt_logMsgUrl
+ */
+function logInvalidOriginForAppId(origin, appId, opt_logMsgUrl) {
+ var logMsg = 'log=originrejected&origin=' + origin + '&appid=' + appId;
+ logMessage(logMsg, opt_logMsgUrl);
+}
+
+/**
+ * Formats response parameters as an object.
+ * @param {string} type type of the post message.
+ * @param {number} code status code of the operation.
+ * @param {Object=} responseData the response data of the operation.
+ * @return {Object} formatted response.
+ */
+function formatWebPageResponse(type, code, responseData) {
+ var responseJsonObject = {};
+ responseJsonObject['type'] = type;
+ responseJsonObject['code'] = code;
+ if (responseData)
+ responseJsonObject['responseData'] = responseData;
+ return responseJsonObject;
+}
+
+/**
+ * @param {!string} string
+ * @return {Array.<number>} SHA256 hash value of string.
+ */
+function sha256HashOfString(string) {
+ var s = new SHA256();
+ s.update(UTIL_StringToBytes(string));
+ return s.digest();
+}
+
+/**
+ * Normalizes the TLS channel ID value:
+ * 1. Converts semantically empty values (undefined, null, 0) to the empty
+ * string.
+ * 2. Converts valid JSON strings to a JS object.
+ * 3. Otherwise, returns the input value unmodified.
+ * @param {Object|string|undefined} opt_tlsChannelId
+ * @return {Object|string} The normalized TLS channel ID value.
+ */
+function tlsChannelIdValue(opt_tlsChannelId) {
+ if (!opt_tlsChannelId) {
+ // Case 1: Always set some value for TLS channel ID, even if it's the empty
+ // string: this browser definitely supports them.
+ return '';
+ }
+ if (typeof opt_tlsChannelId === 'string') {
+ try {
+ var obj = JSON.parse(opt_tlsChannelId);
+ if (!obj) {
+ // Case 1: The string value 'null' parses as the Javascript object null,
+ // so return an empty string: the browser definitely supports TLS
+ // channel id.
+ return '';
+ }
+ // Case 2: return the value as a JS object.
+ return /** @type {Object} */ (obj);
+ } catch (e) {
+ console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId);
+ // Case 3: return the value unmodified.
+ }
+ }
+ return opt_tlsChannelId;
+}
+
+/**
+ * Creates a browser data object with the given values.
+ * @param {!string} type A string representing the "type" of this browser data
+ * object.
+ * @param {!string} serverChallenge The server's challenge, as a base64-
+ * encoded string.
+ * @param {!string} origin The server's origin, as seen by the browser.
+ * @param {Object|string|undefined} opt_tlsChannelId
+ * @return {string} A string representation of the browser data object.
+ */
+function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) {
+ var browserData = {
+ 'typ' : type,
+ 'challenge' : serverChallenge,
+ 'origin' : origin
+ };
+ browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId);
+ return JSON.stringify(browserData);
+}
+
+/**
+ * Creates a browser data object for an enroll request with the given values.
+ * @param {!string} serverChallenge The server's challenge, as a base64-
+ * encoded string.
+ * @param {!string} origin The server's origin, as seen by the browser.
+ * @param {Object|string|undefined} opt_tlsChannelId
+ * @return {string} A string representation of the browser data object.
+ */
+function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) {
+ return makeBrowserData(
+ 'navigator.id.finishEnrollment', serverChallenge, origin,
+ opt_tlsChannelId);
+}
+
+/**
+ * Creates a browser data object for a sign request with the given values.
+ * @param {!string} serverChallenge The server's challenge, as a base64-
+ * encoded string.
+ * @param {!string} origin The server's origin, as seen by the browser.
+ * @param {Object|string|undefined} opt_tlsChannelId
+ * @return {string} A string representation of the browser data object.
+ */
+function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) {
+ return makeBrowserData(
+ 'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId);
+}
+
+/**
+ * @param {string} browserData
+ * @param {string} appId
+ * @param {string} encodedKeyHandle
+ * @param {string=} version
+ * @return {SignHelperChallenge}
+ */
+function makeChallenge(browserData, appId, encodedKeyHandle, version) {
+ var appIdHash = B64_encode(sha256HashOfString(appId));
+ var browserDataHash = B64_encode(sha256HashOfString(browserData));
+ var keyHandle = encodedKeyHandle;
+
+ var challenge = {
+ 'challengeHash': browserDataHash,
+ 'appIdHash': appIdHash,
+ 'keyHandle': keyHandle
+ };
+ // Version is implicitly U2F_V1 if not specified.
+ challenge['version'] = (version || 'U2F_V1');
+ return challenge;
+}