OLD | NEW |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Handles web page requests for gnubby enrollment. | 6 * @fileoverview Handles web page requests for gnubby enrollment. |
7 */ | 7 */ |
8 | 8 |
9 'use strict'; | 9 'use strict'; |
10 | 10 |
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
76 var responseData = | 76 var responseData = |
77 makeEnrollResponseData(enrollChallenge, u2fVersion, | 77 makeEnrollResponseData(enrollChallenge, u2fVersion, |
78 'registrationData', info, 'clientData', browserData); | 78 'registrationData', info, 'clientData', browserData); |
79 var response = makeU2fSuccessResponse(request, responseData); | 79 var response = makeU2fSuccessResponse(request, responseData); |
80 sendResponseOnce(sentResponse, closeable, response, sendResponse); | 80 sendResponseOnce(sentResponse, closeable, response, sendResponse); |
81 } | 81 } |
82 | 82 |
83 closeable = | 83 closeable = |
84 validateAndBeginEnrollRequest( | 84 validateAndBeginEnrollRequest( |
85 sender, request, 'registerRequests', 'signRequests', | 85 sender, request, 'registerRequests', 'signRequests', |
86 sendErrorResponse, sendSuccessResponse); | 86 sendErrorResponse, sendSuccessResponse, 'registeredKeys'); |
87 return closeable; | 87 return closeable; |
88 } | 88 } |
89 | 89 |
90 /** | 90 /** |
91 * Validates an enroll request using the given parameters, and, if valid, begins | 91 * Validates an enroll request using the given parameters, and, if valid, begins |
92 * handling the enroll request. | 92 * handling the enroll request. (The enroll request may be modified as a result |
| 93 * of handling it.) |
93 * @param {MessageSender} sender The sender of the message. | 94 * @param {MessageSender} sender The sender of the message. |
94 * @param {Object} request The web page's enroll request. | 95 * @param {Object} request The web page's enroll request. |
95 * @param {string} enrollChallengesName The name of the enroll challenges value | 96 * @param {string} enrollChallengesName The name of the enroll challenges value |
96 * in the request. | 97 * in the request. |
97 * @param {string} signChallengesName The name of the sign challenges value in | 98 * @param {string} signChallengesName The name of the sign challenges value in |
98 * the request. | 99 * the request. |
99 * @param {function(ErrorCodes)} errorCb Error callback. | 100 * @param {function(ErrorCodes)} errorCb Error callback. |
100 * @param {function(string, string, (string|undefined))} successCb Success | 101 * @param {function(string, string, (string|undefined))} successCb Success |
101 * callback. | 102 * callback. |
| 103 * @param {string=} opt_registeredKeysName The name of the registered keys |
| 104 * value in the request. |
102 * @return {Closeable} Request handler that should be closed when the browser | 105 * @return {Closeable} Request handler that should be closed when the browser |
103 * message channel is closed. | 106 * message channel is closed. |
104 */ | 107 */ |
105 function validateAndBeginEnrollRequest(sender, request, | 108 function validateAndBeginEnrollRequest(sender, request, |
106 enrollChallengesName, signChallengesName, errorCb, successCb) { | 109 enrollChallengesName, signChallengesName, errorCb, successCb, |
| 110 opt_registeredKeysName) { |
107 var origin = getOriginFromUrl(/** @type {string} */ (sender.url)); | 111 var origin = getOriginFromUrl(/** @type {string} */ (sender.url)); |
108 if (!origin) { | 112 if (!origin) { |
109 errorCb(ErrorCodes.BAD_REQUEST); | 113 errorCb(ErrorCodes.BAD_REQUEST); |
110 return null; | 114 return null; |
111 } | 115 } |
112 | 116 |
113 if (!isValidEnrollRequest(request, enrollChallengesName, | 117 if (!isValidEnrollRequest(request, enrollChallengesName, |
114 signChallengesName)) { | 118 signChallengesName, opt_registeredKeysName)) { |
115 errorCb(ErrorCodes.BAD_REQUEST); | 119 errorCb(ErrorCodes.BAD_REQUEST); |
116 return null; | 120 return null; |
117 } | 121 } |
118 | 122 |
119 var enrollChallenges = request[enrollChallengesName]; | 123 var enrollChallenges = request[enrollChallengesName]; |
120 var signChallenges = request[signChallengesName]; | 124 var signChallenges; |
| 125 if (opt_registeredKeysName && |
| 126 request.hasOwnProperty(opt_registeredKeysName)) { |
| 127 // Convert registered keys to sign challenges by adding a challenge value. |
| 128 signChallenges = request[opt_registeredKeysName]; |
| 129 for (var i = 0; i < signChallenges.length; i++) { |
| 130 // The actual value doesn't matter, as long as it's a string. |
| 131 signChallenges[i]['challenge'] = ''; |
| 132 } |
| 133 } else { |
| 134 signChallenges = request[signChallengesName]; |
| 135 } |
121 var logMsgUrl = request['logMsgUrl']; | 136 var logMsgUrl = request['logMsgUrl']; |
122 | 137 |
123 var timer = createTimerForRequest( | 138 var timer = createTimerForRequest( |
124 FACTORY_REGISTRY.getCountdownFactory(), request); | 139 FACTORY_REGISTRY.getCountdownFactory(), request); |
125 var enroller = new Enroller(timer, origin, errorCb, successCb, | 140 var enroller = new Enroller(timer, origin, errorCb, successCb, |
126 sender.tlsChannelId, logMsgUrl); | 141 sender.tlsChannelId, logMsgUrl); |
127 enroller.doEnroll(enrollChallenges, signChallenges); | 142 enroller.doEnroll(enrollChallenges, signChallenges, request['appId']); |
128 return /** @type {Closeable} */ (enroller); | 143 return /** @type {Closeable} */ (enroller); |
129 } | 144 } |
130 | 145 |
131 /** | 146 /** |
132 * Returns whether the request appears to be a valid enroll request. | 147 * Returns whether the request appears to be a valid enroll request. |
133 * @param {Object} request The request. | 148 * @param {Object} request The request. |
134 * @param {string} enrollChallengesName The name of the enroll challenges value | 149 * @param {string} enrollChallengesName The name of the enroll challenges value |
135 * in the request. | 150 * in the request. |
136 * @param {string} signChallengesName The name of the sign challenges value in | 151 * @param {string} signChallengesName The name of the sign challenges value in |
137 * the request. | 152 * the request. |
| 153 * @param {string=} opt_registeredKeysName The name of the registered keys |
| 154 * value in the request. |
138 * @return {boolean} Whether the request appears valid. | 155 * @return {boolean} Whether the request appears valid. |
139 */ | 156 */ |
140 function isValidEnrollRequest(request, enrollChallengesName, | 157 function isValidEnrollRequest(request, enrollChallengesName, |
141 signChallengesName) { | 158 signChallengesName, opt_registeredKeysName) { |
142 if (!request.hasOwnProperty(enrollChallengesName)) | 159 if (!request.hasOwnProperty(enrollChallengesName)) |
143 return false; | 160 return false; |
144 var enrollChallenges = request[enrollChallengesName]; | 161 var enrollChallenges = request[enrollChallengesName]; |
145 if (!enrollChallenges.length) | 162 if (!enrollChallenges.length) |
146 return false; | 163 return false; |
147 if (!isValidEnrollChallengeArray(enrollChallenges)) | 164 var hasAppId = request.hasOwnProperty('appId'); |
| 165 if (!isValidEnrollChallengeArray(enrollChallenges, !hasAppId)) |
148 return false; | 166 return false; |
149 var signChallenges = request[signChallengesName]; | 167 var signChallenges = request[signChallengesName]; |
150 // A missing sign challenge array is ok, in the case the user is not already | 168 // A missing sign challenge array is ok, in the case the user is not already |
151 // enrolled. | 169 // enrolled. |
152 if (signChallenges && !isValidSignChallengeArray(signChallenges)) | 170 if (signChallenges && !isValidSignChallengeArray(signChallenges, !hasAppId)) |
153 return false; | 171 return false; |
| 172 if (opt_registeredKeysName) { |
| 173 var registeredKeys = request[opt_registeredKeysName]; |
| 174 if (registeredKeys && |
| 175 !isValidRegisteredKeyArray(registeredKeys, !hasAppId)) { |
| 176 return false; |
| 177 } |
| 178 } |
154 return true; | 179 return true; |
155 } | 180 } |
156 | 181 |
157 /** | 182 /** |
158 * @typedef {{ | 183 * @typedef {{ |
159 * version: (string|undefined), | 184 * version: (string|undefined), |
160 * challenge: string, | 185 * challenge: string, |
161 * appId: string | 186 * appId: string |
162 * }} | 187 * }} |
163 */ | 188 */ |
164 var EnrollChallenge; | 189 var EnrollChallenge; |
165 | 190 |
166 /** | 191 /** |
167 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to | 192 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges to |
168 * validate. | 193 * validate. |
| 194 * @param {boolean} appIdRequired Whether the appId property is required on |
| 195 * each challenge. |
169 * @return {boolean} Whether the given array of challenges is a valid enroll | 196 * @return {boolean} Whether the given array of challenges is a valid enroll |
170 * challenges array. | 197 * challenges array. |
171 */ | 198 */ |
172 function isValidEnrollChallengeArray(enrollChallenges) { | 199 function isValidEnrollChallengeArray(enrollChallenges, appIdRequired) { |
173 var seenVersions = {}; | 200 var seenVersions = {}; |
174 for (var i = 0; i < enrollChallenges.length; i++) { | 201 for (var i = 0; i < enrollChallenges.length; i++) { |
175 var enrollChallenge = enrollChallenges[i]; | 202 var enrollChallenge = enrollChallenges[i]; |
176 var version = enrollChallenge['version']; | 203 var version = enrollChallenge['version']; |
177 if (!version) { | 204 if (!version) { |
178 // Version is implicitly V1 if not specified. | 205 // Version is implicitly V1 if not specified. |
179 version = 'U2F_V1'; | 206 version = 'U2F_V1'; |
180 } | 207 } |
181 if (version != 'U2F_V1' && version != 'U2F_V2') { | 208 if (version != 'U2F_V1' && version != 'U2F_V2') { |
182 return false; | 209 return false; |
183 } | 210 } |
184 if (seenVersions[version]) { | 211 if (seenVersions[version]) { |
185 // Each version can appear at most once. | 212 // Each version can appear at most once. |
186 return false; | 213 return false; |
187 } | 214 } |
188 seenVersions[version] = version; | 215 seenVersions[version] = version; |
189 if (!enrollChallenge['appId']) { | 216 if (appIdRequired && !enrollChallenge['appId']) { |
190 return false; | 217 return false; |
191 } | 218 } |
192 if (!enrollChallenge['challenge']) { | 219 if (!enrollChallenge['challenge']) { |
193 // The challenge is required. | 220 // The challenge is required. |
194 return false; | 221 return false; |
195 } | 222 } |
196 } | 223 } |
197 return true; | 224 return true; |
198 } | 225 } |
199 | 226 |
(...skipping 93 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
293 /** | 320 /** |
294 * Default timeout value in case the caller never provides a valid timeout. | 321 * Default timeout value in case the caller never provides a valid timeout. |
295 */ | 322 */ |
296 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000; | 323 Enroller.DEFAULT_TIMEOUT_MILLIS = 30 * 1000; |
297 | 324 |
298 /** | 325 /** |
299 * Performs an enroll request with the given enroll and sign challenges. | 326 * Performs an enroll request with the given enroll and sign challenges. |
300 * @param {Array.<EnrollChallenge>} enrollChallenges A set of enroll challenges. | 327 * @param {Array.<EnrollChallenge>} enrollChallenges A set of enroll challenges. |
301 * @param {Array.<SignChallenge>} signChallenges A set of sign challenges for | 328 * @param {Array.<SignChallenge>} signChallenges A set of sign challenges for |
302 * existing enrollments for this user and appId. | 329 * existing enrollments for this user and appId. |
| 330 * @param {string=} opt_appId The app id for the entire request. |
303 */ | 331 */ |
304 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges) { | 332 Enroller.prototype.doEnroll = function(enrollChallenges, signChallenges, |
305 var encodedEnrollChallenges = this.encodeEnrollChallenges_(enrollChallenges); | 333 opt_appId) { |
306 var encodedSignChallenges = encodeSignChallenges(signChallenges); | 334 var encodedEnrollChallenges = |
| 335 this.encodeEnrollChallenges_(enrollChallenges, opt_appId); |
| 336 var encodedSignChallenges = encodeSignChallenges(signChallenges, opt_appId); |
307 var request = { | 337 var request = { |
308 type: 'enroll_helper_request', | 338 type: 'enroll_helper_request', |
309 enrollChallenges: encodedEnrollChallenges, | 339 enrollChallenges: encodedEnrollChallenges, |
310 signData: encodedSignChallenges, | 340 signData: encodedSignChallenges, |
311 logMsgUrl: this.logMsgUrl_ | 341 logMsgUrl: this.logMsgUrl_ |
312 }; | 342 }; |
313 if (!this.timer_.expired()) { | 343 if (!this.timer_.expired()) { |
314 request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0; | 344 request.timeout = this.timer_.millisecondsUntilExpired() / 1000.0; |
315 request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0; | 345 request.timeoutSeconds = this.timer_.millisecondsUntilExpired() / 1000.0; |
316 } | 346 } |
317 | 347 |
318 // Begin fetching/checking the app ids. | 348 // Begin fetching/checking the app ids. |
319 var enrollAppIds = []; | 349 var enrollAppIds = []; |
| 350 if (opt_appId) { |
| 351 enrollAppIds.push(opt_appId); |
| 352 } |
320 for (var i = 0; i < enrollChallenges.length; i++) { | 353 for (var i = 0; i < enrollChallenges.length; i++) { |
321 enrollAppIds.push(enrollChallenges[i]['appId']); | 354 if (enrollChallenges[i].hasOwnProperty('appId')) { |
| 355 enrollAppIds.push(enrollChallenges[i]['appId']); |
| 356 } |
| 357 } |
| 358 // Sanity check |
| 359 if (!enrollAppIds.length) { |
| 360 console.warn(UTIL_fmt('empty enroll app ids?')); |
| 361 this.notifyError_(ErrorCodes.BAD_REQUEST); |
| 362 return; |
322 } | 363 } |
323 var self = this; | 364 var self = this; |
324 this.checkAppIds_(enrollAppIds, signChallenges, function(result) { | 365 this.checkAppIds_(enrollAppIds, signChallenges, function(result) { |
325 if (result) { | 366 if (result) { |
326 self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request); | 367 self.handler_ = FACTORY_REGISTRY.getRequestHelper().getHandler(request); |
327 if (self.handler_) { | 368 if (self.handler_) { |
328 var helperComplete = | 369 var helperComplete = |
329 /** @type {function(HelperReply)} */ | 370 /** @type {function(HelperReply)} */ |
330 (self.helperComplete_.bind(self)); | 371 (self.helperComplete_.bind(self)); |
331 self.handler_.run(helperComplete); | 372 self.handler_.run(helperComplete); |
332 } else { | 373 } else { |
333 self.notifyError_(ErrorCodes.OTHER_ERROR); | 374 self.notifyError_(ErrorCodes.OTHER_ERROR); |
334 } | 375 } |
335 } else { | 376 } else { |
336 self.notifyError_(ErrorCodes.BAD_REQUEST); | 377 self.notifyError_(ErrorCodes.BAD_REQUEST); |
337 } | 378 } |
338 }); | 379 }); |
339 }; | 380 }; |
340 | 381 |
341 /** | 382 /** |
342 * Encodes the enroll challenge as an enroll helper challenge. | 383 * Encodes the enroll challenge as an enroll helper challenge. |
343 * @param {EnrollChallenge} enrollChallenge The enroll challenge to encode. | 384 * @param {EnrollChallenge} enrollChallenge The enroll challenge to encode. |
| 385 * @param {string=} opt_appId The app id for the entire request. |
344 * @return {EnrollHelperChallenge} The encoded challenge. | 386 * @return {EnrollHelperChallenge} The encoded challenge. |
345 * @private | 387 * @private |
346 */ | 388 */ |
347 Enroller.encodeEnrollChallenge_ = function(enrollChallenge) { | 389 Enroller.encodeEnrollChallenge_ = function(enrollChallenge, opt_appId) { |
348 var encodedChallenge = {}; | 390 var encodedChallenge = {}; |
349 var version; | 391 var version; |
350 if (enrollChallenge['version']) { | 392 if (enrollChallenge['version']) { |
351 version = enrollChallenge['version']; | 393 version = enrollChallenge['version']; |
352 } else { | 394 } else { |
353 // Version is implicitly V1 if not specified. | 395 // Version is implicitly V1 if not specified. |
354 version = 'U2F_V1'; | 396 version = 'U2F_V1'; |
355 } | 397 } |
356 encodedChallenge['version'] = version; | 398 encodedChallenge['version'] = version; |
357 encodedChallenge['challenge'] = enrollChallenge['challenge']; | 399 encodedChallenge['challengeHash'] = enrollChallenge['challenge']; |
358 encodedChallenge['appIdHash'] = | 400 var appId; |
359 B64_encode(sha256HashOfString(enrollChallenge['appId'])); | 401 if (enrollChallenge['appId']) { |
| 402 appId = enrollChallenge['appId']; |
| 403 } else { |
| 404 appId = opt_appId; |
| 405 } |
| 406 if (!appId) { |
| 407 // Sanity check. (Other code should fail if it's not set.) |
| 408 console.warn(UTIL_fmt('No appId?')); |
| 409 } |
| 410 encodedChallenge['appIdHash'] = B64_encode(sha256HashOfString(appId)); |
360 return /** @type {EnrollHelperChallenge} */ (encodedChallenge); | 411 return /** @type {EnrollHelperChallenge} */ (encodedChallenge); |
361 }; | 412 }; |
362 | 413 |
363 /** | 414 /** |
364 * Encodes the given enroll challenges using this enroller's state. | 415 * Encodes the given enroll challenges using this enroller's state. |
365 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges. | 416 * @param {Array.<EnrollChallenge>} enrollChallenges The enroll challenges. |
| 417 * @param {string=} opt_appId The app id for the entire request. |
366 * @return {!Array.<EnrollHelperChallenge>} The encoded enroll challenges. | 418 * @return {!Array.<EnrollHelperChallenge>} The encoded enroll challenges. |
367 * @private | 419 * @private |
368 */ | 420 */ |
369 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges) { | 421 Enroller.prototype.encodeEnrollChallenges_ = function(enrollChallenges, |
| 422 opt_appId) { |
370 var challenges = []; | 423 var challenges = []; |
371 for (var i = 0; i < enrollChallenges.length; i++) { | 424 for (var i = 0; i < enrollChallenges.length; i++) { |
372 var enrollChallenge = enrollChallenges[i]; | 425 var enrollChallenge = enrollChallenges[i]; |
373 var version = enrollChallenge.version; | 426 var version = enrollChallenge.version; |
374 if (!version) { | 427 if (!version) { |
375 // Version is implicitly V1 if not specified. | 428 // Version is implicitly V1 if not specified. |
376 version = 'U2F_V1'; | 429 version = 'U2F_V1'; |
377 } | 430 } |
378 | 431 |
379 if (version == 'U2F_V2') { | 432 if (version == 'U2F_V2') { |
380 var modifiedChallenge = {}; | 433 var modifiedChallenge = {}; |
381 for (var k in enrollChallenge) { | 434 for (var k in enrollChallenge) { |
382 modifiedChallenge[k] = enrollChallenge[k]; | 435 modifiedChallenge[k] = enrollChallenge[k]; |
383 } | 436 } |
384 // V2 enroll responses contain signatures over a browser data object, | 437 // V2 enroll responses contain signatures over a browser data object, |
385 // which we're constructing here. The browser data object contains, among | 438 // which we're constructing here. The browser data object contains, among |
386 // other things, the server challenge. | 439 // other things, the server challenge. |
387 var serverChallenge = enrollChallenge['challenge']; | 440 var serverChallenge = enrollChallenge['challenge']; |
388 var browserData = makeEnrollBrowserData( | 441 var browserData = makeEnrollBrowserData( |
389 serverChallenge, this.origin_, this.tlsChannelId_); | 442 serverChallenge, this.origin_, this.tlsChannelId_); |
390 // Replace the challenge with the hash of the browser data. | 443 // Replace the challenge with the hash of the browser data. |
391 modifiedChallenge['challenge'] = | 444 modifiedChallenge['challenge'] = |
392 B64_encode(sha256HashOfString(browserData)); | 445 B64_encode(sha256HashOfString(browserData)); |
393 this.browserData_[version] = | 446 this.browserData_[version] = |
394 B64_encode(UTIL_StringToBytes(browserData)); | 447 B64_encode(UTIL_StringToBytes(browserData)); |
395 challenges.push(Enroller.encodeEnrollChallenge_( | 448 challenges.push(Enroller.encodeEnrollChallenge_( |
396 /** @type {EnrollChallenge} */ (modifiedChallenge))); | 449 /** @type {EnrollChallenge} */ (modifiedChallenge), opt_appId)); |
397 } else { | 450 } else { |
398 challenges.push(Enroller.encodeEnrollChallenge_(enrollChallenge)); | 451 challenges.push( |
| 452 Enroller.encodeEnrollChallenge_(enrollChallenge, opt_appId)); |
399 } | 453 } |
400 } | 454 } |
401 return challenges; | 455 return challenges; |
402 }; | 456 }; |
403 | 457 |
404 /** | 458 /** |
405 * Checks the app ids associated with this enroll request, and calls a callback | 459 * Checks the app ids associated with this enroll request, and calls a callback |
406 * with the result of the check. | 460 * with the result of the check. |
407 * @param {!Array.<string>} enrollAppIds The app ids in the enroll challenge | 461 * @param {!Array.<string>} enrollAppIds The app ids in the enroll challenge |
408 * portion of the enroll request. | 462 * portion of the enroll request. |
(...skipping 89 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
498 // For U2F_V2, the challenge sent to the gnubby is modified to be the hash | 552 // For U2F_V2, the challenge sent to the gnubby is modified to be the hash |
499 // of the browser data. Include the browser data. | 553 // of the browser data. Include the browser data. |
500 browserData = this.browserData_[reply.version]; | 554 browserData = this.browserData_[reply.version]; |
501 } | 555 } |
502 | 556 |
503 this.notifySuccess_(/** @type {string} */ (reply.version), | 557 this.notifySuccess_(/** @type {string} */ (reply.version), |
504 /** @type {string} */ (reply.enrollData), | 558 /** @type {string} */ (reply.enrollData), |
505 browserData); | 559 browserData); |
506 } | 560 } |
507 }; | 561 }; |
OLD | NEW |