tor-browser

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

security.js (14352B)


      1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
      2 /* This Source Code Form is subject to the terms of the Mozilla Public
      3 * License, v. 2.0. If a copy of the MPL was not distributed with this
      4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      5 
      6 const { SiteDataManager } = ChromeUtils.importESModule(
      7  "resource:///modules/SiteDataManager.sys.mjs"
      8 );
      9 const { DownloadUtils } = ChromeUtils.importESModule(
     10  "resource://gre/modules/DownloadUtils.sys.mjs"
     11 );
     12 
     13 /* import-globals-from pageInfo.js */
     14 
     15 ChromeUtils.defineESModuleGetters(this, {
     16  LoginHelper: "resource://gre/modules/LoginHelper.sys.mjs",
     17 });
     18 
     19 var security = {
     20  async init(uri, windowInfo) {
     21    this.uri = uri;
     22    this.windowInfo = windowInfo;
     23    this.securityInfo = await this._getSecurityInfo();
     24  },
     25 
     26  viewCert() {
     27    let certChain = this.securityInfo.certChain;
     28    let certs = certChain.map(elem =>
     29      encodeURIComponent(elem.getBase64DERString())
     30    );
     31    let certsStringURL = certs.map(elem => `cert=${elem}`);
     32    certsStringURL = certsStringURL.join("&");
     33    let url = `about:certificate?${certsStringURL}`;
     34    let win = BrowserWindowTracker.getTopWindow();
     35    if (win) {
     36      win.switchToTabHavingURI(url, true, {});
     37    } else {
     38      URILoadingHelper.openTrustedLinkIn(window, url, "window");
     39    }
     40  },
     41 
     42  async _getSecurityInfo() {
     43    // We don't have separate info for a frame, return null until further notice
     44    // (see bug 138479)
     45    if (!this.windowInfo.isTopWindow) {
     46      return null;
     47    }
     48 
     49    var ui = security._getSecurityUI();
     50    if (!ui) {
     51      return null;
     52    }
     53 
     54    var isBroken = ui.state & Ci.nsIWebProgressListener.STATE_IS_BROKEN;
     55    var isMixed =
     56      ui.state &
     57      (Ci.nsIWebProgressListener.STATE_LOADED_MIXED_ACTIVE_CONTENT |
     58        Ci.nsIWebProgressListener.STATE_LOADED_MIXED_DISPLAY_CONTENT);
     59    var isEV = ui.state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
     60    let uriInformation = URL.parse(gDocInfo.documentURIObject.spec);
     61    // If the Onion site could not be loaded, the view-source will be also be
     62    // about:neterror.
     63    if (uriInformation?.protocol == "view-source:") {
     64      uriInformation = URL.parse(uriInformation.pathname);
     65    }
     66    const isOnion =
     67      ["http:", "https:"].includes(uriInformation?.protocol) &&
     68      uriInformation?.hostname.endsWith(".onion");
     69 
     70    let retval = {
     71      cAName: "",
     72      encryptionAlgorithm: "",
     73      encryptionStrength: 0,
     74      version: "",
     75      isBroken,
     76      isMixed,
     77      isEV,
     78      isOnion,
     79      cert: null,
     80      certificateTransparency: null,
     81    };
     82 
     83    // Only show certificate info for secure contexts. This prevents us from
     84    // showing certificate data for http origins when using a proxy.
     85    // https://searchfox.org/mozilla-central/rev/9c72508fcf2bba709a5b5b9eae9da35e0c707baa/security/manager/ssl/nsSecureBrowserUI.cpp#62-64
     86    if (!ui.isSecureContext) {
     87      return retval;
     88    }
     89 
     90    let secInfo = ui.secInfo;
     91    if (!secInfo) {
     92      return retval;
     93    }
     94 
     95    let cert = secInfo.serverCert;
     96    let issuerName = null;
     97    if (cert) {
     98      issuerName = cert.issuerOrganization || cert.issuerName;
     99    }
    100 
    101    let certChainArray = [];
    102    if (secInfo.succeededCertChain.length) {
    103      certChainArray = secInfo.succeededCertChain;
    104    } else {
    105      certChainArray = secInfo.handshakeCertificates;
    106    }
    107 
    108    retval = {
    109      cAName: issuerName,
    110      encryptionAlgorithm: undefined,
    111      encryptionStrength: undefined,
    112      version: undefined,
    113      isBroken,
    114      isMixed,
    115      isEV,
    116      isOnion,
    117      cert,
    118      certChain: certChainArray,
    119      certificateTransparency: undefined,
    120    };
    121 
    122    var version;
    123    try {
    124      retval.encryptionAlgorithm = secInfo.cipherName;
    125      retval.encryptionStrength = secInfo.secretKeyLength;
    126      version = secInfo.protocolVersion;
    127    } catch (e) {}
    128 
    129    switch (version) {
    130      case Ci.nsITransportSecurityInfo.SSL_VERSION_3:
    131        retval.version = "SSL 3";
    132        break;
    133      case Ci.nsITransportSecurityInfo.TLS_VERSION_1:
    134        retval.version = "TLS 1.0";
    135        break;
    136      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_1:
    137        retval.version = "TLS 1.1";
    138        break;
    139      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_2:
    140        retval.version = "TLS 1.2";
    141        break;
    142      case Ci.nsITransportSecurityInfo.TLS_VERSION_1_3:
    143        retval.version = "TLS 1.3";
    144        break;
    145    }
    146 
    147    // Select the status text to display for Certificate Transparency.
    148    // Since we do not yet enforce the CT Policy on secure connections,
    149    // we must not complain on policy discompliance (it might be viewed
    150    // as a security issue by the user).
    151    switch (secInfo.certificateTransparencyStatus) {
    152      case Ci.nsITransportSecurityInfo.CERTIFICATE_TRANSPARENCY_NOT_APPLICABLE:
    153      case Ci.nsITransportSecurityInfo
    154        .CERTIFICATE_TRANSPARENCY_POLICY_NOT_ENOUGH_SCTS:
    155      case Ci.nsITransportSecurityInfo
    156        .CERTIFICATE_TRANSPARENCY_POLICY_NOT_DIVERSE_SCTS:
    157        retval.certificateTransparency = null;
    158        break;
    159      case Ci.nsITransportSecurityInfo
    160        .CERTIFICATE_TRANSPARENCY_POLICY_COMPLIANT:
    161        retval.certificateTransparency = "Compliant";
    162        break;
    163    }
    164 
    165    return retval;
    166  },
    167 
    168  // Find the secureBrowserUI object (if present)
    169  _getSecurityUI() {
    170    if (window.opener.gBrowser) {
    171      return window.opener.gBrowser.securityUI;
    172    }
    173    return null;
    174  },
    175 
    176  async _updateSiteDataInfo() {
    177    // Save site data info for deleting.
    178    this.siteData = await SiteDataManager.getSite(this.uri.host);
    179 
    180    let clearSiteDataButton = document.getElementById(
    181      "security-clear-sitedata"
    182    );
    183    let siteDataLabel = document.getElementById(
    184      "security-privacy-sitedata-value"
    185    );
    186 
    187    if (!this.siteData) {
    188      document.l10n.setAttributes(siteDataLabel, "security-site-data-no");
    189      clearSiteDataButton.setAttribute("disabled", "true");
    190      return;
    191    }
    192 
    193    let { usage } = this.siteData;
    194    if (usage > 0) {
    195      let size = DownloadUtils.convertByteUnits(usage);
    196      if (this.siteData.cookies.length) {
    197        document.l10n.setAttributes(
    198          siteDataLabel,
    199          "security-site-data-cookies",
    200          { value: size[0], unit: size[1] }
    201        );
    202      } else {
    203        document.l10n.setAttributes(siteDataLabel, "security-site-data-only", {
    204          value: size[0],
    205          unit: size[1],
    206        });
    207      }
    208    } else {
    209      // We're storing cookies, else getSite would have returned null.
    210      document.l10n.setAttributes(
    211        siteDataLabel,
    212        "security-site-data-cookies-only"
    213      );
    214    }
    215 
    216    clearSiteDataButton.removeAttribute("disabled");
    217  },
    218 
    219  /**
    220   * Clear Site Data and Cookies
    221   */
    222  clearSiteData() {
    223    if (this.siteData) {
    224      let { baseDomain } = this.siteData;
    225      if (SiteDataManager.promptSiteDataRemoval(window, [baseDomain])) {
    226        SiteDataManager.remove(baseDomain).then(() =>
    227          this._updateSiteDataInfo()
    228        );
    229      }
    230    }
    231  },
    232 
    233  /**
    234   * Open the login manager window
    235   */
    236  viewPasswords() {
    237    LoginHelper.openPasswordManager(window, {
    238      filterString: this.windowInfo.hostName,
    239      entryPoint: "Pageinfo",
    240    });
    241  },
    242 };
    243 
    244 async function securityOnLoad(uri, windowInfo) {
    245  await security.init(uri, windowInfo);
    246 
    247  let info = security.securityInfo;
    248  if (
    249    !info ||
    250    (uri.scheme === "about" && !uri.spec.startsWith("about:certerror"))
    251  ) {
    252    document.getElementById("securityTab").hidden = true;
    253    return;
    254  }
    255  document.getElementById("securityTab").hidden = false;
    256 
    257  /* Set Identity section text */
    258  setText("security-identity-domain-value", windowInfo.hostName);
    259 
    260  var validity;
    261  if (info.cert && !info.isBroken) {
    262    validity = info.cert.validity.notAfterLocalDay;
    263 
    264    // Try to pull out meaningful values.  Technically these fields are optional
    265    // so we'll employ fallbacks where appropriate.  The EV spec states that Org
    266    // fields must be specified for subject and issuer so that case is simpler.
    267    if (info.isEV) {
    268      setText("security-identity-owner-value", info.cert.organization);
    269      setText("security-identity-verifier-value", info.cAName);
    270    } else {
    271      // Technically, a non-EV cert might specify an owner in the O field or not,
    272      // depending on the CA's issuing policies.  However we don't have any programmatic
    273      // way to tell those apart, and no policy way to establish which organization
    274      // vetting standards are good enough (that's what EV is for) so we default to
    275      // treating these certs as domain-validated only.
    276      document.l10n.setAttributes(
    277        document.getElementById("security-identity-owner-value"),
    278        "page-info-security-no-owner"
    279      );
    280      setText(
    281        "security-identity-verifier-value",
    282        info.cAName || info.cert.issuerCommonName || info.cert.issuerName
    283      );
    284    }
    285  } else {
    286    // We don't have valid identity credentials.
    287    document.l10n.setAttributes(
    288      document.getElementById("security-identity-owner-value"),
    289      "page-info-security-no-owner"
    290    );
    291    document.l10n.setAttributes(
    292      document.getElementById("security-identity-verifier-value"),
    293      "page-info-not-specified"
    294    );
    295  }
    296 
    297  if (validity) {
    298    setText("security-identity-validity-value", validity);
    299  } else {
    300    document.getElementById("security-identity-validity-row").hidden = true;
    301  }
    302 
    303  /* Manage the View Cert button*/
    304  var viewCert = document.getElementById("security-view-cert");
    305  if (info.cert) {
    306    viewCert.collapsed = false;
    307  } else {
    308    viewCert.collapsed = true;
    309  }
    310 
    311  /* Set Privacy & History section text */
    312 
    313  // Only show quota usage data for websites, not internal sites.
    314  if (uri.scheme == "http" || uri.scheme == "https") {
    315    SiteDataManager.updateSites().then(() => security._updateSiteDataInfo());
    316  } else {
    317    document.getElementById("security-privacy-sitedata-row").hidden = true;
    318  }
    319 
    320  if (realmHasPasswords(uri)) {
    321    document.l10n.setAttributes(
    322      document.getElementById("security-privacy-passwords-value"),
    323      "saved-passwords-yes"
    324    );
    325  } else {
    326    document.l10n.setAttributes(
    327      document.getElementById("security-privacy-passwords-value"),
    328      "saved-passwords-no"
    329    );
    330  }
    331 
    332  document.l10n.setAttributes(
    333    document.getElementById("security-privacy-history-value"),
    334    "security-visits-number",
    335    { visits: previousVisitCount(windowInfo.hostName) }
    336  );
    337 
    338  /* Set the Technical Detail section messages */
    339  const pkiBundle = document.getElementById("pkiBundle");
    340  var hdr;
    341  var msg1;
    342  var msg2;
    343 
    344  if (info.isBroken) {
    345    if (info.isMixed) {
    346      hdr = pkiBundle.getString("pageInfo_MixedContent");
    347      msg1 = pkiBundle.getString("pageInfo_MixedContent2");
    348    } else {
    349      hdr = pkiBundle.getFormattedString("pageInfo_BrokenEncryption", [
    350        info.encryptionAlgorithm,
    351        info.encryptionStrength + "",
    352        info.version,
    353      ]);
    354      msg1 = pkiBundle.getString("pageInfo_WeakCipher");
    355    }
    356    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
    357  } else if (info.encryptionStrength > 0) {
    358    if (!info.isOnion) {
    359      hdr = pkiBundle.getFormattedString(
    360        "pageInfo_EncryptionWithBitsAndProtocol",
    361        [info.encryptionAlgorithm, info.encryptionStrength + "", info.version]
    362      );
    363    } else {
    364      try {
    365        hdr = await document.l10n.formatValue(
    366          "page-info-onion-site-encryption-with-bits",
    367          {
    368            "encryption-type": info.encryptionAlgorithm,
    369            "encryption-strength": info.encryptionStrength,
    370            "encryption-version": info.version,
    371          }
    372        );
    373      } catch (err) {
    374        hdr =
    375          "Connection Encrypted (Onion Service, " +
    376          info.encryptionAlgorithm +
    377          ", " +
    378          info.encryptionStrength +
    379          " bit keys, " +
    380          info.version +
    381          ")";
    382      }
    383    }
    384    msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
    385    msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
    386  } else if (!info.isOnion) {
    387    hdr = pkiBundle.getString("pageInfo_NoEncryption");
    388    if (windowInfo.hostName != null) {
    389      msg1 = pkiBundle.getFormattedString("pageInfo_Privacy_None1", [
    390        windowInfo.hostName,
    391      ]);
    392    } else {
    393      msg1 = pkiBundle.getString("pageInfo_Privacy_None4");
    394    }
    395    msg2 = pkiBundle.getString("pageInfo_Privacy_None2");
    396  } else {
    397    hdr = await document.l10n.formatValue(
    398      "page-info-onion-site-encryption-plain"
    399    );
    400 
    401    msg1 = pkiBundle.getString("pageInfo_Privacy_Encrypted1");
    402    msg2 = pkiBundle.getString("pageInfo_Privacy_Encrypted2");
    403  }
    404  setText("security-technical-shortform", hdr);
    405  setText("security-technical-longform1", msg1);
    406  setText("security-technical-longform2", msg2);
    407 
    408  const ctStatus = document.getElementById(
    409    "security-technical-certificate-transparency"
    410  );
    411  if (info.certificateTransparency) {
    412    ctStatus.hidden = false;
    413    ctStatus.value = pkiBundle.getString(
    414      "pageInfo_CertificateTransparency_" + info.certificateTransparency
    415    );
    416  } else {
    417    ctStatus.hidden = true;
    418  }
    419 }
    420 
    421 function setText(id, value) {
    422  var element = document.getElementById(id);
    423  if (!element) {
    424    return;
    425  }
    426  if (element.localName == "input" || element.localName == "label") {
    427    element.value = value;
    428  } else {
    429    element.textContent = value;
    430  }
    431 }
    432 
    433 /**
    434 * Return true iff realm (proto://host:port) (extracted from uri) has
    435 * saved passwords
    436 */
    437 function realmHasPasswords(uri) {
    438  return Services.logins.countLogins(uri.prePath, "", "") > 0;
    439 }
    440 
    441 /**
    442 * Return the number of previous visits recorded for host before today.
    443 *
    444 * @param host - the domain name to look for in history
    445 */
    446 function previousVisitCount(host) {
    447  if (!host) {
    448    return 0;
    449  }
    450 
    451  var historyService = Cc[
    452    "@mozilla.org/browser/nav-history-service;1"
    453  ].getService(Ci.nsINavHistoryService);
    454 
    455  var options = historyService.getNewQueryOptions();
    456  options.resultType = options.RESULTS_AS_VISIT;
    457 
    458  // Search for visits to this host before today
    459  var query = historyService.getNewQuery();
    460  query.endTimeReference = query.TIME_RELATIVE_TODAY;
    461  query.endTime = 0;
    462  query.domain = host;
    463 
    464  var result = historyService.executeQuery(query, options);
    465  result.root.containerOpen = true;
    466  var cc = result.root.childCount;
    467  result.root.containerOpen = false;
    468  return cc;
    469 }