Home | History | Annotate | Download | only in cryptotoken
      1 // Copyright 2014 The Chromium Authors. All rights reserved.
      2 // Use of this source code is governed by a BSD-style license that can be
      3 // found in the LICENSE file.
      4 
      5 /**
      6  * @fileoverview Implements a low-level gnubby driver based on chrome.usb.
      7  */
      8 'use strict';
      9 
     10 /**
     11  * Low level gnubby 'driver'. One per physical USB device.
     12  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
     13  *     in.
     14  * @param {!chrome.usb.ConnectionHandle} dev The device.
     15  * @param {number} id The device's id.
     16  * @param {number} inEndpoint The device's in endpoint.
     17  * @param {number} outEndpoint The device's out endpoint.
     18  * @constructor
     19  * @implements {GnubbyDevice}
     20  */
     21 function UsbGnubbyDevice(gnubbies, dev, id, inEndpoint, outEndpoint) {
     22   /** @private {Gnubbies} */
     23   this.gnubbies_ = gnubbies;
     24   this.dev = dev;
     25   this.id = id;
     26   this.inEndpoint = inEndpoint;
     27   this.outEndpoint = outEndpoint;
     28   this.txqueue = [];
     29   this.clients = [];
     30   this.lockCID = 0;     // channel ID of client holding a lock, if != 0.
     31   this.lockMillis = 0;  // current lock period.
     32   this.lockTID = null;  // timer id of lock timeout.
     33   this.closing = false;  // device to be closed by receive loop.
     34   this.updating = false;  // device firmware is in final stage of updating.
     35   this.inTransferPending = false;
     36   this.outTransferPending = false;
     37 }
     38 
     39 /**
     40  * Namespace for the UsbGnubbyDevice implementation.
     41  * @const
     42  */
     43 UsbGnubbyDevice.NAMESPACE = 'usb';
     44 
     45 /** Destroys this low-level device instance. */
     46 UsbGnubbyDevice.prototype.destroy = function() {
     47   if (!this.dev) return;  // Already dead.
     48 
     49   this.gnubbies_.removeOpenDevice(
     50       {namespace: UsbGnubbyDevice.NAMESPACE, device: this.id});
     51   this.closing = true;
     52 
     53   console.log(UTIL_fmt('UsbGnubbyDevice.destroy()'));
     54 
     55   // Synthesize a close error frame to alert all clients,
     56   // some of which might be in read state.
     57   //
     58   // Use magic CID 0 to address all.
     59   this.publishFrame_(new Uint8Array([
     60         0, 0, 0, 0,  // broadcast CID
     61         GnubbyDevice.CMD_ERROR,
     62         0, 1,  // length
     63         GnubbyDevice.GONE]).buffer);
     64 
     65   // Set all clients to closed status and remove them.
     66   while (this.clients.length != 0) {
     67     var client = this.clients.shift();
     68     if (client) client.closed = true;
     69   }
     70 
     71   if (this.lockTID) {
     72     window.clearTimeout(this.lockTID);
     73     this.lockTID = null;
     74   }
     75 
     76   var dev = this.dev;
     77   this.dev = null;
     78 
     79   chrome.usb.releaseInterface(dev, 0, function() {
     80     if (chrome.runtime.lastError) {
     81       console.warn(UTIL_fmt('Device ' + dev.handle +
     82           ' couldn\'t be released:'));
     83       console.warn(chrome.runtime.lastError);
     84       return;
     85     }
     86     console.log(UTIL_fmt('Device ' + dev.handle + ' released'));
     87     chrome.usb.closeDevice(dev, function() {
     88       if (chrome.runtime.lastError) {
     89         console.warn(UTIL_fmt('Device ' + dev.handle +
     90             ' couldn\'t be closed:'));
     91         console.warn(chrome.runtime.lastError);
     92         return;
     93       }
     94       console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
     95     });
     96   });
     97 };
     98 
     99 /**
    100  * Push frame to all clients.
    101  * @param {ArrayBuffer} f Data frame
    102  * @private
    103  */
    104 UsbGnubbyDevice.prototype.publishFrame_ = function(f) {
    105   var old = this.clients;
    106 
    107   var remaining = [];
    108   var changes = false;
    109   for (var i = 0; i < old.length; ++i) {
    110     var client = old[i];
    111     if (client.receivedFrame(f)) {
    112       // Client still alive; keep on list.
    113       remaining.push(client);
    114     } else {
    115       changes = true;
    116       console.log(UTIL_fmt(
    117           '[' + client.cid.toString(16) + '] left?'));
    118     }
    119   }
    120   if (changes) this.clients = remaining;
    121 };
    122 
    123 /**
    124  * @return {boolean} whether this device is open and ready to use.
    125  * @private
    126  */
    127 UsbGnubbyDevice.prototype.readyToUse_ = function() {
    128   if (this.closing) return false;
    129   if (!this.dev) return false;
    130 
    131   return true;
    132 };
    133 
    134 /**
    135  * Reads one reply from the low-level device.
    136  * @private
    137  */
    138 UsbGnubbyDevice.prototype.readOneReply_ = function() {
    139   if (!this.readyToUse_()) return;  // No point in continuing.
    140   if (this.updating) return;  // Do not bother waiting for final update reply.
    141 
    142   var self = this;
    143 
    144   function inTransferComplete(x) {
    145     self.inTransferPending = false;
    146 
    147     if (!self.readyToUse_()) return;  // No point in continuing.
    148 
    149     if (chrome.runtime.lastError) {
    150       console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
    151       console.log(chrome.runtime.lastError);
    152       window.setTimeout(function() { self.destroy(); }, 0);
    153       return;
    154     }
    155 
    156     if (x.data) {
    157       var u8 = new Uint8Array(x.data);
    158       console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
    159 
    160       self.publishFrame_(x.data);
    161 
    162       // Write another pending request, if any.
    163       window.setTimeout(
    164           function() {
    165             self.txqueue.shift();  // Drop sent frame from queue.
    166             self.writeOneRequest_();
    167           },
    168           0);
    169     } else {
    170       console.log(UTIL_fmt('no x.data!'));
    171       console.log(x);
    172       window.setTimeout(function() { self.destroy(); }, 0);
    173     }
    174   }
    175 
    176   if (this.inTransferPending == false) {
    177     this.inTransferPending = true;
    178     chrome.usb.bulkTransfer(
    179       /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
    180       { direction: 'in', endpoint: this.inEndpoint, length: 2048 },
    181       inTransferComplete);
    182   } else {
    183     throw 'inTransferPending!';
    184   }
    185 };
    186 
    187 /**
    188  * Register a client for this gnubby.
    189  * @param {*} who The client.
    190  */
    191 UsbGnubbyDevice.prototype.registerClient = function(who) {
    192   for (var i = 0; i < this.clients.length; ++i) {
    193     if (this.clients[i] === who) return;  // Already registered.
    194   }
    195   this.clients.push(who);
    196 };
    197 
    198 /**
    199  * De-register a client.
    200  * @param {*} who The client.
    201  * @return {number} The number of remaining listeners for this device, or -1
    202  * Returns number of remaining listeners for this device.
    203  *     if this had no clients to start with.
    204  */
    205 UsbGnubbyDevice.prototype.deregisterClient = function(who) {
    206   var current = this.clients;
    207   if (current.length == 0) return -1;
    208   this.clients = [];
    209   for (var i = 0; i < current.length; ++i) {
    210     var client = current[i];
    211     if (client !== who) this.clients.push(client);
    212   }
    213   return this.clients.length;
    214 };
    215 
    216 /**
    217  * @param {*} who The client.
    218  * @return {boolean} Whether this device has who as a client.
    219  */
    220 UsbGnubbyDevice.prototype.hasClient = function(who) {
    221   if (this.clients.length == 0) return false;
    222   for (var i = 0; i < this.clients.length; ++i) {
    223     if (who === this.clients[i])
    224       return true;
    225   }
    226   return false;
    227 };
    228 
    229 /**
    230  * Stuff queued frames from txqueue[] to device, one by one.
    231  * @private
    232  */
    233 UsbGnubbyDevice.prototype.writeOneRequest_ = function() {
    234   if (!this.readyToUse_()) return;  // No point in continuing.
    235 
    236   if (this.txqueue.length == 0) return;  // Nothing to send.
    237 
    238   var frame = this.txqueue[0];
    239 
    240   var self = this;
    241   function OutTransferComplete(x) {
    242     self.outTransferPending = false;
    243 
    244     if (!self.readyToUse_()) return;  // No point in continuing.
    245 
    246     if (chrome.runtime.lastError) {
    247       console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
    248       console.log(chrome.runtime.lastError);
    249       window.setTimeout(function() { self.destroy(); }, 0);
    250       return;
    251     }
    252 
    253     window.setTimeout(function() { self.readOneReply_(); }, 0);
    254   };
    255 
    256   var u8 = new Uint8Array(frame);
    257 
    258   // See whether this requires scrubbing before logging.
    259   var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') &&
    260                      Gnubby['redactRequestLog'](u8);
    261   if (alternateLog) {
    262     console.log(UTIL_fmt('>' + alternateLog));
    263   } else {
    264     console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
    265   }
    266 
    267   if (this.outTransferPending == false) {
    268     this.outTransferPending = true;
    269     chrome.usb.bulkTransfer(
    270         /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
    271         { direction: 'out', endpoint: this.outEndpoint, data: frame },
    272         OutTransferComplete);
    273   } else {
    274     throw 'outTransferPending!';
    275   }
    276 };
    277 
    278 /**
    279  * Check whether channel is locked for this request or not.
    280  * @param {number} cid Channel id
    281  * @param {number} cmd Command to be sent
    282  * @return {boolean} true if not locked for this request.
    283  * @private
    284  */
    285 UsbGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
    286   if (this.lockCID) {
    287     // We have an active lock.
    288     if (this.lockCID != cid) {
    289       // Some other channel has active lock.
    290 
    291       if (cmd != GnubbyDevice.CMD_SYNC &&
    292           cmd != GnubbyDevice.CMD_INIT) {
    293         // Anything but SYNC|INIT gets an immediate busy.
    294         var busy = new Uint8Array(
    295             [(cid >> 24) & 255,
    296              (cid >> 16) & 255,
    297              (cid >> 8) & 255,
    298              cid & 255,
    299              GnubbyDevice.CMD_ERROR,
    300              0, 1,  // length
    301              GnubbyDevice.BUSY]);
    302         // Log the synthetic busy too.
    303         console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
    304         this.publishFrame_(busy.buffer);
    305         return false;
    306       }
    307 
    308       // SYNC|INIT get to go to the device to flush OS tx/rx queues.
    309       // The usb firmware is to always respond to SYNC|INIT,
    310       // regardless of lock status.
    311     }
    312   }
    313   return true;
    314 };
    315 
    316 /**
    317  * Update or grab lock.
    318  * @param {number} cid Channel id
    319  * @param {number} cmd Command
    320  * @param {number} arg Command argument
    321  * @private
    322  */
    323 UsbGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) {
    324   if (this.lockCID == 0 || this.lockCID == cid) {
    325     // It is this caller's or nobody's lock.
    326     if (this.lockTID) {
    327       window.clearTimeout(this.lockTID);
    328       this.lockTID = null;
    329     }
    330 
    331     if (cmd == GnubbyDevice.CMD_LOCK) {
    332       var nseconds = arg;
    333       if (nseconds != 0) {
    334         this.lockCID = cid;
    335         // Set tracking time to be .1 seconds longer than usb device does.
    336         this.lockMillis = nseconds * 1000 + 100;
    337       } else {
    338         // Releasing lock voluntarily.
    339         this.lockCID = 0;
    340       }
    341     }
    342 
    343     // (re)set the lock timeout if we still hold it.
    344     if (this.lockCID) {
    345       var self = this;
    346       this.lockTID = window.setTimeout(
    347           function() {
    348             console.warn(UTIL_fmt(
    349                 'lock for CID ' + cid.toString(16) + ' expired!'));
    350             self.lockTID = null;
    351             self.lockCID = 0;
    352           },
    353           this.lockMillis);
    354     }
    355   }
    356 };
    357 
    358 /**
    359  * Queue command to be sent.
    360  * If queue was empty, initiate the write.
    361  * @param {number} cid The client's channel ID.
    362  * @param {number} cmd The command to send.
    363  * @param {ArrayBuffer|Uint8Array} data Command argument data
    364  */
    365 UsbGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) {
    366   if (!this.dev) return;
    367   if (!this.checkLock_(cid, cmd)) return;
    368 
    369   var u8 = new Uint8Array(data);
    370   var frame = new Uint8Array(u8.length + 7);
    371 
    372   frame[0] = cid >>> 24;
    373   frame[1] = cid >>> 16;
    374   frame[2] = cid >>> 8;
    375   frame[3] = cid;
    376   frame[4] = cmd;
    377   frame[5] = (u8.length >> 8);
    378   frame[6] = (u8.length & 255);
    379 
    380   frame.set(u8, 7);
    381 
    382   var lockArg = (u8.length > 0) ? u8[0] : 0;
    383   this.updateLock_(cid, cmd, lockArg);
    384 
    385   var wasEmpty = (this.txqueue.length == 0);
    386   this.txqueue.push(frame.buffer);
    387   if (wasEmpty) this.writeOneRequest_();
    388 };
    389 
    390 /**
    391  * @const
    392  */
    393 UsbGnubbyDevice.WINUSB_VID_PIDS = [
    394   {'vendorId': 4176, 'productId': 529}  // Yubico WinUSB
    395 ];
    396 
    397 /**
    398  * @param {function(Array)} cb Enumerate callback
    399  */
    400 UsbGnubbyDevice.enumerate = function(cb) {
    401   var numEnumerated = 0;
    402   var allDevs = [];
    403 
    404   function enumerated(devs) {
    405     allDevs = allDevs.concat(devs);
    406     if (++numEnumerated == UsbGnubbyDevice.WINUSB_VID_PIDS.length) {
    407       cb(allDevs);
    408     }
    409   }
    410 
    411   for (var i = 0; i < UsbGnubbyDevice.WINUSB_VID_PIDS.length; i++) {
    412     chrome.usb.getDevices(UsbGnubbyDevice.WINUSB_VID_PIDS[i], enumerated);
    413   }
    414 };
    415 
    416 /**
    417  * @typedef {?{
    418  *   address: number,
    419  *   type: string,
    420  *   direction: string,
    421  *   maximumPacketSize: number,
    422  *   synchronization: (string|undefined),
    423  *   usage: (string|undefined),
    424  *   pollingInterval: (number|undefined)
    425  * }}
    426  * @see http://developer.chrome.com/apps/usb.html#method-listInterfaces
    427  */
    428 var InterfaceEndpoint;
    429 
    430 
    431 /**
    432  * @typedef {?{
    433  *   interfaceNumber: number,
    434  *   alternateSetting: number,
    435  *   interfaceClass: number,
    436  *   interfaceSubclass: number,
    437  *   interfaceProtocol: number,
    438  *   description: (string|undefined),
    439  *   endpoints: !Array.<!InterfaceEndpoint>
    440  * }}
    441  * @see http://developer.chrome.com/apps/usb.html#method-listInterfaces
    442  */
    443 var InterfaceDescriptor;
    444 
    445 /**
    446  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
    447  *     in.
    448  * @param {number} which The index of the device to open.
    449  * @param {!chrome.usb.Device} dev The device to open.
    450  * @param {function(number, GnubbyDevice=)} cb Called back with the
    451  *     result of opening the device.
    452  */
    453 UsbGnubbyDevice.open = function(gnubbies, which, dev, cb) {
    454   /** @param {chrome.usb.ConnectionHandle=} handle Connection handle */
    455   function deviceOpened(handle) {
    456     if (chrome.runtime.lastError) {
    457       console.warn(UTIL_fmt('failed to open device. permissions issue?'));
    458       cb(-GnubbyDevice.NODEVICE);
    459       return;
    460     }
    461     var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle);
    462     chrome.usb.listInterfaces(nonNullHandle, function(descriptors) {
    463       var inEndpoint, outEndpoint;
    464       for (var i = 0; i < descriptors.length; i++) {
    465         var descriptor = /** @type {InterfaceDescriptor} */ (descriptors[i]);
    466         for (var j = 0; j < descriptor.endpoints.length; j++) {
    467           var endpoint = descriptor.endpoints[j];
    468           if (inEndpoint == undefined && endpoint.type == 'bulk' &&
    469               endpoint.direction == 'in') {
    470             inEndpoint = endpoint.address;
    471           }
    472           if (outEndpoint == undefined && endpoint.type == 'bulk' &&
    473               endpoint.direction == 'out') {
    474             outEndpoint = endpoint.address;
    475           }
    476         }
    477       }
    478       if (inEndpoint == undefined || outEndpoint == undefined) {
    479         console.warn(UTIL_fmt('device lacking an endpoint (broken?)'));
    480         chrome.usb.closeDevice(nonNullHandle);
    481         cb(-GnubbyDevice.NODEVICE);
    482         return;
    483       }
    484       // Try getting it claimed now.
    485       chrome.usb.claimInterface(nonNullHandle, 0, function() {
    486         if (chrome.runtime.lastError) {
    487           console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
    488           console.log(chrome.runtime.lastError);
    489         }
    490         var claimed = !chrome.runtime.lastError;
    491         if (!claimed) {
    492           console.warn(UTIL_fmt('failed to claim interface. busy?'));
    493           // Claim failed? Let the callers know and bail out.
    494           chrome.usb.closeDevice(nonNullHandle);
    495           cb(-GnubbyDevice.BUSY);
    496           return;
    497         }
    498         var gnubby = new UsbGnubbyDevice(gnubbies, nonNullHandle, which,
    499             inEndpoint, outEndpoint);
    500         cb(-GnubbyDevice.OK, gnubby);
    501       });
    502     });
    503   }
    504 
    505   if (UsbGnubbyDevice.runningOnCrOS === undefined) {
    506     UsbGnubbyDevice.runningOnCrOS =
    507         (window.navigator.appVersion.indexOf('; CrOS ') != -1);
    508   }
    509   if (UsbGnubbyDevice.runningOnCrOS) {
    510     chrome.usb.requestAccess(dev, 0, function(success) {
    511       // Even though the argument to requestAccess is a chrome.usb.Device, the
    512       // access request is for access to all devices with the same vid/pid.
    513       // Curiously, if the first chrome.usb.requestAccess succeeds, a second
    514       // call with a separate device with the same vid/pid fails. Since
    515       // chrome.usb.openDevice will fail if a previous access request really
    516       // failed, just ignore the outcome of the access request and move along.
    517       chrome.usb.openDevice(dev, deviceOpened);
    518     });
    519   } else {
    520     chrome.usb.openDevice(dev, deviceOpened);
    521   }
    522 };
    523 
    524 /**
    525  * @param {*} dev Chrome usb device
    526  * @return {GnubbyDeviceId} A device identifier for the device.
    527  */
    528 UsbGnubbyDevice.deviceToDeviceId = function(dev) {
    529   var usbDev = /** @type {!chrome.usb.Device} */ (dev);
    530   var deviceId = {
    531     namespace: UsbGnubbyDevice.NAMESPACE,
    532     device: usbDev.device
    533   };
    534   return deviceId;
    535 };
    536 
    537 /**
    538  * Registers this implementation with gnubbies.
    539  * @param {Gnubbies} gnubbies Gnubbies singleton instance
    540  */
    541 UsbGnubbyDevice.register = function(gnubbies) {
    542   var USB_GNUBBY_IMPL = {
    543     isSharedAccess: false,
    544     enumerate: UsbGnubbyDevice.enumerate,
    545     deviceToDeviceId: UsbGnubbyDevice.deviceToDeviceId,
    546     open: UsbGnubbyDevice.open
    547   };
    548   gnubbies.registerNamespace(UsbGnubbyDevice.NAMESPACE, USB_GNUBBY_IMPL);
    549 };
    550