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

Side by Side Diff: chrome/browser/resources/cryptotoken/llhidgnubby.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 Implements a low-level gnubby driver based on chrome.hid.
7 */
8 'use strict';
9
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.ConnectionHandle} dev The device.
15 * @param {number} id The device's id.
16 * @constructor
17 * @implements {llGnubby}
18 */
19 function llHidGnubby(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.
31 }
32
33 /**
34 * Namespace for the llHidGnubby implementation.
35 * @const
36 */
37 llHidGnubby.NAMESPACE = 'hid';
38
39 /** Destroys this low-level device instance. */
40 llHidGnubby.prototype.destroy = function() {
41 if (!this.dev) return; // Already dead.
42
43 this.closing = true;
44
45 console.log(UTIL_fmt('llHidGnubby.destroy()'));
46
47 // Synthesize a close error frame to alert all clients,
48 // some of which might be in read state.
49 //
50 // Use magic CID 0 to address all.
51 this.publishFrame_(new Uint8Array([
52 0, 0, 0, 0, // broadcast CID
53 llGnubby.CMD_ERROR,
54 0, 1, // length
55 llGnubby.GONE]).buffer);
56
57 // Set all clients to closed status and remove them.
58 while (this.clients.length != 0) {
59 var client = this.clients.shift();
60 if (client) client.closed = true;
61 }
62
63 if (this.lockTID) {
64 window.clearTimeout(this.lockTID);
65 this.lockTID = null;
66 }
67
68 var dev = this.dev;
69 this.dev = null;
70
71 var self = this;
72
73 function onClosed() {
74 console.log(UTIL_fmt('Device ' + dev.handle + ' closed'));
75 self.gnubbies_.removeOpenDevice(
76 {namespace: llHidGnubby.NAMESPACE, device: self.id});
77 }
78
79 chrome.hid.disconnect(dev.connectionId, onClosed);
80 };
81
82 /**
83 * Push frame to all clients.
84 * @param {ArrayBuffer} f
85 * @private
86 */
87 llHidGnubby.prototype.publishFrame_ = function(f) {
88 var old = this.clients;
89
90 var remaining = [];
91 var changes = false;
92 for (var i = 0; i < old.length; ++i) {
93 var client = old[i];
94 if (client.receivedFrame(f)) {
95 // Client still alive; keep on list.
96 remaining.push(client);
97 } else {
98 changes = true;
99 console.log(UTIL_fmt(
100 '[' + client.cid.toString(16) + '] left?'));
101 }
102 }
103 if (changes) this.clients = remaining;
104 };
105
106 /**
107 * @return {boolean} whether this device is open and ready to use.
108 * @private
109 */
110 llHidGnubby.prototype.readyToUse_ = function() {
111 if (this.closing) return false;
112 if (!this.dev) return false;
113
114 return true;
115 };
116
117 /**
118 * Register a client for this gnubby.
119 * @param {*} who The client.
120 */
121 llHidGnubby.prototype.registerClient = function(who) {
122 for (var i = 0; i < this.clients.length; ++i) {
123 if (this.clients[i] === who) return; // Already registered.
124 }
125 this.clients.push(who);
126 if (this.clients.length == 1) {
127 // First client? Kick off read loop.
128 this.readLoop_();
129 }
130 };
131
132 /**
133 * De-register a client.
134 * @param {*} who The client.
135 * @return {number} The number of remaining listeners for this device, or -1
136 * Returns number of remaining listeners for this device.
137 * if this had no clients to start with.
138 */
139 llHidGnubby.prototype.deregisterClient = function(who) {
140 var current = this.clients;
141 if (current.length == 0) return -1;
142 this.clients = [];
143 for (var i = 0; i < current.length; ++i) {
144 var client = current[i];
145 if (client !== who) this.clients.push(client);
146 }
147 return this.clients.length;
148 };
149
150 /**
151 * @param {*} who The client.
152 * @return {boolean} Whether this device has who as a client.
153 */
154 llHidGnubby.prototype.hasClient = function(who) {
155 if (this.clients.length == 0) return false;
156 for (var i = 0; i < this.clients.length; ++i) {
157 if (who === this.clients[i])
158 return true;
159 }
160 return false;
161 };
162
163 /**
164 * Reads all incoming frames and notifies clients of their receipt.
165 * @private
166 */
167 llHidGnubby.prototype.readLoop_ = function() {
168 //console.log(UTIL_fmt('entering readLoop'));
169 if (!this.dev) return;
170
171 if (this.closing) {
172 this.destroy();
173 return;
174 }
175
176 // No interested listeners, yet we hit readLoop().
177 // Must be clean-up. We do this here to make sure no transfer is pending.
178 if (!this.clients.length) {
179 this.closing = true;
180 this.destroy();
181 return;
182 }
183
184 // firmwareUpdate() sets this.updating when writing the last block before
185 // the signature. We process that reply with the already pending
186 // read transfer but we do not want to start another read transfer for the
187 // signature block, since that request will have no reply.
188 // Instead we will see the device drop and re-appear on the bus.
189 // Current libusb on some platforms gets unhappy when transfer are pending
190 // when that happens.
191 // TODO(mschilder): revisit once Chrome stabilizes its behavior.
192 if (this.updating) {
193 console.log(UTIL_fmt('device updating. Ending readLoop()'));
194 return;
195 }
196
197 var self = this;
198 chrome.hid.receive(
199 this.dev.connectionId,
200 64,
201 function(x) {
202 if (chrome.runtime.lastError || !x) {
203 console.log(UTIL_fmt('got lastError'));
204 console.log(chrome.runtime.lastError);
205 window.setTimeout(function() { self.destroy(); }, 0);
206 return;
207 }
208 var u8 = new Uint8Array(x);
209 //console.log(UTIL_fmt('<' + UTIL_BytesToHex(u8)));
210
211 self.publishFrame_(x);
212
213 // Read more.
214 window.setTimeout(function() { self.readLoop_(); }, 0);
215 }
216 );
217 };
218
219 /**
220 * Check whether channel is locked for this request or not.
221 * @param {number} cid
222 * @param {number} cmd
223 * @return {boolean} true if not locked for this request.
224 * @private
225 */
226 llHidGnubby.prototype.checkLock_ = function(cid, cmd) {
227 if (this.lockCID) {
228 // We have an active lock.
229 if (this.lockCID != cid) {
230 // Some other channel has active lock.
231
232 if (cmd != llGnubby.CMD_SYNC) {
233 // Anything but SYNC gets an immediate busy.
234 var busy = new Uint8Array(
235 [(cid >> 24) & 255,
236 (cid >> 16) & 255,
237 (cid >> 8) & 255,
238 cid & 255,
239 llGnubby.CMD_ERROR,
240 0, 1, // length
241 llGnubby.BUSY]);
242 // Log the synthetic busy too.
243 console.log(UTIL_fmt('<' + UTIL_BytesToHex(busy)));
244 this.publishFrame_(busy.buffer);
245 return false;
246 }
247
248 // SYNC gets to go to the device to flush OS tx/rx queues.
249 // The usb firmware always responds to SYNC, regardless of lock status.
250 }
251 }
252 return true;
253 };
254
255 /**
256 * Update or grab lock.
257 * @param {number} cid
258 * @param {number} cmd
259 * @param {number} arg
260 * @private
261 */
262 llHidGnubby.prototype.updateLock_ = function(cid, cmd, arg) {
263 if (this.lockCID == 0 || this.lockCID == cid) {
264 // It is this caller's or nobody's lock.
265 if (this.lockTID) {
266 window.clearTimeout(this.lockTID);
267 this.lockTID = null;
268 }
269
270 if (cmd == llGnubby.CMD_LOCK) {
271 var nseconds = arg;
272 if (nseconds != 0) {
273 this.lockCID = cid;
274 // Set tracking time to be .1 seconds longer than usb device does.
275 this.lockMillis = nseconds * 1000 + 100;
276 } else {
277 // Releasing lock voluntarily.
278 this.lockCID = 0;
279 }
280 }
281
282 // (re)set the lock timeout if we still hold it.
283 if (this.lockCID) {
284 var self = this;
285 this.lockTID = window.setTimeout(
286 function() {
287 console.warn(UTIL_fmt(
288 'lock for CID ' + cid.toString(16) + ' expired!'));
289 self.lockTID = null;
290 self.lockCID = 0;
291 },
292 this.lockMillis);
293 }
294 }
295 };
296
297 /**
298 * Queue command to be sent.
299 * If queue was empty, initiate the write.
300 * @param {number} cid The client's channel ID.
301 * @param {number} cmd The command to send.
302 * @param {ArrayBuffer} data
303 */
304 llHidGnubby.prototype.queueCommand = function(cid, cmd, data) {
305 if (!this.dev) return;
306 if (!this.checkLock_(cid, cmd)) return;
307
308 var u8 = new Uint8Array(data);
309 var f = new Uint8Array(64);
310
311 llHidGnubby.setCid_(f, cid);
312 f[4] = cmd;
313 f[5] = (u8.length >> 8);
314 f[6] = (u8.length & 255);
315
316 var lockArg = (u8.length > 0) ? u8[0] : 0;
317
318 // Fragment over our 64 byte frames.
319 var n = 7;
320 var seq = 0;
321 for (var i = 0; i < u8.length; ++i) {
322 f[n++] = u8[i];
323 if (n == f.length) {
324 this.queueFrame_(f.buffer, cid, cmd, lockArg);
325
326 f = new Uint8Array(64);
327 llHidGnubby.setCid_(f, cid);
328 cmd = f[4] = seq++;
329 n = 5;
330 }
331 }
332 if (n != 5) {
333 this.queueFrame_(f.buffer, cid, cmd, lockArg);
334 }
335 };
336
337 /**
338 * Sets the channel id in the frame.
339 * @param {Uint8Array} frame
340 * @param {number} cid The client's channel ID.
341 * @private
342 */
343 llHidGnubby.setCid_ = function(frame, cid) {
344 frame[0] = cid >>> 24;
345 frame[1] = cid >>> 16;
346 frame[2] = cid >>> 8;
347 frame[3] = cid;
348 };
349
350 /**
351 * Updates the lock, and queues the frame for sending. Also begins sending if
352 * no other writes are outstanding.
353 * @param {ArrayBuffer} frame
354 * @param {number} cid The client's channel ID.
355 * @param {number} cmd The command to send.
356 * @param {number} arg
357 * @private
358 */
359 llHidGnubby.prototype.queueFrame_ = function(frame, cid, cmd, arg) {
360 this.updateLock_(cid, cmd, arg);
361 var wasEmpty = (this.txqueue.length == 0);
362 this.txqueue.push(frame);
363 if (wasEmpty) this.writePump_();
364 };
365
366 /**
367 * Stuff queued frames from txqueue[] to device, one by one.
368 * @private
369 */
370 llHidGnubby.prototype.writePump_ = function() {
371 if (!this.dev) return; // Ignore.
372
373 if (this.txqueue.length == 0) return; // Done with current queue.
374
375 var frame = this.txqueue[0];
376
377 var self = this;
378 function transferComplete(x) {
379 if (chrome.runtime.lastError) {
380 console.log(UTIL_fmt('got lastError'));
381 console.log(chrome.runtime.lastError);
382 window.setTimeout(function() { self.destroy(); }, 0);
383 return;
384 }
385 self.txqueue.shift(); // drop sent frame from queue.
386 if (self.txqueue.length != 0) {
387 window.setTimeout(function() { self.writePump_(); }, 0);
388 }
389 };
390
391 var u8 = new Uint8Array(frame);
392 //console.log(UTIL_fmt('>' + UTIL_BytesToHex(u8)));
393
394 var u8f = new Uint8Array(64);
395 for (var i = 0; i < u8.length; ++i) {
396 u8f[i] = u8[i];
397 }
398
399 chrome.hid.send(
400 this.dev.connectionId,
401 0, // report Id
402 u8f.buffer,
403 transferComplete
404 );
405 };
406 /**
407 * @param {function(Array)} cb
408 */
409 llHidGnubby.enumerate = function(cb) {
410 chrome.hid.getDevices({'vendorId': 4176, 'productId': 512}, cb);
411 };
412
413 /**
414 * @param {Gnubbies} gnubbies The gnubbies instances this device is enumerated
415 * in.
416 * @param {number} which The index of the device to open.
417 * @param {!chrome.hid.HidDeviceInfo} dev The device to open.
418 * @param {function(number, llGnubby=)} cb Called back with the
419 * result of opening the device.
420 */
421 llHidGnubby.open = function(gnubbies, which, dev, cb) {
422 chrome.hid.connect(dev.deviceId, function(handle) {
423 if (chrome.runtime.lastError) {
424 console.log(chrome.runtime.lastError);
425 }
426 if (!handle) {
427 console.warn(UTIL_fmt('failed to connect device. permissions issue?'));
428 cb(-llGnubby.NODEVICE);
429 return;
430 }
431 var nonNullHandle = /** @type {!chrome.hid.HidConnection} */ (handle);
432 var gnubby = new llHidGnubby(gnubbies, nonNullHandle, which);
433 cb(-llGnubby.OK, gnubby);
434 });
435 };
436
437 /**
438 * @param {*} dev
439 * @return {llGnubbyDeviceId} A device identifier for the device.
440 */
441 llHidGnubby.deviceToDeviceId = function(dev) {
442 var hidDev = /** @type {!chrome.hid.HidDeviceInfo} */ (dev);
443 var deviceId = { namespace: llHidGnubby.NAMESPACE, device: hidDev.deviceId };
444 return deviceId;
445 };
446
447 /**
448 * Registers this implementation with gnubbies.
449 * @param {Gnubbies} gnubbies
450 */
451 llHidGnubby.register = function(gnubbies) {
452 var HID_GNUBBY_IMPL = {
453 enumerate: llHidGnubby.enumerate,
454 deviceToDeviceId: llHidGnubby.deviceToDeviceId,
455 open: llHidGnubby.open
456 };
457 gnubbies.registerNamespace(llHidGnubby.NAMESPACE, HID_GNUBBY_IMPL);
458 };
OLDNEW
« no previous file with comments | « chrome/browser/resources/cryptotoken/llgnubby.js ('k') | chrome/browser/resources/cryptotoken/llusbgnubby.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698