Add ICU message format support
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / hidgnubbydevice.js
blob331402363d97fdd109d7c7571c388aef42918e5c
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.hid.
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.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.
33 /**
34  * Namespace for the HidGnubbyDevice implementation.
35  * @const
36  */
37 HidGnubbyDevice.NAMESPACE = 'hid';
39 /** Destroys this low-level device instance. */
40 HidGnubbyDevice.prototype.destroy = function() {
41   if (!this.dev) return;  // Already dead.
43   this.gnubbies_.removeOpenDevice(
44       {namespace: HidGnubbyDevice.NAMESPACE, device: this.id});
45   this.closing = true;
47   console.log(UTIL_fmt('HidGnubbyDevice.destroy()'));
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);
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   }
65   if (this.lockTID) {
66     window.clearTimeout(this.lockTID);
67     this.lockTID = null;
68   }
70   var dev = this.dev;
71   this.dev = null;
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(UTIL_fmt(chrome.runtime.lastError.message));
78       return;
79     }
80     console.log(UTIL_fmt('Device ' + dev.connectionId + ' closed'));
81   });
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;
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           '[' + Gnubby.hexCid(client.cid) + '] left?'));
103     }
104   }
105   if (changes) this.clients = remaining;
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   }
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;
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;
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;
162   if (this.closing) {
163     this.destroy();
164     return;
165   }
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   }
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   }
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('receive got lastError:'));
194         console.log(UTIL_fmt(chrome.runtime.lastError.message));
195         window.setTimeout(function() { self.destroy(); }, 0);
196         return;
197       }
198       var u8 = new Uint8Array(data);
199       console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
201       self.publishFrame_(data);
203       // Read more.
204       window.setTimeout(function() { self.readLoop_(); }, 0);
205     }
206   );
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.
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       }
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;
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     }
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     }
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 ' + Gnubby.hexCid(cid) + ' expired!'));
281             self.lockTID = null;
282             self.lockCID = 0;
283           },
284           this.lockMillis);
285     }
286   }
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;
300   var u8 = new Uint8Array(data);
301   var f = new Uint8Array(64);
303   HidGnubbyDevice.setCid_(f, cid);
304   f[4] = cmd;
305   f[5] = (u8.length >> 8);
306   f[6] = (u8.length & 255);
308   var lockArg = (u8.length > 0) ? u8[0] : 0;
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);
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   }
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;
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_();
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.
365   if (this.txqueue.length == 0) return;  // Done with current queue.
367   var frame = this.txqueue[0];
369   var self = this;
370   function transferComplete() {
371     if (chrome.runtime.lastError) {
372       console.log(UTIL_fmt('send got lastError:'));
373       console.log(UTIL_fmt(chrome.runtime.lastError.message));
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   };
383   var u8 = new Uint8Array(frame);
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   }
394   var u8f = new Uint8Array(64);
395   for (var i = 0; i < u8.length; ++i) {
396     u8f[i] = u8[i];
397   }
399   chrome.hid.send(
400       this.dev.connectionId,
401       0,  // report Id. Must be 0 for our use.
402       u8f.buffer,
403       transferComplete
404   );
408  * List of legacy HID devices that do not support the F1D0 usage page as
409  * mandated by the spec, but still need to be supported.
410  * TODO: remove when these devices no longer need to be supported.
411  * @const
412  */
413 HidGnubbyDevice.HID_VID_PIDS = [
414   {'vendorId': 4176, 'productId': 512}  // Google-specific Yubico HID
418  * @param {function(Array)} cb Enumeration callback
419  */
420 HidGnubbyDevice.enumerate = function(cb) {
421   /**
422    * One pass using getDevices, and one for each of the hardcoded vid/pids.
423    * @const
424    */
425   var ENUMERATE_PASSES = 1 + HidGnubbyDevice.HID_VID_PIDS.length;
426   var numEnumerated = 0;
427   var allDevs = [];
429   function enumerated(devs) {
430     // Don't double-add a device, it'll just confuse things.
431     for (var i = 0; i < devs.length; i++) {
432       var dev = devs[i];
433       // Unfortunately indexOf is not usable, since the two calls produce
434       // different objects. Compare their deviceIds instead.
435       var found = false;
436       for (var j = 0; j < allDevs.length; j++) {
437         if (allDevs[j].deviceId == dev.deviceId) {
438           found = true;
439           break;
440         }
441       }
442       if (!found) {
443         allDevs.push(dev);
444       }
445     }
446     if (++numEnumerated == ENUMERATE_PASSES) {
447       cb(allDevs);
448     }
449   }
451   // Pass 1: usagePage-based enumeration.
452   chrome.hid.getDevices({filters: [{usagePage: 0xf1d0}]}, enumerated);
453   // Pass 2: vid/pid-based enumeration, for legacy devices.
454   for (var i = 0; i < HidGnubbyDevice.HID_VID_PIDS.length; i++) {
455     var dev = HidGnubbyDevice.HID_VID_PIDS[i];
456     chrome.hid.getDevices({filters: [dev]}, enumerated);
457   }
461  * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
462  *     in.
463  * @param {number} which The index of the device to open.
464  * @param {!chrome.hid.HidDeviceInfo} dev The device to open.
465  * @param {function(number, GnubbyDevice=)} cb Called back with the
466  *     result of opening the device.
467  */
468 HidGnubbyDevice.open = function(gnubbies, which, dev, cb) {
469   chrome.hid.connect(dev.deviceId, function(handle) {
470     if (chrome.runtime.lastError) {
471       console.log(UTIL_fmt('connect got lastError:'));
472       console.log(UTIL_fmt(chrome.runtime.lastError.message));
473     }
474     if (!handle) {
475       console.warn(UTIL_fmt('failed to connect device. permissions issue?'));
476       cb(-GnubbyDevice.NODEVICE);
477       return;
478     }
479     var nonNullHandle = /** @type {!chrome.hid.HidConnectInfo} */ (handle);
480     var gnubby = new HidGnubbyDevice(gnubbies, nonNullHandle, which);
481     cb(-GnubbyDevice.OK, gnubby);
482   });
486  * @param {*} dev A browser API device object
487  * @return {GnubbyDeviceId} A device identifier for the device.
488  */
489 HidGnubbyDevice.deviceToDeviceId = function(dev) {
490   var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev);
491   var deviceId = {
492     namespace: HidGnubbyDevice.NAMESPACE,
493     device: hidDev.deviceId
494   };
495   return deviceId;
499  * Registers this implementation with gnubbies.
500  * @param {Gnubbies} gnubbies Gnubbies registry
501  */
502 HidGnubbyDevice.register = function(gnubbies) {
503   var HID_GNUBBY_IMPL = {
504     isSharedAccess: true,
505     enumerate: HidGnubbyDevice.enumerate,
506     deviceToDeviceId: HidGnubbyDevice.deviceToDeviceId,
507     open: HidGnubbyDevice.open
508   };
509   gnubbies.registerNamespace(HidGnubbyDevice.NAMESPACE, HID_GNUBBY_IMPL);