| "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); |
| } |
| } |