Elim cr-checkbox
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / usbgnubbydevice.js
blob723d1252f239534abda5458a6d3fc0fee2cf31a1
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.
5 /**
6  * @fileoverview Implements a low-level gnubby driver based on chrome.usb.
7  */
8 'use strict';
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;
39 /**
40  * Namespace for the UsbGnubbyDevice implementation.
41  * @const
42  */
43 UsbGnubbyDevice.NAMESPACE = 'usb';
45 /** Destroys this low-level device instance. */
46 UsbGnubbyDevice.prototype.destroy = function() {
47   if (!this.dev) return;  // Already dead.
49   this.gnubbies_.removeOpenDevice(
50       {namespace: UsbGnubbyDevice.NAMESPACE, device: this.id});
51   this.closing = true;
53   console.log(UTIL_fmt('UsbGnubbyDevice.destroy()'));
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);
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   }
71   if (this.lockTID) {
72     window.clearTimeout(this.lockTID);
73     this.lockTID = null;
74   }
76   var dev = this.dev;
77   this.dev = null;
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(UTIL_fmt(chrome.runtime.lastError.message));
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(UTIL_fmt(chrome.runtime.lastError.message));
92         return;
93       }
94       console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
95     });
96   });
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;
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           '[' + Gnubby.hexCid(client.cid) + '] left?'));
118     }
119   }
120   if (changes) this.clients = remaining;
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;
131   return true;
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.
142   var self = this;
144   function inTransferComplete(x) {
145     self.inTransferPending = false;
147     if (!self.readyToUse_()) return;  // No point in continuing.
149     if (chrome.runtime.lastError) {
150       console.warn(UTIL_fmt('in bulkTransfer got lastError: '));
151       console.warn(UTIL_fmt(chrome.runtime.lastError.message));
152       window.setTimeout(function() { self.destroy(); }, 0);
153       return;
154     }
156     if (x.data) {
157       var u8 = new Uint8Array(x.data);
158       console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
160       self.publishFrame_(x.data);
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   }
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   }
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);
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;
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;
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.
236   if (this.txqueue.length == 0) return;  // Nothing to send.
238   var frame = this.txqueue[0];
240   var self = this;
241   function OutTransferComplete(x) {
242     self.outTransferPending = false;
244     if (!self.readyToUse_()) return;  // No point in continuing.
246     if (chrome.runtime.lastError) {
247       console.warn(UTIL_fmt('out bulkTransfer lastError: '));
248       console.warn(UTIL_fmt(chrome.runtime.lastError.message));
249       window.setTimeout(function() { self.destroy(); }, 0);
250       return;
251     }
253     window.setTimeout(function() { self.readOneReply_(); }, 0);
254   };
256   var u8 = new Uint8Array(frame);
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   }
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   }
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.
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       }
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;
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     }
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     }
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 ' + Gnubby.hexCid(cid) + ' expired!'));
350             self.lockTID = null;
351             self.lockCID = 0;
352           },
353           this.lockMillis);
354     }
355   }
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;
369   var u8 = new Uint8Array(data);
370   var frame = new Uint8Array(u8.length + 7);
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);
380   frame.set(u8, 7);
382   var lockArg = (u8.length > 0) ? u8[0] : 0;
383   this.updateLock_(cid, cmd, lockArg);
385   var wasEmpty = (this.txqueue.length == 0);
386   this.txqueue.push(frame.buffer);
387   if (wasEmpty) this.writeOneRequest_();
391  * @const
392  */
393 UsbGnubbyDevice.WINUSB_VID_PIDS = [
394   {'vendorId': 4176, 'productId': 529}  // Yubico WinUSB
398  * @param {function(Array)} cb Enumerate callback
399  */
400 UsbGnubbyDevice.enumerate = function(cb) {
401   var numEnumerated = 0;
402   var allDevs = [];
404   function enumerated(devs) {
405     allDevs = allDevs.concat(devs);
406     if (++numEnumerated == UsbGnubbyDevice.WINUSB_VID_PIDS.length) {
407       cb(allDevs);
408     }
409   }
411   for (var i = 0; i < UsbGnubbyDevice.WINUSB_VID_PIDS.length; i++) {
412     chrome.usb.getDevices(UsbGnubbyDevice.WINUSB_VID_PIDS[i], enumerated);
413   }
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;
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;
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('openDevice got lastError:'));
458       console.warn(UTIL_fmt(chrome.runtime.lastError.message));
459       console.warn(UTIL_fmt('failed to open device. permissions issue?'));
460       cb(-GnubbyDevice.NODEVICE);
461       return;
462     }
463     var nonNullHandle = /** @type {!chrome.usb.ConnectionHandle} */ (handle);
464     chrome.usb.listInterfaces(nonNullHandle, function(descriptors) {
465       var inEndpoint, outEndpoint;
466       for (var i = 0; i < descriptors.length; i++) {
467         var descriptor = /** @type {InterfaceDescriptor} */ (descriptors[i]);
468         for (var j = 0; j < descriptor.endpoints.length; j++) {
469           var endpoint = descriptor.endpoints[j];
470           if (inEndpoint == undefined && endpoint.type == 'bulk' &&
471               endpoint.direction == 'in') {
472             inEndpoint = endpoint.address;
473           }
474           if (outEndpoint == undefined && endpoint.type == 'bulk' &&
475               endpoint.direction == 'out') {
476             outEndpoint = endpoint.address;
477           }
478         }
479       }
480       if (inEndpoint == undefined || outEndpoint == undefined) {
481         console.warn(UTIL_fmt('device lacking an endpoint (broken?)'));
482         chrome.usb.closeDevice(nonNullHandle);
483         cb(-GnubbyDevice.NODEVICE);
484         return;
485       }
486       // Try getting it claimed now.
487       chrome.usb.claimInterface(nonNullHandle, 0, function() {
488         if (chrome.runtime.lastError) {
489           console.warn(UTIL_fmt('lastError: ' + chrome.runtime.lastError));
490           console.log(chrome.runtime.lastError);
491         }
492         var claimed = !chrome.runtime.lastError;
493         if (!claimed) {
494           console.warn(UTIL_fmt('failed to claim interface. busy?'));
495           // Claim failed? Let the callers know and bail out.
496           chrome.usb.closeDevice(nonNullHandle);
497           cb(-GnubbyDevice.BUSY);
498           return;
499         }
500         var gnubby = new UsbGnubbyDevice(gnubbies, nonNullHandle, which,
501             inEndpoint, outEndpoint);
502         cb(-GnubbyDevice.OK, gnubby);
503       });
504     });
505   }
507   if (UsbGnubbyDevice.runningOnCrOS === undefined) {
508     UsbGnubbyDevice.runningOnCrOS =
509         (window.navigator.appVersion.indexOf('; CrOS ') != -1);
510   }
511   if (UsbGnubbyDevice.runningOnCrOS) {
512     chrome.usb.requestAccess(dev, 0, function(success) {
513       // Even though the argument to requestAccess is a chrome.usb.Device, the
514       // access request is for access to all devices with the same vid/pid.
515       // Curiously, if the first chrome.usb.requestAccess succeeds, a second
516       // call with a separate device with the same vid/pid fails. Since
517       // chrome.usb.openDevice will fail if a previous access request really
518       // failed, just ignore the outcome of the access request and move along.
519       chrome.usb.openDevice(dev, deviceOpened);
520     });
521   } else {
522     chrome.usb.openDevice(dev, deviceOpened);
523   }
527  * @param {*} dev Chrome usb device
528  * @return {GnubbyDeviceId} A device identifier for the device.
529  */
530 UsbGnubbyDevice.deviceToDeviceId = function(dev) {
531   var usbDev = /** @type {!chrome.usb.Device} */ (dev);
532   var deviceId = {
533     namespace: UsbGnubbyDevice.NAMESPACE,
534     device: usbDev.device
535   };
536   return deviceId;
540  * Registers this implementation with gnubbies.
541  * @param {Gnubbies} gnubbies Gnubbies singleton instance
542  */
543 UsbGnubbyDevice.register = function(gnubbies) {
544   var USB_GNUBBY_IMPL = {
545     isSharedAccess: false,
546     enumerate: UsbGnubbyDevice.enumerate,
547     deviceToDeviceId: UsbGnubbyDevice.deviceToDeviceId,
548     open: UsbGnubbyDevice.open
549   };
550   gnubbies.registerNamespace(UsbGnubbyDevice.NAMESPACE, USB_GNUBBY_IMPL);