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;
+}