tor-browser

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

AboutProtectionsParent.sys.mjs (13962B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
      6 
      7 const lazy = {};
      8 
      9 ChromeUtils.defineESModuleGetters(lazy, {
     10  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     11  BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
     12  FXA_PWDMGR_HOST: "resource://gre/modules/FxAccountsCommon.sys.mjs",
     13  FXA_PWDMGR_REALM: "resource://gre/modules/FxAccountsCommon.sys.mjs",
     14  LoginBreaches: "resource:///modules/LoginBreaches.sys.mjs",
     15  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
     16  PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
     17  Region: "resource://gre/modules/Region.sys.mjs",
     18 });
     19 
     20 ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
     21  return ChromeUtils.importESModule(
     22    "resource://gre/modules/FxAccounts.sys.mjs"
     23  ).getFxAccountsSingleton();
     24 });
     25 
     26 XPCOMUtils.defineLazyServiceGetter(
     27  lazy,
     28  "TrackingDBService",
     29  "@mozilla.org/tracking-db-service;1",
     30  Ci.nsITrackingDBService
     31 );
     32 
     33 let idToTextMap = new Map([
     34  [Ci.nsITrackingDBService.TRACKERS_ID, "tracker"],
     35  [Ci.nsITrackingDBService.TRACKING_COOKIES_ID, "cookie"],
     36  [Ci.nsITrackingDBService.CRYPTOMINERS_ID, "cryptominer"],
     37  [Ci.nsITrackingDBService.FINGERPRINTERS_ID, "fingerprinter"],
     38  // We map the suspicious fingerprinter to fingerprinter category to aggregate
     39  // the number.
     40  [Ci.nsITrackingDBService.SUSPICIOUS_FINGERPRINTERS_ID, "fingerprinter"],
     41  [Ci.nsITrackingDBService.SOCIAL_ID, "social"],
     42 ]);
     43 
     44 const MONITOR_API_ENDPOINT = Services.urlFormatter.formatURLPref(
     45  "browser.contentblocking.report.endpoint_url"
     46 );
     47 
     48 const SECURE_PROXY_ADDON_ID = "secure-proxy@mozilla.com";
     49 
     50 const SCOPE_MONITOR = [
     51  "profile:uid",
     52  "https://identity.mozilla.com/apps/monitor",
     53 ];
     54 
     55 const SCOPE_VPN = "profile https://identity.mozilla.com/account/subscriptions";
     56 const VPN_ENDPOINT = `${Services.prefs.getStringPref(
     57  "identity.fxaccounts.auth.uri"
     58 )}oauth/subscriptions/active`;
     59 
     60 // The ID of the vpn subscription, if we see this ID attached to a user's account then they have subscribed to vpn.
     61 const VPN_SUB_ID = Services.prefs.getStringPref(
     62  "browser.contentblocking.report.vpn_sub_id"
     63 );
     64 
     65 // Error messages
     66 const INVALID_OAUTH_TOKEN = "Invalid OAuth token";
     67 const USER_UNSUBSCRIBED_TO_MONITOR = "User is not subscribed to Monitor";
     68 const SERVICE_UNAVAILABLE = "Service unavailable";
     69 const UNEXPECTED_RESPONSE = "Unexpected response";
     70 const UNKNOWN_ERROR = "Unknown error";
     71 
     72 // Valid response info for successful Monitor data
     73 const MONITOR_RESPONSE_PROPS = [
     74  "monitoredEmails",
     75  "numBreaches",
     76  "passwords",
     77  "numBreachesResolved",
     78  "passwordsResolved",
     79 ];
     80 
     81 let gTestOverride = null;
     82 let monitorResponse = null;
     83 let entrypoint = "direct";
     84 
     85 export class AboutProtectionsParent extends JSWindowActorParent {
     86  constructor() {
     87    super();
     88  }
     89 
     90  // Some tests wish to override certain functions with ones that mostly do nothing.
     91  static setTestOverride(callback) {
     92    gTestOverride = callback;
     93  }
     94 
     95  /**
     96   * Fetches and validates data from the Monitor endpoint. If successful, then return
     97   * expected data. Otherwise, throw the appropriate error depending on the status code.
     98   *
     99   * @return valid data from endpoint.
    100   */
    101  async fetchUserBreachStats(token) {
    102    if (monitorResponse && monitorResponse.timestamp) {
    103      var timeDiff = Date.now() - monitorResponse.timestamp;
    104      let oneDayInMS = 24 * 60 * 60 * 1000;
    105      if (timeDiff >= oneDayInMS) {
    106        monitorResponse = null;
    107      } else {
    108        return monitorResponse;
    109      }
    110    }
    111 
    112    // Make the request
    113    const headers = new Headers();
    114    headers.append("Authorization", `Bearer ${token}`);
    115    const request = new Request(MONITOR_API_ENDPOINT, { headers });
    116    const response = await fetch(request);
    117 
    118    if (response.ok) {
    119      // Validate the shape of the response is what we're expecting.
    120      const json = await response.json();
    121 
    122      // Make sure that we're getting the expected data.
    123      let isValid = null;
    124      for (let prop in json) {
    125        isValid = MONITOR_RESPONSE_PROPS.includes(prop);
    126 
    127        if (!isValid) {
    128          break;
    129        }
    130      }
    131 
    132      monitorResponse = isValid ? json : new Error(UNEXPECTED_RESPONSE);
    133      if (isValid) {
    134        monitorResponse.timestamp = Date.now();
    135      }
    136    } else {
    137      // Check the reason for the error
    138      switch (response.status) {
    139        case 400:
    140        case 401:
    141          monitorResponse = new Error(INVALID_OAUTH_TOKEN);
    142          break;
    143        case 404:
    144          monitorResponse = new Error(USER_UNSUBSCRIBED_TO_MONITOR);
    145          break;
    146        case 503:
    147          monitorResponse = new Error(SERVICE_UNAVAILABLE);
    148          break;
    149        default:
    150          monitorResponse = new Error(UNKNOWN_ERROR);
    151          break;
    152      }
    153    }
    154 
    155    if (monitorResponse instanceof Error) {
    156      throw monitorResponse;
    157    }
    158    return monitorResponse;
    159  }
    160 
    161  /**
    162   * Retrieves login data for the user.
    163   *
    164   * @return {{numLogins: number, potentiallyBreachedLogins: number, mobileDeviceConnected: boolean }}
    165   */
    166  async getLoginData() {
    167    if (gTestOverride && "getLoginData" in gTestOverride) {
    168      return gTestOverride.getLoginData();
    169    }
    170 
    171    try {
    172      if (await lazy.fxAccounts.getSignedInUser()) {
    173        await lazy.fxAccounts.device.refreshDeviceList();
    174      }
    175    } catch (e) {
    176      console.error("There was an error fetching login data: ", e.message);
    177    }
    178 
    179    const userFacingLogins =
    180      Services.logins.countLogins("", "", "") -
    181      Services.logins.countLogins(
    182        lazy.FXA_PWDMGR_HOST,
    183        null,
    184        lazy.FXA_PWDMGR_REALM
    185      );
    186 
    187    let potentiallyBreachedLogins = null;
    188    // Get the stats for number of potentially breached Lockwise passwords
    189    // if the Primary Password isn't locked.
    190    if (userFacingLogins && Services.logins.isLoggedIn) {
    191      const logins = await lazy.LoginHelper.getAllUserFacingLogins();
    192      potentiallyBreachedLogins =
    193        await lazy.LoginBreaches.getPotentialBreachesByLoginGUID(logins);
    194    }
    195 
    196    let mobileDeviceConnected =
    197      lazy.fxAccounts.device.recentDeviceList &&
    198      lazy.fxAccounts.device.recentDeviceList.filter(
    199        device => device.type == "mobile"
    200      ).length;
    201 
    202    return {
    203      numLogins: userFacingLogins,
    204      potentiallyBreachedLogins: potentiallyBreachedLogins
    205        ? potentiallyBreachedLogins.size
    206        : 0,
    207      mobileDeviceConnected,
    208    };
    209  }
    210 
    211  /**
    212   * @typedef {object} ProtectionsMonitorData
    213   * @property {number} monitoredEmails
    214   * @property {number} numBreaches
    215   * @property {number} passwords
    216   * @property {?string} userEmail
    217   * @property {boolean} error
    218   */
    219 
    220  /**
    221   * Retrieves monitor data for the user.
    222   *
    223   * @return {ProtectionsMonitorData}
    224   */
    225  async getMonitorData() {
    226    if (gTestOverride && "getMonitorData" in gTestOverride) {
    227      monitorResponse = gTestOverride.getMonitorData();
    228      monitorResponse.timestamp = Date.now();
    229      // In a test, expect this to not fetch from the monitor endpoint due to the timestamp guaranteeing we use the cache.
    230      monitorResponse = await this.fetchUserBreachStats();
    231      return monitorResponse;
    232    }
    233 
    234    let monitorData = {};
    235    let userEmail = null;
    236    let token = await this.getMonitorScopedOAuthToken();
    237 
    238    try {
    239      if (token) {
    240        monitorData = await this.fetchUserBreachStats(token);
    241 
    242        // Send back user's email so the protections report can direct them to the proper
    243        // OAuth flow on Monitor.
    244        const { email } = await lazy.fxAccounts.getSignedInUser();
    245        userEmail = email;
    246      } else {
    247        // If no account exists, then the user is not logged in with an fxAccount.
    248        monitorData = {
    249          errorMessage: "No account",
    250        };
    251      }
    252    } catch (e) {
    253      console.error(e.message);
    254      monitorData.errorMessage = e.message;
    255 
    256      // If the user's OAuth token is invalid, we clear the cached token and refetch
    257      // again. If OAuth token is invalid after the second fetch, then the monitor UI
    258      // will simply show the "no logins" UI version.
    259      if (e.message === INVALID_OAUTH_TOKEN) {
    260        await lazy.fxAccounts.removeCachedOAuthToken({ token });
    261        token = await this.getMonitorScopedOAuthToken();
    262 
    263        try {
    264          monitorData = await this.fetchUserBreachStats(token);
    265        } catch (_) {
    266          console.error(e.message);
    267        }
    268      } else if (e.message === USER_UNSUBSCRIBED_TO_MONITOR) {
    269        // Send back user's email so the protections report can direct them to the proper
    270        // OAuth flow on Monitor.
    271        const { email } = await lazy.fxAccounts.getSignedInUser();
    272        userEmail = email;
    273      } else {
    274        monitorData.errorMessage = e.message || "An error ocurred.";
    275      }
    276    }
    277 
    278    return {
    279      ...monitorData,
    280      userEmail,
    281      error: !!monitorData.errorMessage,
    282    };
    283  }
    284 
    285  async getMonitorScopedOAuthToken() {
    286    let token = null;
    287 
    288    try {
    289      token = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_MONITOR });
    290    } catch (e) {
    291      console.error(
    292        "There was an error fetching the user's token: ",
    293        e.message
    294      );
    295    }
    296 
    297    return token;
    298  }
    299 
    300  /**
    301   * The proxy card will only show if the user is in the US, has the browser language in "en-US",
    302   * and does not yet have Proxy installed.
    303   */
    304  async shouldShowProxyCard() {
    305    const region = lazy.Region.home || "";
    306    const languages = Services.locale.acceptLanguages;
    307    const alreadyInstalled = await lazy.AddonManager.getAddonByID(
    308      SECURE_PROXY_ADDON_ID
    309    );
    310 
    311    return (
    312      region.toLowerCase() === "us" &&
    313      !alreadyInstalled &&
    314      languages.toLowerCase().includes("en-us")
    315    );
    316  }
    317 
    318  async VPNSubStatus() {
    319    // For testing, set vpn sub status manually
    320    if (gTestOverride && "vpnOverrides" in gTestOverride) {
    321      return gTestOverride.vpnOverrides();
    322    }
    323 
    324    let vpnToken;
    325    try {
    326      vpnToken = await lazy.fxAccounts.getOAuthToken({ scope: SCOPE_VPN });
    327    } catch (e) {
    328      console.error(
    329        "There was an error fetching the user's token: ",
    330        e.message
    331      );
    332      // there was an error, assume user is not subscribed to VPN
    333      return false;
    334    }
    335    let headers = new Headers();
    336    headers.append("Authorization", `Bearer ${vpnToken}`);
    337    const request = new Request(VPN_ENDPOINT, { headers });
    338    const res = await fetch(request);
    339    if (res.ok) {
    340      const result = await res.json();
    341      for (let sub of result) {
    342        if (sub.subscriptionId == VPN_SUB_ID) {
    343          return true;
    344        }
    345      }
    346      return false;
    347    }
    348    // unknown logic: assume user is not subscribed to VPN
    349    return false;
    350  }
    351 
    352  async receiveMessage(aMessage) {
    353    let win = this.browsingContext.top.embedderElement.ownerGlobal;
    354    switch (aMessage.name) {
    355      case "OpenAboutLogins":
    356        lazy.LoginHelper.openPasswordManager(win, {
    357          entryPoint: "Aboutprotections",
    358        });
    359        break;
    360      case "OpenContentBlockingPreferences":
    361        win.openPreferences("privacy-trackingprotection", {
    362          origin: "about-protections",
    363        });
    364        break;
    365      case "OpenSyncPreferences":
    366        win.openTrustedLinkIn("about:preferences#sync", "tab");
    367        break;
    368      case "FetchContentBlockingEvents": {
    369        let dataToSend = {};
    370        let displayNames = new Services.intl.DisplayNames(undefined, {
    371          type: "weekday",
    372          style: "abbreviated",
    373          calendar: "gregory",
    374        });
    375 
    376        // Weekdays starting Sunday (7) to Saturday (6).
    377        let weekdays = [7, 1, 2, 3, 4, 5, 6].map(day => displayNames.of(day));
    378        dataToSend.weekdays = weekdays;
    379 
    380        if (lazy.PrivateBrowsingUtils.isWindowPrivate(win)) {
    381          dataToSend.isPrivate = true;
    382          return dataToSend;
    383        }
    384        let sumEvents = await lazy.TrackingDBService.sumAllEvents();
    385        let earliestDate =
    386          await lazy.TrackingDBService.getEarliestRecordedDate();
    387        let eventsByDate = await lazy.TrackingDBService.getEventsByDateRange(
    388          aMessage.data.from,
    389          aMessage.data.to
    390        );
    391        let largest = 0;
    392 
    393        for (let result of eventsByDate) {
    394          let count = result.getResultByName("count");
    395          let type = result.getResultByName("type");
    396          let timestamp = result.getResultByName("timestamp");
    397          let typeStr = idToTextMap.get(type);
    398          dataToSend[timestamp] = dataToSend[timestamp] ?? { total: 0 };
    399          let currentCnt = dataToSend[timestamp][typeStr] ?? 0;
    400          currentCnt += count;
    401          dataToSend[timestamp][typeStr] = currentCnt;
    402          dataToSend[timestamp].total += count;
    403          // Record the largest amount of tracking events found per day,
    404          // to create the tallest column on the graph and compare other days to.
    405          if (largest < dataToSend[timestamp].total) {
    406            largest = dataToSend[timestamp].total;
    407          }
    408        }
    409        dataToSend.largest = largest;
    410        dataToSend.earliestDate = earliestDate;
    411        dataToSend.sumEvents = sumEvents;
    412 
    413        return dataToSend;
    414      }
    415 
    416      case "FetchMonitorData":
    417        return this.getMonitorData();
    418 
    419      case "FetchUserLoginsData":
    420        return this.getLoginData();
    421 
    422      case "ClearMonitorCache":
    423        monitorResponse = null;
    424        break;
    425 
    426      case "GetShowProxyCard":
    427        return await this.shouldShowProxyCard();
    428 
    429      case "RecordEntryPoint":
    430        entrypoint = aMessage.data.entrypoint;
    431        break;
    432 
    433      case "FetchEntryPoint":
    434        return entrypoint;
    435 
    436      case "FetchVPNSubStatus":
    437        return this.VPNSubStatus();
    438 
    439      case "FetchShowVPNCard":
    440        return lazy.BrowserUtils.shouldShowVPNPromo();
    441    }
    442 
    443    return undefined;
    444  }
    445 }