• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 Low level usb cruft to talk gnubby.
7 */
8
9'use strict';
10
11// Global Gnubby instance counter.
12var gnubbyId = 0;
13
14/**
15 * Creates a worker Gnubby instance.
16 * @constructor
17 * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result.
18 */
19function usbGnubby(opt_busySeconds) {
20  this.dev = null;
21  this.cid = (++gnubbyId) & 0x00ffffff;  // Pick unique channel.
22  this.rxframes = [];
23  this.synccnt = 0;
24  this.rxcb = null;
25  this.closed = false;
26  this.commandPending = false;
27  this.notifyOnClose = [];
28  this.busyMillis = (opt_busySeconds ? opt_busySeconds * 1000 : 2500);
29}
30
31/**
32 * Sets usbGnubby's Gnubbies singleton.
33 * @param {Gnubbies} gnubbies Gnubbies singleton instance
34 */
35usbGnubby.setGnubbies = function(gnubbies) {
36  /** @private {Gnubbies} */
37  usbGnubby.gnubbies_ = gnubbies;
38};
39
40/**
41 * @param {function(number, Array.<llGnubbyDeviceId>)} cb Called back with the
42 *     result of enumerating.
43 */
44usbGnubby.prototype.enumerate = function(cb) {
45  if (!cb) cb = usbGnubby.defaultCallback;
46  if (this.closed) {
47    cb(-llGnubby.NODEVICE);
48    return;
49  }
50  if (!usbGnubby.gnubbies_) {
51    cb(-llGnubby.NODEVICE);
52    return;
53  }
54
55  usbGnubby.gnubbies_.enumerate(cb);
56};
57
58/**
59 * Opens the gnubby with the given index, or the first found gnubby if no
60 * index is specified.
61 * @param {llGnubbyDeviceId} which The device to open. If null, the first
62 *     gnubby found is opened.
63 * @param {function(number)|undefined} opt_cb Called with result of opening the
64 *     gnubby.
65 */
66usbGnubby.prototype.open = function(which, opt_cb) {
67  var cb = opt_cb ? opt_cb : usbGnubby.defaultCallback;
68  if (this.closed) {
69    cb(-llGnubby.NODEVICE);
70    return;
71  }
72  this.closingWhenIdle = false;
73
74  if (document.location.href.indexOf('_generated_') == -1) {
75    // Not background page.
76    // Pick more random cid to tell things apart on the usb bus.
77    var rnd = UTIL_getRandom(2);
78    this.cid ^= (rnd[0] << 16) | (rnd[1] << 8);
79  }
80
81  var self = this;
82
83  function setCid(which) {
84    self.cid &= 0x00ffffff;
85    self.cid |= ((which.device + 1) << 24);  // For debugging.
86  }
87
88  var enumerateRetriesRemaining = 3;
89  function enumerated(rc, devs) {
90    if (!devs.length)
91      rc = -llGnubby.NODEVICE;
92    if (rc) {
93      cb(rc);
94      return;
95    }
96    which = devs[0];
97    setCid(which);
98    usbGnubby.gnubbies_.addClient(which, self, function(rc, device) {
99      if (rc == -llGnubby.NODEVICE && enumerateRetriesRemaining-- > 0) {
100        // We were trying to open the first device, but now it's not there?
101        // Do over.
102        usbGnubby.gnubbies_.enumerate(enumerated);
103        return;
104      }
105      self.dev = device;
106      cb(rc);
107    });
108  }
109
110  if (which) {
111    setCid(which);
112    usbGnubby.gnubbies_.addClient(which, self, function(rc, device) {
113      self.dev = device;
114      cb(rc);
115    });
116  } else {
117    usbGnubby.gnubbies_.enumerate(enumerated);
118  }
119};
120
121/**
122 * @return {boolean} Whether this gnubby has any command outstanding.
123 * @private
124 */
125usbGnubby.prototype.inUse_ = function() {
126  return this.commandPending;
127};
128
129/** Closes this gnubby. */
130usbGnubby.prototype.close = function() {
131  this.closed = true;
132
133  if (this.dev) {
134    console.log(UTIL_fmt('usbGnubby.close()'));
135    this.rxframes = [];
136    this.rxcb = null;
137    var dev = this.dev;
138    this.dev = null;
139    var self = this;
140    // Wait a bit in case simpleton client tries open next gnubby.
141    // Without delay, gnubbies would drop all idle devices, before client
142    // gets to the next one.
143    window.setTimeout(
144        function() {
145          usbGnubby.gnubbies_.removeClient(dev, self);
146        }, 300);
147  }
148};
149
150/**
151 * Asks this gnubby to close when it gets a chance.
152 * @param {Function=} cb called back when closed.
153 */
154usbGnubby.prototype.closeWhenIdle = function(cb) {
155  if (!this.inUse_()) {
156    this.close();
157    if (cb) cb();
158    return;
159  }
160  this.closingWhenIdle = true;
161  if (cb) this.notifyOnClose.push(cb);
162};
163
164/**
165 * Close and notify every caller that it is now closed.
166 * @private
167 */
168usbGnubby.prototype.idleClose_ = function() {
169  this.close();
170  while (this.notifyOnClose.length != 0) {
171    var cb = this.notifyOnClose.shift();
172    cb();
173  }
174};
175
176/**
177 * Notify callback for every frame received.
178 * @param {function()} cb Callback
179 * @private
180 */
181usbGnubby.prototype.notifyFrame_ = function(cb) {
182  if (this.rxframes.length != 0) {
183    // Already have frames; continue.
184    if (cb) window.setTimeout(cb, 0);
185  } else {
186    this.rxcb = cb;
187  }
188};
189
190/**
191 * Called by low level driver with a frame.
192 * @param {ArrayBuffer|Uint8Array} frame Data frame
193 * @return {boolean} Whether this client is still interested in receiving
194 *     frames from its device.
195 */
196usbGnubby.prototype.receivedFrame = function(frame) {
197  if (this.closed) return false;  // No longer interested.
198
199  if (!this.checkCID_(frame)) {
200    // Not for me, ignore.
201    return true;
202  }
203
204  this.rxframes.push(frame);
205
206  // Callback self in case we were waiting. Once.
207  var cb = this.rxcb;
208  this.rxcb = null;
209  if (cb) window.setTimeout(cb, 0);
210
211  return true;
212};
213
214/**
215 * @return {ArrayBuffer|Uint8Array} oldest received frame. Throw if none.
216 * @private
217 */
218usbGnubby.prototype.readFrame_ = function() {
219  if (this.rxframes.length == 0) throw 'rxframes empty!';
220
221  var frame = this.rxframes.shift();
222  return frame;
223};
224
225/** Poll from rxframes[].
226 * @param {number} cmd Command
227 * @param {number} timeout timeout in seconds.
228 * @param {?function(...)} cb Callback
229 * @private
230 */
231usbGnubby.prototype.read_ = function(cmd, timeout, cb) {
232  if (this.closed) { cb(-llGnubby.GONE); return; }
233  if (!this.dev) { cb(-llGnubby.GONE); return; }
234
235  var tid = null;  // timeout timer id.
236  var callback = cb;
237  var self = this;
238
239  var msg = null;
240  var seqno = 0;
241  var count = 0;
242
243  /**
244   * Schedule call to cb if not called yet.
245   * @param {number} a Return code.
246   * @param {Object=} b Optional data.
247   */
248  function schedule_cb(a, b) {
249    self.commandPending = false;
250    if (tid) {
251      // Cancel timeout timer.
252      window.clearTimeout(tid);
253      tid = null;
254    }
255    var c = callback;
256    if (c) {
257      callback = null;
258      window.setTimeout(function() { c(a, b); }, 0);
259    }
260    if (self.closingWhenIdle) self.idleClose_();
261  };
262
263  function read_timeout() {
264    if (!callback || !tid) return;  // Already done.
265
266    console.error(UTIL_fmt(
267        '[' + self.cid.toString(16) + '] timeout!'));
268
269    if (self.dev) {
270      self.dev.destroy();  // Stop pretending this thing works.
271    }
272
273    tid = null;
274
275    schedule_cb(-llGnubby.TIMEOUT);
276  };
277
278  function cont_frame() {
279    if (!callback || !tid) return;  // Already done.
280
281    var f = new Uint8Array(self.readFrame_());
282    var rcmd = f[4];
283    var totalLen = (f[5] << 8) + f[6];
284
285    if (rcmd == llGnubby.CMD_ERROR && totalLen == 1) {
286      // Error from device; forward.
287      console.log(UTIL_fmt(
288          '[' + self.cid.toString(16) + '] error frame ' +
289          UTIL_BytesToHex(f)));
290      if (f[7] == llGnubby.GONE) {
291        self.closed = true;
292      }
293      schedule_cb(-f[7]);
294      return;
295    }
296
297    if ((rcmd & 0x80)) {
298      // Not an CONT frame, ignore.
299      console.log(UTIL_fmt(
300          '[' + self.cid.toString(16) + '] ignoring non-cont frame ' +
301          UTIL_BytesToHex(f)));
302      self.notifyFrame_(cont_frame);
303      return;
304    }
305
306    var seq = (rcmd & 0x7f);
307    if (seq != seqno++) {
308      console.log(UTIL_fmt(
309          '[' + self.cid.toString(16) + '] bad cont frame ' +
310          UTIL_BytesToHex(f)));
311      schedule_cb(-llGnubby.INVALID_SEQ);
312      return;
313    }
314
315    // Copy payload.
316    for (var i = 5; i < f.length && count < msg.length; ++i) {
317      msg[count++] = f[i];
318    }
319
320    if (count == msg.length) {
321      // Done.
322      schedule_cb(-llGnubby.OK, msg.buffer);
323    } else {
324      // Need more CONT frame(s).
325      self.notifyFrame_(cont_frame);
326    }
327  }
328
329  function init_frame() {
330    if (!callback || !tid) return;  // Already done.
331
332    var f = new Uint8Array(self.readFrame_());
333
334    var rcmd = f[4];
335    var totalLen = (f[5] << 8) + f[6];
336
337    if (rcmd == llGnubby.CMD_ERROR && totalLen == 1) {
338      // Error from device; forward.
339      // Don't log busy frames, they're "normal".
340      if (f[7] != llGnubby.BUSY) {
341        console.log(UTIL_fmt(
342            '[' + self.cid.toString(16) + '] error frame ' +
343            UTIL_BytesToHex(f)));
344      }
345      if (f[7] == llGnubby.GONE) {
346        self.closed = true;
347      }
348      schedule_cb(-f[7]);
349      return;
350    }
351
352    if (!(rcmd & 0x80)) {
353      // Not an init frame, ignore.
354      console.log(UTIL_fmt(
355          '[' + self.cid.toString(16) + '] ignoring non-init frame ' +
356          UTIL_BytesToHex(f)));
357      self.notifyFrame_(init_frame);
358      return;
359    }
360
361    if (rcmd != cmd) {
362      // Not expected ack, read more.
363      console.log(UTIL_fmt(
364          '[' + self.cid.toString(16) + '] ignoring non-ack frame ' +
365          UTIL_BytesToHex(f)));
366      self.notifyFrame_(init_frame);
367      return;
368    }
369
370    // Copy payload.
371    msg = new Uint8Array(totalLen);
372    for (var i = 7; i < f.length && count < msg.length; ++i) {
373      msg[count++] = f[i];
374    }
375
376    if (count == msg.length) {
377      // Done.
378      schedule_cb(-llGnubby.OK, msg.buffer);
379    } else {
380      // Need more CONT frame(s).
381      self.notifyFrame_(cont_frame);
382    }
383  }
384
385  // Start timeout timer.
386  tid = window.setTimeout(read_timeout, 1000.0 * timeout);
387
388  // Schedule read of first frame.
389  self.notifyFrame_(init_frame);
390};
391
392/**
393 * @param {ArrayBuffer|Uint8Array} frame Data frame
394 * @return {boolean} Whether frame is for my channel.
395 * @private
396 */
397usbGnubby.prototype.checkCID_ = function(frame) {
398  var f = new Uint8Array(frame);
399  var c = (f[0] << 24) |
400          (f[1] << 16) |
401          (f[2] << 8) |
402          (f[3]);
403  return c === this.cid ||
404         c === 0;  // Generic notification.
405};
406
407/**
408 * Queue command for sending.
409 * @param {number} cmd The command to send.
410 * @param {ArrayBuffer|Uint8Array} data Command data
411 * @private
412 */
413usbGnubby.prototype.write_ = function(cmd, data) {
414  if (this.closed) return;
415  if (!this.dev) return;
416
417  this.commandPending = true;
418
419  this.dev.queueCommand(this.cid, cmd, data);
420};
421
422/**
423 * Writes the command, and calls back when the command's reply is received.
424 * @param {number} cmd The command to send.
425 * @param {ArrayBuffer|Uint8Array} data Command data
426 * @param {number} timeout Timeout in seconds.
427 * @param {function(number, ArrayBuffer=)} cb Callback
428 * @private
429 */
430usbGnubby.prototype.exchange_ = function(cmd, data, timeout, cb) {
431  var busyWait = new CountdownTimer(this.busyMillis);
432  var self = this;
433
434  function retryBusy(rc, rc_data) {
435    if (rc == -llGnubby.BUSY && !busyWait.expired()) {
436      if (usbGnubby.gnubbies_) {
437        usbGnubby.gnubbies_.resetInactivityTimer(timeout * 1000);
438      }
439      self.write_(cmd, data);
440      self.read_(cmd, timeout, retryBusy);
441    } else {
442      busyWait.clearTimeout();
443      cb(rc, rc_data);
444    }
445  }
446
447  retryBusy(-llGnubby.BUSY, undefined);  // Start work.
448};
449
450/** Default callback for commands. Simply logs to console.
451 * @param {number} rc Result status code
452 * @param {(ArrayBuffer|Uint8Array|Array.<number>|null)} data Result data
453 */
454usbGnubby.defaultCallback = function(rc, data) {
455  var msg = 'defaultCallback(' + rc;
456  if (data) {
457    if (typeof data == 'string') msg += ', ' + data;
458    else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data));
459  }
460  msg += ')';
461  console.log(UTIL_fmt(msg));
462};
463
464/** Send nonce to device, flush read queue until match.
465 * @param {?function(...)} cb Callback
466 */
467usbGnubby.prototype.sync = function(cb) {
468  if (!cb) cb = usbGnubby.defaultCallback;
469  if (this.closed) {
470    cb(-llGnubby.GONE);
471    return;
472  }
473
474  var done = false;
475  var trycount = 6;
476  var tid = null;
477  var self = this;
478
479  function callback(rc) {
480    done = true;
481    self.commandPending = false;
482    if (tid) {
483      window.clearTimeout(tid);
484      tid = null;
485    }
486    if (rc) console.warn(UTIL_fmt('sync failed: ' + rc));
487    if (cb) cb(rc);
488    if (self.closingWhenIdle) self.idleClose_();
489  }
490
491  function sendSentinel() {
492    var data = new Uint8Array(1);
493    data[0] = ++self.synccnt;
494    self.write_(llGnubby.CMD_SYNC, data.buffer);
495  }
496
497  function checkSentinel() {
498    var f = new Uint8Array(self.readFrame_());
499
500    // Device disappeared on us.
501    if (f[4] == llGnubby.CMD_ERROR &&
502        f[5] == 0 && f[6] == 1 &&
503        f[7] == llGnubby.GONE) {
504      self.closed = true;
505      callback(-llGnubby.GONE);
506      return;
507    }
508
509    // Eat everything else but expected sync reply.
510    if (f[4] != llGnubby.CMD_SYNC ||
511        (f.length > 7 && /* fw pre-0.2.1 bug: does not echo sentinel */
512         f[7] != self.synccnt)) {
513      // Read more.
514      self.notifyFrame_(checkSentinel);
515      return;
516    }
517
518    // Done.
519    callback(-llGnubby.OK);
520  };
521
522  function timeoutLoop() {
523    if (done) return;
524
525    if (trycount == 0) {
526      // Failed.
527      callback(-llGnubby.TIMEOUT);
528      return;
529    }
530
531    --trycount;  // Try another one.
532    sendSentinel();
533    self.notifyFrame_(checkSentinel);
534    tid = window.setTimeout(timeoutLoop, 500);
535  };
536
537  timeoutLoop();
538};
539
540/** Short timeout value in seconds */
541usbGnubby.SHORT_TIMEOUT = 1;
542/** Normal timeout value in seconds */
543usbGnubby.NORMAL_TIMEOUT = 3;
544// Max timeout usb firmware has for smartcard response is 30 seconds.
545// Make our application level tolerance a little longer.
546/** Maximum timeout in seconds */
547usbGnubby.MAX_TIMEOUT = 31;
548
549/** Blink led
550 * @param {number|ArrayBuffer|Uint8Array} data Command data or number
551 *     of seconds to blink
552 * @param {?function(...)} cb Callback
553 */
554usbGnubby.prototype.blink = function(data, cb) {
555  if (!cb) cb = usbGnubby.defaultCallback;
556  if (typeof data == 'number') {
557    var d = new Uint8Array([data]);
558    data = d.buffer;
559  }
560  this.exchange_(llGnubby.CMD_PROMPT, data, usbGnubby.NORMAL_TIMEOUT, cb);
561};
562
563/** Lock the gnubby
564 * @param {number|ArrayBuffer|Uint8Array} data Command data
565 * @param {?function(...)} cb Callback
566 */
567usbGnubby.prototype.lock = function(data, cb) {
568  if (!cb) cb = usbGnubby.defaultCallback;
569  if (typeof data == 'number') {
570    var d = new Uint8Array([data]);
571    data = d.buffer;
572  }
573  this.exchange_(llGnubby.CMD_LOCK, data, usbGnubby.NORMAL_TIMEOUT, cb);
574};
575
576/** Unlock the gnubby
577 * @param {?function(...)} cb Callback
578 */
579usbGnubby.prototype.unlock = function(cb) {
580  if (!cb) cb = usbGnubby.defaultCallback;
581  var data = new Uint8Array([0]);
582  this.exchange_(llGnubby.CMD_LOCK, data.buffer,
583      usbGnubby.NORMAL_TIMEOUT, cb);
584};
585
586/** Request system information data.
587 * @param {?function(...)} cb Callback
588 */
589usbGnubby.prototype.sysinfo = function(cb) {
590  if (!cb) cb = usbGnubby.defaultCallback;
591  this.exchange_(llGnubby.CMD_SYSINFO, new ArrayBuffer(0),
592      usbGnubby.NORMAL_TIMEOUT, cb);
593};
594
595/** Send wink command
596 * @param {?function(...)} cb Callback
597 */
598usbGnubby.prototype.wink = function(cb) {
599  if (!cb) cb = usbGnubby.defaultCallback;
600  this.exchange_(llGnubby.CMD_WINK, new ArrayBuffer(0),
601      usbGnubby.NORMAL_TIMEOUT, cb);
602};
603
604/** Send DFU (Device firmware upgrade) command
605 * @param {ArrayBuffer|Uint8Array} data Command data
606 * @param {?function(...)} cb Callback
607 */
608usbGnubby.prototype.dfu = function(data, cb) {
609  if (!cb) cb = usbGnubby.defaultCallback;
610  this.exchange_(llGnubby.CMD_DFU, data, usbGnubby.NORMAL_TIMEOUT, cb);
611};
612
613/** Ping the gnubby
614 * @param {number|ArrayBuffer|Uint8Array} data Command data
615 * @param {?function(...)} cb Callback
616 */
617usbGnubby.prototype.ping = function(data, cb) {
618  if (!cb) cb = usbGnubby.defaultCallback;
619  if (typeof data == 'number') {
620    var d = new Uint8Array(data);
621    window.crypto.getRandomValues(d);
622    data = d.buffer;
623  }
624  this.exchange_(llGnubby.CMD_PING, data, usbGnubby.NORMAL_TIMEOUT, cb);
625};
626
627/** Send a raw APDU command
628 * @param {ArrayBuffer|Uint8Array} data Command data
629 * @param {?function(...)} cb Callback
630 */
631usbGnubby.prototype.apdu = function(data, cb) {
632  if (!cb) cb = usbGnubby.defaultCallback;
633  this.exchange_(llGnubby.CMD_APDU, data, usbGnubby.MAX_TIMEOUT, cb);
634};
635
636/** Reset gnubby
637 * @param {?function(...)} cb Callback
638 */
639usbGnubby.prototype.reset = function(cb) {
640  if (!cb) cb = usbGnubby.defaultCallback;
641  this.exchange_(llGnubby.CMD_ATR, new ArrayBuffer(0),
642      usbGnubby.NORMAL_TIMEOUT, cb);
643};
644
645// byte args[3] = [delay-in-ms before disabling interrupts,
646//                 delay-in-ms before disabling usb (aka remove),
647//                 delay-in-ms before reboot (aka insert)]
648/** Send usb test command
649 * @param {ArrayBuffer|Uint8Array} args Command data
650 * @param {?function(...)} cb Callback
651 */
652usbGnubby.prototype.usb_test = function(args, cb) {
653  if (!cb) cb = usbGnubby.defaultCallback;
654  var u8 = new Uint8Array(args);
655  this.exchange_(llGnubby.CMD_USB_TEST, u8.buffer,
656      usbGnubby.NORMAL_TIMEOUT, cb);
657};
658
659/** APDU command with reply
660 * @param {ArrayBuffer|Uint8Array} request The request
661 * @param {?function(...)} cb Callback
662 * @param {boolean=} opt_nowink Do not wink
663 * @private
664 */
665usbGnubby.prototype.apduReply_ = function(request, cb, opt_nowink) {
666  if (!cb) cb = usbGnubby.defaultCallback;
667  var self = this;
668
669  this.apdu(request, function(rc, data) {
670    if (rc == 0) {
671      var r8 = new Uint8Array(data);
672      if (r8[r8.length - 2] == 0x90 && r8[r8.length - 1] == 0x00) {
673        // strip trailing 9000
674        var buf = new Uint8Array(r8.subarray(0, r8.length - 2));
675        cb(-llGnubby.OK, buf.buffer);
676        return;
677      } else {
678        // return non-9000 as rc
679        rc = r8[r8.length - 2] * 256 + r8[r8.length - 1];
680        // wink gnubby at hand if it needs touching.
681        if (rc == 0x6985 && !opt_nowink) {
682          self.wink(function() { cb(rc); });
683          return;
684        }
685      }
686    }
687    // Warn on errors other than waiting for touch, wrong data, and
688    // unrecognized command.
689    if (rc != 0x6985 && rc != 0x6a80 && rc != 0x6d00) {
690      console.warn(UTIL_fmt('apduReply_ fail: ' + rc.toString(16)));
691    }
692    cb(rc);
693  });
694};
695