Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / cryptotoken / gnubby.js
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.
12 var 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  */
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);
29 }
30
31 /**
32  * Sets usbGnubby's Gnubbies singleton.
33  * @param {Gnubbies} gnubbies Gnubbies singleton instance
34  */
35 usbGnubby.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  */
44 usbGnubby.prototype.enumerate = function(cb) {
45   if (!cb) cb = usbGnubby.defaultCallback;
46   if (this.closed) {
47     cb(-llGnubby.GONE);
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|undefined} opt_which The device to open.
62  * @param {function(number)|undefined} opt_cb Called with result of opening the
63  *     gnubby.
64  */
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;
70   }
71   this.closingWhenIdle = false;
72
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);
78   }
79
80   var self = this;
81   function addSelfAsClient(which) {
82     self.cid &= 0x00ffffff;
83     self.cid |= ((which.device + 1) << 24);  // For debugging.
84
85     usbGnubby.gnubbies_.addClient(which, self, function(rc, device) {
86       self.dev = device;
87       cb(rc);
88     });
89   }
90
91   if (!usbGnubby.gnubbies_) {
92     cb(-llGnubby.NODEVICE);
93     return;
94   }
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;
102       }
103       addSelfAsClient(devs[0]);
104     });
105   }
106 };
107
108 /**
109  * @return {boolean} Whether this gnubby has any command outstanding.
110  * @private
111  */
112 usbGnubby.prototype.inUse_ = function() {
113   return this.commandPending;
114 };
115
116 /** Closes this gnubby. */
117 usbGnubby.prototype.close = function() {
118   this.closed = true;
119
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);
134   }
135 };
136
137 /**
138  * Asks this gnubby to close when it gets a chance.
139  * @param {Function=} cb called back when closed.
140  */
141 usbGnubby.prototype.closeWhenIdle = function(cb) {
142   if (!this.inUse_()) {
143     this.close();
144     if (cb) cb();
145     return;
146   }
147   this.closingWhenIdle = true;
148   if (cb) this.notifyOnClose.push(cb);
149 };
150
151 /**
152  * Close and notify every caller that it is now closed.
153  * @private
154  */
155 usbGnubby.prototype.idleClose_ = function() {
156   this.close();
157   while (this.notifyOnClose.length != 0) {
158     var cb = this.notifyOnClose.shift();
159     cb();
160   }
161 };
162
163 /**
164  * Notify callback for every frame received.
165  * @param {function()} cb Callback
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|Uint8Array} frame Data 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|Uint8Array} 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  * @param {number} cmd Command
214  * @param {number} timeout timeout in seconds.
215  * @param {?function(...)} cb Callback
216  * @private
217  */
218 usbGnubby.prototype.read_ = function(cmd, timeout, cb) {
219   if (this.closed) { cb(-llGnubby.GONE); return; }
220   if (!this.dev) { cb(-llGnubby.NODEVICE); return; }
221
222   var tid = null;  // timeout timer id.
223   var callback = cb;
224   var self = this;
225
226   var msg = null;
227   var seqno = 0;
228   var count = 0;
229
230   /**
231    * Schedule call to cb if not called yet.
232    * @param {number} a Return code.
233    * @param {Object=} b Optional data.
234    */
235   function schedule_cb(a, b) {
236     self.commandPending = false;
237     if (tid) {
238       // Cancel timeout timer.
239       window.clearTimeout(tid);
240       tid = null;
241     }
242     var c = callback;
243     if (c) {
244       callback = null;
245       window.setTimeout(function() { c(a, b); }, 0);
246     }
247     if (self.closingWhenIdle) self.idleClose_();
248   };
249
250   function read_timeout() {
251     if (!callback || !tid) return;  // Already done.
252
253     console.error(UTIL_fmt(
254         '[' + self.cid.toString(16) + '] timeout!'));
255
256     if (self.dev) {
257       self.dev.destroy();  // Stop pretending this thing works.
258     }
259
260     tid = null;
261
262     schedule_cb(-llGnubby.TIMEOUT);
263   };
264
265   function cont_frame() {
266     if (!callback || !tid) return;  // Already done.
267
268     var f = new Uint8Array(self.readFrame_());
269     var rcmd = f[4];
270     var totalLen = (f[5] << 8) + f[6];
271
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;
279       }
280       schedule_cb(-f[7]);
281       return;
282     }
283
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;
291     }
292
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;
300     }
301
302     // Copy payload.
303     for (var i = 5; i < f.length && count < msg.length; ++i) {
304       msg[count++] = f[i];
305     }
306
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);
313     }
314   }
315
316   function init_frame() {
317     if (!callback || !tid) return;  // Already done.
318
319     var f = new Uint8Array(self.readFrame_());
320
321     var rcmd = f[4];
322     var totalLen = (f[5] << 8) + f[6];
323
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)));
331       }
332       if (f[7] == llGnubby.GONE) {
333         self.closed = true;
334       }
335       schedule_cb(-f[7]);
336       return;
337     }
338
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;
346     }
347
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;
355     }
356
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];
361     }
362
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);
369     }
370   }
371
372   // Start timeout timer.
373   tid = window.setTimeout(read_timeout, 1000.0 * timeout);
374
375   // Schedule read of first frame.
376   self.notifyFrame_(init_frame);
377 };
378
379 /**
380  * @param {ArrayBuffer|Uint8Array} frame Data frame
381  * @return {boolean} Whether frame is for my channel.
382  * @private
383  */
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.
392 };
393
394 /**
395  * Queue command for sending.
396  * @param {number} cmd The command to send.
397  * @param {ArrayBuffer|Uint8Array} data Command data
398  * @private
399  */
400 usbGnubby.prototype.write_ = function(cmd, data) {
401   if (this.closed) return;
402   if (!this.dev) return;
403
404   this.commandPending = true;
405
406   this.dev.queueCommand(this.cid, cmd, data);
407 };
408
409 /**
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
416  */
417 usbGnubby.prototype.exchange_ = function(cmd, data, timeout, cb) {
418   var busyWait = new CountdownTimer(this.busyMillis);
419   var self = this;
420
421   function retryBusy(rc, rc_data) {
422     if (rc == -llGnubby.BUSY && !busyWait.expired()) {
423       if (usbGnubby.gnubbies_) {
424         usbGnubby.gnubbies_.resetInactivityTimer(timeout * 1000);
425       }
426       self.write_(cmd, data);
427       self.read_(cmd, timeout, retryBusy);
428     } else {
429       busyWait.clearTimeout();
430       cb(rc, rc_data);
431     }
432   }
433
434   retryBusy(-llGnubby.BUSY, undefined);  // Start work.
435 };
436
437 /** Default callback for commands. Simply logs to console.
438  * @param {number} rc Result status code
439  * @param {*} data Result data
440  */
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));
446   }
447   msg += ')';
448   console.log(UTIL_fmt(msg));
449 };
450
451 /** Send nonce to device, flush read queue until match.
452  * @param {?function(...)} cb Callback
453  */
454 usbGnubby.prototype.sync = function(cb) {
455   if (!cb) cb = usbGnubby.defaultCallback;
456   if (this.closed) {
457     cb(-llGnubby.GONE);
458     return;
459   }
460
461   var done = false;
462   var trycount = 6;
463   var tid = null;
464   var self = this;
465
466   function callback(rc) {
467     done = true;
468     self.commandPending = false;
469     if (tid) {
470       window.clearTimeout(tid);
471       tid = null;
472     }
473     if (rc) console.warn(UTIL_fmt('sync failed: ' + rc));
474     if (cb) cb(rc);
475     if (self.closingWhenIdle) self.idleClose_();
476   }
477
478   function sendSentinel() {
479     var data = new Uint8Array(1);
480     data[0] = ++self.synccnt;
481     self.write_(llGnubby.CMD_SYNC, data.buffer);
482   }
483
484   function checkSentinel() {
485     var f = new Uint8Array(self.readFrame_());
486
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;
494     }
495
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;
503     }
504
505     // Done.
506     callback(-llGnubby.OK);
507   };
508
509   function timeoutLoop() {
510     if (done) return;
511
512     if (trycount == 0) {
513       // Failed.
514       callback(-llGnubby.TIMEOUT);
515       return;
516     }
517
518     --trycount;  // Try another one.
519     sendSentinel();
520     self.notifyFrame_(checkSentinel);
521     tid = window.setTimeout(timeoutLoop, 500);
522   };
523
524   timeoutLoop();
525 };
526
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;
535
536 /** Blink led
537  * @param {number|ArrayBuffer|Uint8Array} data Command data or number
538  *     of seconds to blink
539  * @param {?function(...)} cb Callback
540  */
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;
546   }
547   this.exchange_(llGnubby.CMD_PROMPT, data, usbGnubby.NORMAL_TIMEOUT, cb);
548 };
549
550 /** Lock the gnubby
551  * @param {number|ArrayBuffer|Uint8Array} data Command data
552  * @param {?function(...)} cb Callback
553  */
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;
559   }
560   this.exchange_(llGnubby.CMD_LOCK, data, usbGnubby.NORMAL_TIMEOUT, cb);
561 };
562
563 /** Unlock the gnubby
564  * @param {?function(...)} cb Callback
565  */
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);
571 };
572
573 /** Request system information data.
574  * @param {?function(...)} cb Callback
575  */
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);
580 };
581
582 /** Send wink command
583  * @param {?function(...)} cb Callback
584  */
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);
589 };
590
591 /** Send DFU (Device firmware upgrade) command
592  * @param {ArrayBuffer|Uint8Array} data Command data
593  * @param {?function(...)} cb Callback
594  */
595 usbGnubby.prototype.dfu = function(data, cb) {
596   if (!cb) cb = usbGnubby.defaultCallback;
597   this.exchange_(llGnubby.CMD_DFU, data, usbGnubby.NORMAL_TIMEOUT, cb);
598 };
599
600 /** Ping the gnubby
601  * @param {number|ArrayBuffer|Uint8Array} data Command data
602  * @param {?function(...)} cb Callback
603  */
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;
610   }
611   this.exchange_(llGnubby.CMD_PING, data, usbGnubby.NORMAL_TIMEOUT, cb);
612 };
613
614 /** Send a raw APDU command
615  * @param {ArrayBuffer|Uint8Array} data Command data
616  * @param {?function(...)} cb Callback
617  */
618 usbGnubby.prototype.apdu = function(data, cb) {
619   if (!cb) cb = usbGnubby.defaultCallback;
620   this.exchange_(llGnubby.CMD_APDU, data, usbGnubby.MAX_TIMEOUT, cb);
621 };
622
623 /** Reset gnubby
624  * @param {?function(...)} cb Callback
625  */
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);
630 };
631
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
638  */
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);
644 };
645
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
651  */
652 usbGnubby.prototype.apduReply_ = function(request, cb, opt_nowink) {
653   if (!cb) cb = usbGnubby.defaultCallback;
654   var self = this;
655
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;
671         }
672       }
673     }
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)));
678     }
679     cb(rc);
680   });
681 };