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 }