utils.js (10220B)
1 "use strict"; 2 3 // Encodes |data| into base64url string. There is no '=' padding, and the 4 // characters '-' and '_' must be used instead of '+' and '/', respectively. 5 function base64urlEncode(data) { 6 let result = btoa(data); 7 return result.replace(/=+$/g, '').replace(/\+/g, "-").replace(/\//g, "_"); 8 } 9 10 // Decode |encoded| using base64url decoding. 11 function base64urlDecode(encoded) { 12 return atob(encoded.replace(/\-/g, "+").replace(/\_/g, "/")); 13 } 14 15 // Encodes a Uint8Array as a base64url string. 16 function uint8ArrayToBase64url(array) { 17 return base64urlEncode(String.fromCharCode.apply(null, array)); 18 } 19 20 // Encodes a Uint8Array to lowercase hex. 21 function uint8ArrayToHex(array) { 22 const hexTable = '0123456789abcdef'; 23 let s = ''; 24 for (let i = 0; i < array.length; i++) { 25 s += hexTable.charAt(array[i] >> 4); 26 s += hexTable.charAt(array[i] & 15); 27 } 28 return s; 29 } 30 31 // Convert a EC signature from DER to a concatenation of the r and s parameters, 32 // as expected by the subtle crypto API. 33 function convertDERSignatureToSubtle(der) { 34 let index = -1; 35 const SEQUENCE = 0x30; 36 const INTEGER = 0x02; 37 assert_equals(der[++index], SEQUENCE); 38 39 let size = der[++index]; 40 assert_equals(size + 2, der.length); 41 42 assert_equals(der[++index], INTEGER); 43 let rSize = der[++index]; 44 ++index; 45 while (der[index] == 0) { 46 ++index; 47 --rSize; 48 } 49 let r = der.slice(index, index + rSize); 50 index += rSize; 51 52 assert_equals(der[index], INTEGER); 53 let sSize = der[++index]; 54 ++index; 55 while (der[index] == 0) { 56 ++index; 57 --sSize; 58 } 59 let s = der.slice(index, index + sSize); 60 assert_equals(index + sSize, der.length); 61 62 let result = new Uint8Array(64); 63 result.set(r, 32 - rSize); 64 result.set(s, 64 - sSize); 65 return result; 66 }; 67 68 function coseObjectToJWK(cose) { 69 // Convert an object representing a COSE_Key encoded public key into a JSON 70 // Web Key object. 71 // https://tools.ietf.org/html/rfc7517 72 73 // The example used on the test is a ES256 key, so we only implement that. 74 let jwk = {}; 75 if (cose.type != 2) 76 assert_unreached("Unknown type: " + cose.type); 77 78 jwk.kty = "EC"; 79 if (cose.alg != ES256_ID) 80 assert_unreached("Unknown alg: " + cose.alg); 81 82 if (cose.crv != 1) 83 assert_unreached("Unknown curve: " + jwk.crv); 84 85 jwk.crv = "P-256"; 86 jwk.x = uint8ArrayToBase64url(cose.x); 87 jwk.y = uint8ArrayToBase64url(cose.y); 88 return jwk; 89 } 90 91 function parseCosePublicKey(coseKey) { 92 // Parse a CTAP2 canonical CBOR encoding form key. 93 // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#ctap2-canonical-cbor-encoding-form 94 let parsed = new Cbor(coseKey); 95 let cbor = parsed.getCBOR(); 96 let key = { 97 type: cbor[1], 98 alg: cbor[3], 99 }; 100 if (key.type != 2) 101 assert_unreached("Unknown key type: " + key.type); 102 103 key.crv = cbor[-1]; 104 key.x = new Uint8Array(cbor[-2]); 105 key.y = new Uint8Array(cbor[-3]); 106 return key; 107 } 108 109 function parseAttestedCredentialData(attestedCredentialData) { 110 // Parse the attested credential data according to 111 // https://w3c.github.io/webauthn/#attested-credential-data 112 let aaguid = attestedCredentialData.slice(0, 16); 113 let credentialIdLength = (attestedCredentialData[16] << 8) 114 + attestedCredentialData[17]; 115 let credentialId = 116 attestedCredentialData.slice(18, 18 + credentialIdLength); 117 let credentialPublicKey = parseCosePublicKey( 118 attestedCredentialData.slice(18 + credentialIdLength, 119 attestedCredentialData.length)); 120 121 return { aaguid, credentialIdLength, credentialId, credentialPublicKey }; 122 } 123 124 function parseAuthenticatorData(authenticatorData) { 125 // Parse the authenticator data according to 126 // https://w3c.github.io/webauthn/#sctn-authenticator-data 127 assert_greater_than_equal(authenticatorData.length, 37); 128 let flags = authenticatorData[32]; 129 let counter = authenticatorData.slice(33, 37); 130 131 let attestedCredentialData = authenticatorData.length > 37 ? 132 parseAttestedCredentialData(authenticatorData.slice(37)) : null; 133 let extensions = null; 134 if (attestedCredentialData && 135 authenticatorData.length > 37 + attestedCredentialData.length) { 136 extensions = authenticatorData.slice(37 + attestedCredentialData.length); 137 } 138 139 return { 140 rpIdHash: authenticatorData.slice(0, 32), 141 flags: { 142 up: !!(flags & 0x01), 143 uv: !!(flags & 0x04), 144 at: !!(flags & 0x40), 145 ed: !!(flags & 0x80), 146 }, 147 counter: (counter[0] << 24) 148 + (counter[1] << 16) 149 + (counter[2] << 8) 150 + counter[3], 151 attestedCredentialData, 152 extensions, 153 }; 154 } 155 156 // Taken from 157 // https://cs.chromium.org/chromium/src/chrome/browser/resources/cryptotoken/cbor.js?rcl=c9b6055cf9c158fb4119afd561a591f8fc95aefe 158 class Cbor { 159 constructor(buffer) { 160 this.slice = new Uint8Array(buffer); 161 } 162 get data() { 163 return this.slice; 164 } 165 get length() { 166 return this.slice.length; 167 } 168 get empty() { 169 return this.slice.length == 0; 170 } 171 get hex() { 172 return uint8ArrayToHex(this.data); 173 } 174 compare(other) { 175 if (this.length < other.length) { 176 return -1; 177 } else if (this.length > other.length) { 178 return 1; 179 } 180 for (let i = 0; i < this.length; i++) { 181 if (this.slice[i] < other.slice[i]) { 182 return -1; 183 } else if (this.slice[i] > other.slice[i]) { 184 return 1; 185 } 186 } 187 return 0; 188 } 189 getU8() { 190 if (this.empty) { 191 throw('Cbor: empty during getU8'); 192 } 193 const byte = this.slice[0]; 194 this.slice = this.slice.subarray(1); 195 return byte; 196 } 197 skip(n) { 198 if (this.length < n) { 199 throw('Cbor: too few bytes to skip'); 200 } 201 this.slice = this.slice.subarray(n); 202 } 203 getBytes(n) { 204 if (this.length < n) { 205 throw('Cbor: insufficient bytes in getBytes'); 206 } 207 const ret = this.slice.subarray(0, n); 208 this.slice = this.slice.subarray(n); 209 return ret; 210 } 211 getCBORHeader() { 212 const copy = new Cbor(this.slice); 213 const a = this.getU8(); 214 const majorType = a >> 5; 215 const info = a & 31; 216 if (info < 24) { 217 return [majorType, info, new Cbor(copy.getBytes(1))]; 218 } else if (info < 28) { 219 const lengthLength = 1 << (info - 24); 220 let data = this.getBytes(lengthLength); 221 let value = 0; 222 for (let i = 0; i < lengthLength; i++) { 223 // Javascript has problems handling uint64s given the limited range of 224 // a double. 225 if (value > 35184372088831) { 226 throw('Cbor: cannot represent CBOR number'); 227 } 228 // Not using bitwise operations to avoid truncating to 32 bits. 229 value *= 256; 230 value += data[i]; 231 } 232 switch (lengthLength) { 233 case 1: 234 if (value < 24) { 235 throw('Cbor: value should have been encoded in single byte'); 236 } 237 break; 238 case 2: 239 if (value < 256) { 240 throw('Cbor: non-minimal integer'); 241 } 242 break; 243 case 4: 244 if (value < 65536) { 245 throw('Cbor: non-minimal integer'); 246 } 247 break; 248 case 8: 249 if (value < 4294967296) { 250 throw('Cbor: non-minimal integer'); 251 } 252 break; 253 } 254 return [majorType, value, new Cbor(copy.getBytes(1 + lengthLength))]; 255 } else { 256 throw('Cbor: CBOR contains unhandled info value ' + info); 257 } 258 } 259 getCBOR() { 260 const [major, value] = this.getCBORHeader(); 261 switch (major) { 262 case 0: 263 return value; 264 case 1: 265 return 0 - (1 + value); 266 case 2: 267 return this.getBytes(value); 268 case 3: 269 return this.getBytes(value); 270 case 4: { 271 let ret = new Array(value); 272 for (let i = 0; i < value; i++) { 273 ret[i] = this.getCBOR(); 274 } 275 return ret; 276 } 277 case 5: 278 if (value == 0) { 279 return {}; 280 } 281 let copy = new Cbor(this.data); 282 const [firstKeyMajor] = copy.getCBORHeader(); 283 if (firstKeyMajor == 3) { 284 // String-keyed map. 285 let lastKeyHeader = new Cbor(new Uint8Array(0)); 286 let lastKeyBytes = new Cbor(new Uint8Array(0)); 287 let ret = {}; 288 for (let i = 0; i < value; i++) { 289 const [keyMajor, keyLength, keyHeader] = this.getCBORHeader(); 290 if (keyMajor != 3) { 291 throw('Cbor: non-string in string-valued map'); 292 } 293 const keyBytes = new Cbor(this.getBytes(keyLength)); 294 if (i > 0) { 295 const headerCmp = lastKeyHeader.compare(keyHeader); 296 if (headerCmp > 0 || 297 (headerCmp == 0 && lastKeyBytes.compare(keyBytes) >= 0)) { 298 throw( 299 'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + 300 '/' + lastKeyBytes.hex + ' ' + keyHeader.hex + '/' + 301 keyBytes.hex); 302 } 303 } 304 lastKeyHeader = keyHeader; 305 lastKeyBytes = keyBytes; 306 ret[keyBytes.parseUTF8()] = this.getCBOR(); 307 } 308 return ret; 309 } else if (firstKeyMajor == 0 || firstKeyMajor == 1) { 310 // Number-keyed map. 311 let lastKeyHeader = new Cbor(new Uint8Array(0)); 312 let ret = {}; 313 for (let i = 0; i < value; i++) { 314 let [keyMajor, keyValue, keyHeader] = this.getCBORHeader(); 315 if (keyMajor != 0 && keyMajor != 1) { 316 throw('Cbor: non-number in number-valued map'); 317 } 318 if (i > 0 && lastKeyHeader.compare(keyHeader) >= 0) { 319 throw( 320 'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + ' ' + 321 keyHeader.hex); 322 } 323 lastKeyHeader = keyHeader; 324 if (keyMajor == 1) { 325 keyValue = 0 - (1 + keyValue); 326 } 327 ret[keyValue] = this.getCBOR(); 328 } 329 return ret; 330 } else { 331 throw('Cbor: map keyed by invalid major type ' + firstKeyMajor); 332 } 333 default: 334 throw('Cbor: unhandled major type ' + major); 335 } 336 } 337 parseUTF8() { 338 return (new TextDecoder('utf-8')).decode(this.slice); 339 } 340 }