tor-browser

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

test_pairing.js (10007B)


      1 /* Any copyright is dedicated to the Public Domain.
      2 * http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 "use strict";
      5 
      6 const { FxAccountsPairingFlow } = ChromeUtils.importESModule(
      7  "resource://gre/modules/FxAccountsPairing.sys.mjs"
      8 );
      9 const { EventEmitter } = ChromeUtils.importESModule(
     10  "resource://gre/modules/EventEmitter.sys.mjs"
     11 );
     12 ChromeUtils.defineESModuleGetters(this, {
     13  jwcrypto: "moz-src:///services/crypto/modules/jwcrypto.sys.mjs",
     14 });
     15 
     16 const CHANNEL_ID = "sW-UA97Q6Dljqen7XRlYPw";
     17 const CHANNEL_KEY = crypto.getRandomValues(new Uint8Array(32));
     18 
     19 const SENDER_SUPP = {
     20  ua: "Firefox Supp",
     21  city: "Nice",
     22  region: "PACA",
     23  country: "France",
     24  remote: "127.0.0.1",
     25 };
     26 const UID = "abcd";
     27 const EMAIL = "foo@bar.com";
     28 const AVATAR = "https://foo.bar/avatar";
     29 const DISPLAY_NAME = "Foo bar";
     30 const DEVICE_NAME = "Foo's computer";
     31 
     32 const PAIR_URI = "https://foo.bar/pair";
     33 const OAUTH_URI = "https://foo.bar/oauth";
     34 const KSYNC = "myksync";
     35 const SESSION = "mysession";
     36 const fxaConfig = {
     37  promisePairingURI() {
     38    return PAIR_URI;
     39  },
     40  promiseOAuthURI() {
     41    return OAUTH_URI;
     42  },
     43 };
     44 const fxAccounts = {
     45  getSignedInUser() {
     46    return {
     47      uid: UID,
     48      email: EMAIL,
     49      avatar: AVATAR,
     50      displayName: DISPLAY_NAME,
     51    };
     52  },
     53  async _withVerifiedAccountState(cb) {
     54    return cb({
     55      async getUserAccountData() {
     56        return {
     57          sessionToken: SESSION,
     58        };
     59      },
     60    });
     61  },
     62  _internal: {
     63    keys: {
     64      getKeyForScope() {
     65        return {
     66          kid: "123456",
     67          k: KSYNC,
     68          kty: "oct",
     69        };
     70      },
     71    },
     72    fxAccountsClient: {
     73      async getScopedKeyData() {
     74        return {
     75          [SCOPE_APP_SYNC]: {
     76            identifier: SCOPE_APP_SYNC,
     77            keyRotationTimestamp: 12345678,
     78          },
     79        };
     80      },
     81      async oauthAuthorize() {
     82        return { code: "mycode", state: "mystate" };
     83      },
     84    },
     85  },
     86 };
     87 const weave = {
     88  Service: { clientsEngine: { localName: DEVICE_NAME } },
     89 };
     90 
     91 class MockPairingChannel extends EventTarget {
     92  get channelId() {
     93    return CHANNEL_ID;
     94  }
     95 
     96  get channelKey() {
     97    return CHANNEL_KEY;
     98  }
     99 
    100  send(data) {
    101    this.dispatchEvent(
    102      new CustomEvent("send", {
    103        detail: { data },
    104      })
    105    );
    106  }
    107 
    108  simulateIncoming(data) {
    109    this.dispatchEvent(
    110      new CustomEvent("message", {
    111        detail: { data, sender: SENDER_SUPP },
    112      })
    113    );
    114  }
    115 
    116  close() {
    117    this.closed = true;
    118  }
    119 }
    120 
    121 add_task(async function testFullFlow() {
    122  const emitter = new EventEmitter();
    123  const pairingChannel = new MockPairingChannel();
    124  const pairingUri = await FxAccountsPairingFlow.start({
    125    emitter,
    126    pairingChannel,
    127    fxAccounts,
    128    fxaConfig,
    129    weave,
    130  });
    131  Assert.equal(
    132    pairingUri,
    133    `${PAIR_URI}#channel_id=${CHANNEL_ID}&channel_key=${ChromeUtils.base64URLEncode(
    134      CHANNEL_KEY,
    135      { pad: false }
    136    )}`
    137  );
    138 
    139  const flow = FxAccountsPairingFlow.get(CHANNEL_ID);
    140 
    141  const promiseSwitchToWebContent = emitter.once("view:SwitchToWebContent");
    142  const promiseMetadataSent = promiseOutgoingMessage(pairingChannel);
    143  const epk = await generateEphemeralKeypair();
    144 
    145  pairingChannel.simulateIncoming({
    146    message: "pair:supp:request",
    147    data: {
    148      client_id: "client_id_1",
    149      state: "mystate",
    150      keys_jwk: ChromeUtils.base64URLEncode(
    151        new TextEncoder().encode(JSON.stringify(epk.publicJWK)),
    152        { pad: false }
    153      ),
    154      scope: `profile ${SCOPE_APP_SYNC}`,
    155      code_challenge: "chal",
    156      code_challenge_method: "S256",
    157    },
    158  });
    159  const sentAuthMetadata = await promiseMetadataSent;
    160  Assert.deepEqual(sentAuthMetadata, {
    161    message: "pair:auth:metadata",
    162    data: {
    163      email: EMAIL,
    164      avatar: AVATAR,
    165      displayName: DISPLAY_NAME,
    166      deviceName: DEVICE_NAME,
    167    },
    168  });
    169  const oauthUrl = await promiseSwitchToWebContent;
    170  Assert.equal(
    171    oauthUrl,
    172    `${OAUTH_URI}?client_id=client_id_1&scope=profile+${encodeURIComponent(
    173      SCOPE_APP_SYNC
    174    )}&email=foo%40bar.com&uid=abcd&channel_id=${CHANNEL_ID}&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob%3Apair-auth-webchannel`
    175  );
    176 
    177  let pairSuppMetadata = await simulateIncomingWebChannel(
    178    flow,
    179    "fxaccounts:pair_supplicant_metadata"
    180  );
    181  Assert.deepEqual(
    182    {
    183      ua: "Firefox Supp",
    184      city: "Nice",
    185      region: "PACA",
    186      country: "France",
    187      ipAddress: "127.0.0.1",
    188    },
    189    pairSuppMetadata
    190  );
    191 
    192  const generateJWE = sinon.spy(jwcrypto, "generateJWE");
    193  const oauthAuthorize = sinon.spy(
    194    fxAccounts._internal.fxAccountsClient,
    195    "oauthAuthorize"
    196  );
    197  const promiseOAuthParamsMsg = promiseOutgoingMessage(pairingChannel);
    198  await simulateIncomingWebChannel(flow, "fxaccounts:pair_authorize");
    199  // We should have generated the expected JWE.
    200  Assert.ok(generateJWE.calledOnce);
    201  const generateArgs = generateJWE.firstCall.args;
    202  Assert.deepEqual(generateArgs[0], epk.publicJWK);
    203  Assert.deepEqual(JSON.parse(new TextDecoder().decode(generateArgs[1])), {
    204    [SCOPE_APP_SYNC]: {
    205      kid: "123456",
    206      k: KSYNC,
    207      kty: "oct",
    208    },
    209  });
    210  // We should have authorized an oauth code with expected parameters.
    211  Assert.ok(oauthAuthorize.calledOnce);
    212  const oauthCodeArgs = oauthAuthorize.firstCall.args[1];
    213  console.log(oauthCodeArgs);
    214  Assert.ok(!oauthCodeArgs.keys_jwk);
    215  Assert.deepEqual(
    216    oauthCodeArgs.keys_jwe,
    217    await generateJWE.firstCall.returnValue
    218  );
    219  Assert.equal(oauthCodeArgs.client_id, "client_id_1");
    220  Assert.equal(oauthCodeArgs.access_type, "offline");
    221  Assert.equal(oauthCodeArgs.state, "mystate");
    222  Assert.equal(oauthCodeArgs.scope, `profile ${SCOPE_APP_SYNC}`);
    223  Assert.equal(oauthCodeArgs.code_challenge, "chal");
    224  Assert.equal(oauthCodeArgs.code_challenge_method, "S256");
    225 
    226  const oAuthParams = await promiseOAuthParamsMsg;
    227  Assert.deepEqual(oAuthParams, {
    228    message: "pair:auth:authorize",
    229    data: { code: "mycode", state: "mystate" },
    230  });
    231 
    232  let heartbeat = await simulateIncomingWebChannel(
    233    flow,
    234    "fxaccounts:pair_heartbeat"
    235  );
    236  Assert.ok(!heartbeat.suppAuthorized);
    237 
    238  await pairingChannel.simulateIncoming({
    239    message: "pair:supp:authorize",
    240  });
    241 
    242  heartbeat = await simulateIncomingWebChannel(
    243    flow,
    244    "fxaccounts:pair_heartbeat"
    245  );
    246  Assert.ok(heartbeat.suppAuthorized);
    247 
    248  await simulateIncomingWebChannel(flow, "fxaccounts:pair_complete");
    249  // The flow should have been destroyed!
    250  Assert.ok(!FxAccountsPairingFlow.get(CHANNEL_ID));
    251  Assert.ok(pairingChannel.closed);
    252  generateJWE.restore();
    253  oauthAuthorize.restore();
    254 });
    255 
    256 add_task(async function testUnknownPairingMessage() {
    257  const emitter = new EventEmitter();
    258  const pairingChannel = new MockPairingChannel();
    259  await FxAccountsPairingFlow.start({
    260    emitter,
    261    pairingChannel,
    262    fxAccounts,
    263    fxaConfig,
    264    weave,
    265  });
    266  const flow = FxAccountsPairingFlow.get(CHANNEL_ID);
    267  const viewErrorObserved = emitter.once("view:Error");
    268  pairingChannel.simulateIncoming({
    269    message: "pair:boom",
    270  });
    271  await viewErrorObserved;
    272  let heartbeat = await simulateIncomingWebChannel(
    273    flow,
    274    "fxaccounts:pair_heartbeat"
    275  );
    276  Assert.ok(heartbeat.err);
    277 });
    278 
    279 add_task(async function testUnknownWebChannelCommand() {
    280  const emitter = new EventEmitter();
    281  const pairingChannel = new MockPairingChannel();
    282  await FxAccountsPairingFlow.start({
    283    emitter,
    284    pairingChannel,
    285    fxAccounts,
    286    fxaConfig,
    287    weave,
    288  });
    289  const flow = FxAccountsPairingFlow.get(CHANNEL_ID);
    290  const viewErrorObserved = emitter.once("view:Error");
    291  await simulateIncomingWebChannel(flow, "fxaccounts:boom");
    292  await viewErrorObserved;
    293  let heartbeat = await simulateIncomingWebChannel(
    294    flow,
    295    "fxaccounts:pair_heartbeat"
    296  );
    297  Assert.ok(heartbeat.err);
    298 });
    299 
    300 add_task(async function testPairingChannelFailure() {
    301  const emitter = new EventEmitter();
    302  const pairingChannel = new MockPairingChannel();
    303  await FxAccountsPairingFlow.start({
    304    emitter,
    305    pairingChannel,
    306    fxAccounts,
    307    fxaConfig,
    308    weave,
    309  });
    310  const flow = FxAccountsPairingFlow.get(CHANNEL_ID);
    311  const viewErrorObserved = emitter.once("view:Error");
    312  sinon.stub(pairingChannel, "send").callsFake(() => {
    313    throw new Error("Boom!");
    314  });
    315  pairingChannel.simulateIncoming({
    316    message: "pair:supp:request",
    317    data: {
    318      client_id: "client_id_1",
    319      state: "mystate",
    320      scope: `profile ${SCOPE_APP_SYNC}`,
    321      code_challenge: "chal",
    322      code_challenge_method: "S256",
    323    },
    324  });
    325  await viewErrorObserved;
    326 
    327  let heartbeat = await simulateIncomingWebChannel(
    328    flow,
    329    "fxaccounts:pair_heartbeat"
    330  );
    331  Assert.ok(heartbeat.err);
    332 });
    333 
    334 add_task(async function testFlowTimeout() {
    335  const emitter = new EventEmitter();
    336  const pairingChannel = new MockPairingChannel();
    337  const viewErrorObserved = emitter.once("view:Error");
    338  await FxAccountsPairingFlow.start({
    339    emitter,
    340    pairingChannel,
    341    fxAccounts,
    342    fxaConfig,
    343    weave,
    344    flowTimeout: 1,
    345  });
    346  const flow = FxAccountsPairingFlow.get(CHANNEL_ID);
    347  await viewErrorObserved;
    348 
    349  let heartbeat = await simulateIncomingWebChannel(
    350    flow,
    351    "fxaccounts:pair_heartbeat"
    352  );
    353  Assert.ok(heartbeat.err.match(/Timeout/));
    354 });
    355 
    356 async function simulateIncomingWebChannel(flow, command) {
    357  return flow.onWebChannelMessage(command);
    358 }
    359 
    360 async function promiseOutgoingMessage(pairingChannel) {
    361  return new Promise(res => {
    362    const onMessage = event => {
    363      pairingChannel.removeEventListener("send", onMessage);
    364      res(event.detail.data);
    365    };
    366    pairingChannel.addEventListener("send", onMessage);
    367  });
    368 }
    369 
    370 async function generateEphemeralKeypair() {
    371  const keypair = await crypto.subtle.generateKey(
    372    { name: "ECDH", namedCurve: "P-256" },
    373    true,
    374    ["deriveKey"]
    375  );
    376  const publicJWK = await crypto.subtle.exportKey("jwk", keypair.publicKey);
    377  const privateJWK = await crypto.subtle.exportKey("jwk", keypair.privateKey);
    378  delete publicJWK.key_ops;
    379  return {
    380    publicJWK,
    381    privateJWK,
    382  };
    383 }