"use strict"; // Encodes |data| into base64url string. There is no '=' padding, and the // characters '-' and '_' must be used instead of '+' and '/', respectively. function base64urlEncode(data) { let result = btoa(data); return result.replace(/=+$/g, '').replace(/\+/g, "-").replace(/\//g, "_"); } // Decode |encoded| using base64url decoding. function base64urlDecode(encoded) { return atob(encoded.replace(/\-/g, "+").replace(/\_/g, "/")); } // Encodes a Uint8Array as a base64url string. function uint8ArrayToBase64url(array) { return base64urlEncode(String.fromCharCode.apply(null, array)); } // Encodes a Uint8Array to lowercase hex. function uint8ArrayToHex(array) { const hexTable = '0123456789abcdef'; let s = ''; for (let i = 0; i < array.length; i++) { s += hexTable.charAt(array[i] >> 4); s += hexTable.charAt(array[i] & 15); } return s; } // Convert a EC signature from DER to a concatenation of the r and s parameters, // as expected by the subtle crypto API. function convertDERSignatureToSubtle(der) { let index = -1; const SEQUENCE = 0x30; const INTEGER = 0x02; assert_equals(der[++index], SEQUENCE); let size = der[++index]; assert_equals(size + 2, der.length); assert_equals(der[++index], INTEGER); let rSize = der[++index]; ++index; while (der[index] == 0) { ++index; --rSize; } let r = der.slice(index, index + rSize); index += rSize; assert_equals(der[index], INTEGER); let sSize = der[++index]; ++index; while (der[index] == 0) { ++index; --sSize; } let s = der.slice(index, index + sSize); assert_equals(index + sSize, der.length); let result = new Uint8Array(64); result.set(r, 32 - rSize); result.set(s, 64 - sSize); return result; }; function coseObjectToJWK(cose) { // Convert an object representing a COSE_Key encoded public key into a JSON // Web Key object. // https://tools.ietf.org/html/rfc7517 // The example used on the test is a ES256 key, so we only implement that. let jwk = {}; if (cose.type != 2) assert_unreached("Unknown type: " + cose.type); jwk.kty = "EC"; if (cose.alg != ES256_ID) assert_unreached("Unknown alg: " + cose.alg); if (cose.crv != 1) assert_unreached("Unknown curve: " + jwk.crv); jwk.crv = "P-256"; jwk.x = uint8ArrayToBase64url(cose.x); jwk.y = uint8ArrayToBase64url(cose.y); return jwk; } function parseCosePublicKey(coseKey) { // Parse a CTAP2 canonical CBOR encoding form key. // https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#ctap2-canonical-cbor-encoding-form let parsed = new Cbor(coseKey); let cbor = parsed.getCBOR(); let key = { type: cbor[1], alg: cbor[3], }; if (key.type != 2) assert_unreached("Unknown key type: " + key.type); key.crv = cbor[-1]; key.x = new Uint8Array(cbor[-2]); key.y = new Uint8Array(cbor[-3]); return key; } function parseAttestedCredentialData(attestedCredentialData) { // Parse the attested credential data according to // https://w3c.github.io/webauthn/#attested-credential-data let aaguid = attestedCredentialData.slice(0, 16); let credentialIdLength = (attestedCredentialData[16] << 8) + attestedCredentialData[17]; let credentialId = attestedCredentialData.slice(18, 18 + credentialIdLength); let credentialPublicKey = parseCosePublicKey( attestedCredentialData.slice(18 + credentialIdLength, attestedCredentialData.length)); return { aaguid, credentialIdLength, credentialId, credentialPublicKey }; } function parseAuthenticatorData(authenticatorData) { // Parse the authenticator data according to // https://w3c.github.io/webauthn/#sctn-authenticator-data assert_greater_than_equal(authenticatorData.length, 37); let flags = authenticatorData[32]; let counter = authenticatorData.slice(33, 37); let attestedCredentialData = authenticatorData.length > 37 ? parseAttestedCredentialData(authenticatorData.slice(37)) : null; let extensions = null; if (attestedCredentialData && authenticatorData.length > 37 + attestedCredentialData.length) { extensions = authenticatorData.slice(37 + attestedCredentialData.length); } return { rpIdHash: authenticatorData.slice(0, 32), flags: { up: !!(flags & 0x01), uv: !!(flags & 0x04), at: !!(flags & 0x40), ed: !!(flags & 0x80), }, counter: (counter[0] << 24) + (counter[1] << 16) + (counter[2] << 8) + counter[3], attestedCredentialData, extensions, }; } // Taken from // https://cs.chromium.org/chromium/src/chrome/browser/resources/cryptotoken/cbor.js?rcl=c9b6055cf9c158fb4119afd561a591f8fc95aefe class Cbor { constructor(buffer) { this.slice = new Uint8Array(buffer); } get data() { return this.slice; } get length() { return this.slice.length; } get empty() { return this.slice.length == 0; } get hex() { return uint8ArrayToHex(this.data); } compare(other) { if (this.length < other.length) { return -1; } else if (this.length > other.length) { return 1; } for (let i = 0; i < this.length; i++) { if (this.slice[i] < other.slice[i]) { return -1; } else if (this.slice[i] > other.slice[i]) { return 1; } } return 0; } getU8() { if (this.empty) { throw('Cbor: empty during getU8'); } const byte = this.slice[0]; this.slice = this.slice.subarray(1); return byte; } skip(n) { if (this.length < n) { throw('Cbor: too few bytes to skip'); } this.slice = this.slice.subarray(n); } getBytes(n) { if (this.length < n) { throw('Cbor: insufficient bytes in getBytes'); } const ret = this.slice.subarray(0, n); this.slice = this.slice.subarray(n); return ret; } getCBORHeader() { const copy = new Cbor(this.slice); const a = this.getU8(); const majorType = a >> 5; const info = a & 31; if (info < 24) { return [majorType, info, new Cbor(copy.getBytes(1))]; } else if (info < 28) { const lengthLength = 1 << (info - 24); let data = this.getBytes(lengthLength); let value = 0; for (let i = 0; i < lengthLength; i++) { // Javascript has problems handling uint64s given the limited range of // a double. if (value > 35184372088831) { throw('Cbor: cannot represent CBOR number'); } // Not using bitwise operations to avoid truncating to 32 bits. value *= 256; value += data[i]; } switch (lengthLength) { case 1: if (value < 24) { throw('Cbor: value should have been encoded in single byte'); } break; case 2: if (value < 256) { throw('Cbor: non-minimal integer'); } break; case 4: if (value < 65536) { throw('Cbor: non-minimal integer'); } break; case 8: if (value < 4294967296) { throw('Cbor: non-minimal integer'); } break; } return [majorType, value, new Cbor(copy.getBytes(1 + lengthLength))]; } else { throw('Cbor: CBOR contains unhandled info value ' + info); } } getCBOR() { const [major, value] = this.getCBORHeader(); switch (major) { case 0: return value; case 1: return 0 - (1 + value); case 2: return this.getBytes(value); case 3: return this.getBytes(value); case 4: { let ret = new Array(value); for (let i = 0; i < value; i++) { ret[i] = this.getCBOR(); } return ret; } case 5: if (value == 0) { return {}; } let copy = new Cbor(this.data); const [firstKeyMajor] = copy.getCBORHeader(); if (firstKeyMajor == 3) { // String-keyed map. let lastKeyHeader = new Cbor(new Uint8Array(0)); let lastKeyBytes = new Cbor(new Uint8Array(0)); let ret = {}; for (let i = 0; i < value; i++) { const [keyMajor, keyLength, keyHeader] = this.getCBORHeader(); if (keyMajor != 3) { throw('Cbor: non-string in string-valued map'); } const keyBytes = new Cbor(this.getBytes(keyLength)); if (i > 0) { const headerCmp = lastKeyHeader.compare(keyHeader); if (headerCmp > 0 || (headerCmp == 0 && lastKeyBytes.compare(keyBytes) >= 0)) { throw( 'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + '/' + lastKeyBytes.hex + ' ' + keyHeader.hex + '/' + keyBytes.hex); } } lastKeyHeader = keyHeader; lastKeyBytes = keyBytes; ret[keyBytes.parseUTF8()] = this.getCBOR(); } return ret; } else if (firstKeyMajor == 0 || firstKeyMajor == 1) { // Number-keyed map. let lastKeyHeader = new Cbor(new Uint8Array(0)); let ret = {}; for (let i = 0; i < value; i++) { let [keyMajor, keyValue, keyHeader] = this.getCBORHeader(); if (keyMajor != 0 && keyMajor != 1) { throw('Cbor: non-number in number-valued map'); } if (i > 0 && lastKeyHeader.compare(keyHeader) >= 0) { throw( 'Cbor: map keys in wrong order: ' + lastKeyHeader.hex + ' ' + keyHeader.hex); } lastKeyHeader = keyHeader; if (keyMajor == 1) { keyValue = 0 - (1 + keyValue); } ret[keyValue] = this.getCBOR(); } return ret; } else { throw('Cbor: map keyed by invalid major type ' + firstKeyMajor); } default: throw('Cbor: unhandled major type ' + major); } } parseUTF8() { return (new TextDecoder('utf-8')).decode(this.slice); } }