Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(328)

Side by Side Diff: chrome/browser/resources/cryptotoken/gnubby.js

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

Powered by Google App Engine
This is Rietveld 408576698