forked from google/u2f-ref-code
-
Notifications
You must be signed in to change notification settings - Fork 0
/
webrequest.js
421 lines (395 loc) · 14.1 KB
/
webrequest.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
// Copyright 2014 Google Inc. All rights reserved
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
/**
* @fileoverview Does common handling for requests coming from web pages and
* routes them to the provided handler.
*/
/**
* FIDO U2F Javascript API Version
* @const
* @type {number}
*/
var JS_API_VERSION = 1.1;
/**
* Gets the scheme + origin from a web url.
* @param {string} url Input url
* @return {?string} Scheme and origin part if url parses
*/
function getOriginFromUrl(url) {
var re = new RegExp('^(https?://)[^/]*/?');
var originarray = re.exec(url);
if (originarray == null) return originarray;
var origin = originarray[0];
while (origin.charAt(origin.length - 1) == '/') {
origin = origin.substring(0, origin.length - 1);
}
if (origin == 'http:' || origin == 'https:')
return null;
return origin;
}
/**
* Returns whether the registered key appears to be valid.
* @param {Object} registeredKey The registered key object.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the object appears valid.
*/
function isValidRegisteredKey(registeredKey, appIdRequired) {
if (appIdRequired && !registeredKey.hasOwnProperty('appId')) {
return false;
}
if (!registeredKey.hasOwnProperty('keyHandle'))
return false;
if (registeredKey['version']) {
if (registeredKey['version'] != 'U2F_V1' &&
registeredKey['version'] != 'U2F_V2') {
return false;
}
}
return true;
}
/**
* Returns whether the array of registered keys appears to be valid.
* @param {Array<Object>} registeredKeys The array of registered keys.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the array appears valid.
*/
function isValidRegisteredKeyArray(registeredKeys, appIdRequired) {
return registeredKeys.every(function(key) {
return isValidRegisteredKey(key, appIdRequired);
});
}
/**
* Gets the sign challenges from the request. The sign challenges may be the
* U2F 1.0 variant, signRequests, or the U2F 1.1 version, registeredKeys.
* @param {Object} request The request.
* @return {!Array<SignChallenge>|undefined} The sign challenges, if found.
*/
function getSignChallenges(request) {
if (!request) {
return undefined;
}
var signChallenges;
if (request.hasOwnProperty('signRequests')) {
signChallenges = request['signRequests'];
} else if (request.hasOwnProperty('registeredKeys')) {
signChallenges = request['registeredKeys'];
}
return signChallenges;
}
/**
* Returns whether the array of SignChallenges appears to be valid.
* @param {Array<SignChallenge>} signChallenges The array of sign challenges.
* @param {boolean} challengeValueRequired Whether each challenge object
* requires a challenge value.
* @param {boolean} appIdRequired Whether the appId property is required on
* each challenge.
* @return {boolean} Whether the array appears valid.
*/
function isValidSignChallengeArray(signChallenges, challengeValueRequired,
appIdRequired) {
for (var i = 0; i < signChallenges.length; i++) {
var incomingChallenge = signChallenges[i];
if (challengeValueRequired &&
!incomingChallenge.hasOwnProperty('challenge'))
return false;
if (!isValidRegisteredKey(incomingChallenge, appIdRequired)) {
return false;
}
}
return true;
}
/**
* @param {Object} request Request object
* @param {MessageSender} sender Sender frame
* @param {Function} sendResponse Response callback
* @return {?Closeable} Optional handler object that should be closed when port
* closes
*/
function handleWebPageRequest(request, sender, sendResponse) {
switch (request.type) {
case MessageTypes.U2F_REGISTER_REQUEST:
return handleU2fEnrollRequest(sender, request, sendResponse);
case MessageTypes.U2F_SIGN_REQUEST:
return handleU2fSignRequest(sender, request, sendResponse);
case MessageTypes.U2F_GET_API_VERSION_REQUEST:
sendResponse(
makeU2fGetApiVersionResponse(request, JS_API_VERSION,
MessageTypes.U2F_GET_API_VERSION_RESPONSE));
return null;
default:
sendResponse(
makeU2fErrorResponse(request, ErrorCodes.BAD_REQUEST, undefined,
MessageTypes.U2F_REGISTER_RESPONSE));
return null;
}
}
/**
* Makes a response to a request.
* @param {Object} request The request to make a response to.
* @param {string} responseSuffix How to name the response's type.
* @param {string=} opt_defaultType The default response type, if none is
* present in the request.
* @return {Object} The response object.
*/
function makeResponseForRequest(request, responseSuffix, opt_defaultType) {
var type;
if (request && request.type) {
type = request.type.replace(/_request$/, responseSuffix);
} else {
type = opt_defaultType;
}
var reply = { 'type': type };
if (request && request.requestId) {
reply.requestId = request.requestId;
}
return reply;
}
/**
* Makes a response to a U2F request with an error code.
* @param {Object} request The request to make a response to.
* @param {ErrorCodes} code The error code to return.
* @param {string=} opt_detail An error detail string.
* @param {string=} opt_defaultType The default response type, if none is
* present in the request.
* @return {Object} The U2F error.
*/
function makeU2fErrorResponse(request, code, opt_detail, opt_defaultType) {
var reply = makeResponseForRequest(request, '_response', opt_defaultType);
var error = {'errorCode': code};
if (opt_detail) {
error['errorMessage'] = opt_detail;
}
reply['responseData'] = error;
return reply;
}
/**
* Makes a success response to a web request with a responseData object.
* @param {Object} request The request to make a response to.
* @param {Object} responseData The response data.
* @return {Object} The web error.
*/
function makeU2fSuccessResponse(request, responseData) {
var reply = makeResponseForRequest(request, '_response');
reply['responseData'] = responseData;
return reply;
}
/**
* Maps a helper's error code from the DeviceStatusCodes namespace to a
* U2fError.
* @param {number} code Error code from DeviceStatusCodes namespace.
* @return {U2fError} An error.
*/
function mapDeviceStatusCodeToU2fError(code) {
switch (code) {
case DeviceStatusCodes.WRONG_DATA_STATUS:
return {errorCode: ErrorCodes.DEVICE_INELIGIBLE};
case DeviceStatusCodes.TIMEOUT_STATUS:
case DeviceStatusCodes.WAIT_TOUCH_STATUS:
return {errorCode: ErrorCodes.TIMEOUT};
default:
var reportedError = {
errorCode: ErrorCodes.OTHER_ERROR,
errorMessage: 'device status code: ' + code.toString(16)
};
return reportedError;
}
}
/**
* Sends a response, using the given sentinel to ensure at most one response is
* sent. Also closes the closeable, if it's given.
* @param {boolean} sentResponse Whether a response has already been sent.
* @param {?Closeable} closeable A thing to close.
* @param {*} response The response to send.
* @param {Function} sendResponse A function to send the response.
*/
function sendResponseOnce(sentResponse, closeable, response, sendResponse) {
if (closeable) {
closeable.close();
}
if (!sentResponse) {
sentResponse = true;
try {
// If the page has gone away or the connection has otherwise gone,
// sendResponse fails.
sendResponse(response);
} catch (exception) {
console.warn('sendResponse failed: ' + exception);
}
} else {
console.warn(UTIL_fmt('Tried to reply more than once! Juan, FIX ME'));
}
}
/**
* @param {!string} string Input string
* @return {Array<number>} SHA256 hash value of string.
*/
function sha256HashOfString(string) {
var s = new SHA256();
s.update(UTIL_StringToBytes(string));
return s.digest();
}
/**
* Normalizes the TLS channel ID value:
* 1. Converts semantically empty values (undefined, null, 0) to the empty
* string.
* 2. Converts valid JSON strings to a JS object.
* 3. Otherwise, returns the input value unmodified.
* @param {Object|string|undefined} opt_tlsChannelId TLS Channel id
* @return {Object|string} The normalized TLS channel ID value.
*/
function tlsChannelIdValue(opt_tlsChannelId) {
if (!opt_tlsChannelId) {
// Case 1: Always set some value for TLS channel ID, even if it's the empty
// string: this browser definitely supports them.
return '';
}
if (typeof opt_tlsChannelId === 'string') {
try {
var obj = JSON.parse(opt_tlsChannelId);
if (!obj) {
// Case 1: The string value 'null' parses as the Javascript object null,
// so return an empty string: the browser definitely supports TLS
// channel id.
return '';
}
// Case 2: return the value as a JS object.
return /** @type {Object} */ (obj);
} catch (e) {
console.warn('Unparseable TLS channel ID value ' + opt_tlsChannelId);
// Case 3: return the value unmodified.
}
}
return opt_tlsChannelId;
}
/**
* Creates a browser data object with the given values.
* @param {!string} type A string representing the "type" of this browser data
* object.
* @param {!string} serverChallenge The server's challenge, as a base64-
* encoded string.
* @param {!string} origin The server's origin, as seen by the browser.
* @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
* @return {string} A string representation of the browser data object.
*/
function makeBrowserData(type, serverChallenge, origin, opt_tlsChannelId) {
var browserData = {
'typ' : type,
'challenge' : serverChallenge,
'origin' : origin
};
if (BROWSER_SUPPORTS_TLS_CHANNEL_ID) {
browserData['cid_pubkey'] = tlsChannelIdValue(opt_tlsChannelId);
}
return JSON.stringify(browserData);
}
/**
* Creates a browser data object for an enroll request with the given values.
* @param {!string} serverChallenge The server's challenge, as a base64-
* encoded string.
* @param {!string} origin The server's origin, as seen by the browser.
* @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
* @return {string} A string representation of the browser data object.
*/
function makeEnrollBrowserData(serverChallenge, origin, opt_tlsChannelId) {
return makeBrowserData(
'navigator.id.finishEnrollment', serverChallenge, origin,
opt_tlsChannelId);
}
/**
* Creates a browser data object for a sign request with the given values.
* @param {!string} serverChallenge The server's challenge, as a base64-
* encoded string.
* @param {!string} origin The server's origin, as seen by the browser.
* @param {Object|string|undefined} opt_tlsChannelId TLS Channel Id
* @return {string} A string representation of the browser data object.
*/
function makeSignBrowserData(serverChallenge, origin, opt_tlsChannelId) {
return makeBrowserData(
'navigator.id.getAssertion', serverChallenge, origin, opt_tlsChannelId);
}
/**
* Makes a response to a U2F request with an error code.
* @param {Object} request The request to make a response to.
* @param {number=} version The JS API version to return.
* @param {string=} opt_defaultType The default response type, if none is
* present in the request.
* @return {Object} The GetJsApiVersionResponse.
*/
function makeU2fGetApiVersionResponse(request, version, opt_defaultType) {
var reply = makeResponseForRequest(request, '_response', opt_defaultType);
var data = {'js_api_version': version};
reply['responseData'] = data;
return reply;
}
/**
* Encodes the sign data as an array of sign helper challenges.
* @param {Array<SignChallenge>} signChallenges The sign challenges to encode.
* @param {string|undefined} opt_defaultChallenge A default sign challenge
* value, if a request does not provide one.
* @param {string=} opt_defaultAppId The app id to use for each challenge, if
* the challenge contains none.
* @param {function(string, string): string=} opt_challengeHashFunction
* A function that produces, from a key handle and a raw challenge, a hash
* of the raw challenge. If none is provided, a default hash function is
* used.
* @return {!Array<SignHelperChallenge>} The sign challenges, encoded.
*/
function encodeSignChallenges(signChallenges, opt_defaultChallenge,
opt_defaultAppId, opt_challengeHashFunction) {
function encodedSha256(keyHandle, challenge) {
return B64_encode(sha256HashOfString(challenge));
}
var challengeHashFn = opt_challengeHashFunction || encodedSha256;
var encodedSignChallenges = [];
if (signChallenges) {
for (var i = 0; i < signChallenges.length; i++) {
var challenge = signChallenges[i];
var keyHandle = challenge['keyHandle'];
var challengeValue;
if (challenge.hasOwnProperty('challenge')) {
challengeValue = challenge['challenge'];
} else {
challengeValue = opt_defaultChallenge;
}
var challengeHash = challengeHashFn(keyHandle, challengeValue);
var appId;
if (challenge.hasOwnProperty('appId')) {
appId = challenge['appId'];
} else {
appId = opt_defaultAppId;
}
var encodedChallenge = {
'challengeHash': challengeHash,
'appIdHash': B64_encode(sha256HashOfString(appId)),
'keyHandle': keyHandle,
'version': (challenge['version'] || 'U2F_V1')
};
encodedSignChallenges.push(encodedChallenge);
}
}
return encodedSignChallenges;
}
/**
* Makes a sign helper request from an array of challenges.
* @param {Array<SignHelperChallenge>} challenges The sign challenges.
* @param {number=} opt_timeoutSeconds Timeout value.
* @param {string=} opt_logMsgUrl URL to log to.
* @return {SignHelperRequest} The sign helper request.
*/
function makeSignHelperRequest(challenges, opt_timeoutSeconds, opt_logMsgUrl) {
var request = {
'type': 'sign_helper_request',
'signData': challenges,
'timeout': opt_timeoutSeconds || 0,
'timeoutSeconds': opt_timeoutSeconds || 0
};
if (opt_logMsgUrl !== undefined) {
request.logMsgUrl = opt_logMsgUrl;
}
return request;
}