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.hid.
      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.hid.HidConnectInfo} dev The connection to the device.
     15  * @param {number} id The device's id.
     16  * @constructor
     17  * @implements {GnubbyDevice}
     18  */
     19 function HidGnubbyDevice(gnubbies, dev, id) {
     20   /** @private {Gnubbies} */
     21   this.gnubbies_ = gnubbies;
     22   this.dev = dev;
     23   this.id = id;
     24   this.txqueue = [];
     25   this.clients = [];
     26   this.lockCID = 0;     // channel ID of client holding a lock, if != 0.
     27   this.lockMillis = 0;  // current lock period.
     28   this.lockTID = null;  // timer id of lock timeout.
     29   this.closing = false;  // device to be closed by receive loop.
     30   this.updating = false;  // device firmware is in final stage of updating.
     31 }
     32 
     33 /**
     34  * Namespace for the HidGnubbyDevice implementation.
     35  * @const
     36  */
     37 HidGnubbyDevice.NAMESPACE = 'hid';
     38 
     39 /** Destroys this low-level device instance. */
     40 HidGnubbyDevice.prototype.destroy = function() {
     41   if (!this.dev) return;  // Already dead.
     42 
     43   this.gnubbies_.removeOpenDevice(
     44       {namespace: HidGnubbyDevice.NAMESPACE, device: this.id});
     45   this.closing = true;
     46 
     47   console.log(UTIL_fmt('HidGnubbyDevice.destroy()'));
     48 
     49   // Synthesize a close error frame to alert all clients,
     50   // some of which might be in read state.
     51   //
     52   // Use magic CID 0 to address all.
     53   this.publishFrame_(new Uint8Array([
     54         0, 0, 0, 0,  // broadcast CID
     55         GnubbyDevice.CMD_ERROR,
     56         0, 1,  // length
     57         GnubbyDevice.GONE]).buffer);
     58 
     59   // Set all clients to closed status and remove them.
     60   while (this.clients.length != 0) {
     61     var client = this.clients.shift();
     62     if (client) client.closed = true;
     63   }
     64 
     65   if (this.lockTID) {
     66     window.clearTimeout(this.lockTID);
     67     this.lockTID = null;
     68   }
     69 
     70   var dev = this.dev;
     71   this.dev = null;
     72 
     73   chrome.hid.disconnect(dev.connectionId, function() {
     74     if (chrome.runtime.lastError) {
     75       console.warn(UTIL_fmt('Device ' + dev.connectionId +
     76           ' couldn\'t be disconnected:'));
     77       console.warn(chrome.runtime.lastError);
     78       return;
     79     }
     80     console.log(UTIL_fmt('Device ' + dev.connectionId + ' closed'));
     81   });
     82 };
     83 
     84 /**
     85  * Push frame to all clients.
     86  * @param {ArrayBuffer} f Data to push
     87  * @private
     88  */
     89 HidGnubbyDevice.prototype.publishFrame_ = function(f) {
     90   var old = this.clients;
     91 
     92   var remaining = [];
     93   var changes = false;
     94   for (var i = 0; i < old.length; ++i) {
     95     var client = old[i];
     96     if (client.receivedFrame(f)) {
     97       // Client still alive; keep on list.
     98       remaining.push(client);
     99     } else {
    100       changes = true;
    101       console.log(UTIL_fmt(
    102           '[' + client.cid.toString(16) + '] left?'));
    103     }
    104   }
    105   if (changes) this.clients = remaining;
    106 };
    107 
    108 /**
    109  * Register a client for this gnubby.
    110  * @param {*} who The client.
    111  */
    112 HidGnubbyDevice.prototype.registerClient = function(who) {
    113   for (var i = 0; i < this.clients.length; ++i) {
    114     if (this.clients[i] === who) return;  // Already registered.
    115   }
    116   this.clients.push(who);
    117   if (this.clients.length == 1) {
    118     // First client? Kick off read loop.
    119     this.readLoop_();
    120   }
    121 };
    122 
    123 /**
    124  * De-register a client.
    125  * @param {*} who The client.
    126  * @return {number} The number of remaining listeners for this device, or -1
    127  * Returns number of remaining listeners for this device.
    128  *     if this had no clients to start with.
    129  */
    130 HidGnubbyDevice.prototype.deregisterClient = function(who) {
    131   var current = this.clients;
    132   if (current.length == 0) return -1;
    133   this.clients = [];
    134   for (var i = 0; i < current.length; ++i) {
    135     var client = current[i];
    136     if (client !== who) this.clients.push(client);
    137   }
    138   return this.clients.length;
    139 };
    140 
    141 /**
    142  * @param {*} who The client.
    143  * @return {boolean} Whether this device has who as a client.
    144  */
    145 HidGnubbyDevice.prototype.hasClient = function(who) {
    146   if (this.clients.length == 0) return false;
    147   for (var i = 0; i < this.clients.length; ++i) {
    148     if (who === this.clients[i])
    149       return true;
    150   }
    151   return false;
    152 };
    153 
    154 /**
    155  * Reads all incoming frames and notifies clients of their receipt.
    156  * @private
    157  */
    158 HidGnubbyDevice.prototype.readLoop_ = function() {
    159   //console.log(UTIL_fmt('entering readLoop'));
    160   if (!this.dev) return;
    161 
    162   if (this.closing) {
    163     this.destroy();
    164     return;
    165   }
    166 
    167   // No interested listeners, yet we hit readLoop().
    168   // Must be clean-up. We do this here to make sure no transfer is pending.
    169   if (!this.clients.length) {
    170     this.closing = true;
    171     this.destroy();
    172     return;
    173   }
    174 
    175   // firmwareUpdate() sets this.updating when writing the last block before
    176   // the signature. We process that reply with the already pending
    177   // read transfer but we do not want to start another read transfer for the
    178   // signature block, since that request will have no reply.
    179   // Instead we will see the device drop and re-appear on the bus.
    180   // Current libusb on some platforms gets unhappy when transfer are pending
    181   // when that happens.
    182   // TODO: revisit once Chrome stabilizes its behavior.
    183   if (this.updating) {
    184     console.log(UTIL_fmt('device updating. Ending readLoop()'));
    185     return;
    186   }
    187 
    188   var self = this;
    189   chrome.hid.receive(
    190     this.dev.connectionId,
    191     function(report_id, data) {
    192       if (chrome.runtime.lastError || !data) {
    193         console.log(UTIL_fmt('got lastError'));
    194         console.log(chrome.runtime.lastError);
    195         window.setTimeout(function() { self.destroy(); }, 0);
    196         return;
    197       }
    198       var u8 = new Uint8Array(data);
    199       console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
    200 
    201       self.publishFrame_(data);
    202 
    203       // Read more.
    204       window.setTimeout(function() { self.readLoop_(); }, 0);
    205     }
    206   );
    207 };
    208 
    209 /**
    210  * Check whether channel is locked for this request or not.
    211  * @param {number} cid Channel id
    212  * @param {number} cmd Request command
    213  * @return {boolean} true if not locked for this request.
    214  * @private
    215  */
    216 HidGnubbyDevice.prototype.checkLock_ = function(cid, cmd) {
    217   if (this.lockCID) {
    218     // We have an active lock.
    219     if (this.lockCID != cid) {
    220       // Some other channel has active lock.
    221 
    222       if (cmd != GnubbyDevice.CMD_SYNC &&
    223           cmd != GnubbyDevice.CMD_INIT) {
    224         // Anything but SYNC|INIT gets an immediate busy.
    225         var busy = new Uint8Array(
    226             [(cid >> 24) & 255,
    227              (cid >> 16) & 255,
    228              (cid >> 8) & 255,
    229              cid & 255,
    230              GnubbyDevice.CMD_ERROR,
    231              0, 1,  // length
    232              GnubbyDevice.BUSY]);
    233         // Log the synthetic busy too.
    234         console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
    235         this.publishFrame_(busy.buffer);
    236         return false;
    237       }
    238 
    239       // SYNC|INIT gets to go to the device to flush OS tx/rx queues.
    240       // The usb firmware is to alway respond to SYNC/INIT,
    241       // regardless of lock status.
    242     }
    243   }
    244   return true;
    245 };
    246 
    247 /**
    248  * Update or grab lock.
    249  * @param {number} cid Channel ID
    250  * @param {number} cmd Command
    251  * @param {number} arg Command argument
    252  * @private
    253  */
    254 HidGnubbyDevice.prototype.updateLock_ = function(cid, cmd, arg) {
    255   if (this.lockCID == 0 || this.lockCID == cid) {
    256     // It is this caller's or nobody's lock.
    257     if (this.lockTID) {
    258       window.clearTimeout(this.lockTID);
    259       this.lockTID = null;
    260     }
    261 
    262     if (cmd == GnubbyDevice.CMD_LOCK) {
    263       var nseconds = arg;
    264       if (nseconds != 0) {
    265         this.lockCID = cid;
    266         // Set tracking time to be .1 seconds longer than usb device does.
    267         this.lockMillis = nseconds * 1000 + 100;
    268       } else {
    269         // Releasing lock voluntarily.
    270         this.lockCID = 0;
    271       }
    272     }
    273 
    274     // (re)set the lock timeout if we still hold it.
    275     if (this.lockCID) {
    276       var self = this;
    277       this.lockTID = window.setTimeout(
    278           function() {
    279             console.warn(UTIL_fmt(
    280                 'lock for CID ' + cid.toString(16) + ' expired!'));
    281             self.lockTID = null;
    282             self.lockCID = 0;
    283           },
    284           this.lockMillis);
    285     }
    286   }
    287 };
    288 
    289 /**
    290  * Queue command to be sent.
    291  * If queue was empty, initiate the write.
    292  * @param {number} cid The client's channel ID.
    293  * @param {number} cmd The command to send.
    294  * @param {ArrayBuffer|Uint8Array} data Command arguments
    295  */
    296 HidGnubbyDevice.prototype.queueCommand = function(cid, cmd, data) {
    297   if (!this.dev) return;
    298   if (!this.checkLock_(cid, cmd)) return;
    299 
    300   var u8 = new Uint8Array(data);
    301   var f = new Uint8Array(64);
    302 
    303   HidGnubbyDevice.setCid_(f, cid);
    304   f[4] = cmd;
    305   f[5] = (u8.length >> 8);
    306   f[6] = (u8.length & 255);
    307 
    308   var lockArg = (u8.length > 0) ? u8[0] : 0;
    309 
    310   // Fragment over our 64 byte frames.
    311   var n = 7;
    312   var seq = 0;
    313   for (var i = 0; i < u8.length; ++i) {
    314     f[n++] = u8[i];
    315     if (n == f.length) {
    316       this.queueFrame_(f.buffer, cid, cmd, lockArg);
    317 
    318       f = new Uint8Array(64);
    319       HidGnubbyDevice.setCid_(f, cid);
    320       cmd = f[4] = seq++;
    321       n = 5;
    322     }
    323   }
    324   if (n != 5) {
    325     this.queueFrame_(f.buffer, cid, cmd, lockArg);
    326   }
    327 };
    328 
    329 /**
    330  * Sets the channel id in the frame.
    331  * @param {Uint8Array} frame Data frame
    332  * @param {number} cid The client's channel ID.
    333  * @private
    334  */
    335 HidGnubbyDevice.setCid_ = function(frame, cid) {
    336   frame[0] = cid >>> 24;
    337   frame[1] = cid >>> 16;
    338   frame[2] = cid >>> 8;
    339   frame[3] = cid;
    340 };
    341 
    342 /**
    343  * Updates the lock, and queues the frame for sending. Also begins sending if
    344  * no other writes are outstanding.
    345  * @param {ArrayBuffer} frame Data frame
    346  * @param {number} cid The client's channel ID.
    347  * @param {number} cmd The command to send.
    348  * @param {number} arg Command argument
    349  * @private
    350  */
    351 HidGnubbyDevice.prototype.queueFrame_ = function(frame, cid, cmd, arg) {
    352   this.updateLock_(cid, cmd, arg);
    353   var wasEmpty = (this.txqueue.length == 0);
    354   this.txqueue.push(frame);
    355   if (wasEmpty) this.writePump_();
    356 };
    357 
    358 /**
    359  * Stuff queued frames from txqueue[] to device, one by one.
    360  * @private
    361  */
    362 HidGnubbyDevice.prototype.writePump_ = function() {
    363   if (!this.dev) return;  // Ignore.
    364 
    365   if (this.txqueue.length == 0) return;  // Done with current queue.
    366 
    367   var frame = this.txqueue[0];
    368 
    369   var self = this;
    370   function transferComplete() {
    371     if (chrome.runtime.lastError) {
    372       console.log(UTIL_fmt('got lastError'));
    373       console.log(chrome.runtime.lastError);
    374       window.setTimeout(function() { self.destroy(); }, 0);
    375       return;
    376     }
    377     self.txqueue.shift();  // drop sent frame from queue.
    378     if (self.txqueue.length != 0) {
    379       window.setTimeout(function() { self.writePump_(); }, 0);
    380     }
    381   };
    382 
    383   var u8 = new Uint8Array(frame);
    384 
    385   // See whether this requires scrubbing before logging.
    386   var alternateLog = Gnubby.hasOwnProperty('redactRequestLog') &&
    387                      Gnubby['redactRequestLog'](u8);
    388   if (alternateLog) {
    389     console.log(UTIL_fmt('>' + alternateLog));
    390   } else {
    391     console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
    392   }
    393 
    394   var u8f = new Uint8Array(64);
    395   for (var i = 0; i < u8.length; ++i) {
    396     u8f[i] = u8[i];
    397   }
    398 
    399   chrome.hid.send(
    400       this.dev.connectionId,
    401       0,  // report Id. Must be 0 for our use.
    402       u8f.buffer,
    403       transferComplete
    404   );
    405 };
    406 
    407 /**
    408  * @param {function(Array)} cb Enumeration callback
    409  */
    410 HidGnubbyDevice.enumerate = function(cb) {
    411   var permittedDevs;
    412   var numEnumerated = 0;
    413   var allDevs = [];
    414 
    415   function enumerated(devs) {
    416     allDevs = allDevs.concat(devs);
    417     if (++numEnumerated == permittedDevs.length) {
    418       cb(allDevs);
    419     }
    420   }
    421 
    422   try {
    423     chrome.hid.getDevices({filters: [{usagePage: 0xf1d0}]}, cb);
    424   } catch (e) {
    425     console.log(e);
    426     console.log(UTIL_fmt('falling back to vid/pid enumeration'));
    427     GnubbyDevice.getPermittedUsbDevices(function(devs) {
    428       permittedDevs = devs;
    429       for (var i = 0; i < devs.length; i++) {
    430         chrome.hid.getDevices(devs[i], enumerated);
    431       }
    432     });
    433   }
    434 };
    435 
    436 /**
    437  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
    438  *     in.
    439  * @param {number} which The index of the device to open.
    440  * @param {!chrome.hid.HidDeviceInfo} dev The device to open.
    441  * @param {function(number, GnubbyDevice=)} cb Called back with the
    442  *     result of opening the device.
    443  */
    444 HidGnubbyDevice.open = function(gnubbies, which, dev, cb) {
    445   chrome.hid.connect(dev.deviceId, function(handle) {
    446     if (chrome.runtime.lastError) {
    447       console.log(chrome.runtime.lastError);
    448     }
    449     if (!handle) {
    450       console.warn(UTIL_fmt('failed to connect device. permissions issue?'));
    451       cb(-GnubbyDevice.NODEVICE);
    452       return;
    453     }
    454     var nonNullHandle = /** @type {!chrome.hid.HidConnectInfo} */ (handle);
    455     var gnubby = new HidGnubbyDevice(gnubbies, nonNullHandle, which);
    456     cb(-GnubbyDevice.OK, gnubby);
    457   });
    458 };
    459 
    460 /**
    461  * @param {*} dev A browser API device object
    462  * @return {GnubbyDeviceId} A device identifier for the device.
    463  */
    464 HidGnubbyDevice.deviceToDeviceId = function(dev) {
    465   var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev);
    466   var deviceId = {
    467     namespace: HidGnubbyDevice.NAMESPACE,
    468     device: hidDev.deviceId
    469   };
    470   return deviceId;
    471 };
    472 
    473 /**
    474  * Registers this implementation with gnubbies.
    475  * @param {Gnubbies} gnubbies Gnubbies registry
    476  */
    477 HidGnubbyDevice.register = function(gnubbies) {
    478   var HID_GNUBBY_IMPL = {
    479     isSharedAccess: true,
    480     enumerate: HidGnubbyDevice.enumerate,
    481     deviceToDeviceId: HidGnubbyDevice.deviceToDeviceId,
    482     open: HidGnubbyDevice.open
    483   };
    484   gnubbies.registerNamespace(HidGnubbyDevice.NAMESPACE, HID_GNUBBY_IMPL);
    485 };
    486