[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / resources / cryptotoken / gnubby.js
blob27f8c86aa8352996699b1f0ffe954d0700128525
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 Low level usb cruft to talk gnubby.
7 */
9 'use strict';
11 // Global Gnubby instance counter.
12 var gnubbyId = 0;
14 /**
15 * Creates a worker Gnubby instance.
16 * @constructor
17 * @param {number=} opt_busySeconds to retry an exchange upon a BUSY result.
19 function 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);
31 /**
32 * Sets usbGnubby's Gnubbies singleton.
33 * @param {Gnubbies} gnubbies Gnubbies singleton instance
35 usbGnubby.setGnubbies = function(gnubbies) {
36 /** @private {Gnubbies} */
37 usbGnubby.gnubbies_ = gnubbies;
40 /**
41 * @param {function(number, Array.<llGnubbyDeviceId>)} cb Called back with the
42 * result of enumerating.
44 usbGnubby.prototype.enumerate = function(cb) {
45 if (!cb) cb = usbGnubby.defaultCallback;
46 if (this.closed) {
47 cb(-llGnubby.GONE);
48 return;
50 if (!usbGnubby.gnubbies_) {
51 cb(-llGnubby.NODEVICE);
52 return;
55 usbGnubby.gnubbies_.enumerate(cb);
58 /**
59 * Opens the gnubby with the given index, or the first found gnubby if no
60 * index is specified.
61 * @param {llGnubbyDeviceId|undefined} opt_which The device to open.
62 * @param {function(number)|undefined} opt_cb Called with result of opening the
63 * gnubby.
65 usbGnubby.prototype.open = function(opt_which, opt_cb) {
66 var cb = opt_cb ? opt_cb : usbGnubby.defaultCallback;
67 if (this.closed) {
68 cb(-llGnubby.GONE);
69 return;
71 this.closingWhenIdle = false;
73 if (document.location.href.indexOf('_generated_') == -1) {
74 // Not background page.
75 // Pick more random cid to tell things apart on the usb bus.
76 var rnd = UTIL_getRandom(2);
77 this.cid ^= (rnd[0] << 16) | (rnd[1] << 8);
80 var self = this;
81 function addSelfAsClient(which) {
82 self.cid &= 0x00ffffff;
83 self.cid |= ((which.device + 1) << 24); // For debugging.
85 usbGnubby.gnubbies_.addClient(which, self, function(rc, device) {
86 self.dev = device;
87 cb(rc);
88 });
91 if (!usbGnubby.gnubbies_) {
92 cb(-llGnubby.NODEVICE);
93 return;
95 if (opt_which) {
96 addSelfAsClient(opt_which);
97 } else {
98 usbGnubby.gnubbies_.enumerate(function(rc, devs) {
99 if (rc || !devs.length) {
100 cb(-llGnubby.NODEVICE);
101 return;
103 addSelfAsClient(devs[0]);
109 * @return {boolean} Whether this gnubby has any command outstanding.
110 * @private
112 usbGnubby.prototype.inUse_ = function() {
113 return this.commandPending;
116 /** Closes this gnubby. */
117 usbGnubby.prototype.close = function() {
118 this.closed = true;
120 if (this.dev) {
121 console.log(UTIL_fmt('usbGnubby.close()'));
122 this.rxframes = [];
123 this.rxcb = null;
124 var dev = this.dev;
125 this.dev = null;
126 var self = this;
127 // Wait a bit in case simpleton client tries open next gnubby.
128 // Without delay, gnubbies would drop all idle devices, before client
129 // gets to the next one.
130 window.setTimeout(
131 function() {
132 usbGnubby.gnubbies_.removeClient(dev, self);
133 }, 300);
138 * Asks this gnubby to close when it gets a chance.
139 * @param {Function=} cb called back when closed.
141 usbGnubby.prototype.closeWhenIdle = function(cb) {
142 if (!this.inUse_()) {
143 this.close();
144 if (cb) cb();
145 return;
147 this.closingWhenIdle = true;
148 if (cb) this.notifyOnClose.push(cb);
152 * Close and notify every caller that it is now closed.
153 * @private
155 usbGnubby.prototype.idleClose_ = function() {
156 this.close();
157 while (this.notifyOnClose.length != 0) {
158 var cb = this.notifyOnClose.shift();
159 cb();
164 * Notify callback for every frame received.
165 * @param {function()} cb Callback
166 * @private
168 usbGnubby.prototype.notifyFrame_ = function(cb) {
169 if (this.rxframes.length != 0) {
170 // Already have frames; continue.
171 if (cb) window.setTimeout(cb, 0);
172 } else {
173 this.rxcb = cb;
178 * Called by low level driver with a frame.
179 * @param {ArrayBuffer|Uint8Array} frame Data frame
180 * @return {boolean} Whether this client is still interested in receiving
181 * frames from its device.
183 usbGnubby.prototype.receivedFrame = function(frame) {
184 if (this.closed) return false; // No longer interested.
186 if (!this.checkCID_(frame)) {
187 // Not for me, ignore.
188 return true;
191 this.rxframes.push(frame);
193 // Callback self in case we were waiting. Once.
194 var cb = this.rxcb;
195 this.rxcb = null;
196 if (cb) window.setTimeout(cb, 0);
198 return true;
202 * @return {ArrayBuffer|Uint8Array} oldest received frame. Throw if none.
203 * @private
205 usbGnubby.prototype.readFrame_ = function() {
206 if (this.rxframes.length == 0) throw 'rxframes empty!';
208 var frame = this.rxframes.shift();
209 return frame;
212 /** Poll from rxframes[].
213 * @param {number} cmd Command
214 * @param {number} timeout timeout in seconds.
215 * @param {?function(...)} cb Callback
216 * @private
218 usbGnubby.prototype.read_ = function(cmd, timeout, cb) {
219 if (this.closed) { cb(-llGnubby.GONE); return; }
220 if (!this.dev) { cb(-llGnubby.NODEVICE); return; }
222 var tid = null; // timeout timer id.
223 var callback = cb;
224 var self = this;
226 var msg = null;
227 var seqno = 0;
228 var count = 0;
231 * Schedule call to cb if not called yet.
232 * @param {number} a Return code.
233 * @param {Object=} b Optional data.
235 function schedule_cb(a, b) {
236 self.commandPending = false;
237 if (tid) {
238 // Cancel timeout timer.
239 window.clearTimeout(tid);
240 tid = null;
242 var c = callback;
243 if (c) {
244 callback = null;
245 window.setTimeout(function() { c(a, b); }, 0);
247 if (self.closingWhenIdle) self.idleClose_();
250 function read_timeout() {
251 if (!callback || !tid) return; // Already done.
253 console.error(UTIL_fmt(
254 '[' + self.cid.toString(16) + '] timeout!'));
256 if (self.dev) {
257 self.dev.destroy(); // Stop pretending this thing works.
260 tid = null;
262 schedule_cb(-llGnubby.TIMEOUT);
265 function cont_frame() {
266 if (!callback || !tid) return; // Already done.
268 var f = new Uint8Array(self.readFrame_());
269 var rcmd = f[4];
270 var totalLen = (f[5] << 8) + f[6];
272 if (rcmd == llGnubby.CMD_ERROR && totalLen == 1) {
273 // Error from device; forward.
274 console.log(UTIL_fmt(
275 '[' + self.cid.toString(16) + '] error frame ' +
276 UTIL_BytesToHex(f)));
277 if (f[7] == llGnubby.GONE) {
278 self.closed = true;
280 schedule_cb(-f[7]);
281 return;
284 if ((rcmd & 0x80)) {
285 // Not an CONT frame, ignore.
286 console.log(UTIL_fmt(
287 '[' + self.cid.toString(16) + '] ignoring non-cont frame ' +
288 UTIL_BytesToHex(f)));
289 self.notifyFrame_(cont_frame);
290 return;
293 var seq = (rcmd & 0x7f);
294 if (seq != seqno++) {
295 console.log(UTIL_fmt(
296 '[' + self.cid.toString(16) + '] bad cont frame ' +
297 UTIL_BytesToHex(f)));
298 schedule_cb(-llGnubby.INVALID_SEQ);
299 return;
302 // Copy payload.
303 for (var i = 5; i < f.length && count < msg.length; ++i) {
304 msg[count++] = f[i];
307 if (count == msg.length) {
308 // Done.
309 schedule_cb(-llGnubby.OK, msg.buffer);
310 } else {
311 // Need more CONT frame(s).
312 self.notifyFrame_(cont_frame);
316 function init_frame() {
317 if (!callback || !tid) return; // Already done.
319 var f = new Uint8Array(self.readFrame_());
321 var rcmd = f[4];
322 var totalLen = (f[5] << 8) + f[6];
324 if (rcmd == llGnubby.CMD_ERROR && totalLen == 1) {
325 // Error from device; forward.
326 // Don't log busy frames, they're "normal".
327 if (f[7] != llGnubby.BUSY) {
328 console.log(UTIL_fmt(
329 '[' + self.cid.toString(16) + '] error frame ' +
330 UTIL_BytesToHex(f)));
332 if (f[7] == llGnubby.GONE) {
333 self.closed = true;
335 schedule_cb(-f[7]);
336 return;
339 if (!(rcmd & 0x80)) {
340 // Not an init frame, ignore.
341 console.log(UTIL_fmt(
342 '[' + self.cid.toString(16) + '] ignoring non-init frame ' +
343 UTIL_BytesToHex(f)));
344 self.notifyFrame_(init_frame);
345 return;
348 if (rcmd != cmd) {
349 // Not expected ack, read more.
350 console.log(UTIL_fmt(
351 '[' + self.cid.toString(16) + '] ignoring non-ack frame ' +
352 UTIL_BytesToHex(f)));
353 self.notifyFrame_(init_frame);
354 return;
357 // Copy payload.
358 msg = new Uint8Array(totalLen);
359 for (var i = 7; i < f.length && count < msg.length; ++i) {
360 msg[count++] = f[i];
363 if (count == msg.length) {
364 // Done.
365 schedule_cb(-llGnubby.OK, msg.buffer);
366 } else {
367 // Need more CONT frame(s).
368 self.notifyFrame_(cont_frame);
372 // Start timeout timer.
373 tid = window.setTimeout(read_timeout, 1000.0 * timeout);
375 // Schedule read of first frame.
376 self.notifyFrame_(init_frame);
380 * @param {ArrayBuffer|Uint8Array} frame Data frame
381 * @return {boolean} Whether frame is for my channel.
382 * @private
384 usbGnubby.prototype.checkCID_ = function(frame) {
385 var f = new Uint8Array(frame);
386 var c = (f[0] << 24) |
387 (f[1] << 16) |
388 (f[2] << 8) |
389 (f[3]);
390 return c === this.cid ||
391 c === 0; // Generic notification.
395 * Queue command for sending.
396 * @param {number} cmd The command to send.
397 * @param {ArrayBuffer|Uint8Array} data Command data
398 * @private
400 usbGnubby.prototype.write_ = function(cmd, data) {
401 if (this.closed) return;
402 if (!this.dev) return;
404 this.commandPending = true;
406 this.dev.queueCommand(this.cid, cmd, data);
410 * Writes the command, and calls back when the command's reply is received.
411 * @param {number} cmd The command to send.
412 * @param {ArrayBuffer|Uint8Array} data Command data
413 * @param {number} timeout Timeout in seconds.
414 * @param {function(number, ArrayBuffer=)} cb Callback
415 * @private
417 usbGnubby.prototype.exchange_ = function(cmd, data, timeout, cb) {
418 var busyWait = new CountdownTimer(this.busyMillis);
419 var self = this;
421 function retryBusy(rc, rc_data) {
422 if (rc == -llGnubby.BUSY && !busyWait.expired()) {
423 if (usbGnubby.gnubbies_) {
424 usbGnubby.gnubbies_.resetInactivityTimer(timeout * 1000);
426 self.write_(cmd, data);
427 self.read_(cmd, timeout, retryBusy);
428 } else {
429 busyWait.clearTimeout();
430 cb(rc, rc_data);
434 retryBusy(-llGnubby.BUSY, undefined); // Start work.
437 /** Default callback for commands. Simply logs to console.
438 * @param {number} rc Result status code
439 * @param {*} data Result data
441 usbGnubby.defaultCallback = function(rc, data) {
442 var msg = 'defaultCallback(' + rc;
443 if (data) {
444 if (typeof data == 'string') msg += ', ' + data;
445 else msg += ', ' + UTIL_BytesToHex(new Uint8Array(data));
447 msg += ')';
448 console.log(UTIL_fmt(msg));
451 /** Send nonce to device, flush read queue until match.
452 * @param {?function(...)} cb Callback
454 usbGnubby.prototype.sync = function(cb) {
455 if (!cb) cb = usbGnubby.defaultCallback;
456 if (this.closed) {
457 cb(-llGnubby.GONE);
458 return;
461 var done = false;
462 var trycount = 6;
463 var tid = null;
464 var self = this;
466 function callback(rc) {
467 done = true;
468 self.commandPending = false;
469 if (tid) {
470 window.clearTimeout(tid);
471 tid = null;
473 if (rc) console.warn(UTIL_fmt('sync failed: ' + rc));
474 if (cb) cb(rc);
475 if (self.closingWhenIdle) self.idleClose_();
478 function sendSentinel() {
479 var data = new Uint8Array(1);
480 data[0] = ++self.synccnt;
481 self.write_(llGnubby.CMD_SYNC, data.buffer);
484 function checkSentinel() {
485 var f = new Uint8Array(self.readFrame_());
487 // Device disappeared on us.
488 if (f[4] == llGnubby.CMD_ERROR &&
489 f[5] == 0 && f[6] == 1 &&
490 f[7] == llGnubby.GONE) {
491 self.closed = true;
492 callback(-llGnubby.GONE);
493 return;
496 // Eat everything else but expected sync reply.
497 if (f[4] != llGnubby.CMD_SYNC ||
498 (f.length > 7 && /* fw pre-0.2.1 bug: does not echo sentinel */
499 f[7] != self.synccnt)) {
500 // Read more.
501 self.notifyFrame_(checkSentinel);
502 return;
505 // Done.
506 callback(-llGnubby.OK);
509 function timeoutLoop() {
510 if (done) return;
512 if (trycount == 0) {
513 // Failed.
514 callback(-llGnubby.TIMEOUT);
515 return;
518 --trycount; // Try another one.
519 sendSentinel();
520 self.notifyFrame_(checkSentinel);
521 tid = window.setTimeout(timeoutLoop, 500);
524 timeoutLoop();
527 /** Short timeout value in seconds */
528 usbGnubby.SHORT_TIMEOUT = 1;
529 /** Normal timeout value in seconds */
530 usbGnubby.NORMAL_TIMEOUT = 3;
531 // Max timeout usb firmware has for smartcard response is 30 seconds.
532 // Make our application level tolerance a little longer.
533 /** Maximum timeout in seconds */
534 usbGnubby.MAX_TIMEOUT = 31;
536 /** Blink led
537 * @param {number|ArrayBuffer|Uint8Array} data Command data or number
538 * of seconds to blink
539 * @param {?function(...)} cb Callback
541 usbGnubby.prototype.blink = function(data, cb) {
542 if (!cb) cb = usbGnubby.defaultCallback;
543 if (typeof data == 'number') {
544 var d = new Uint8Array([data]);
545 data = d.buffer;
547 this.exchange_(llGnubby.CMD_PROMPT, data, usbGnubby.NORMAL_TIMEOUT, cb);
550 /** Lock the gnubby
551 * @param {number|ArrayBuffer|Uint8Array} data Command data
552 * @param {?function(...)} cb Callback
554 usbGnubby.prototype.lock = function(data, cb) {
555 if (!cb) cb = usbGnubby.defaultCallback;
556 if (typeof data == 'number') {
557 var d = new Uint8Array([data]);
558 data = d.buffer;
560 this.exchange_(llGnubby.CMD_LOCK, data, usbGnubby.NORMAL_TIMEOUT, cb);
563 /** Unlock the gnubby
564 * @param {?function(...)} cb Callback
566 usbGnubby.prototype.unlock = function(cb) {
567 if (!cb) cb = usbGnubby.defaultCallback;
568 var data = new Uint8Array([0]);
569 this.exchange_(llGnubby.CMD_LOCK, data.buffer,
570 usbGnubby.NORMAL_TIMEOUT, cb);
573 /** Request system information data.
574 * @param {?function(...)} cb Callback
576 usbGnubby.prototype.sysinfo = function(cb) {
577 if (!cb) cb = usbGnubby.defaultCallback;
578 this.exchange_(llGnubby.CMD_SYSINFO, new ArrayBuffer(0),
579 usbGnubby.NORMAL_TIMEOUT, cb);
582 /** Send wink command
583 * @param {?function(...)} cb Callback
585 usbGnubby.prototype.wink = function(cb) {
586 if (!cb) cb = usbGnubby.defaultCallback;
587 this.exchange_(llGnubby.CMD_WINK, new ArrayBuffer(0),
588 usbGnubby.NORMAL_TIMEOUT, cb);
591 /** Send DFU (Device firmware upgrade) command
592 * @param {ArrayBuffer|Uint8Array} data Command data
593 * @param {?function(...)} cb Callback
595 usbGnubby.prototype.dfu = function(data, cb) {
596 if (!cb) cb = usbGnubby.defaultCallback;
597 this.exchange_(llGnubby.CMD_DFU, data, usbGnubby.NORMAL_TIMEOUT, cb);
600 /** Ping the gnubby
601 * @param {number|ArrayBuffer|Uint8Array} data Command data
602 * @param {?function(...)} cb Callback
604 usbGnubby.prototype.ping = function(data, cb) {
605 if (!cb) cb = usbGnubby.defaultCallback;
606 if (typeof data == 'number') {
607 var d = new Uint8Array(data);
608 window.crypto.getRandomValues(d);
609 data = d.buffer;
611 this.exchange_(llGnubby.CMD_PING, data, usbGnubby.NORMAL_TIMEOUT, cb);
614 /** Send a raw APDU command
615 * @param {ArrayBuffer|Uint8Array} data Command data
616 * @param {?function(...)} cb Callback
618 usbGnubby.prototype.apdu = function(data, cb) {
619 if (!cb) cb = usbGnubby.defaultCallback;
620 this.exchange_(llGnubby.CMD_APDU, data, usbGnubby.MAX_TIMEOUT, cb);
623 /** Reset gnubby
624 * @param {?function(...)} cb Callback
626 usbGnubby.prototype.reset = function(cb) {
627 if (!cb) cb = usbGnubby.defaultCallback;
628 this.exchange_(llGnubby.CMD_ATR, new ArrayBuffer(0),
629 usbGnubby.NORMAL_TIMEOUT, cb);
632 // byte args[3] = [delay-in-ms before disabling interrupts,
633 // delay-in-ms before disabling usb (aka remove),
634 // delay-in-ms before reboot (aka insert)]
635 /** Send usb test command
636 * @param {ArrayBuffer|Uint8Array} args Command data
637 * @param {?function(...)} cb Callback
639 usbGnubby.prototype.usb_test = function(args, cb) {
640 if (!cb) cb = usbGnubby.defaultCallback;
641 var u8 = new Uint8Array(args);
642 this.exchange_(llGnubby.CMD_USB_TEST, u8.buffer,
643 usbGnubby.NORMAL_TIMEOUT, cb);
646 /** APDU command with reply
647 * @param {ArrayBuffer|Uint8Array} request The request
648 * @param {?function(...)} cb Callback
649 * @param {boolean=} opt_nowink Do not wink
650 * @private
652 usbGnubby.prototype.apduReply_ = function(request, cb, opt_nowink) {
653 if (!cb) cb = usbGnubby.defaultCallback;
654 var self = this;
656 this.apdu(request, function(rc, data) {
657 if (rc == 0) {
658 var r8 = new Uint8Array(data);
659 if (r8[r8.length - 2] == 0x90 && r8[r8.length - 1] == 0x00) {
660 // strip trailing 9000
661 var buf = new Uint8Array(r8.subarray(0, r8.length - 2));
662 cb(-llGnubby.OK, buf.buffer);
663 return;
664 } else {
665 // return non-9000 as rc
666 rc = r8[r8.length - 2] * 256 + r8[r8.length - 1];
667 // wink gnubby at hand if it needs touching.
668 if (rc == 0x6985 && !opt_nowink) {
669 self.wink(function() { cb(rc); });
670 return;
674 // Warn on errors other than waiting for touch, wrong data, and
675 // unrecognized command.
676 if (rc != 0x6985 && rc != 0x6a80 && rc != 0x6d00) {
677 console.warn(UTIL_fmt('apduReply_ fail: ' + rc.toString(16)));
679 cb(rc);