tor-browser

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

ba-fledge-util.sub.js (14030B)


      1 'use strict';
      2 
      3 let BA = {};
      4 
      5 (function(BA) {
      6 const TestPrivateKey = new Uint8Array([
      7  0xff, 0x1f, 0x47, 0xb1, 0x68, 0xb6, 0xb9, 0xea, 0x65, 0xf7, 0x97,
      8  0x4f, 0xf2, 0x2e, 0xf2, 0x36, 0x94, 0xe2, 0xf6, 0xb6, 0x8d, 0x66,
      9  0xf3, 0xa7, 0x64, 0x14, 0x28, 0xd4, 0x45, 0x35, 0x01, 0x8f
     10 ]);
     11 
     12 const _hpkeModulePromise = import('../third_party/hpke-js/hpke.js');
     13 
     14 // Common utilities.
     15 
     16 function _get16(buffer, offset) {
     17  return buffer[offset] << 8 | buffer[offset + 1];
     18 }
     19 
     20 function _get32(buffer, offset) {
     21  return buffer[offset] << 24 | buffer[offset + 1] << 16 |
     22      buffer[offset + 2] << 8 | buffer[offset + 3];
     23 }
     24 
     25 function _put16(buffer, offset, val) {
     26  buffer[offset] = val >> 8;
     27  buffer[offset + 1] = val & 0xFF;
     28 }
     29 
     30 function _put32(buffer, offset, val) {
     31  buffer[offset] = (val >> 24) & 0xFF;
     32  buffer[offset + 1] = (val >> 16) & 0xFF;
     33  buffer[offset + 2] = (val >> 8) & 0xFF;
     34  buffer[offset + 3] = val & 0xFF;
     35 }
     36 
     37 // Concatenates two Uint8Array's.
     38 function _concat(a, b) {
     39  let c = new Uint8Array(a.length + b.length);
     40  for (var i = 0; i < a.length; ++i) {
     41    c[i] = a[i];
     42  }
     43  for (var i = 0; i < b.length; ++i) {
     44    c[i + a.length] = b[i];
     45  }
     46  return c;
     47 }
     48 
     49 function _toArrayBuffer(typedArray) {
     50  return typedArray.buffer.slice(
     51      typedArray.byteOffset, typedArray.byteOffset + typedArray.byteLength);
     52 }
     53 
     54 function _toBytesArrayBuffer(str) {
     55  return _toArrayBuffer(new TextEncoder().encode(str));
     56 }
     57 
     58 function _bufferAsStream(buffer) {
     59  return new ReadableStream({
     60    start: controller => {
     61      controller.enqueue(buffer);
     62      controller.close();
     63    }
     64  });
     65 }
     66 
     67 // Returns an ArrayBuffer.
     68 async function _applyTransform(inData, transform) {
     69  const resultResponse =
     70      new Response(_bufferAsStream(inData).pipeThrough(transform));
     71  const resultBlob = await resultResponse.blob();
     72  return await resultBlob.arrayBuffer();
     73 }
     74 
     75 // Returns an ArrayBuffer (promise).
     76 async function _gzip(inData) {
     77  const compress = new CompressionStream('gzip');
     78  return _applyTransform(inData, compress);
     79 }
     80 
     81 // Returns an ArrayBuffer (promise).
     82 async function _gunzip(inData) {
     83  const decompress = new DecompressionStream('gzip');
     84  return _applyTransform(inData, decompress);
     85 }
     86 
     87 // InterestGroupData decoding helpers.
     88 
     89 function _decodeIgDataHeader(igData) {
     90  if (igData.length < 8) {
     91    throw 'Not enough data for B&A and OHTTP headers';
     92  }
     93  return {
     94    version: igData[0],
     95    keyId: igData[1],
     96    kemId: _get16(igData, 2),
     97    kdfId: _get16(igData, 4),
     98    aeadId: _get16(igData, 6),
     99    payload: igData.slice(8)
    100  };
    101 }
    102 
    103 // Splits up the actual B&A IG Data into the enc and ct portions
    104 // for HPKE, using `suite` for sizing; and also figures out the appropriate
    105 // info string.
    106 function _splitIgDataPayloadIntoEncAndCt(header, suite) {
    107  const RequestMessageType = 'message/auction request';
    108 
    109  // From RFC 9458 (Oblivious HTTP):
    110  // "2.  Build a sequence of bytes (info) by concatenating the ASCII-
    111  //      encoded string "message/bhttp request"; a zero byte; key_id as an
    112  //      8-bit integer; plus kem_id, kdf_id, and aead_id as three 16-bit
    113  //      integers."
    114  // (except we use a different message type string).
    115  const infoLength = RequestMessageType.length + 1 + 1 + 6;
    116  let info = new Uint8Array(infoLength);
    117  for (let pos = 0; pos < RequestMessageType.length; ++pos) {
    118    info[pos] = RequestMessageType.charCodeAt(pos);
    119  }
    120  info[RequestMessageType.length] = 0;
    121  info[RequestMessageType.length + 1] = header.keyId;
    122  _put16(info, RequestMessageType.length + 2, header.kemId);
    123  _put16(info, RequestMessageType.length + 4, header.kdfId);
    124  _put16(info, RequestMessageType.length + 6, header.aeadId);
    125  return {
    126    info: info,
    127    enc: header.payload.slice(0, suite.kem.encSize),
    128    ct: header.payload.slice(suite.kem.encSize)
    129  };
    130 }
    131 
    132 // Unwraps the padding envelope.
    133 function _decodeIgDataPaddingHeader(decryptedText) {
    134  let length = _get32(decryptedText, 1);
    135  let format = decryptedText[0];
    136 
    137  // We currently only support format 2, which version = 0, and gzip
    138  // compression.
    139  assert_equals(format, 2);
    140  return {
    141    format: format,
    142    data: decryptedText.slice(5, 5 + length)
    143  };
    144 }
    145 
    146 // serverResponse encoding helpers.
    147 
    148 // Takes an ArrayBuffer, returns a Uint8Array.
    149 function _frameServerResponse(arrayBuffer) {
    150  let array = new Uint8Array(arrayBuffer);
    151  let framedLength = 5 + array.length;
    152  let framed = new Uint8Array(framedLength);
    153  framed[0] = 2;  // gzip + ver 0.
    154  _put32(framed, 1, array.length);
    155  for (let i = 0; i < array.length; ++i) {
    156    framed[i + 5] = array[i];
    157  }
    158  return framed;
    159 }
    160 
    161 async function _encryptServerResponse(payload, decoded) {
    162  // This again follows RFC 9458 (Oblivious HTTP), "Encapsulation of
    163  // Responses", just with different message type:
    164  const ResponseMessageType = 'message/auction response';
    165  const Nk = decoded.cipherSuite.aead.keySize;
    166  const Nn = decoded.cipherSuite.aead.nonceSize;
    167  let secret = await decoded.receiveContext.export(
    168      _toBytesArrayBuffer(ResponseMessageType), Math.max(Nk, Nn));
    169  let responseNonce = new Uint8Array(Math.max(Nk, Nn));
    170  crypto.getRandomValues(responseNonce);
    171  let salt = _concat(decoded.enc, responseNonce);
    172  let prk = await decoded.cipherSuite.kdf.extract(salt, secret);
    173  let aeadKey =
    174      await decoded.cipherSuite.kdf.expand(prk, _toBytesArrayBuffer('key'), Nk);
    175  let aeadNonce = await decoded.cipherSuite.kdf.expand(
    176      prk, _toBytesArrayBuffer('nonce'), Nn);
    177  let encContext = decoded.cipherSuite.aead.createEncryptionContext(aeadKey);
    178  let ct = await encContext.seal(
    179      /*iv=*/ aeadNonce, /*data=*/ payload,
    180      /*aad=*/ _toBytesArrayBuffer(''));
    181  return _concat(responseNonce, new Uint8Array(ct));
    182 }
    183 
    184 // CBOR requires property names to be in sorted order; but the library we use
    185 // doesn't do it automatically. Since it's easy for a test to fail for the
    186 // wrong reason if the response isn't specified correctly, this ensures the
    187 // proper ordering. It assumes a very simple data model, so no arrays with
    188 // holes, no mixture of different kinds of indices in the map, etc.
    189 // Getting the sort order right in more complicated cases is outside the
    190 // scope of this helper.
    191 function _sortForCbor(input) {
    192  if (input === null || typeof input !== 'object') {
    193    return input;
    194  }
    195 
    196  if (input instanceof Array) {
    197    let out = [];
    198    for (let i = 0; i < input.length; ++i) {
    199      out[i] = _sortForCbor(input[i]);
    200    }
    201    return out;
    202  } else if (input instanceof Uint8Array) {
    203    return input;
    204  } else {
    205    let keys = Object.getOwnPropertyNames(input).sort((a, b) => {
    206      // CBOR order compares lengths before values.
    207      if (a.length < b.length)
    208        return -1;
    209      if (a.length > b.length)
    210        return 1;
    211      if (a < b)
    212        return -1;
    213      if (a > b)
    214        return 1;
    215      return 0;
    216    });
    217    let out = {};
    218    for (let key of keys) {
    219      out[key] = _sortForCbor(input[key]);
    220    }
    221    return out;
    222  }
    223 }
    224 
    225 // Works on both ArrayBuffer and Uint8Array, returns the same type.
    226 function _injectFault(input) {
    227  let uint8Input;
    228  if (input instanceof ArrayBuffer) {
    229    uint8Input = new Uint8Array(input);
    230  } else {
    231    assert_true(input instanceof Uint8Array);
    232    uint8Input = input;
    233  }
    234 
    235  // Just mess up the 0th byte.
    236  uint8Input[0] = uint8Input[0] ^ 0x4e;
    237 
    238  if (input instanceof ArrayBuffer) {
    239    return _toArrayBuffer(uint8Input);
    240  } else {
    241    return uint8Input;
    242  }
    243 }
    244 
    245 // Exported API.
    246 
    247 // Decodes the request payload produced by getInterestGroupAdAuctionData into
    248 // {paddedSize: ..., message: ..., cipherSuite: ... , receiveContext: ...,
    249 //  enc:...}
    250 BA.decodeInterestGroupData = async function(igData) {
    251  const hpke = await _hpkeModulePromise;
    252 
    253  // Decode B&A level headers, and check them.
    254  const header = _decodeIgDataHeader(igData);
    255 
    256  // Only version 0 in use now.
    257  assert_equals(header.version, 0);
    258 
    259  // Test config uses keyId = 0x14 only
    260  // If the feature is not set up properly we may get a different, non-test key.
    261  // We can't use assert_equals because it includes the (random) non-test key
    262  // in the error message if testing support for this feature is not present.
    263  assert_true(header.keyId === 0x14, "valid key Id");
    264 
    265  // Current cipher config.
    266  assert_equals(header.kemId, hpke.KemId.DhkemX25519HkdfSha256);
    267  assert_equals(header.kdfId, hpke.KdfId.HkdfSha256);
    268  assert_equals(header.aeadId, hpke.AeadId.Aes256Gcm);
    269 
    270  const suite = new hpke.CipherSuite({
    271    kem: header.kemId,
    272    kdf: header.kdfId,
    273    aead: header.aeadId,
    274  });
    275 
    276  // Split up the ciphertext from encapsulated key, and also compute
    277  // the expected message info.
    278  const pieces = _splitIgDataPayloadIntoEncAndCt(header, suite);
    279 
    280  // We can now decode the ciphertext.
    281  const privateKey = await suite.kem.importKey('raw', TestPrivateKey);
    282  const recipient = await suite.createRecipientContext(
    283      {recipientKey: privateKey, info: pieces.info, enc: pieces.enc});
    284  const pt = new Uint8Array(await recipient.open(pieces.ct));
    285 
    286  // The resulting text has yet another envelope with version and size info,
    287  // and a bunch of padding.
    288  const withoutPadding = _decodeIgDataPaddingHeader(pt);
    289  const decoded = CBOR.decode(_toArrayBuffer(withoutPadding.data));
    290 
    291  // Decompress IGs, CBOR-decode them, and replace in-place.
    292  for (let key of Object.getOwnPropertyNames(decoded.interestGroups)) {
    293    let val = decoded.interestGroups[key];
    294    let decompressedVal = await _gunzip(val);
    295    decoded.interestGroups[key] = CBOR.decode(decompressedVal);
    296  }
    297 
    298  return {
    299    paddedSize: pt.length,
    300    message: decoded,
    301    receiveContext: recipient,
    302    cipherSuite: suite,
    303    enc: pieces.enc
    304  };
    305 };
    306 
    307 BA.injectCborFault = 1;
    308 BA.injectGzipFault = 2;
    309 BA.injectFrameFault = 4;
    310 BA.injectEncryptFault = 8;
    311 
    312 // Encodes, compresses, encrypts, etc., `responseObject` into a proper
    313 // serverResponse in reply to `decoded`.
    314 BA.encodeServerResponse =
    315    async function(responseObject, decoded, injectFaults = 0) {
    316  let cborPayload = new Uint8Array(CBOR.encode(_sortForCbor(responseObject)));
    317  if (injectFaults & BA.injectCborFault) {
    318    cborPayload = _injectFault(cborPayload);
    319  }
    320 
    321  let gzipPayload = await _gzip(cborPayload);
    322  if (injectFaults & BA.injectGzipFault) {
    323    gzipPayload = _injectFault(gzipPayload);
    324  }
    325 
    326  let framedPayload = _toArrayBuffer(_frameServerResponse(gzipPayload));
    327  if (injectFaults & BA.injectFrameFault) {
    328    framedPayload = _injectFault(framedPayload);
    329  }
    330 
    331  let encrypted = await _encryptServerResponse(framedPayload, decoded);
    332  if (injectFaults & BA.injectEncryptFault) {
    333    encrypted = _injectFault(encrypted);
    334  }
    335 
    336  return encrypted;
    337 };
    338 
    339 // Returns a hash string that can be used to authorize a given response,
    340 // formatted for use in an Ad-Auction-Result HTTP header.
    341 BA.payloadHash = async function(serverResponse) {
    342  let hash =
    343      new Uint8Array(await crypto.subtle.digest('SHA-256', serverResponse));
    344  let hashString = ''
    345  for (let i = 0; i < hash.length; ++i) {
    346    hashString += String.fromCharCode(hash[i]);
    347  }
    348  return btoa(hashString)
    349      .replace(/\+/g, '-')
    350      .replace(/\//g, '_')
    351      .replace(/=+$/g, '');
    352 };
    353 
    354 // Authorizes each serverResponse hash in `hashes` to be used for
    355 // B&A auction result.
    356 BA.authorizeServerResponseHashes = async function(hashes) {
    357  let authorizeURL =
    358      new URL('resources/authorize-server-response.py', window.location);
    359  authorizeURL.searchParams.append('hashes', hashes.join(','));
    360  await fetch(authorizeURL, {adAuctionHeaders: true});
    361 };
    362 
    363 // Authorizes each serverResponse nonce in `nonces` to be used for
    364 // B&A auction result.
    365 BA.authorizeServerResponseNonces = async function(nonces) {
    366  let authorizeURL =
    367      new URL('resources/authorize-server-response.py', window.location);
    368  authorizeURL.searchParams.append('nonces', nonces.join(','));
    369  await fetch(authorizeURL, {adAuctionHeaders: true});
    370 };
    371 
    372 BA.configureCoordinator = async function() {
    373  // This is async in hope it can eventually use testdriver to configure this.
    374  return 'https://{{hosts[][]}}';
    375 };
    376 
    377 // Runs responseMutator on a minimal correct server response, and expects
    378 // either success/failure based on expectWin.
    379 BA.testWithMutatedServerResponse = async function(
    380    test, expectWin, responseMutator, igMutator = undefined,
    381    ownerOverride = null) {
    382  let finalIgOwner = ownerOverride ? ownerOverride : window.location.origin;
    383  const uuid = generateUuid(test);
    384  const adA = createTrackerURL(finalIgOwner, uuid, 'track_get', 'a');
    385  const adB = createTrackerURL(finalIgOwner, uuid, 'track_get', 'b');
    386  const adsArray =
    387      [{renderURL: adA, adRenderId: 'a'}, {renderURL: adB, adRenderId: 'b'}];
    388  let ig = {ads: adsArray};
    389  if (igMutator) {
    390    igMutator(ig, uuid);
    391  }
    392  if (ownerOverride !== null) {
    393    await joinCrossOriginInterestGroup(test, uuid, ownerOverride, ig);
    394  } else {
    395    await joinInterestGroup(test, uuid, ig);
    396  }
    397 
    398  const result = await navigator.getInterestGroupAdAuctionData({
    399    coordinatorOrigin: await BA.configureCoordinator(),
    400    seller: window.location.origin
    401  });
    402  assert_true(result.requestId !== null);
    403  assert_true(result.request.length > 0);
    404 
    405  let decoded = await BA.decodeInterestGroupData(result.request);
    406 
    407  let serverResponseMsg = {
    408    'biddingGroups': {},
    409    'adRenderURL': ig.ads[0].renderURL,
    410    'interestGroupName': DEFAULT_INTEREST_GROUP_NAME,
    411    'interestGroupOwner': finalIgOwner,
    412  };
    413  serverResponseMsg.biddingGroups[finalIgOwner] = [0];
    414  await responseMutator(serverResponseMsg, uuid);
    415 
    416  let serverResponse =
    417      await BA.encodeServerResponse(serverResponseMsg, decoded);
    418 
    419  let hashString = await BA.payloadHash(serverResponse);
    420  await BA.authorizeServerResponseHashes([hashString]);
    421 
    422  let auctionResult = await navigator.runAdAuction({
    423    'seller': window.location.origin,
    424    'interestGroupBuyers': [finalIgOwner],
    425    'requestId': result.requestId,
    426    'serverResponse': serverResponse,
    427    'resolveToConfig': true,
    428  });
    429  if (expectWin) {
    430    expectSuccess(auctionResult);
    431    return auctionResult;
    432  } else {
    433    expectNoWinner(auctionResult);
    434  }
    435 };
    436 
    437 })(BA);