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

Side by Side Diff: chrome/browser/resources/cryptotoken/enroller.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 Handles web page requests for gnubby enrollment.
7 * @author juanlang@google.com (Juan Lang)
8 */
9
10 'use strict';
11
12 /**
13 * Handles an enroll request.
14 * @param {!EnrollHelperFactory} factory Factory to create an enroll helper.
15 * @param {MessageSender} sender The sender of the message.
16 * @param {Object} request The web page's enroll request.
17 * @param {boolean} enforceAppIdValid Whether to enforce that the appId in the
18 * request matches the sender's origin.
19 * @param {Function} sendResponse Called back with the result of the enroll.
20 * @param {boolean} toleratesMultipleResponses Whether the sendResponse
21 * callback can be called more than once, e.g. for progress updates.
22 * @return {Closeable}
23 */
24 function handleEnrollRequest(factory, sender, request, enforceAppIdValid,
25 sendResponse, toleratesMultipleResponses) {
26 var sentResponse = false;
27 function sendResponseOnce(r) {
28 if (enroller) {
29 enroller.close();
30 enroller = null;
31 }
32 if (!sentResponse) {
33 sentResponse = true;
34 try {
35 // If the page has gone away or the connection has otherwise gone,
36 // sendResponse fails.
37 sendResponse(r);
38 } catch (exception) {
39 console.warn('sendResponse failed: ' + exception);
40 }
41 } else {
42 console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
43 }
44 }
45
46 function sendErrorResponse(code) {
47 console.log(UTIL_fmt('code=' + code));
48 var response = formatWebPageResponse(GnubbyMsgTypes.ENROLL_WEB_REPLY, code);
49 if (request['requestId']) {
50 response['requestId'] = request['requestId'];
51 }
52 sendResponseOnce(response);
53 }
54
55 var origin = getOriginFromUrl(/** @type {string} */ (sender.url));
56 if (!origin) {
57 sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
58 return null;
59 }
60
61 if (!isValidEnrollRequest(request)) {
62 sendErrorResponse(GnubbyCodeTypes.BAD_REQUEST);
63 return null;
64 }
65
66 var signData = request['signData'];
67 var enrollChallenges = request['enrollChallenges'];
68 var logMsgUrl = request['logMsgUrl'];
69 var timeoutMillis = Enroller.DEFAULT_TIMEOUT_MILLIS;
70 if (request['timeout']) {
71 // Request timeout is in seconds.
72 timeoutMillis = request['timeout'] * 1000;
73 }
74
75 function findChallengeOfVersion(enrollChallenges, version) {
76 for (var i = 0; i < enrollChallenges.length; i++) {
77 if (enrollChallenges[i]['version'] == version) {
78 return enrollChallenges[i];
79 }
80 }
81 return null;
82 }
83
84 function sendSuccessResponse(u2fVersion, info, browserData) {
85 var enrollChallenge = findChallengeOfVersion(enrollChallenges, u2fVersion);
86 if (!enrollChallenge) {
87 sendErrorResponse(GnubbyCodeTypes.UNKNOWN_ERROR);
88 return;
89 }
90 var enrollUpdateData = {};
91 enrollUpdateData['enrollData'] = info;
92 // Echo the used challenge back in the reply.
93 for (var k in enrollChallenge) {
94 enrollUpdateData[k] = enrollChallenge[k];
95 }
96 if (u2fVersion == 'U2F_V2') {
97 // For U2F_V2, the challenge sent to the gnubby is modified to be the
98 // hash of the browser data. Include the browser data.
99 enrollUpdateData['browserData'] = browserData;
100 }
101 var response = formatWebPageResponse(
102 GnubbyMsgTypes.ENROLL_WEB_REPLY, GnubbyCodeTypes.OK, enrollUpdateData);
103 sendResponseOnce(response);
104 }
105
106 function sendNotification(code) {
107 console.log(UTIL_fmt('notification, code=' + code));
108 // Can the callback handle progress updates? If so, send one.
109 if (toleratesMultipleResponses) {
110 var response = formatWebPageResponse(
111 GnubbyMsgTypes.ENROLL_WEB_NOTIFICATION, code);
112 if (request['requestId']) {
113 response['requestId'] = request['requestId'];
114 }
115 sendResponse(response);
116 }
117 }
118
119 var timer = new CountdownTimer(timeoutMillis);
120 var enroller = new Enroller(factory, timer, origin, sendErrorResponse,
121 sendSuccessResponse, sendNotification, sender.tlsChannelId, logMsgUrl);
122 enroller.doEnroll(enrollChallenges, signData, enforceAppIdValid);
123 return /** @type {Closeable} */ (enroller);
124 }
125
126 /**
127 * Returns whether the request appears to be a valid enroll request.
128 * @param {Object} request the request.
129 * @return {boolean} whether the request appears valid.
130 */
131 function isValidEnrollRequest(request) {
132 if (!request.hasOwnProperty('enrollChallenges'))
133 return false;
134 var enrollChallenges = request['enrollChallenges'];
135 if (!enrollChallenges.length)
136 return false;
137 var seenVersions = {};
138 for (var i = 0; i < enrollChallenges.length; i++) {
139 var enrollChallenge = enrollChallenges[i];
140 var version = enrollChallenge['version'];
141 if (!version) {
142 // Version is implicitly V1 if not specified.
143 version = 'U2F_V1';
144 }
145 if (version != 'U2F_V1' && version != 'U2F_V2') {
146 return false;
147 }
148 if (seenVersions[version]) {
149 // Each version can appear at most once.
150 return false;
151 }
152 seenVersions[version] = version;
153 if (!enrollChallenge['appId']) {
154 return false;
155 }
156 if (!enrollChallenge['challenge']) {
157 // The challenge is required.
158 return false;
159 }
160 }
161 var signData = request['signData'];
162 // An empty signData is ok, in the case the user is not already enrolled.
163 if (signData && !isValidSignData(signData))
164 return false;
165 return true;
166 }
167
168 /**
169 * Creates a new object to track enrolling with a gnubby.
170 * @param {!EnrollHelperFactory} helperFactory factory to create an enroll
171 * helper.
172 * @param {!Countdown} timer Timer for enroll request.
173 * @param {string} origin The origin making the request.
174 * @param {function(number)} errorCb Called upon enroll failure with an error
175 * code.
176 * @param {function(string, string, (string|undefined))} successCb Called upon
177 * enroll success with the version of the succeeding gnubby, the enroll
178 * data, and optionally the browser data associated with the enrollment.
179 * @param {(function(number)|undefined)} opt_progressCb Called with progress
180 * updates to the enroll request.
181 * @param {string=} opt_tlsChannelId the TLS channel ID, if any, of the origin
182 * making the request.
183 * @param {string=} opt_logMsgUrl The url to post log messages to.
184 * @constructor
185 */
186 function Enroller(helperFactory, timer, origin, errorCb, successCb,
187 opt_progressCb, opt_tlsChannelId, opt_logMsgUrl) {
188 /** @private {Countdown} */
189 this.timer_ = timer;
190 /** @private {string} */
191 this.origin_ = origin;
192 /** @private {function(number)} */
193 this.errorCb_ = errorCb;
194 /** @private {function(string, string, (string|undefined))} */
195 this.successCb_ = successCb;
196 /** @private {(function(number)|undefined)} */
197 this.progressCb_ = opt_progressCb;
198 /** @private {string|undefined} */
199 this.tlsChannelId_ = opt_tlsChannelId;
200 /** @private {string|undefined} */
201 this.logMsgUrl_ = opt_logMsgUrl;
202
203 /** @private {boolean} */
204 this.done_ = false;
205 /** @private {number|undefined} */
206 this.lastProgressUpdate_ = undefined;
207
208 /** @private {Object.<string, string>} */
209 this.browserData_ = {};
210 /** @private {Array.<EnrollHelperChallenge>} */
211 this.encodedEnrollChallenges_ = [];
212 /** @private {Array.<SignHelperChallenge>} */
213 this.encodedSignChallenges_ = [];
214 // Allow http appIds for http origins. (Broken, but the caller deserves
215 // what they get.)
216 /** @private {boolean} */
217 this.allowHttp_ = this.origin_ ? this.origin_.indexOf('http://') == 0 : false;
218
219 /** @private {EnrollHelper} */
220 this.helper_ = helperFactory.createHelper(timer,
221 this.helperError_.bind(this), this.helperSuccess_.bind(this),
222 this.helperProgress_.bind(this));
223 }
224
225 /**
226 * Default timeout value in case the caller never provides a valid timeout.
227 */
228 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000;
229
230 /**
231 * Performs an enroll request with the given enroll and sign challenges.
232 * @param {Array.<Object>} enrollChallenges
233 * @param {Array.<Object>} signChallenges
234 * @param {boolean} enforceAppIdValid
235 */
236 Enroller.prototype.doEnroll =
237 function(enrollChallenges, signChallenges, enforceAppIdValid) {
238 this.setEnrollChallenges_(enrollChallenges);
239 this.setSignChallenges_(signChallenges);
240
241 if (!enforceAppIdValid) {
242 // If not enforcing app id validity, begin enrolling right away.
243 this.helper_.doEnroll(this.encodedEnrollChallenges_,
244 this.encodedSignChallenges_);
245 }
246 // Whether or not enforcing app id validity, begin fetching/checking the
247 // app ids.
248 var enrollAppIds = [];
249 for (var i = 0; i < enrollChallenges.length; i++) {
250 enrollAppIds.push(enrollChallenges[i]['appId']);
251 }
252 var self = this;
253 this.checkAppIds_(enrollAppIds, signChallenges, function(result) {
254 if (!enforceAppIdValid) {
255 // Nothing to do, move along.
256 return;
257 }
258 if (result) {
259 self.helper_.doEnroll(self.encodedEnrollChallenges_,
260 self.encodedSignChallenges_);
261 } else {
262 self.notifyError_(GnubbyCodeTypes.BAD_APP_ID);
263 }
264 });
265 };
266
267 /**
268 * Encodes the enroll challenges for use by an enroll helper.
269 * @param {Array.<Object>} enrollChallenges
270 * @return {Array.<EnrollHelperChallenge>} the encoded challenges.
271 * @private
272 */
273 Enroller.encodeEnrollChallenges_ = function(enrollChallenges) {
274 var encodedChallenges = [];
275 for (var i = 0; i < enrollChallenges.length; i++) {
276 var enrollChallenge = enrollChallenges[i];
277 var encodedChallenge = {};
278 var version;
279 if (enrollChallenge['version']) {
280 version = enrollChallenge['version'];
281 } else {
282 // Version is implicitly V1 if not specified.
283 version = 'U2F_V1';
284 }
285 encodedChallenge['version'] = version;
286 encodedChallenge['challenge'] = enrollChallenge['challenge'];
287 encodedChallenge['appIdHash'] =
288 B64_encode(sha256HashOfString(enrollChallenge['appId']));
289 encodedChallenges.push(encodedChallenge);
290 }
291 return encodedChallenges;
292 };
293
294 /**
295 * Sets this enroller's enroll challenges.
296 * @param {Array.<Object>} enrollChallenges The enroll challenges.
297 * @private
298 */
299 Enroller.prototype.setEnrollChallenges_ = function(enrollChallenges) {
300 var challenges = [];
301 for (var i = 0; i < enrollChallenges.length; i++) {
302 var enrollChallenge = enrollChallenges[i];
303 var version = enrollChallenge.version;
304 if (!version) {
305 // Version is implicitly V1 if not specified.
306 version = 'U2F_V1';
307 }
308
309 if (version == 'U2F_V2') {
310 var modifiedChallenge = {};
311 for (var k in enrollChallenge) {
312 modifiedChallenge[k] = enrollChallenge[k];
313 }
314 // V2 enroll responses contain signatures over a browser data object,
315 // which we're constructing here. The browser data object contains, among
316 // other things, the server challenge.
317 var serverChallenge = enrollChallenge['challenge'];
318 var browserData = makeEnrollBrowserData(
319 serverChallenge, this.origin_, this.tlsChannelId_);
320 // Replace the challenge with the hash of the browser data.
321 modifiedChallenge['challenge'] =
322 B64_encode(sha256HashOfString(browserData));
323 this.browserData_[version] =
324 B64_encode(UTIL_StringToBytes(browserData));
325 challenges.push(modifiedChallenge);
326 } else {
327 challenges.push(enrollChallenge);
328 }
329 }
330 // Store the encoded challenges for use by the enroll helper.
331 this.encodedEnrollChallenges_ =
332 Enroller.encodeEnrollChallenges_(challenges);
333 };
334
335 /**
336 * Sets this enroller's sign data.
337 * @param {Array=} signData the sign challenges to add.
338 * @private
339 */
340 Enroller.prototype.setSignChallenges_ = function(signData) {
341 this.encodedSignChallenges_ = [];
342 if (signData) {
343 for (var i = 0; i < signData.length; i++) {
344 var incomingChallenge = signData[i];
345 var serverChallenge = incomingChallenge['challenge'];
346 var appId = incomingChallenge['appId'];
347 var encodedKeyHandle = incomingChallenge['keyHandle'];
348
349 var challenge = makeChallenge(serverChallenge, appId, encodedKeyHandle,
350 incomingChallenge['version']);
351
352 this.encodedSignChallenges_.push(challenge);
353 }
354 }
355 };
356
357 /**
358 * Checks the app ids associated with this enroll request, and calls a callback
359 * with the result of the check.
360 * @param {!Array.<string>} enrollAppIds The app ids in the enroll challenge
361 * portion of the enroll request.
362 * @param {SignData} signData The sign data associated with the request.
363 * @param {function(boolean)} cb Called with the result of the check.
364 * @private
365 */
366 Enroller.prototype.checkAppIds_ = function(enrollAppIds, signData, cb) {
367 if (!enrollAppIds || !enrollAppIds.length) {
368 // Defensive programming check: the enroll request is required to contain
369 // its own app ids, so if there aren't any, reject the request.
370 cb(false);
371 return;
372 }
373
374 /** @private {Array.<string>} */
375 this.distinctAppIds_ =
376 UTIL_unionArrays(enrollAppIds, getDistinctAppIds(signData));
377 /** @private {boolean} */
378 this.anyInvalidAppIds_ = false;
379 /** @private {boolean} */
380 this.appIdFailureReported_ = false;
381 /** @private {number} */
382 this.fetchedAppIds_ = 0;
383
384 for (var i = 0; i < this.distinctAppIds_.length; i++) {
385 var appId = this.distinctAppIds_[i];
386 if (appId == this.origin_) {
387 // Trivially allowed.
388 this.fetchedAppIds_++;
389 if (this.fetchedAppIds_ == this.distinctAppIds_.length &&
390 !this.anyInvalidAppIds_) {
391 // Last app id was fetched, and they were all valid: we're done.
392 // (Note that the case when anyInvalidAppIds_ is true doesn't need to
393 // be handled here: the callback was already called with false at that
394 // point, see fetchedAllowedOriginsForAppId_.)
395 cb(true);
396 }
397 } else {
398 var start = new Date();
399 fetchAllowedOriginsForAppId(appId, this.allowHttp_,
400 this.fetchedAllowedOriginsForAppId_.bind(this, appId, start, cb));
401 }
402 }
403 };
404
405 /**
406 * Called with the result of an app id fetch.
407 * @param {string} appId the app id that was fetched.
408 * @param {Date} start the time the fetch request started.
409 * @param {function(boolean)} cb Called with the result of the app id check.
410 * @param {number} rc The HTTP response code for the app id fetch.
411 * @param {!Array.<string>} allowedOrigins The origins allowed for this app id.
412 * @private
413 */
414 Enroller.prototype.fetchedAllowedOriginsForAppId_ =
415 function(appId, start, cb, rc, allowedOrigins) {
416 var end = new Date();
417 this.fetchedAppIds_++;
418 logFetchAppIdResult(appId, end - start, allowedOrigins, this.logMsgUrl_);
419 if (rc != 200 && !(rc >= 400 && rc < 500)) {
420 if (this.timer_.expired()) {
421 // Act as though the helper timed out.
422 this.helperError_(DeviceStatusCodes.TIMEOUT_STATUS, false);
423 } else {
424 start = new Date();
425 fetchAllowedOriginsForAppId(appId, this.allowHttp_,
426 this.fetchedAllowedOriginsForAppId_.bind(this, appId, start, cb));
427 }
428 return;
429 }
430 if (!isValidAppIdForOrigin(appId, this.origin_, allowedOrigins)) {
431 logInvalidOriginForAppId(this.origin_, appId, this.logMsgUrl_);
432 this.anyInvalidAppIds_ = true;
433 if (!this.appIdFailureReported_) {
434 // Only the failure case can happen more than once, so only report
435 // it the first time.
436 this.appIdFailureReported_ = true;
437 cb(false);
438 }
439 }
440 if (this.fetchedAppIds_ == this.distinctAppIds_.length &&
441 !this.anyInvalidAppIds_) {
442 // Last app id was fetched, and they were all valid: we're done.
443 cb(true);
444 }
445 };
446
447 /** Closes this enroller. */
448 Enroller.prototype.close = function() {
449 if (this.helper_) this.helper_.close();
450 };
451
452 /**
453 * Notifies the caller with the error code.
454 * @param {number} code
455 * @private
456 */
457 Enroller.prototype.notifyError_ = function(code) {
458 if (this.done_)
459 return;
460 this.close();
461 this.done_ = true;
462 this.errorCb_(code);
463 };
464
465 /**
466 * Notifies the caller of success with the provided response data.
467 * @param {string} u2fVersion
468 * @param {string} info
469 * @param {string|undefined} opt_browserData
470 * @private
471 */
472 Enroller.prototype.notifySuccess_ =
473 function(u2fVersion, info, opt_browserData) {
474 if (this.done_)
475 return;
476 this.close();
477 this.done_ = true;
478 this.successCb_(u2fVersion, info, opt_browserData);
479 };
480
481 /**
482 * Notifies the caller of progress with the error code.
483 * @param {number} code
484 * @private
485 */
486 Enroller.prototype.notifyProgress_ = function(code) {
487 if (this.done_)
488 return;
489 if (code != this.lastProgressUpdate_) {
490 this.lastProgressUpdate_ = code;
491 // If there is no progress callback, treat it like an error and clean up.
492 if (this.progressCb_) {
493 this.progressCb_(code);
494 } else {
495 this.notifyError_(code);
496 }
497 }
498 };
499
500 /**
501 * Maps an enroll helper's error code namespace to the page's error code
502 * namespace.
503 * @param {number} code Error code from DeviceStatusCodes namespace.
504 * @param {boolean} anyGnubbies Whether any gnubbies were found.
505 * @return {number} A GnubbyCodeTypes error code.
506 * @private
507 */
508 Enroller.mapError_ = function(code, anyGnubbies) {
509 var reportedError = GnubbyCodeTypes.UNKNOWN_ERROR;
510 switch (code) {
511 case DeviceStatusCodes.WRONG_DATA_STATUS:
512 reportedError = anyGnubbies ? GnubbyCodeTypes.ALREADY_ENROLLED :
513 GnubbyCodeTypes.NO_GNUBBIES;
514 break;
515
516 case DeviceStatusCodes.WAIT_TOUCH_STATUS:
517 reportedError = GnubbyCodeTypes.WAIT_TOUCH;
518 break;
519
520 case DeviceStatusCodes.BUSY_STATUS:
521 reportedError = GnubbyCodeTypes.BUSY;
522 break;
523 }
524 return reportedError;
525 };
526
527 /**
528 * Called by the helper upon error.
529 * @param {number} code
530 * @param {boolean} anyGnubbies
531 * @private
532 */
533 Enroller.prototype.helperError_ = function(code, anyGnubbies) {
534 var reportedError = Enroller.mapError_(code, anyGnubbies);
535 console.log(UTIL_fmt('helper reported ' + code.toString(16) +
536 ', returning ' + reportedError));
537 this.notifyError_(reportedError);
538 };
539
540 /**
541 * Called by helper upon success.
542 * @param {string} u2fVersion gnubby version.
543 * @param {string} info enroll data.
544 * @private
545 */
546 Enroller.prototype.helperSuccess_ = function(u2fVersion, info) {
547 console.log(UTIL_fmt('Gnubby enrollment succeeded!!!!!'));
548
549 var browserData;
550 if (u2fVersion == 'U2F_V2') {
551 // For U2F_V2, the challenge sent to the gnubby is modified to be the hash
552 // of the browser data. Include the browser data.
553 browserData = this.browserData_[u2fVersion];
554 }
555
556 this.notifySuccess_(u2fVersion, info, browserData);
557 };
558
559 /**
560 * Called by helper to notify progress.
561 * @param {number} code
562 * @param {boolean} anyGnubbies
563 * @private
564 */
565 Enroller.prototype.helperProgress_ = function(code, anyGnubbies) {
566 var reportedError = Enroller.mapError_(code, anyGnubbies);
567 console.log(UTIL_fmt('helper notified ' + code.toString(16) +
568 ', returning ' + reportedError));
569 this.notifyProgress_(reportedError);
570 };
OLDNEW
« no previous file with comments | « chrome/browser/resources/cryptotoken/devicestatuscodes.js ('k') | chrome/browser/resources/cryptotoken/enrollhelper.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698