tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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 }