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 {llGnubby}
     20  */
     21 function llUsbGnubby(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 llUsbGnubby implementation.
     41  * @const
     42  */
     43 llUsbGnubby.NAMESPACE = 'usb';
     44 
     45 /** Destroys this low-level device instance. */
     46 llUsbGnubby.prototype.destroy = function() {
     47   if (!this.dev) return;  // Already dead.
     48 
     49   this.gnubbies_.removeOpenDevice(
     50       {namespace: llUsbGnubby.NAMESPACE, device: this.id});
     51   this.closing = true;
     52 
     53   console.log(UTIL_fmt('llUsbGnubby.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         llGnubby.CMD_ERROR,
     62         0, 1,  // length
     63         llGnubby.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     console.log(UTIL_fmt('Device ' + dev.handle + ' released'));
     81     chrome.usb.closeDevice(dev, function() {
     82       console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
     83     });
     84   });
     85 };
     86 
     87 /**
     88  * Push frame to all clients.
     89  * @param {ArrayBuffer} f Data frame
     90  * @private
     91  */
     92 llUsbGnubby.prototype.publishFrame_ = function(f) {
     93   var old = this.clients;
     94 
     95   var remaining = [];
     96   var changes = false;
     97   for (var i = 0; i < old.length; ++i) {
     98     var client = old[i];
     99     if (client.receivedFrame(f)) {
    100       // Client still alive; keep on list.
    101       remaining.push(client);
    102     } else {
    103       changes = true;
    104       console.log(UTIL_fmt(
    105           '[' + client.cid.toString(16) + '] left?'));
    106     }
    107   }
    108   if (changes) this.clients = remaining;
    109 };
    110 
    111 /**
    112  * @return {boolean} whether this device is open and ready to use.
    113  * @private
    114  */
    115 llUsbGnubby.prototype.readyToUse_ = function() {
    116   if (this.closing) return false;
    117   if (!this.dev) return false;
    118 
    119   return true;
    120 };
    121 
    122 /**
    123  * Reads one reply from the low-level device.
    124  * @private
    125  */
    126 llUsbGnubby.prototype.readOneReply_ = function() {
    127   if (!this.readyToUse_()) return;  // No point in continuing.
    128   if (this.updating) return;  // Do not bother waiting for final update reply.
    129 
    130   var self = this;
    131 
    132   function inTransferComplete(x) {
    133     self.inTransferPending = false;
    134 
    135     if (!self.readyToUse_()) return;  // No point in continuing.
    136 
    137     if (chrome.runtime.lastError) {
    138       console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
    139       console.log(chrome.runtime.lastError);
    140       window.setTimeout(function() { self.destroy(); }, 0);
    141       return;
    142     }
    143 
    144     if (x.data) {
    145       var u8 = new Uint8Array(x.data);
    146       console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
    147 
    148       self.publishFrame_(x.data);
    149 
    150       // Write another pending request, if any.
    151       window.setTimeout(
    152           function() {
    153             self.txqueue.shift();  // Drop sent frame from queue.
    154             self.writeOneRequest_();
    155           },
    156           0);
    157     } else {
    158       console.log(UTIL_fmt('no x.data!'));
    159       console.log(x);
    160       window.setTimeout(function() { self.destroy(); }, 0);
    161     }
    162   }
    163 
    164   if (this.inTransferPending == false) {
    165     this.inTransferPending = true;
    166     chrome.usb.bulkTransfer(
    167       /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
    168       { direction: 'in', endpoint: this.inEndpoint, length: 2048 },
    169       inTransferComplete);
    170   } else {
    171     throw 'inTransferPending!';
    172   }
    173 };
    174 
    175 /**
    176  * Register a client for this gnubby.
    177  * @param {*} who The client.
    178  */
    179 llUsbGnubby.prototype.registerClient = function(who) {
    180   for (var i = 0; i < this.clients.length; ++i) {
    181     if (this.clients[i] === who) return;  // Already registered.
    182   }
    183   this.clients.push(who);
    184 };
    185 
    186 /**
    187  * De-register a client.
    188  * @param {*} who The client.
    189  * @return {number} The number of remaining listeners for this device, or -1
    190  * Returns number of remaining listeners for this device.
    191  *     if this had no clients to start with.
    192  */
    193 llUsbGnubby.prototype.deregisterClient = function(who) {
    194   var current = this.clients;
    195   if (current.length == 0) return -1;
    196   this.clients = [];
    197   for (var i = 0; i < current.length; ++i) {
    198     var client = current[i];
    199     if (client !== who) this.clients.push(client);
    200   }
    201   return this.clients.length;
    202 };
    203 
    204 /**
    205  * @param {*} who The client.
    206  * @return {boolean} Whether this device has who as a client.
    207  */
    208 llUsbGnubby.prototype.hasClient = function(who) {
    209   if (this.clients.length == 0) return false;
    210   for (var i = 0; i < this.clients.length; ++i) {
    211     if (who === this.clients[i])
    212       return true;
    213   }
    214   return false;
    215 };
    216 
    217 /**
    218  * Stuff queued frames from txqueue[] to device, one by one.
    219  * @private
    220  */
    221 llUsbGnubby.prototype.writeOneRequest_ = function() {
    222   if (!this.readyToUse_()) return;  // No point in continuing.
    223 
    224   if (this.txqueue.length == 0) return;  // Nothing to send.
    225 
    226   var frame = this.txqueue[0];
    227 
    228   var self = this;
    229   function OutTransferComplete(x) {
    230     self.outTransferPending = false;
    231 
    232     if (!self.readyToUse_()) return;  // No point in continuing.
    233 
    234     if (chrome.runtime.lastError) {
    235       console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
    236       console.log(chrome.runtime.lastError);
    237       window.setTimeout(function() { self.destroy(); }, 0);
    238       return;
    239     }
    240 
    241     window.setTimeout(function() { self.readOneReply_(); }, 0);
    242   };
    243 
    244   var u8 = new Uint8Array(frame);
    245   console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
    246 
    247   if (this.outTransferPending == false) {
    248     this.outTransferPending = true;
    249     chrome.usb.bulkTransfer(
    250         /** @type {!chrome.usb.ConnectionHandle} */(this.dev),
    251         { direction: 'out', endpoint: this.outEndpoint, data: frame },
    252         OutTransferComplete);
    253   } else {
    254     throw 'outTransferPending!';
    255   }
    256 };
    257 
    258 /**
    259  * Check whether channel is locked for this request or not.
    260  * @param {number} cid Channel id
    261  * @param {number} cmd Command to be sent
    262  * @return {boolean} true if not locked for this request.
    263  * @private
    264  */
    265 llUsbGnubby.prototype.checkLock_ = function(cid, cmd) {
    266   if (this.lockCID) {
    267     // We have an active lock.
    268     if (this.lockCID != cid) {
    269       // Some other channel has active lock.
    270 
    271       if (cmd != llGnubby.CMD_SYNC) {
    272         // Anything but SYNC gets an immediate busy.
    273         var busy = new Uint8Array(
    274             [(cid >> 24) & 255,
    275              (cid >> 16) & 255,
    276              (cid >> 8) & 255,
    277              cid & 255,
    278              llGnubby.CMD_ERROR,
    279              0, 1,  // length
    280              llGnubby.BUSY]);
    281         // Log the synthetic busy too.
    282         console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
    283         this.publishFrame_(busy.buffer);
    284         return false;
    285       }
    286 
    287       // SYNC gets to go to the device to flush OS tx/rx queues.
    288       // The usb firmware always responds to SYNC, regardless of lock status.
    289     }
    290   }
    291   return true;
    292 };
    293 
    294 /**
    295  * Update or grab lock.
    296  * @param {number} cid Channel id
    297  * @param {number} cmd Command
    298  * @param {number} arg Command argument
    299  * @private
    300  */
    301 llUsbGnubby.prototype.updateLock_ = function(cid, cmd, arg) {
    302   if (this.lockCID == 0 || this.lockCID == cid) {
    303     // It is this caller's or nobody's lock.
    304     if (this.lockTID) {
    305       window.clearTimeout(this.lockTID);
    306       this.lockTID = null;
    307     }
    308 
    309     if (cmd == llGnubby.CMD_LOCK) {
    310       var nseconds = arg;
    311       if (nseconds != 0) {
    312         this.lockCID = cid;
    313         // Set tracking time to be .1 seconds longer than usb device does.
    314         this.lockMillis = nseconds * 1000 + 100;
    315       } else {
    316         // Releasing lock voluntarily.
    317         this.lockCID = 0;
    318       }
    319     }
    320 
    321     // (re)set the lock timeout if we still hold it.
    322     if (this.lockCID) {
    323       var self = this;
    324       this.lockTID = window.setTimeout(
    325           function() {
    326             console.warn(UTIL_fmt(
    327                 'lock for CID ' + cid.toString(16) + ' expired!'));
    328             self.lockTID = null;
    329             self.lockCID = 0;
    330           },
    331           this.lockMillis);
    332     }
    333   }
    334 };
    335 
    336 /**
    337  * Queue command to be sent.
    338  * If queue was empty, initiate the write.
    339  * @param {number} cid The client's channel ID.
    340  * @param {number} cmd The command to send.
    341  * @param {ArrayBuffer|Uint8Array} data Command argument data
    342  */
    343 llUsbGnubby.prototype.queueCommand = function(cid, cmd, data) {
    344   if (!this.dev) return;
    345   if (!this.checkLock_(cid, cmd)) return;
    346 
    347   var u8 = new Uint8Array(data);
    348   var frame = new Uint8Array(u8.length + 7);
    349 
    350   frame[0] = cid >>> 24;
    351   frame[1] = cid >>> 16;
    352   frame[2] = cid >>> 8;
    353   frame[3] = cid;
    354   frame[4] = cmd;
    355   frame[5] = (u8.length >> 8);
    356   frame[6] = (u8.length & 255);
    357 
    358   frame.set(u8, 7);
    359 
    360   var lockArg = (u8.length > 0) ? u8[0] : 0;
    361   this.updateLock_(cid, cmd, lockArg);
    362 
    363   var wasEmpty = (this.txqueue.length == 0);
    364   this.txqueue.push(frame.buffer);
    365   if (wasEmpty) this.writeOneRequest_();
    366 };
    367 
    368 /**
    369  * @param {function(Array)} cb Enumerate callback
    370  */
    371 llUsbGnubby.enumerate = function(cb) {
    372   chrome.usb.getDevices({'vendorId': 4176, 'productId': 529}, cb);
    373 };
    374 
    375 /**
    376  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
    377  *     in.
    378  * @param {number} which The index of the device to open.
    379  * @param {!chrome.usb.Device} dev The device to open.
    380  * @param {function(number, llGnubby=)} cb Called back with the
    381  *     result of opening the device.
    382  */
    383 llUsbGnubby.open = function(gnubbies, which, dev, cb) {
    384   /** @param {chrome.usb.ConnectionHandle=} handle Connection handle */
    385   function deviceOpened(handle) {
    386     if (!handle) {
    387       console.warn(UTIL_fmt('failed to open device. permissions issue?'));
    388       cb(-llGnubby.NODEVICE);
    389       return;
    390     }
    391     var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle);
    392     chrome.usb.listInterfaces(nonNullHandle, function(descriptors) {
    393       var inEndpoint, outEndpoint;
    394       for (var i = 0; i < descriptors.length; i++) {
    395         var descriptor = descriptors[i];
    396         for (var j = 0; j < descriptor.endpoints.length; j++) {
    397           var endpoint = descriptor.endpoints[j];
    398           if (inEndpoint == undefined && endpoint.type == 'bulk' &&
    399               endpoint.direction == 'in') {
    400             inEndpoint = endpoint.address;
    401           }
    402           if (outEndpoint == undefined && endpoint.type == 'bulk' &&
    403               endpoint.direction == 'out') {
    404             outEndpoint = endpoint.address;
    405           }
    406         }
    407       }
    408       if (inEndpoint == undefined || outEndpoint == undefined) {
    409         console.warn(UTIL_fmt('device lacking an endpoint (broken?)'));
    410         chrome.usb.closeDevice(nonNullHandle);
    411         cb(-llGnubby.NODEVICE);
    412         return;
    413       }
    414       // Try getting it claimed now.
    415       chrome.usb.claimInterface(nonNullHandle, 0, function() {
    416         if (chrome.runtime.lastError) {
    417           console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
    418           console.log(chrome.runtime.lastError);
    419         }
    420         var claimed = !chrome.runtime.lastError;
    421         if (!claimed) {
    422           console.warn(UTIL_fmt('failed to claim interface. busy?'));
    423           // Claim failed? Let the callers know and bail out.
    424           chrome.usb.closeDevice(nonNullHandle);
    425           cb(-llGnubby.BUSY);
    426           return;
    427         }
    428         var gnubby = new llUsbGnubby(gnubbies, nonNullHandle, which, inEndpoint,
    429             outEndpoint);
    430         cb(-llGnubby.OK, gnubby);
    431       });
    432     });
    433   }
    434 
    435   if (llUsbGnubby.runningOnCrOS === undefined) {
    436     llUsbGnubby.runningOnCrOS =
    437         (window.navigator.appVersion.indexOf('; CrOS ') != -1);
    438   }
    439   if (llUsbGnubby.runningOnCrOS) {
    440     chrome.usb.requestAccess(dev, 0, function(success) {
    441       // Even though the argument to requestAccess is a chrome.usb.Device, the
    442       // access request is for access to all devices with the same vid/pid.
    443       // Curiously, if the first chrome.usb.requestAccess succeeds, a second
    444       // call with a separate device with the same vid/pid fails. Since
    445       // chrome.usb.openDevice will fail if a previous access request really
    446       // failed, just ignore the outcome of the access request and move along.
    447       chrome.usb.openDevice(dev, deviceOpened);
    448     });
    449   } else {
    450     chrome.usb.openDevice(dev, deviceOpened);
    451   }
    452 };
    453 
    454 /**
    455  * @param {*} dev Chrome usb device
    456  * @return {llGnubbyDeviceId} A device identifier for the device.
    457  */
    458 llUsbGnubby.deviceToDeviceId = function(dev) {
    459   var usbDev = /** @type {!chrome.usb.Device} */ (dev);
    460   var deviceId = { namespace: llUsbGnubby.NAMESPACE, device: usbDev.device };
    461   return deviceId;
    462 };
    463 
    464 /**
    465  * Registers this implementation with gnubbies.
    466  * @param {Gnubbies} gnubbies Gnubbies singleton instance
    467  */
    468 llUsbGnubby.register = function(gnubbies) {
    469   var USB_GNUBBY_IMPL = {
    470     enumerate: llUsbGnubby.enumerate,
    471     deviceToDeviceId: llUsbGnubby.deviceToDeviceId,
    472     open: llUsbGnubby.open
    473   };
    474   gnubbies.registerNamespace(llUsbGnubby.NAMESPACE, USB_GNUBBY_IMPL);
    475 };
    476