tor-commits
  Threads by month 
                
            - ----- 2025 -----
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2024 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2023 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2022 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2021 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2020 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2019 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2018 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2017 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2016 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2015 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2014 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2013 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2012 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
- January
- ----- 2011 -----
- December
- November
- October
- September
- August
- July
- June
- May
- April
- March
- February
August 2021
- 15 participants
- 1354 discussions
 
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 40073: Disable remote Public Suffix List fetching
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit b0c1c0289eded4a1b10b828385d5b82a571c6f6d
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Thu Aug 13 11:05:03 2020 +0200
    Bug 40073: Disable remote Public Suffix List fetching
    
    In https://bugzilla.mozilla.org/show_bug.cgi?id=1563246 Firefox implemented
    fetching the Public Suffix List via RemoteSettings and replacing the default
    one at runtime, which we do not want.
---
 browser/components/BrowserGlue.jsm | 5 -----
 1 file changed, 5 deletions(-)
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index e08e461a27ff..ec38d0ca8b33 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -695,7 +695,6 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   PluralForm: "resource://gre/modules/PluralForm.jsm",
   PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
   ProcessHangMonitor: "resource:///modules/ProcessHangMonitor.jsm",
-  PublicSuffixList: "resource://gre/modules/netwerk-dns/PublicSuffixList.jsm",
   RemoteSettings: "resource://services-settings/remote-settings.js",
   RemoteSecuritySettings:
     "resource://gre/modules/psm/RemoteSecuritySettings.jsm",
@@ -2575,10 +2574,6 @@ BrowserGlue.prototype = {
         this._addBreachesSyncHandler();
       },
 
-      () => {
-        PublicSuffixList.init();
-      },
-
       () => {
         RemoteSecuritySettings.init();
       },
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] 40209: Implement Basic Crypto Safety
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit 55d6aa91565fa0d8e3f8c903fced608d57c06259
Author: sanketh <me(a)snkth.com>
Date:   Mon Feb 8 20:12:44 2021 -0500
    40209: Implement Basic Crypto Safety
    
    Adds a CryptoSafety actor which detects when you've copied a crypto
    address from a HTTP webpage and shows a warning.
    
    Closes #40209.
    
    Bug 40428: Fix string attribute names
---
 browser/actors/CryptoSafetyChild.jsm         |  87 ++++++++++++++++
 browser/actors/CryptoSafetyParent.jsm        | 142 +++++++++++++++++++++++++++
 browser/actors/moz.build                     |   2 +
 browser/base/content/popup-notifications.inc |  14 +++
 browser/components/BrowserGlue.jsm           |  17 ++++
 browser/modules/TorStrings.jsm               |  48 +++++++++
 browser/themes/shared/browser.inc.css        |   5 +
 toolkit/content/license.html                 |  32 ++++++
 toolkit/modules/Bech32Decode.jsm             | 103 +++++++++++++++++++
 toolkit/modules/moz.build                    |   1 +
 10 files changed, 451 insertions(+)
diff --git a/browser/actors/CryptoSafetyChild.jsm b/browser/actors/CryptoSafetyChild.jsm
new file mode 100644
index 000000000000..87ff261d4915
--- /dev/null
+++ b/browser/actors/CryptoSafetyChild.jsm
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Copyright (c) 2020, The Tor Project, Inc.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["CryptoSafetyChild"];
+
+const { Bech32Decode } = ChromeUtils.import(
+  "resource://gre/modules/Bech32Decode.jsm"
+);
+
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const kPrefCryptoSafety = "security.cryptoSafety";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+  this,
+  "isCryptoSafetyEnabled",
+  kPrefCryptoSafety,
+  true /* defaults to true */
+);
+
+function looksLikeCryptoAddress(s) {
+  // P2PKH and P2SH addresses
+  // https://stackoverflow.com/a/24205650
+  const bitcoinAddr = /^[13][a-km-zA-HJ-NP-Z1-9]{25,39}$/;
+  if (bitcoinAddr.test(s)) {
+    return true;
+  }
+
+  // Bech32 addresses
+  if (Bech32Decode(s) !== null) {
+    return true;
+  }
+
+  // regular addresses
+  const etherAddr = /^0x[a-fA-F0-9]{40}$/;
+  if (etherAddr.test(s)) {
+    return true;
+  }
+
+  // t-addresses
+  // https://www.reddit.com/r/zec/comments/8mxj6x/simple_regex_to_validate_a_zca…
+  const zcashAddr = /^t1[a-zA-Z0-9]{33}$/;
+  if (zcashAddr.test(s)) {
+    return true;
+  }
+
+  // Standard, Integrated, and 256-bit Integrated addresses
+  // https://monero.stackexchange.com/a/10627
+  const moneroAddr = /^4(?:[0-9AB]|[1-9A-HJ-NP-Za-km-z]{12}(?:[1-9A-HJ-NP-Za-km-z]{30})?)[1-9A-HJ-NP-Za-km-z]{93}$/;
+  if (moneroAddr.test(s)) {
+    return true;
+  }
+
+  return false;
+}
+
+class CryptoSafetyChild extends JSWindowActorChild {
+  handleEvent(event) {
+    if (isCryptoSafetyEnabled) {
+      // Ignore non-HTTP addresses
+      if (!this.document.documentURIObject.schemeIs("http")) {
+        return;
+      }
+      // Ignore onion addresses
+      if (this.document.documentURIObject.host.endsWith(".onion")) {
+        return;
+      }
+
+      if (event.type == "copy" || event.type == "cut") {
+        this.contentWindow.navigator.clipboard.readText().then(clipText => {
+          const selection = clipText.trim();
+          if (looksLikeCryptoAddress(selection)) {
+            this.sendAsyncMessage("CryptoSafety:CopiedText", {
+              selection,
+            });
+          }
+        });
+      }
+    }
+  }
+}
diff --git a/browser/actors/CryptoSafetyParent.jsm b/browser/actors/CryptoSafetyParent.jsm
new file mode 100644
index 000000000000..bac151df5511
--- /dev/null
+++ b/browser/actors/CryptoSafetyParent.jsm
@@ -0,0 +1,142 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* Copyright (c) 2020, The Tor Project, Inc.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["CryptoSafetyParent"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+  TorStrings: "resource:///modules/TorStrings.jsm",
+});
+
+const kPrefCryptoSafety = "security.cryptoSafety";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+  this,
+  "isCryptoSafetyEnabled",
+  kPrefCryptoSafety,
+  true /* defaults to true */
+);
+
+class CryptoSafetyParent extends JSWindowActorParent {
+  getBrowser() {
+    return this.browsingContext.top.embedderElement;
+  }
+
+  receiveMessage(aMessage) {
+    if (isCryptoSafetyEnabled) {
+      if (aMessage.name == "CryptoSafety:CopiedText") {
+        showPopup(this.getBrowser(), aMessage.data.selection);
+      }
+    }
+  }
+}
+
+function trimAddress(cryptoAddr) {
+  if (cryptoAddr.length <= 32) {
+    return cryptoAddr;
+  }
+  return cryptoAddr.substring(0, 32) + "...";
+}
+
+function showPopup(aBrowser, cryptoAddr) {
+  const chromeDoc = aBrowser.ownerDocument;
+  if (chromeDoc) {
+    const win = chromeDoc.defaultView;
+    const cryptoSafetyPrompt = new CryptoSafetyPrompt(
+      aBrowser,
+      win,
+      cryptoAddr
+    );
+    cryptoSafetyPrompt.show();
+  }
+}
+
+class CryptoSafetyPrompt {
+  constructor(aBrowser, aWin, cryptoAddr) {
+    this._browser = aBrowser;
+    this._win = aWin;
+    this._cryptoAddr = cryptoAddr;
+  }
+
+  show() {
+    const primaryAction = {
+      label: TorStrings.cryptoSafetyPrompt.primaryAction,
+      accessKey: TorStrings.cryptoSafetyPrompt.primaryActionAccessKey,
+      callback: () => {
+        this._win.torbutton_new_circuit();
+      },
+    };
+
+    const secondaryAction = {
+      label: TorStrings.cryptoSafetyPrompt.secondaryAction,
+      accessKey: TorStrings.cryptoSafetyPrompt.secondaryActionAccessKey,
+      callback: () => {},
+    };
+
+    let _this = this;
+    const options = {
+      popupIconURL: "chrome://browser/skin/cert-error.svg",
+      eventCallback(aTopic) {
+        if (aTopic === "showing") {
+          _this._onPromptShowing();
+        }
+      },
+    };
+
+    const cryptoWarningText = TorStrings.cryptoSafetyPrompt.cryptoWarning.replace(
+      "%S",
+      trimAddress(this._cryptoAddr)
+    );
+
+    if (this._win.PopupNotifications) {
+      this._prompt = this._win.PopupNotifications.show(
+        this._browser,
+        "crypto-safety-warning",
+        cryptoWarningText,
+        null /* anchor ID */,
+        primaryAction,
+        [secondaryAction],
+        options
+      );
+    }
+  }
+
+  _onPromptShowing() {
+    let xulDoc = this._browser.ownerDocument;
+
+    let whatCanHeading = xulDoc.getElementById(
+      "crypto-safety-warning-notification-what-can-heading"
+    );
+    if (whatCanHeading) {
+      whatCanHeading.textContent = TorStrings.cryptoSafetyPrompt.whatCanHeading;
+    }
+
+    let whatCanBody = xulDoc.getElementById(
+      "crypto-safety-warning-notification-what-can-body"
+    );
+    if (whatCanBody) {
+      whatCanBody.textContent = TorStrings.cryptoSafetyPrompt.whatCanBody;
+    }
+
+    let learnMoreElem = xulDoc.getElementById(
+      "crypto-safety-warning-notification-learnmore"
+    );
+    if (learnMoreElem) {
+      learnMoreElem.setAttribute(
+        "value",
+        TorStrings.cryptoSafetyPrompt.learnMore
+      );
+      learnMoreElem.setAttribute(
+        "href",
+        TorStrings.cryptoSafetyPrompt.learnMoreURL
+      );
+    }
+  }
+}
diff --git a/browser/actors/moz.build b/browser/actors/moz.build
index e70f0f09fe3a..9eb5ca397060 100644
--- a/browser/actors/moz.build
+++ b/browser/actors/moz.build
@@ -50,6 +50,8 @@ FINAL_TARGET_FILES.actors += [
     'ContentSearchParent.jsm',
     'ContextMenuChild.jsm',
     'ContextMenuParent.jsm',
+    'CryptoSafetyChild.jsm',
+    'CryptoSafetyParent.jsm',
     'DOMFullscreenChild.jsm',
     'DOMFullscreenParent.jsm',
     'FormValidationChild.jsm',
diff --git a/browser/base/content/popup-notifications.inc b/browser/base/content/popup-notifications.inc
index 42e17e90c648..ff6f8cdeca80 100644
--- a/browser/base/content/popup-notifications.inc
+++ b/browser/base/content/popup-notifications.inc
@@ -114,3 +114,17 @@
         </vbox>
       </popupnotificationfooter>
     </popupnotification>
+
+    <popupnotification id="crypto-safety-warning-notification" hidden="true">
+      <popupnotificationcontent orient="vertical">
+        <description id="crypto-safety-warning-notification-desc"/>
+        <html:div id="crypto-safety-warning-notification-what-can">
+        <html:strong id="crypto-safety-warning-notification-what-can-heading" />
+        <html:br/>
+        <html:span id="crypto-safety-warning-notification-what-can-body" />
+        </html:div>
+        <label id="crypto-safety-warning-notification-learnmore"
+               class="popup-notification-learnmore-link"
+               is="text-link"/>
+      </popupnotificationcontent>
+    </popupnotification>
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 3750230a250b..5f708fca3d5c 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -297,6 +297,23 @@ let JSWINDOWACTORS = {
     allFrames: true,
   },
 
+  CryptoSafety: {
+    parent: {
+      moduleURI: "resource:///actors/CryptoSafetyParent.jsm",
+    },
+
+    child: {
+      moduleURI: "resource:///actors/CryptoSafetyChild.jsm",
+      group: "browsers",
+      events: {
+        copy: { mozSystemGroup: true },
+        cut: { mozSystemGroup: true },
+      },
+    },
+
+    allFrames: true,
+  },
+
   DOMFullscreen: {
     parent: {
       moduleURI: "resource:///actors/DOMFullscreenParent.jsm",
diff --git a/browser/modules/TorStrings.jsm b/browser/modules/TorStrings.jsm
index e8a8d37ae373..1e08b168e4af 100644
--- a/browser/modules/TorStrings.jsm
+++ b/browser/modules/TorStrings.jsm
@@ -101,6 +101,54 @@ class TorPropertyStringBundle {
   Security Level Strings
 */
 var TorStrings = {
+  /*
+    CryptoSafetyPrompt Strings
+  */
+  cryptoSafetyPrompt: (function() {
+    let tsb = new TorPropertyStringBundle(
+      "chrome://torbutton/locale/torbutton.properties",
+      "cryptoSafetyPrompt."
+    );
+    let getString = function(key, fallback) {
+      return tsb.getString(key, fallback);
+    };
+
+    let retval = {
+      cryptoWarning: getString(
+        "cryptoWarning",
+        "A cryptocurrency address (%S) has been copied from an insecure website. It could have been modified."
+      ),
+      whatCanHeading: getString(
+        "whatCanHeading",
+        "What can you do about it?"
+      ),
+      whatCanBody: getString(
+        "whatCanBody",
+        "You can try reconnecting with a new circuit to establish a secure connection, or accept the risk and dismiss this warning."
+      ),
+      learnMore: getString("learnMore", "Learn more"),
+      learnMoreURL: `https://support.torproject.org/${getLocale()}/`,
+      primaryAction: getString(
+        "primaryAction",
+        "Reload Tab with a New Circuit"
+      ),
+      primaryActionAccessKey: getString(
+        "primaryActionAccessKey",
+        "R"
+      ),
+      secondaryAction: getString(
+        "secondaryAction",
+        "Dismiss Warning"
+      ),
+      secondaryActionAccessKey: getString(
+        "secondaryActionAccessKey",
+        "D"
+      ),
+    };
+
+    return retval;
+  })() /* CryptoSafetyPrompt Strings */,
+
   /*
     Tor Browser Security Level Strings
   */
diff --git a/browser/themes/shared/browser.inc.css b/browser/themes/shared/browser.inc.css
index 0113466e8e56..4ef27d880754 100644
--- a/browser/themes/shared/browser.inc.css
+++ b/browser/themes/shared/browser.inc.css
@@ -620,3 +620,8 @@ menupopup::part(drop-indicator) {
 #sharing-warning-proceed-to-tab:hover {
   background-color: rgb(0,62,170);
 }
+
+#crypto-safety-warning-notification-what-can {
+  display: block;
+  margin: 5px;
+}
diff --git a/toolkit/content/license.html b/toolkit/content/license.html
index e44c31ec6d4e..90995236b41b 100644
--- a/toolkit/content/license.html
+++ b/toolkit/content/license.html
@@ -72,6 +72,7 @@
       <li><a href="about:license#arm">ARM License</a></li>
       <li><a href="about:license#babel">Babel License</a></li>
       <li><a href="about:license#babylon">Babylon License</a></li>
+      <li><a href="about:license#bech32">Bech32 License</a></li>
       <li><a href="about:license#bincode">bincode License</a></li>
       <li><a href="about:license#bsd2clause">BSD 2-Clause License</a></li>
       <li><a href="about:license#bsd3clause">BSD 3-Clause License</a></li>
@@ -2795,6 +2796,37 @@ furnished to do so, subject to the following conditions:
 The above copyright notice and this permission notice shall be included in
 all copies or substantial portions of the Software.
 
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+</pre>
+
+
+    <hr>
+
+    <h1><a id="bech32"></a>Bech32 License</h1>
+
+    <p>This license applies to the file
+      <code>toolkit/modules/Bech32Decode.jsm</code>.
+    </p>
+
+<pre>
+Copyright (c) 2017 Pieter Wuille
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
diff --git a/toolkit/modules/Bech32Decode.jsm b/toolkit/modules/Bech32Decode.jsm
new file mode 100644
index 000000000000..3a2bc7ae0a10
--- /dev/null
+++ b/toolkit/modules/Bech32Decode.jsm
@@ -0,0 +1,103 @@
+// Adapted from the reference implementation of Bech32
+// https://github.com/sipa/bech32
+
+// Copyright (c) 2017 Pieter Wuille
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+"use strict";
+
+/**
+ * JS module implementation of Bech32 decoding adapted from the reference
+ * implementation https://github.com/sipa/bech32.
+ */
+
+var EXPORTED_SYMBOLS = ["Bech32Decode"];
+
+var CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";
+var GENERATOR = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
+
+function polymod(values) {
+  var chk = 1;
+  for (var p = 0; p < values.length; ++p) {
+    var top = chk >> 25;
+    chk = ((chk & 0x1ffffff) << 5) ^ values[p];
+    for (var i = 0; i < 5; ++i) {
+      if ((top >> i) & 1) {
+        chk ^= GENERATOR[i];
+      }
+    }
+  }
+  return chk;
+}
+
+function hrpExpand(hrp) {
+  var ret = [];
+  var p;
+  for (p = 0; p < hrp.length; ++p) {
+    ret.push(hrp.charCodeAt(p) >> 5);
+  }
+  ret.push(0);
+  for (p = 0; p < hrp.length; ++p) {
+    ret.push(hrp.charCodeAt(p) & 31);
+  }
+  return ret;
+}
+
+function verifyChecksum(hrp, data) {
+  return polymod(hrpExpand(hrp).concat(data)) === 1;
+}
+
+function Bech32Decode(bechString) {
+  var p;
+  var has_lower = false;
+  var has_upper = false;
+  for (p = 0; p < bechString.length; ++p) {
+    if (bechString.charCodeAt(p) < 33 || bechString.charCodeAt(p) > 126) {
+      return null;
+    }
+    if (bechString.charCodeAt(p) >= 97 && bechString.charCodeAt(p) <= 122) {
+      has_lower = true;
+    }
+    if (bechString.charCodeAt(p) >= 65 && bechString.charCodeAt(p) <= 90) {
+      has_upper = true;
+    }
+  }
+  if (has_lower && has_upper) {
+    return null;
+  }
+  bechString = bechString.toLowerCase();
+  var pos = bechString.lastIndexOf("1");
+  if (pos < 1 || pos + 7 > bechString.length || bechString.length > 90) {
+    return null;
+  }
+  var hrp = bechString.substring(0, pos);
+  var data = [];
+  for (p = pos + 1; p < bechString.length; ++p) {
+    var d = CHARSET.indexOf(bechString.charAt(p));
+    if (d === -1) {
+      return null;
+    }
+    data.push(d);
+  }
+  if (!verifyChecksum(hrp, data)) {
+    return null;
+  }
+  return { hrp: hrp, data: data.slice(0, data.length - 6) };
+}
diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build
index e1f1eb5759c5..698d2773a7ed 100644
--- a/toolkit/modules/moz.build
+++ b/toolkit/modules/moz.build
@@ -160,6 +160,7 @@ EXTRA_JS_MODULES += [
     'ActorManagerParent.jsm',
     'AppMenuNotifications.jsm',
     'AsyncPrefs.jsm',
+    'Bech32Decode.jsm',
     'BinarySearch.jsm',
     'BrowserUtils.jsm',
     'CanonicalJSON.jsm',
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 40002: Remove about:pioneer
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit ad5e32217926354173a4d574f749310e59d02c58
Author: Kathy Brade <brade(a)pearlcrescent.com>
Date:   Fri Aug 14 09:06:33 2020 -0400
    Bug 40002: Remove about:pioneer
    
    Firefox Pioneer is an opt-in program in which people volunteer to
    participate in studies that collect detailed, sensitive data about
    how they use their browser.
---
 browser/components/about/AboutRedirector.cpp | 2 --
 browser/components/about/components.conf     | 1 -
 2 files changed, 3 deletions(-)
diff --git a/browser/components/about/AboutRedirector.cpp b/browser/components/about/AboutRedirector.cpp
index 544e21782729..e7c377d655e7 100644
--- a/browser/components/about/AboutRedirector.cpp
+++ b/browser/components/about/AboutRedirector.cpp
@@ -114,8 +114,6 @@ static const RedirEntry kRedirMap[] = {
      nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
          nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
          nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT},
-    {"pioneer", "chrome://browser/content/pioneer.html",
-     nsIAboutModule::ALLOW_SCRIPT | nsIAboutModule::HIDE_FROM_ABOUTABOUT},
 #ifdef TOR_BROWSER_UPDATE
     {"tbupdate", "chrome://browser/content/abouttbupdate/aboutTBUpdate.xhtml",
      nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
diff --git a/browser/components/about/components.conf b/browser/components/about/components.conf
index d78de142e2e4..8e04467c05da 100644
--- a/browser/components/about/components.conf
+++ b/browser/components/about/components.conf
@@ -14,7 +14,6 @@ pages = [
     'logins',
     'newinstall',
     'newtab',
-    'pioneer',
     'pocket-saved',
     'pocket-signup',
     'policies',
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 28005: Implement .onion alias urlbar rewrites
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit b41bdfe29465a8c31125bc5cdc3b7fc89a9ccf0d
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Thu Feb 13 13:24:33 2020 +0100
    Bug 28005: Implement .onion alias urlbar rewrites
    
    A custom HTTPS Everywhere update channel is installed,
    which provides rules for locally redirecting some memorable
    .tor.onion URLs to non-memorable .onion URLs.
    
    When these redirects occur, we also rewrite the URL in the urlbar
    to display the human-memorable hostname instead of the actual
    .onion.
    
    Bug 34196: Update site info URL with the onion name
    
    Bug 40456: Update the SecureDrop HTTPS-Everywhere update channel
    Bug 40478: Onion alias url rewrite is broken
---
 browser/actors/ClickHandlerChild.jsm               |  20 ++
 browser/actors/ClickHandlerParent.jsm              |   1 +
 browser/actors/ContextMenuChild.jsm                |   4 +
 browser/base/content/browser-places.js             |  12 +-
 browser/base/content/browser-siteIdentity.js       |  12 +-
 browser/base/content/browser.js                    |  43 ++++-
 browser/base/content/nsContextMenu.js              |  18 ++
 browser/base/content/pageinfo/pageInfo.js          |   2 +-
 browser/base/content/pageinfo/pageInfo.xhtml       |  10 +
 browser/base/content/pageinfo/security.js          |  17 +-
 browser/base/content/tabbrowser.js                 |   7 +
 browser/base/content/utilityOverlay.js             |  12 ++
 browser/components/BrowserGlue.jsm                 |   8 +
 .../onionservices/ExtensionMessaging.jsm           |  86 +++++++++
 .../onionservices/HttpsEverywhereControl.jsm       | 147 +++++++++++++++
 .../components/onionservices/OnionAliasStore.jsm   | 201 +++++++++++++++++++++
 browser/components/onionservices/moz.build         |   6 +
 browser/components/urlbar/UrlbarInput.jsm          |  13 +-
 docshell/base/nsDocShell.cpp                       |  52 ++++++
 docshell/base/nsDocShell.h                         |   6 +
 docshell/base/nsDocShellLoadState.cpp              |   4 +
 docshell/base/nsIDocShell.idl                      |   5 +
 docshell/base/nsIWebNavigation.idl                 |   5 +
 docshell/shistory/SessionHistoryEntry.cpp          |  14 ++
 docshell/shistory/SessionHistoryEntry.h            |   1 +
 docshell/shistory/nsISHEntry.idl                   |   5 +
 docshell/shistory/nsSHEntry.cpp                    |  22 ++-
 docshell/shistory/nsSHEntry.h                      |   1 +
 dom/interfaces/base/nsIBrowser.idl                 |   3 +-
 dom/ipc/BrowserChild.cpp                           |   2 +
 dom/ipc/BrowserParent.cpp                          |   3 +-
 dom/ipc/PBrowser.ipdl                              |   1 +
 modules/libpref/init/StaticPrefList.yaml           |   6 +
 netwerk/dns/effective_tld_names.dat                |   2 +
 netwerk/ipc/DocumentLoadListener.cpp               |  10 +
 toolkit/content/widgets/browser-custom-element.js  |  13 +-
 toolkit/modules/sessionstore/SessionHistory.jsm    |   5 +
 37 files changed, 757 insertions(+), 22 deletions(-)
diff --git a/browser/actors/ClickHandlerChild.jsm b/browser/actors/ClickHandlerChild.jsm
index 7dd060e83061..206a8fc97a4d 100644
--- a/browser/actors/ClickHandlerChild.jsm
+++ b/browser/actors/ClickHandlerChild.jsm
@@ -138,6 +138,26 @@ class ClickHandlerChild extends JSWindowActorChild {
       json.originStoragePrincipal = ownerDoc.effectiveStoragePrincipal;
       json.triggeringPrincipal = ownerDoc.nodePrincipal;
 
+      // Check if the link needs to be opened with .tor.onion urlbar rewrites
+      // allowed. Only when the owner doc has onionUrlbarRewritesAllowed = true
+      // and the same origin we should allow this.
+      json.onionUrlbarRewritesAllowed = false;
+      if (this.docShell.onionUrlbarRewritesAllowed) {
+        const sm = Services.scriptSecurityManager;
+        try {
+          let targetURI = Services.io.newURI(href);
+          let isPrivateWin =
+            ownerDoc.nodePrincipal.originAttributes.privateBrowsingId > 0;
+          sm.checkSameOriginURI(
+            docshell.currentDocumentChannel.URI,
+            targetURI,
+            false,
+            isPrivateWin
+          );
+          json.onionUrlbarRewritesAllowed = true;
+        } catch (e) {}
+      }
+
       // If a link element is clicked with middle button, user wants to open
       // the link somewhere rather than pasting clipboard content.  Therefore,
       // when it's clicked with middle button, we should prevent multiple
diff --git a/browser/actors/ClickHandlerParent.jsm b/browser/actors/ClickHandlerParent.jsm
index 454c0fe69b27..42ab7a0f6e2a 100644
--- a/browser/actors/ClickHandlerParent.jsm
+++ b/browser/actors/ClickHandlerParent.jsm
@@ -102,6 +102,7 @@ class ClickHandlerParent extends JSWindowActorParent {
       charset: browser.characterSet,
       referrerInfo: E10SUtils.deserializeReferrerInfo(data.referrerInfo),
       allowMixedContent: data.allowMixedContent,
+      onionUrlbarRewritesAllowed: data.onionUrlbarRewritesAllowed,
       isContentWindowPrivate: data.isContentWindowPrivate,
       originPrincipal: data.originPrincipal,
       originStoragePrincipal: data.originStoragePrincipal,
diff --git a/browser/actors/ContextMenuChild.jsm b/browser/actors/ContextMenuChild.jsm
index 16a3f8ecbc81..1a1b38aa6375 100644
--- a/browser/actors/ContextMenuChild.jsm
+++ b/browser/actors/ContextMenuChild.jsm
@@ -575,6 +575,9 @@ class ContextMenuChild extends JSWindowActorChild {
     // The same-origin check will be done in nsContextMenu.openLinkInTab.
     let parentAllowsMixedContent = !!this.docShell.mixedContentChannel;
 
+    let parentAllowsOnionUrlbarRewrites = this.docShell
+      .onionUrlbarRewritesAllowed;
+
     let disableSetDesktopBackground = null;
 
     // Media related cache info parent needs for saving
@@ -687,6 +690,7 @@ class ContextMenuChild extends JSWindowActorChild {
       frameBrowsingContextID,
       disableSetDesktopBackground,
       parentAllowsMixedContent,
+      parentAllowsOnionUrlbarRewrites,
     };
 
     if (context.inFrame && !context.inSrcdocFrame) {
diff --git a/browser/base/content/browser-places.js b/browser/base/content/browser-places.js
index d4b71f87da04..9a9bb3e0dad4 100644
--- a/browser/base/content/browser-places.js
+++ b/browser/base/content/browser-places.js
@@ -452,7 +452,8 @@ var PlacesCommandHook = {
    */
   async bookmarkPage() {
     let browser = gBrowser.selectedBrowser;
-    let url = new URL(browser.currentURI.spec);
+    const uri = browser.currentOnionAliasURI || browser.currentURI;
+    let url = new URL(uri.spec);
     let info = await PlacesUtils.bookmarks.fetch({ url });
     let isNewBookmark = !info;
     let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks;
@@ -556,7 +557,7 @@ var PlacesCommandHook = {
 
     tabs.forEach(tab => {
       let browser = tab.linkedBrowser;
-      let uri = browser.currentURI;
+      let uri = browser.currentOnionAliasURI || browser.currentURI;
       let title = browser.contentTitle || tab.label;
       let spec = uri.spec;
       if (!(spec in uniquePages)) {
@@ -1655,14 +1656,17 @@ var BookmarkingUI = {
   },
 
   onLocationChange: function BUI_onLocationChange() {
-    if (this._uri && gBrowser.currentURI.equals(this._uri)) {
+    const uri =
+      gBrowser.selectedBrowser.currentOnionAliasURI || gBrowser.currentURI;
+    if (this._uri && uri.equals(this._uri)) {
       return;
     }
     this.updateStarState();
   },
 
   updateStarState: function BUI_updateStarState() {
-    this._uri = gBrowser.currentURI;
+    this._uri =
+      gBrowser.selectedBrowser.currentOnionAliasURI || gBrowser.currentURI;
     this._itemGuids.clear();
     let guids = new Set();
 
diff --git a/browser/base/content/browser-siteIdentity.js b/browser/base/content/browser-siteIdentity.js
index b7fa432d9478..539d6d4056a3 100644
--- a/browser/base/content/browser-siteIdentity.js
+++ b/browser/base/content/browser-siteIdentity.js
@@ -474,13 +474,13 @@ var gIdentityHandler = {
    *        nsIURI for which the identity UI should be displayed, already
    *        processed by createExposableURI.
    */
-  updateIdentity(state, uri) {
+  updateIdentity(state, uri, onionAliasURI) {
     let shouldHidePopup = this._uri && this._uri.spec != uri.spec;
     this._state = state;
 
     // Firstly, populate the state properties required to display the UI. See
     // the documentation of the individual properties for details.
-    this.setURI(uri);
+    this.setURI(uri, onionAliasURI);
     this._secInfo = gBrowser.securityUI.secInfo;
     this._isSecureContext = gBrowser.securityUI.isSecureContext;
 
@@ -566,17 +566,18 @@ var gIdentityHandler = {
    * Attempt to provide proper IDN treatment for host names
    */
   getEffectiveHost() {
+    let uri = this._onionAliasURI || this._uri;
     if (!this._IDNService) {
       this._IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(
         Ci.nsIIDNService
       );
     }
     try {
-      return this._IDNService.convertToDisplayIDN(this._uri.host, {});
+      return this._IDNService.convertToDisplayIDN(uri.host, {});
     } catch (e) {
       // If something goes wrong (e.g. host is an IP address) just fail back
       // to the full domain.
-      return this._uri.host;
+      return uri.host;
     }
   },
 
@@ -1000,8 +1001,9 @@ var gIdentityHandler = {
     this.updateSitePermissions();
   },
 
-  setURI(uri) {
+  setURI(uri, onionAliasURI) {
     this._uri = uri;
+    this._onionAliasURI = onionAliasURI;
 
     try {
       // Account for file: urls and catch when "" is the value
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index 9f5f0c608536..bd5f10cb6f64 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -77,6 +77,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.jsm",
   Translation: "resource:///modules/translation/TranslationParent.jsm",
+  OnionAliasStore: "resource:///modules/OnionAliasStore.jsm",
   UITour: "resource:///modules/UITour.jsm",
   UpdateUtils: "resource://gre/modules/UpdateUtils.jsm",
   UrlbarInput: "resource:///modules/UrlbarInput.jsm",
@@ -2325,6 +2326,7 @@ var gBrowserInit = {
         //                 [9]: allowInheritPrincipal (bool)
         //                 [10]: csp (nsIContentSecurityPolicy)
         //                 [11]: nsOpenWindowInfo
+        //                 [12]: onionUrlbarRewritesAllowed (bool)
         let userContextId =
           window.arguments[5] != undefined
             ? window.arguments[5]
@@ -2344,7 +2346,8 @@ var gBrowserInit = {
           // TODO fix allowInheritPrincipal to default to false.
           // Default to true unless explicitly set to false because of bug 1475201.
           window.arguments[9] !== false,
-          window.arguments[10]
+          window.arguments[10],
+          window.arguments[12]
         );
         window.focus();
       } else {
@@ -3230,7 +3233,8 @@ function loadURI(
   forceAboutBlankViewerInCurrent,
   triggeringPrincipal,
   allowInheritPrincipal = false,
-  csp = null
+  csp = null,
+  onionUrlbarRewritesAllowed = false
 ) {
   if (!triggeringPrincipal) {
     throw new Error("Must load with a triggering Principal");
@@ -3248,6 +3252,7 @@ function loadURI(
       csp,
       forceAboutBlankViewerInCurrent,
       allowInheritPrincipal,
+      onionUrlbarRewritesAllowed,
     });
   } catch (e) {
     Cu.reportError(e);
@@ -5359,11 +5364,24 @@ var XULBrowserWindow = {
         this.reloadCommand.removeAttribute("disabled");
       }
 
+      // The onion memorable alias needs to be used in gURLBar.setURI, but also in
+      // other parts of the code (like the bookmarks UI), so we save it.
+      if (gBrowser.selectedBrowser.onionUrlbarRewritesAllowed) {
+        gBrowser.selectedBrowser.currentOnionAliasURI = OnionAliasStore.getShortURI(
+          aLocationURI
+        );
+      } else {
+        gBrowser.selectedBrowser.currentOnionAliasURI = null;
+      }
+
       // We want to update the popup visibility if we received this notification
       // via simulated locationchange events such as switching between tabs, however
       // if this is a document navigation then PopupNotifications will be updated
       // via TabsProgressListener.onLocationChange and we do not want it called twice
-      gURLBar.setURI(aLocationURI, aIsSimulated);
+      gURLBar.setURI(
+        gBrowser.selectedBrowser.currentOnionAliasURI || aLocationURI,
+        aIsSimulated
+      );
 
       BookmarkingUI.onLocationChange();
 
@@ -5534,6 +5552,7 @@ var XULBrowserWindow = {
     // Don't need to do anything if the data we use to update the UI hasn't
     // changed
     let uri = gBrowser.currentURI;
+    let onionAliasURI = gBrowser.selectedBrowser.currentOnionAliasURI;
     let spec = uri.spec;
     if (this._state == aState && this._lastLocation == spec) {
       // Switching to a tab of the same URL doesn't change most security
@@ -5551,7 +5570,7 @@ var XULBrowserWindow = {
     try {
       uri = Services.io.createExposableURI(uri);
     } catch (e) {}
-    gIdentityHandler.updateIdentity(this._state, uri);
+    gIdentityHandler.updateIdentity(this._state, uri, onionAliasURI);
   },
 
   // simulate all change notifications after switching tabs
@@ -7011,6 +7030,21 @@ function handleLinkClick(event, href, linkNode) {
     } catch (e) {}
   }
 
+  // Check if the link needs to be opened with .tor.onion urlbar rewrites
+  // allowed. Only when the owner doc has onionUrlbarRewritesAllowed = true
+  // and the same origin we should allow this.
+  let persistOnionUrlbarRewritesAllowedInChildTab = false;
+  if (where == "tab" && gBrowser.docShell.onionUrlbarRewritesAllowed) {
+    const sm = Services.scriptSecurityManager;
+    try {
+      let tURI = makeURI(href);
+      let isPrivateWin =
+        doc.nodePrincipal.originAttributes.privateBrowsingId > 0;
+      sm.checkSameOriginURI(doc.documentURIObject, tURI, false, isPrivateWin);
+      persistOnionUrlbarRewritesAllowedInChildTab = true;
+    } catch (e) {}
+  }
+
   let frameOuterWindowID = WebNavigationFrames.getFrameId(doc.defaultView);
 
   urlSecurityCheck(href, doc.nodePrincipal);
@@ -7023,6 +7057,7 @@ function handleLinkClick(event, href, linkNode) {
     triggeringPrincipal: doc.nodePrincipal,
     csp: doc.csp,
     frameOuterWindowID,
+    onionUrlbarRewritesAllowed: persistOnionUrlbarRewritesAllowedInChildTab,
   };
 
   // The new tab/window must use the same userContextId
diff --git a/browser/base/content/nsContextMenu.js b/browser/base/content/nsContextMenu.js
index 354b504ce0b4..ca351c71f1cb 100644
--- a/browser/base/content/nsContextMenu.js
+++ b/browser/base/content/nsContextMenu.js
@@ -57,6 +57,7 @@ function openContextMenu(aMessage, aBrowser, aActor) {
     disableSetDesktopBackground: data.disableSetDesktopBackground,
     loginFillInfo: data.loginFillInfo,
     parentAllowsMixedContent: data.parentAllowsMixedContent,
+    parentAllowsOnionUrlbarRewrites: data.parentAllowsOnionUrlbarRewrites,
     userContextId: data.userContextId,
     webExtContextData: data.webExtContextData,
   };
@@ -1059,6 +1060,7 @@ class nsContextMenu {
       triggeringPrincipal: this.principal,
       csp: this.csp,
       frameOuterWindowID: this.contentData.frameOuterWindowID,
+      onionUrlbarRewritesAllowed: false,
     };
     for (let p in extra) {
       params[p] = extra[p];
@@ -1082,6 +1084,22 @@ class nsContextMenu {
     }
 
     params.referrerInfo = referrerInfo;
+
+    // Check if the link needs to be opened with .tor.onion urlbar rewrites
+    // allowed. Only when parent has onionUrlbarRewritesAllowed = true
+    // and the same origin we should allow this.
+    if (this.contentData.parentAllowsOnionUrlbarRewrites) {
+      let referrerURI = this.contentData.documentURIObject;
+      const sm = Services.scriptSecurityManager;
+      try {
+        let targetURI = this.linkURI;
+        let isPrivateWin =
+          this.browser.contentPrincipal.originAttributes.privateBrowsingId > 0;
+        sm.checkSameOriginURI(referrerURI, targetURI, false, isPrivateWin);
+        params.onionUrlbarRewritesAllowed = true;
+      } catch (e) {}
+    }
+
     return params;
   }
 
diff --git a/browser/base/content/pageinfo/pageInfo.js b/browser/base/content/pageinfo/pageInfo.js
index 664dace84d9f..cd03cea3b6d9 100644
--- a/browser/base/content/pageinfo/pageInfo.js
+++ b/browser/base/content/pageinfo/pageInfo.js
@@ -390,7 +390,7 @@ async function onNonMediaPageInfoLoad(browser, pageInfoData, imageInfo) {
     );
   }
   onLoadPermission(uri, principal);
-  securityOnLoad(uri, windowInfo);
+  securityOnLoad(uri, windowInfo, browser.currentOnionAliasURI);
 }
 
 function resetPageInfo(args) {
diff --git a/browser/base/content/pageinfo/pageInfo.xhtml b/browser/base/content/pageinfo/pageInfo.xhtml
index f40ffd3778d8..a23f2bb5748c 100644
--- a/browser/base/content/pageinfo/pageInfo.xhtml
+++ b/browser/base/content/pageinfo/pageInfo.xhtml
@@ -312,6 +312,16 @@
               <input id="security-identity-domain-value" readonly="readonly"/>
             </td>
           </tr>
+          <!-- Onion Alias -->
+          <tr id="security-view-identity-onionalias-row">
+            <th>
+              <xul:label id="security-view-identity-onionalias"
+                     control="security-view-identity-onionalias-value"/>
+            </th>
+            <td>
+              <input id="security-view-identity-onionalias-value" readonly="true"/>
+            </td>
+          </tr>
           <!-- Owner -->
           <tr>
             <th>
diff --git a/browser/base/content/pageinfo/security.js b/browser/base/content/pageinfo/security.js
index 4331ebc4b219..29395e96ce57 100644
--- a/browser/base/content/pageinfo/security.js
+++ b/browser/base/content/pageinfo/security.js
@@ -250,7 +250,7 @@ var security = {
   },
 };
 
-async function securityOnLoad(uri, windowInfo) {
+async function securityOnLoad(uri, windowInfo, onionAliasURI) {
   await security.init(uri, windowInfo);
 
   let info = security.securityInfo;
@@ -263,6 +263,21 @@ async function securityOnLoad(uri, windowInfo) {
   }
   document.getElementById("securityTab").hidden = false;
 
+  if (onionAliasURI) {
+    setText(
+      "security-view-identity-onionalias",
+      gTorButtonBundle.GetStringFromName("pageInfo_OnionName")
+    );
+    setText("security-view-identity-onionalias-value", onionAliasURI.host);
+    document.getElementById(
+      "security-view-identity-onionalias-row"
+    ).hidden = false;
+  } else {
+    document.getElementById(
+      "security-view-identity-onionalias-row"
+    ).hidden = true;
+  }
+
   /* Set Identity section text */
   setText("security-identity-domain-value", windowInfo.hostName);
 
diff --git a/browser/base/content/tabbrowser.js b/browser/base/content/tabbrowser.js
index 8ce4a2710cb8..8c313eaaffcb 100644
--- a/browser/base/content/tabbrowser.js
+++ b/browser/base/content/tabbrowser.js
@@ -1547,6 +1547,7 @@
       var aRelatedToCurrent;
       var aAllowInheritPrincipal;
       var aAllowMixedContent;
+      var aOnionUrlbarRewritesAllowed;
       var aSkipAnimation;
       var aForceNotRemote;
       var aPreferredRemoteType;
@@ -1577,6 +1578,7 @@
         aRelatedToCurrent = params.relatedToCurrent;
         aAllowInheritPrincipal = !!params.allowInheritPrincipal;
         aAllowMixedContent = params.allowMixedContent;
+        aOnionUrlbarRewritesAllowed = params.onionUrlbarRewritesAllowed;
         aSkipAnimation = params.skipAnimation;
         aForceNotRemote = params.forceNotRemote;
         aPreferredRemoteType = params.preferredRemoteType;
@@ -1618,6 +1620,7 @@
         relatedToCurrent: aRelatedToCurrent,
         skipAnimation: aSkipAnimation,
         allowMixedContent: aAllowMixedContent,
+        onionUrlbarRewritesAllowed: aOnionUrlbarRewritesAllowed,
         forceNotRemote: aForceNotRemote,
         createLazyBrowser: aCreateLazyBrowser,
         preferredRemoteType: aPreferredRemoteType,
@@ -2504,6 +2507,7 @@
       {
         allowInheritPrincipal,
         allowMixedContent,
+        onionUrlbarRewritesAllowed,
         allowThirdPartyFixup,
         bulkOrderedOpen,
         charset,
@@ -2833,6 +2837,9 @@
           if (allowMixedContent) {
             flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT;
           }
+          if (onionUrlbarRewritesAllowed) {
+            flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES;
+          }
           if (!allowInheritPrincipal) {
             flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_PRINCIPAL;
           }
diff --git a/browser/base/content/utilityOverlay.js b/browser/base/content/utilityOverlay.js
index a23d6f05e6a7..eb13d5a3435c 100644
--- a/browser/base/content/utilityOverlay.js
+++ b/browser/base/content/utilityOverlay.js
@@ -367,6 +367,7 @@ function openLinkIn(url, where, params) {
   var aRelatedToCurrent = params.relatedToCurrent;
   var aAllowInheritPrincipal = !!params.allowInheritPrincipal;
   var aAllowMixedContent = params.allowMixedContent;
+  var aOnionUrlbarRewritesAllowed = params.onionUrlbarRewritesAllowed;
   var aForceAllowDataURI = params.forceAllowDataURI;
   var aInBackground = params.inBackground;
   var aInitiatingDoc = params.initiatingDoc;
@@ -482,6 +483,11 @@ function openLinkIn(url, where, params) {
     ].createInstance(Ci.nsISupportsPRBool);
     allowThirdPartyFixupSupports.data = aAllowThirdPartyFixup;
 
+    var onionUrlbarRewritesAllowed = Cc[
+      "@mozilla.org/supports-PRBool;1"
+    ].createInstance(Ci.nsISupportsPRBool);
+    onionUrlbarRewritesAllowed.data = aOnionUrlbarRewritesAllowed;
+
     var userContextIdSupports = Cc[
       "@mozilla.org/supports-PRUint32;1"
     ].createInstance(Ci.nsISupportsPRUint32);
@@ -498,6 +504,8 @@ function openLinkIn(url, where, params) {
     sa.appendElement(aTriggeringPrincipal);
     sa.appendElement(null); // allowInheritPrincipal
     sa.appendElement(aCsp);
+    sa.appendElement(null); // nsOpenWindowInfo
+    sa.appendElement(onionUrlbarRewritesAllowed);
 
     const sourceWindow = w || window;
     let win;
@@ -614,6 +622,9 @@ function openLinkIn(url, where, params) {
       if (aForceAllowDataURI) {
         flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FORCE_ALLOW_DATA_URI;
       }
+      if (aOnionUrlbarRewritesAllowed) {
+        flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES;
+      }
 
       let { URI_INHERITS_SECURITY_CONTEXT } = Ci.nsIProtocolHandler;
       if (
@@ -661,6 +672,7 @@ function openLinkIn(url, where, params) {
         relatedToCurrent: aRelatedToCurrent,
         skipAnimation: aSkipTabAnimation,
         allowMixedContent: aAllowMixedContent,
+        onionUrlbarRewritesAllowed: aOnionUrlbarRewritesAllowed,
         userContextId: aUserContextId,
         originPrincipal: aPrincipal,
         originStoragePrincipal: aStoragePrincipal,
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 7d147d01d561..d30abff54562 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -703,6 +703,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   TabCrashHandler: "resource:///modules/ContentCrashHandlers.jsm",
   TabUnloader: "resource:///modules/TabUnloader.jsm",
   TRRRacer: "resource:///modules/TRRPerformance.jsm",
+  OnionAliasStore: "resource:///modules/OnionAliasStore.jsm",
   UIState: "resource://services-sync/UIState.jsm",
   WebChannel: "resource://gre/modules/WebChannel.jsm",
   WindowsRegistry: "resource://gre/modules/WindowsRegistry.jsm",
@@ -2046,6 +2047,7 @@ BrowserGlue.prototype = {
 
     Normandy.uninit();
     RFPHelper.uninit();
+    OnionAliasStore.uninit();
   },
 
   // Set up a listener to enable/disable the screenshots extension
@@ -2412,6 +2414,12 @@ BrowserGlue.prototype = {
         },
       },
 
+      {
+        task: () => {
+          OnionAliasStore.init();
+        },
+      },
+
       {
         task: () => {
           Blocklist.loadBlocklistAsync();
diff --git a/browser/components/onionservices/ExtensionMessaging.jsm b/browser/components/onionservices/ExtensionMessaging.jsm
new file mode 100644
index 000000000000..b5d69df93807
--- /dev/null
+++ b/browser/components/onionservices/ExtensionMessaging.jsm
@@ -0,0 +1,86 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["ExtensionMessaging"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { ExtensionUtils } = ChromeUtils.import(
+  "resource://gre/modules/ExtensionUtils.jsm"
+);
+const { MessageChannel } = ChromeUtils.import(
+  "resource://gre/modules/MessageChannel.jsm"
+);
+const { AddonManager } = ChromeUtils.import(
+  "resource://gre/modules/AddonManager.jsm"
+);
+
+class ExtensionMessaging {
+  constructor() {
+    this._callback = null;
+    this._handlers = new Map();
+    this._messageManager = Services.cpmm;
+  }
+
+  async sendMessage(msg, extensionId) {
+    this._init();
+
+    const addon = await AddonManager.getAddonByID(extensionId);
+    if (!addon) {
+      throw new Error(`extension '${extensionId} does not exist`);
+    }
+    await addon.startupPromise;
+
+    const channelId = ExtensionUtils.getUniqueId();
+    return new Promise((resolve, reject) => {
+      this._handlers.set(channelId, { resolve, reject });
+      this._messageManager.sendAsyncMessage("MessageChannel:Messages", [
+        {
+          messageName: "Extension:Message",
+          sender: {
+            id: extensionId,
+            extensionId,
+          },
+          recipient: { extensionId },
+          data: new StructuredCloneHolder(msg),
+          channelId,
+          responseType: MessageChannel.RESPONSE_FIRST,
+        },
+      ]);
+    });
+  }
+
+  unload() {
+    if (this._callback) {
+      this._handlers.clear();
+      this._messageManager.removeMessageListener(
+        "MessageChannel:Response",
+        this._callback
+      );
+      this._callback = null;
+    }
+  }
+
+  _onMessage({ data }) {
+    const channelId = data.messageName;
+    if (this._handlers.has(channelId)) {
+      const { resolve, reject } = this._handlers.get(channelId);
+      this._handlers.delete(channelId);
+      if (data.error) {
+        reject(new Error(data.error.message));
+      } else {
+        resolve(data.value);
+      }
+    }
+  }
+
+  _init() {
+    if (this._callback === null) {
+      this._callback = this._onMessage.bind(this);
+      this._messageManager.addMessageListener(
+        "MessageChannel:Response",
+        this._callback
+      );
+    }
+  }
+}
diff --git a/browser/components/onionservices/HttpsEverywhereControl.jsm b/browser/components/onionservices/HttpsEverywhereControl.jsm
new file mode 100644
index 000000000000..525ed5233be7
--- /dev/null
+++ b/browser/components/onionservices/HttpsEverywhereControl.jsm
@@ -0,0 +1,147 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["HttpsEverywhereControl"];
+
+const { ExtensionMessaging } = ChromeUtils.import(
+  "resource:///modules/ExtensionMessaging.jsm"
+);
+const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm");
+
+const EXTENSION_ID = "https-everywhere-eff(a)eff.org";
+const SECUREDROP_TOR_ONION_CHANNEL_2020 = {
+  name: "SecureDropTorOnion",
+  jwk: {
+    kty: "RSA",
+    e: "AQAB",
+    n:
+      "p10BbUVc5Xj2S_-MH3bACNBaISo_r9e3PVPyTTjsGsdg2qSXvqUO42fBtpFAy0zUzIGS83v4JjiRdvKJaZTIvbC8AcpymzdsTqujMm8RPTSy3hO_8mXzGa4DEsIB1uNLnUWRBKXvSGCmT9kFyxhTpkYqokNBzafVihTU34tN2Md1xFHnmZGqfYtPtbJLWAa5Z1M11EyR4lIyUxIiPTV9t1XstDbWr3iS83REJrGEFmjG1-BAgx8_lDUTa41799N2yYEhgZud7bL0M3ei8s5OERjiion5uANkUV3-s2QqUZjiVA-XR_HizXjciaUWNd683KqekpNOZ_0STh_UGwpcwU-KwG07QyiCrLrRpz8S_vH8CqGrrcWY3GSzYe9dp34jJdO65oA-G8tK6fMXtvTCFDZI6oNNaXJH71F5J0YbqO2ZqwKYc2WSi0gKVl2wd9roOVjaBmkJqvocntYuNM7t38fDEWHn5KUkmrTbiG68Cy56tDUfpKl3D9Uj4LaMvxJ1tKGvzQ4k_60odT7gIxu6DqYjXUHZpwPsSGBq3njaD7boe4CUXF2K7ViOc87BsKxRNCzDD8OklRjjXzOTOBH3PqFJ93CJ-4ECE5t9STU20aZ8E-2zKB8vjKyCySE4-kcIvBBsnkwVaJTPy9Ft1qYybo-soXEWVEZATANNWklBt8k",
+  },
+  update_path_prefix: "https://securedrop.org/https-everywhere/",
+  scope:
+    "^https?:\\/\\/[a-z0-9-]+(?:\\.[a-z0-9-]+)*\\.securedrop\\.tor\\.onion\\/",
+  replaces_default_rulesets: false,
+};
+
+const SECUREDROP_TOR_ONION_CHANNEL = {
+  name: "SecureDropTorOnion2021",
+  jwk: {
+    kty: "RSA",
+    e: "AQAB",
+    n:
+      "vsC7BNafkRe8Uh1DUgCkv6RbPQMdJgAKKnWdSqQd7tQzU1mXfmo_k1Py_2MYMZXOWmqSZ9iwIYkykZYywJ2VyMGve4byj1sLn6YQoOkG8g5Z3V4y0S2RpEfmYumNjTzfq8nxtLnwjaYd4sCUd5wa0SzeLrpRQuXo2bF3QuUF2xcbLJloxX1MmlsMMCdBc-qGNonLJ7bpn_JuyXlDWy1Fkeyw1qgjiOdiRIbMC1x302zgzX6dSrBrNB8Cpsh-vCE0ZjUo8M9caEv06F6QbYmdGJHM0ZZY34OHMSNdf-_qUKIV_SuxuSuFE99tkAeWnbWpyI1V-xhVo1sc7NzChP8ci2TdPvI3_0JyAuCvL6zIFqJUJkZibEUghhg6F09-oNJKpy7rhUJq7zZyLXJsvuXnn0gnIxfjRvMcDfZAKUVMZKRdw7fwWzwQril4Ib0MQOVda9vb_4JMk7Gup-TUI4sfuS4NKwsnKoODIO-2U5QpJWdtp1F4AQ1pBv8ajFl1WTrVGvkRGK0woPWaO6pWyJ4kRnhnxrV2FyNNt3JSR-0JEjhFWws47kjBvpr0VRiVRFppKA-plKs4LPlaaCff39TleYmY3mETe3w1GIGc2Lliad32Jpbx496IgDe1K3FMBEoKFZfhmtlRSXft8NKgSzPt2zkatM9bFKfaCYRaSy7akbk",
+  },
+  update_path_prefix: "https://securedrop.org/https-everywhere-2021/",
+  scope:
+    "^https?:\\/\\/[a-z0-9-]+(?:\\.[a-z0-9-]+)*\\.securedrop\\.tor\\.onion\\/",
+  replaces_default_rulesets: false,
+};
+
+class HttpsEverywhereControl {
+  constructor() {
+    this._extensionMessaging = null;
+  }
+
+  async _sendMessage(type, object) {
+    return this._extensionMessaging.sendMessage(
+      {
+        type,
+        object,
+      },
+      EXTENSION_ID
+    );
+  }
+
+  static async wait(seconds = 1) {
+    return new Promise(resolve => setTimeout(resolve, seconds * 1000));
+  }
+
+  /**
+   * Installs the .tor.onion update channel in https-everywhere
+   */
+  async installTorOnionUpdateChannel(retries = 5) {
+    this._init();
+
+    // TODO: https-everywhere store is initialized asynchronously, so sending a message
+    // immediately results in a `store.get is undefined` error.
+    // For now, let's wait a bit and retry a few times if there is an error, but perhaps
+    // we could suggest https-everywhere to send a message when that happens and listen
+    // for that here.
+    await HttpsEverywhereControl.wait();
+
+    try {
+      // Delete the previous channel signing key, and add the new one below.
+      await this._sendMessage(
+        "delete_update_channel",
+        SECUREDROP_TOR_ONION_CHANNEL_2020.name
+      );
+    } catch (e) {
+      if (retries <= 0) {
+        throw new Error("Could not uninstall SecureDropTorOnion update channel");
+      }
+      await this.installTorOnionUpdateChannel(retries - 1);
+      return;
+    }
+
+    try {
+      // TODO: we may want a way to "lock" this update channel, so that it cannot be modified
+      // by the user via UI, but I think this is not possible at the time of writing via
+      // the existing messages in https-everywhere.
+      await this._sendMessage(
+        "create_update_channel",
+        SECUREDROP_TOR_ONION_CHANNEL.name
+      );
+    } catch (e) {
+      if (retries <= 0) {
+        throw new Error("Could not install SecureDropTorOnion update channel");
+      }
+      await this.installTorOnionUpdateChannel(retries - 1);
+      return;
+    }
+
+    await this._sendMessage(
+      "update_update_channel",
+      SECUREDROP_TOR_ONION_CHANNEL
+    );
+  }
+
+  /**
+   * Returns the .tor.onion rulesets available in https-everywhere
+   */
+  async getTorOnionRules() {
+    return this._sendMessage("get_simple_rules_ending_with", ".tor.onion");
+  }
+
+  /**
+   * Returns the timestamp of the last .tor.onion update channel update.
+   */
+  async getRulesetTimestamp() {
+    const rulesets = await this._sendMessage("get_update_channel_timestamps");
+    const securedrop =
+      rulesets &&
+      rulesets.find(([{ name }]) => name === SECUREDROP_TOR_ONION_CHANNEL.name);
+    if (securedrop) {
+      const [
+        updateChannel, // This has the same structure as SECUREDROP_TOR_ONION_CHANNEL
+        lastUpdatedTimestamp, // An integer, 0 if the update channel was never updated
+      ] = securedrop;
+      void updateChannel; // Ignore eslint unused warning for ruleset
+      return lastUpdatedTimestamp;
+    }
+    return null;
+  }
+
+  unload() {
+    if (this._extensionMessaging) {
+      this._extensionMessaging.unload();
+      this._extensionMessaging = null;
+    }
+  }
+
+  _init() {
+    if (!this._extensionMessaging) {
+      this._extensionMessaging = new ExtensionMessaging();
+    }
+  }
+}
diff --git a/browser/components/onionservices/OnionAliasStore.jsm b/browser/components/onionservices/OnionAliasStore.jsm
new file mode 100644
index 000000000000..66cf569227bf
--- /dev/null
+++ b/browser/components/onionservices/OnionAliasStore.jsm
@@ -0,0 +1,201 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+const EXPORTED_SYMBOLS = ["OnionAliasStore"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+  "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { setTimeout, clearTimeout } = ChromeUtils.import(
+  "resource://gre/modules/Timer.jsm"
+);
+const { HttpsEverywhereControl } = ChromeUtils.import(
+  "resource:///modules/HttpsEverywhereControl.jsm"
+);
+
+// Logger adapted from CustomizableUI.jsm
+const kPrefOnionAliasDebug = "browser.onionalias.debug";
+XPCOMUtils.defineLazyPreferenceGetter(
+  this,
+  "gDebuggingEnabled",
+  kPrefOnionAliasDebug,
+  false,
+  (pref, oldVal, newVal) => {
+    if (typeof log != "undefined") {
+      log.maxLogLevel = newVal ? "all" : "log";
+    }
+  }
+);
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+  let scope = {};
+  ChromeUtils.import("resource://gre/modules/Console.jsm", scope);
+  let consoleOptions = {
+    maxLogLevel: gDebuggingEnabled ? "all" : "log",
+    prefix: "OnionAlias",
+  };
+  return new scope.ConsoleAPI(consoleOptions);
+});
+
+function observe(topic, callback) {
+  let observer = {
+    observe(aSubject, aTopic, aData) {
+      if (topic === aTopic) {
+        callback(aSubject, aData);
+      }
+    },
+  };
+  Services.obs.addObserver(observer, topic);
+  return () => Services.obs.removeObserver(observer, topic);
+}
+
+class _OnionAliasStore {
+  static get RULESET_CHECK_INTERVAL() {
+    return 1000 * 60; // 1 minute
+  }
+
+  static get RULESET_CHECK_INTERVAL_FAST() {
+    return 1000 * 5; // 5 seconds
+  }
+
+  constructor() {
+    this._onionMap = new Map();
+    this._rulesetTimeout = null;
+    this._removeObserver = () => {};
+    this._canLoadRules = false;
+    this._rulesetTimestamp = null;
+    this._updateChannelInstalled = false;
+  }
+
+  async _periodicRulesetCheck() {
+    // TODO: it would probably be preferable to listen to some message broadcasted by
+    // the https-everywhere extension when some update channel is updated, instead of
+    // polling every N seconds.
+    log.debug("Checking for new rules");
+    const ts = await this.httpsEverywhereControl.getRulesetTimestamp();
+    log.debug(
+      `Found ruleset timestamp ${ts}, current is ${this._rulesetTimestamp}`
+    );
+    if (ts !== this._rulesetTimestamp) {
+      this._rulesetTimestamp = ts;
+      log.debug("New rules found, updating");
+      // We clear the mappings even if we cannot load the rules from https-everywhere,
+      // since we cannot be sure if the stored mappings are correct anymore.
+      this._clear();
+      if (this._canLoadRules) {
+        await this._loadRules();
+      }
+    }
+    // If the timestamp is 0, that means the update channel was not yet updated, so
+    // we schedule a check soon.
+    this._rulesetTimeout = setTimeout(
+      () => this._periodicRulesetCheck(),
+      ts === 0
+        ? _OnionAliasStore.RULESET_CHECK_INTERVAL_FAST
+        : _OnionAliasStore.RULESET_CHECK_INTERVAL
+    );
+  }
+
+  async init() {
+    this.httpsEverywhereControl = new HttpsEverywhereControl();
+
+    // Setup .tor.onion rule loading.
+    // The http observer is a fallback, and is removed in _loadRules() as soon as we are able
+    // to load some rules from HTTPS Everywhere.
+    this._loadHttpObserver();
+    try {
+      await this.httpsEverywhereControl.installTorOnionUpdateChannel();
+      this._updateChannelInstalled = true;
+      await this.httpsEverywhereControl.getTorOnionRules();
+      this._canLoadRules = true;
+    } catch (e) {
+      // Loading rules did not work, probably because "get_simple_rules_ending_with" is not yet
+      // working in https-everywhere. Use an http observer as a fallback for learning the rules.
+      log.debug(`Could not load rules: ${e.message}`);
+    }
+
+    // Setup checker for https-everywhere ruleset updates
+    if (this._updateChannelInstalled) {
+      this._periodicRulesetCheck();
+    }
+  }
+
+  /**
+   * Loads the .tor.onion mappings from https-everywhere.
+   */
+  async _loadRules() {
+    const rules = await this.httpsEverywhereControl.getTorOnionRules();
+    // Remove http observer if we are able to load some rules directly.
+    if (rules.length) {
+      this._removeObserver();
+      this._removeObserver = () => {};
+    }
+    this._clear();
+    log.debug(`Loading ${rules.length} rules`, rules);
+    for (const rule of rules) {
+      // Here we are trusting that the securedrop ruleset follows some conventions so that we can
+      // assume there is a host mapping from `rule.host` to the hostname of the URL in `rule.to`.
+      try {
+        const url = new URL(rule.to);
+        const shortHost = rule.host;
+        const longHost = url.hostname;
+        this._addMapping(shortHost, longHost);
+      } catch (e) {
+        log.error("Could not process rule:", rule);
+      }
+    }
+  }
+
+  /**
+   * Loads a http observer to listen for local redirects for populating
+   * the .tor.onion -> .onion mappings. Should only be used if we cannot ask https-everywhere
+   * directly for the mappings.
+   */
+  _loadHttpObserver() {
+    this._removeObserver = observe("http-on-before-connect", channel => {
+      if (
+        channel.isMainDocumentChannel &&
+        channel.originalURI.host.endsWith(".tor.onion")
+      ) {
+        this._addMapping(channel.originalURI.host, channel.URI.host);
+      }
+    });
+  }
+
+  uninit() {
+    this._clear();
+    this._removeObserver();
+    this._removeObserver = () => {};
+    if (this.httpsEverywhereControl) {
+      this.httpsEverywhereControl.unload();
+      delete this.httpsEverywhereControl;
+    }
+    clearTimeout(this._rulesetTimeout);
+    this._rulesetTimeout = null;
+    this._rulesetTimestamp = null;
+  }
+
+  _clear() {
+    this._onionMap.clear();
+  }
+
+  _addMapping(shortOnionHost, longOnionHost) {
+    this._onionMap.set(longOnionHost, shortOnionHost);
+  }
+
+  getShortURI(onionURI) {
+    if (
+      (onionURI.schemeIs("http") || onionURI.schemeIs("https")) &&
+      this._onionMap.has(onionURI.host)
+    ) {
+      return onionURI
+        .mutate()
+        .setHost(this._onionMap.get(onionURI.host))
+        .finalize();
+    }
+    return null;
+  }
+}
+
+let OnionAliasStore = new _OnionAliasStore();
diff --git a/browser/components/onionservices/moz.build b/browser/components/onionservices/moz.build
index 7e103239c8d6..e4b6d73f8f40 100644
--- a/browser/components/onionservices/moz.build
+++ b/browser/components/onionservices/moz.build
@@ -1 +1,7 @@
 JAR_MANIFESTS += ['jar.mn']
+
+EXTRA_JS_MODULES += [
+    'ExtensionMessaging.jsm',
+    'HttpsEverywhereControl.jsm',
+    'OnionAliasStore.jsm',
+]
diff --git a/browser/components/urlbar/UrlbarInput.jsm b/browser/components/urlbar/UrlbarInput.jsm
index db1c497bcace..13b1279105f2 100644
--- a/browser/components/urlbar/UrlbarInput.jsm
+++ b/browser/components/urlbar/UrlbarInput.jsm
@@ -277,7 +277,10 @@ class UrlbarInput {
     // bar if the user has deleted the URL and we'd just put the same URL
     // back. See bug 304198.
     if (value === null) {
-      uri = uri || this.window.gBrowser.currentURI;
+      uri =
+        uri ||
+        this.window.gBrowser.selectedBrowser.currentOnionAliasURI ||
+        this.window.gBrowser.currentURI;
       // Strip off usernames and passwords for the location bar
       try {
         uri = Services.io.createExposableURI(uri);
@@ -1541,7 +1544,13 @@ class UrlbarInput {
     }
 
     let uri;
-    if (this.getAttribute("pageproxystate") == "valid") {
+    // When we rewrite .onion to an alias, gBrowser.currentURI will be different than
+    // the URI displayed in the urlbar. We need to use the urlbar value to copy the
+    // alias instead of the actual .onion URI that is loaded.
+    if (
+      this.getAttribute("pageproxystate") == "valid" &&
+      !this.window.gBrowser.selectedBrowser.currentOnionAliasURI
+    ) {
       uri = this.window.gBrowser.currentURI;
     } else {
       // The value could be:
diff --git a/docshell/base/nsDocShell.cpp b/docshell/base/nsDocShell.cpp
index 8731e44dd4eb..bf9639c82612 100644
--- a/docshell/base/nsDocShell.cpp
+++ b/docshell/base/nsDocShell.cpp
@@ -5657,6 +5657,10 @@ void nsDocShell::OnRedirectStateChange(nsIChannel* aOldChannel,
     return;
   }
 
+  if (!mOnionUrlbarRewritesAllowed && IsTorOnionRedirect(oldURI, newURI)) {
+    mOnionUrlbarRewritesAllowed = true;
+  }
+
   // DocumentChannel adds redirect chain to global history in the parent
   // process. The redirect chain can't be queried from the content process, so
   // there's no need to update global history here.
@@ -8693,6 +8697,20 @@ nsresult nsDocShell::HandleSameDocumentNavigation(
   return NS_OK;
 }
 
+/* static */
+bool nsDocShell::IsTorOnionRedirect(nsIURI* aOldURI, nsIURI* aNewURI) {
+    nsAutoCString oldHost;
+    nsAutoCString newHost;
+    if (aOldURI && aNewURI && NS_SUCCEEDED(aOldURI->GetHost(oldHost)) &&
+        StringEndsWith(oldHost, NS_LITERAL_CSTRING(".tor.onion")) &&
+        NS_SUCCEEDED(aNewURI->GetHost(newHost)) &&
+        StringEndsWith(newHost, NS_LITERAL_CSTRING(".onion")) &&
+        !StringEndsWith(newHost, NS_LITERAL_CSTRING(".tor.onion"))) {
+      return true;
+    }
+    return false;
+  }
+
 nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState,
                                   nsIDocShell** aDocShell,
                                   nsIRequest** aRequest) {
@@ -8844,6 +8862,30 @@ nsresult nsDocShell::InternalLoad(nsDocShellLoadState* aLoadState,
 
   mAllowKeywordFixup =
       aLoadState->HasLoadFlags(INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP);
+
+  if (mOnionUrlbarRewritesAllowed) {
+    mOnionUrlbarRewritesAllowed = false;
+    nsCOMPtr<nsIURI> referrer;
+    nsIReferrerInfo* referrerInfo = aLoadState->GetReferrerInfo();
+    if (referrerInfo) {
+      referrerInfo->GetOriginalReferrer(getter_AddRefs(referrer));
+      bool isPrivateWin = false;
+      Document* doc = GetDocument();
+      if (doc) {
+        isPrivateWin =
+            doc->NodePrincipal()->OriginAttributesRef().mPrivateBrowsingId > 0;
+        nsCOMPtr<nsIScriptSecurityManager> secMan =
+            do_GetService(NS_SCRIPTSECURITYMANAGER_CONTRACTID);
+        mOnionUrlbarRewritesAllowed =
+            secMan && NS_SUCCEEDED(secMan->CheckSameOriginURI(
+                          aLoadState->URI(), referrer, false, isPrivateWin));
+      }
+    }
+  }
+  mOnionUrlbarRewritesAllowed =
+      mOnionUrlbarRewritesAllowed ||
+      aLoadState->HasLoadFlags(INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES);
+
   mURIResultedInDocument = false;  // reset the clock...
 
   // See if this is actually a load between two history entries for the same
@@ -10996,6 +11038,7 @@ nsresult nsDocShell::AddToSessionHistory(
                 mDynamicallyCreated, originalURI, resultPrincipalURI,
                 loadReplace, referrerInfo, srcdoc, srcdocEntry, baseURI,
                 saveLayoutState, expired);
+  entry->SetOnionUrlbarRewritesAllowed(mOnionUrlbarRewritesAllowed);
 
   if (root == static_cast<nsIDocShellTreeItem*>(this) && GetSessionHistory()) {
     bool shouldPersist = ShouldAddToSessionHistory(aURI, aChannel);
@@ -12793,3 +12836,12 @@ bool nsDocShell::GetIsAttemptingToNavigate() {
 
   return false;
 }
+
+NS_IMETHODIMP
+nsDocShell::GetOnionUrlbarRewritesAllowed(bool* aOnionUrlbarRewritesAllowed) {
+  NS_ENSURE_ARG(aOnionUrlbarRewritesAllowed);
+  *aOnionUrlbarRewritesAllowed =
+      StaticPrefs::browser_urlbar_onionRewrites_enabled() &&
+      mOnionUrlbarRewritesAllowed;
+  return NS_OK;
+}
diff --git a/docshell/base/nsDocShell.h b/docshell/base/nsDocShell.h
index dcffed8e5537..d403a06f4c9f 100644
--- a/docshell/base/nsDocShell.h
+++ b/docshell/base/nsDocShell.h
@@ -152,6 +152,9 @@ class nsDocShell final : public nsDocLoader,
 
     // Whether the load should go through LoadURIDelegate.
     INTERNAL_LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE = 0x2000,
+
+    // Whether rewriting the urlbar to a short .onion alias is allowed.
+    INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES = 0x4000,
   };
 
   // Event type dispatched by RestorePresentation
@@ -582,6 +585,8 @@ class nsDocShell final : public nsDocLoader,
 
   virtual void DestroyChildren() override;
 
+  static bool IsTorOnionRedirect(nsIURI* aOldURI, nsIURI* aNewURI);
+
   // Overridden from nsDocLoader, this provides more information than the
   // normal OnStateChange with flags STATE_REDIRECTING
   virtual void OnRedirectStateChange(nsIChannel* aOldChannel,
@@ -1264,6 +1269,7 @@ class nsDocShell final : public nsDocLoader,
   bool mCSSErrorReportingEnabled : 1;
   bool mAllowAuth : 1;
   bool mAllowKeywordFixup : 1;
+  bool mOnionUrlbarRewritesAllowed : 1;
   bool mIsOffScreenBrowser : 1;
   bool mDisableMetaRefreshWhenInactive : 1;
   bool mIsAppTab : 1;
diff --git a/docshell/base/nsDocShellLoadState.cpp b/docshell/base/nsDocShellLoadState.cpp
index d91bb6034f9d..fe1216bd2509 100644
--- a/docshell/base/nsDocShellLoadState.cpp
+++ b/docshell/base/nsDocShellLoadState.cpp
@@ -730,6 +730,10 @@ void nsDocShellLoadState::CalculateLoadURIFlags() {
     mLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP;
   }
 
+  if (oldLoadFlags & nsIWebNavigation::LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES) {
+    mLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES;
+  }
+
   if (oldLoadFlags & nsIWebNavigation::LOAD_FLAGS_FIRST_LOAD) {
     mLoadFlags |= nsDocShell::INTERNAL_LOAD_FLAGS_FIRST_LOAD;
   }
diff --git a/docshell/base/nsIDocShell.idl b/docshell/base/nsIDocShell.idl
index 01dc4734eb19..305acfd3a98f 100644
--- a/docshell/base/nsIDocShell.idl
+++ b/docshell/base/nsIDocShell.idl
@@ -1019,4 +1019,9 @@ interface nsIDocShell : nsIDocShellTreeItem
    * until session history state is moved into the parent process.
    */
   void persistLayoutHistoryState();
+
+  /**
+   * Whether rewriting the urlbar to a short .onion alias is allowed.
+   */
+  [infallible] readonly attribute boolean onionUrlbarRewritesAllowed;
 };
diff --git a/docshell/base/nsIWebNavigation.idl b/docshell/base/nsIWebNavigation.idl
index bbc3eb7583a2..a4cceb52b2a0 100644
--- a/docshell/base/nsIWebNavigation.idl
+++ b/docshell/base/nsIWebNavigation.idl
@@ -237,6 +237,11 @@ interface nsIWebNavigation : nsISupports
    */
   const unsigned long LOAD_FLAGS_BYPASS_LOAD_URI_DELEGATE = 0x4000000;
 
+  /**
+   * Allow rewriting the urlbar to a short .onion alias.
+   */
+  const unsigned long LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES = 0x8000000;
+
   /**
    * Loads a given URI.  This will give priority to loading the requested URI
    * in the object implementing this interface.  If it can't be loaded here
diff --git a/docshell/shistory/SessionHistoryEntry.cpp b/docshell/shistory/SessionHistoryEntry.cpp
index 07a28d1291c9..566b7d68fae5 100644
--- a/docshell/shistory/SessionHistoryEntry.cpp
+++ b/docshell/shistory/SessionHistoryEntry.cpp
@@ -490,6 +490,20 @@ SessionHistoryEntry::SetPersist(bool aPersist) {
   return NS_OK;
 }
 
+NS_IMETHODIMP
+SessionHistoryEntry::GetOnionUrlbarRewritesAllowed(
+    bool* aOnionUrlbarRewritesAllowed) {
+  *aOnionUrlbarRewritesAllowed = mInfo->mOnionUrlbarRewritesAllowed;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+SessionHistoryEntry::SetOnionUrlbarRewritesAllowed(
+    bool aOnionUrlbarRewritesAllowed) {
+  mInfo->mOnionUrlbarRewritesAllowed = aOnionUrlbarRewritesAllowed;
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 SessionHistoryEntry::GetScrollPosition(int32_t* aX, int32_t* aY) {
   *aX = mInfo->mScrollPositionX;
diff --git a/docshell/shistory/SessionHistoryEntry.h b/docshell/shistory/SessionHistoryEntry.h
index fa6108952688..c51682a65b0a 100644
--- a/docshell/shistory/SessionHistoryEntry.h
+++ b/docshell/shistory/SessionHistoryEntry.h
@@ -58,6 +58,7 @@ class SessionHistoryInfo {
   bool mIsSrcdocEntry = false;
   bool mScrollRestorationIsManual = false;
   bool mPersist = false;
+  bool mOnionUrlbarRewritesAllowed = false;
 };
 
 // XXX Not sure that the id shouldn't just live in SessionHistoryInfo.
diff --git a/docshell/shistory/nsISHEntry.idl b/docshell/shistory/nsISHEntry.idl
index 8bbf2a68687c..bf410bfcc05a 100644
--- a/docshell/shistory/nsISHEntry.idl
+++ b/docshell/shistory/nsISHEntry.idl
@@ -242,6 +242,11 @@ interface nsISHEntry : nsISupports
      */
     [infallible] attribute boolean persist;
 
+    /**
+     * Whether rewriting the urlbar to a short .onion alias is allowed.
+     */
+    [infallible] attribute boolean onionUrlbarRewritesAllowed;
+
     /**
      * Set/Get the visual viewport scroll position if session history is
      * changed through anchor navigation or pushState.
diff --git a/docshell/shistory/nsSHEntry.cpp b/docshell/shistory/nsSHEntry.cpp
index 229b15eff25d..12d38f4c55fe 100644
--- a/docshell/shistory/nsSHEntry.cpp
+++ b/docshell/shistory/nsSHEntry.cpp
@@ -44,7 +44,8 @@ nsSHEntry::nsSHEntry(nsISHistory* aSHistory)
       mIsSrcdocEntry(false),
       mScrollRestorationIsManual(false),
       mLoadedInThisProcess(false),
-      mPersist(true) {}
+      mPersist(true),
+      mOnionUrlbarRewritesAllowed(false) {}
 
 nsSHEntry::nsSHEntry(const nsSHEntry& aOther)
     : mShared(aOther.mShared),
@@ -70,7 +71,8 @@ nsSHEntry::nsSHEntry(const nsSHEntry& aOther)
       mIsSrcdocEntry(aOther.mIsSrcdocEntry),
       mScrollRestorationIsManual(false),
       mLoadedInThisProcess(aOther.mLoadedInThisProcess),
-      mPersist(aOther.mPersist) {}
+      mPersist(aOther.mPersist),
+      mOnionUrlbarRewritesAllowed(aOther.mOnionUrlbarRewritesAllowed) {}
 
 nsSHEntry::~nsSHEntry() {
   // Null out the mParent pointers on all our kids.
@@ -824,6 +826,18 @@ nsSHEntry::SetPersist(bool aPersist) {
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsSHEntry::GetOnionUrlbarRewritesAllowed(bool* aOnionUrlbarRewritesAllowed) {
+  *aOnionUrlbarRewritesAllowed = mOnionUrlbarRewritesAllowed;
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSHEntry::SetOnionUrlbarRewritesAllowed(bool aOnionUrlbarRewritesAllowed) {
+  mOnionUrlbarRewritesAllowed = aOnionUrlbarRewritesAllowed;
+  return NS_OK;
+}
+
 NS_IMETHODIMP
 nsSHEntry::CreateLoadInfo(nsDocShellLoadState** aLoadState) {
   nsCOMPtr<nsIURI> uri = GetURI();
@@ -873,6 +887,10 @@ nsSHEntry::CreateLoadInfo(nsDocShellLoadState** aLoadState) {
   } else {
     srcdoc = VoidString();
   }
+  if (GetOnionUrlbarRewritesAllowed()) {
+    flags |= nsDocShell::InternalLoad::
+        INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES;
+  }
   loadState->SetSrcdocData(srcdoc);
   loadState->SetBaseURI(baseURI);
   loadState->SetLoadFlags(flags);
diff --git a/docshell/shistory/nsSHEntry.h b/docshell/shistory/nsSHEntry.h
index 351f6f200a33..6bc9de521437 100644
--- a/docshell/shistory/nsSHEntry.h
+++ b/docshell/shistory/nsSHEntry.h
@@ -63,6 +63,7 @@ class nsSHEntry : public nsISHEntry {
   bool mScrollRestorationIsManual;
   bool mLoadedInThisProcess;
   bool mPersist;
+  bool mOnionUrlbarRewritesAllowed;
 };
 
 #endif /* nsSHEntry_h */
diff --git a/dom/interfaces/base/nsIBrowser.idl b/dom/interfaces/base/nsIBrowser.idl
index 300b09e13824..a9ff00e964b7 100644
--- a/dom/interfaces/base/nsIBrowser.idl
+++ b/dom/interfaces/base/nsIBrowser.idl
@@ -158,7 +158,8 @@ interface nsIBrowser : nsISupports
                                in uint64_t aInnerWindowID,
                                in boolean aHasRequestContextID,
                                in uint64_t aRequestContextID,
-                               in AString aContentType);
+                               in AString aContentType,
+                               in boolean aOnionUrlbarRewritesAllowed);
 
   /**
    * Called by Gecko when it wants to change the process which is currently
diff --git a/dom/ipc/BrowserChild.cpp b/dom/ipc/BrowserChild.cpp
index 2b17323d8a93..f72aa8faa11d 100644
--- a/dom/ipc/BrowserChild.cpp
+++ b/dom/ipc/BrowserChild.cpp
@@ -3645,6 +3645,8 @@ NS_IMETHODIMP BrowserChild::OnLocationChange(nsIWebProgress* aWebProgress,
         docShell->GetMayEnableCharacterEncodingMenu();
     locationChangeData->charsetAutodetected() =
         docShell->GetCharsetAutodetected();
+    locationChangeData->onionUrlbarRewritesAllowed() =
+        docShell->GetOnionUrlbarRewritesAllowed();
 
     locationChangeData->contentPrincipal() = document->NodePrincipal();
     locationChangeData->contentStoragePrincipal() =
diff --git a/dom/ipc/BrowserParent.cpp b/dom/ipc/BrowserParent.cpp
index 7749792cafb4..086ecd95e9e7 100644
--- a/dom/ipc/BrowserParent.cpp
+++ b/dom/ipc/BrowserParent.cpp
@@ -2698,7 +2698,8 @@ mozilla::ipc::IPCResult BrowserParent::RecvOnLocationChange(
         aWebProgressData->innerDOMWindowID(),
         aLocationChangeData->requestContextID().isSome(),
         aLocationChangeData->requestContextID().valueOr(0),
-        aLocationChangeData->contentType());
+        aLocationChangeData->contentType(),
+        aLocationChangeData->onionUrlbarRewritesAllowed());
   }
 
   Unused << managerAsListener->OnLocationChange(webProgress, request, aLocation,
diff --git a/dom/ipc/PBrowser.ipdl b/dom/ipc/PBrowser.ipdl
index f36e2c6db353..15756e7fef4f 100644
--- a/dom/ipc/PBrowser.ipdl
+++ b/dom/ipc/PBrowser.ipdl
@@ -142,6 +142,7 @@ struct WebProgressLocationChangeData
   bool isSyntheticDocument;
   bool mayEnableCharacterEncodingMenu;
   bool charsetAutodetected;
+  bool onionUrlbarRewritesAllowed;
   nsString contentType;
   nsString title;
   nsString charset;
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
index fa5628a01c94..1e8dc23fdfd7 100644
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -1077,6 +1077,12 @@
   value: true
   mirror: always
 
+  # Whether rewriting the urlbar to a short .onion alias is allowed.
+- name: browser.urlbar.onionRewrites.enabled
+  type: RelaxedAtomicBool
+  value: true
+  mirror: always
+
 - name: browser.viewport.desktopWidth
   type: RelaxedAtomicInt32
   value: 980
diff --git a/netwerk/dns/effective_tld_names.dat b/netwerk/dns/effective_tld_names.dat
index 84e6c2c53630..5fe3186e3729 100644
--- a/netwerk/dns/effective_tld_names.dat
+++ b/netwerk/dns/effective_tld_names.dat
@@ -5518,6 +5518,8 @@ pro.om
 
 // onion : https://tools.ietf.org/html/rfc7686
 onion
+tor.onion
+securedrop.tor.onion
 
 // org : https://en.wikipedia.org/wiki/.org
 org
diff --git a/netwerk/ipc/DocumentLoadListener.cpp b/netwerk/ipc/DocumentLoadListener.cpp
index 901ce11b57ca..988ec94a82d4 100644
--- a/netwerk/ipc/DocumentLoadListener.cpp
+++ b/netwerk/ipc/DocumentLoadListener.cpp
@@ -1856,6 +1856,16 @@ DocumentLoadListener::AsyncOnChannelRedirect(
         mLoadStateLoadType, nsIWebNavigation::LOAD_FLAGS_ALLOW_MIXED_CONTENT));
   }
 
+  // Like the code above for allowing mixed content, we need to check this here
+  // in case the redirect is not handled in the docshell.
+  nsCOMPtr<nsIURI> oldURI, newURI;
+  aOldChannel->GetURI(getter_AddRefs(oldURI));
+  aNewChannel->GetURI(getter_AddRefs(newURI));
+  if (nsDocShell::IsTorOnionRedirect(oldURI, newURI)) {
+     mLoadStateLoadFlags |=
+      nsDocShell::INTERNAL_LOAD_FLAGS_ALLOW_ONION_URLBAR_REWRITES;
+  }
+
   // We need the original URI of the current channel to use to open the real
   // channel in the content process. Unfortunately we overwrite the original
   // uri of the new channel with the original pre-redirect URI, so grab
diff --git a/toolkit/content/widgets/browser-custom-element.js b/toolkit/content/widgets/browser-custom-element.js
index 62a1ab1d6796..67fa1e5645c0 100644
--- a/toolkit/content/widgets/browser-custom-element.js
+++ b/toolkit/content/widgets/browser-custom-element.js
@@ -261,6 +261,8 @@
 
       this._mayEnableCharacterEncodingMenu = null;
 
+      this._onionUrlbarRewritesAllowed = false;
+
       this._charsetAutodetected = false;
 
       this._contentPrincipal = null;
@@ -686,6 +688,12 @@
       }
     }
 
+    get onionUrlbarRewritesAllowed() {
+      return this.isRemoteBrowser
+        ? this._onionUrlbarRewritesAllowed
+        : this.docShell.onionUrlbarRewritesAllowed;
+    }
+
     get charsetAutodetected() {
       return this.isRemoteBrowser
         ? this._charsetAutodetected
@@ -1292,7 +1300,8 @@
       aInnerWindowID,
       aHaveRequestContextID,
       aRequestContextID,
-      aContentType
+      aContentType,
+      aOnionUrlbarRewritesAllowed
     ) {
       if (this.isRemoteBrowser && this.messageManager) {
         if (aCharset != null) {
@@ -1316,6 +1325,7 @@
         this._contentRequestContextID = aHaveRequestContextID
           ? aRequestContextID
           : null;
+        this._onionUrlbarRewritesAllowed = aOnionUrlbarRewritesAllowed;
       }
     }
 
@@ -1708,6 +1718,7 @@
             "_contentStoragePrincipal",
             "_isSyntheticDocument",
             "_innerWindowID",
+            "_onionUrlbarRewritesAllowed",
           ]
         );
       }
diff --git a/toolkit/modules/sessionstore/SessionHistory.jsm b/toolkit/modules/sessionstore/SessionHistory.jsm
index 6c16ac331659..cc47f437824f 100644
--- a/toolkit/modules/sessionstore/SessionHistory.jsm
+++ b/toolkit/modules/sessionstore/SessionHistory.jsm
@@ -318,6 +318,7 @@ var SessionHistoryInternal = {
     }
 
     entry.persist = shEntry.persist;
+    entry.onionUrlbarRewritesAllowed = shEntry.onionUrlbarRewritesAllowed;
 
     return entry;
   },
@@ -601,6 +602,10 @@ var SessionHistoryInternal = {
       }
     }
 
+    if (entry.onionUrlbarRewritesAllowed) {
+      shEntry.onionUrlbarRewritesAllowed = entry.onionUrlbarRewritesAllowed;
+    }
+
     return shEntry;
   },
 
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 33342: Avoid disconnect search addon error after removal.
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit 93ac4bea76ef71f7ad063f06f515de762be03e6b
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Fri Mar 13 18:19:30 2020 +0100
    Bug 33342: Avoid disconnect search addon error after removal.
    
    We removed the addon in #32767, but it was still being loaded
    from addonStartup.json.lz4 and throwing an error on startup
    because its resource: location is not available anymore.
---
 toolkit/mozapps/extensions/internal/XPIProvider.jsm | 6 ++++++
 1 file changed, 6 insertions(+)
diff --git a/toolkit/mozapps/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
index 5e467fb4f14c..794c206fb453 100644
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -970,6 +970,12 @@ var BuiltInLocation = new (class _BuiltInLocation extends XPIStateLocation {
   get enumerable() {
     return false;
   }
+
+  restore(saved) {
+    super.restore(saved);
+    // Bug 33342: avoid restoring disconnect addon from addonStartup.json.lz4.
+    this.removeAddon("disconnect(a)search.mozilla.org");
+  }
 })();
 
 /**
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 1658881 - When failing to create a channel and an image request, make sure to set the image blocking status appropriately. r=tnikkel
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit 560ad40889736dac79200b277cd2fea1dd54d1a4
Author: Emilio Cobos Álvarez <emilio(a)crisal.io>
Date:   Wed Sep 9 22:58:29 2020 +0000
    Bug 1658881 - When failing to create a channel and an image request, make sure to set the image blocking status appropriately. r=tnikkel
    
    This is the same status as we do for known no-data protocols here:
    
      https://searchfox.org/mozilla-central/rev/ac142717cc067d875e83e4b1316f004f6…
    
    This ensures we treat these two cases the same.
    
    Differential Revision: https://phabricator.services.mozilla.com/D89382
---
 dom/base/nsImageLoadingContent.cpp              | 7 ++++++-
 layout/reftests/image/reftest.list              | 1 +
 layout/reftests/image/unknown-protocol-ref.html | 1 +
 layout/reftests/image/unknown-protocol.html     | 1 +
 4 files changed, 9 insertions(+), 1 deletion(-)
diff --git a/dom/base/nsImageLoadingContent.cpp b/dom/base/nsImageLoadingContent.cpp
index 23b1fd791c1f..85de63bef02d 100644
--- a/dom/base/nsImageLoadingContent.cpp
+++ b/dom/base/nsImageLoadingContent.cpp
@@ -1207,7 +1207,12 @@ nsresult nsImageLoadingContent::LoadImage(nsIURI* aNewURI, bool aForce,
     MOZ_ASSERT(!req, "Shouldn't have non-null request here");
     // If we don't have a current URI, we might as well store this URI so people
     // know what we tried (and failed) to load.
-    if (!mCurrentRequest) mCurrentURI = aNewURI;
+    if (!mCurrentRequest) {
+      mCurrentURI = aNewURI;
+      if (mImageBlockingStatus == nsIContentPolicy::ACCEPT) {
+        mImageBlockingStatus = nsIContentPolicy::REJECT_REQUEST;
+      }
+    }
 
     FireEvent(NS_LITERAL_STRING("error"));
     FireEvent(NS_LITERAL_STRING("loadend"));
diff --git a/layout/reftests/image/reftest.list b/layout/reftests/image/reftest.list
index a8a91c13ed3a..3c561fe3a7c8 100644
--- a/layout/reftests/image/reftest.list
+++ b/layout/reftests/image/reftest.list
@@ -69,3 +69,4 @@ random-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) == image-srcset-basic-selec
 pref(dom.image-lazy-loading.enabled,true) == moz-broken-matching-lazy-load.html moz-broken-matching-1-ref.html
 
 == img-invalidation-local-transform-1.html img-invalidation-local-transform-1-ref.html
+== unknown-protocol.html unknown-protocol-ref.html
diff --git a/layout/reftests/image/unknown-protocol-ref.html b/layout/reftests/image/unknown-protocol-ref.html
new file mode 100644
index 000000000000..b5bb326eef57
--- /dev/null
+++ b/layout/reftests/image/unknown-protocol-ref.html
@@ -0,0 +1 @@
+<img src="mailto://foo">
diff --git a/layout/reftests/image/unknown-protocol.html b/layout/reftests/image/unknown-protocol.html
new file mode 100644
index 000000000000..ef06881b7bcb
--- /dev/null
+++ b/layout/reftests/image/unknown-protocol.html
@@ -0,0 +1 @@
+<img src="foobar://baz">
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 32418: Allow updates to be disabled via an enterprise policy.
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit d5790c1c1fc3a195c342ae8fedb9c5d6c707c5aa
Author: Kathy Brade <brade(a)pearlcrescent.com>
Date:   Thu Apr 16 17:07:09 2020 -0400
    Bug 32418: Allow updates to be disabled via an enterprise policy.
    
    Restrict the Enterprise Policies mechanism to only consult a
    policies.json file (avoiding the Windows Registry and macOS's
    file system attributes).
    
    Add a few disabledByPolicy() checks to the update service to
    avoid extraneous (and potentially confusing) log messages when
    updates are disabled by policy.
    
    Sample content for distribution/policies.json:
    {
      "policies": {
        "DisableAppUpdate": true
      }
    }
    
    On Linux, avoid reading policies from /etc/firefox/policies/policies.json
---
 .../components/enterprisepolicies/EnterprisePolicies.js  | 12 ++++++++++++
 toolkit/components/enterprisepolicies/moz.build          |  4 +++-
 toolkit/mozapps/update/UpdateService.jsm                 | 16 ++++++++++++++++
 3 files changed, 31 insertions(+), 1 deletion(-)
diff --git a/toolkit/components/enterprisepolicies/EnterprisePolicies.js b/toolkit/components/enterprisepolicies/EnterprisePolicies.js
index 070d5fe1f16b..adb073a2350c 100644
--- a/toolkit/components/enterprisepolicies/EnterprisePolicies.js
+++ b/toolkit/components/enterprisepolicies/EnterprisePolicies.js
@@ -2,6 +2,10 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
+// To ensure that policies intended for Firefox or another browser will not
+// be used, Tor Browser only looks for policies in ${InstallDir}/distribution
+#define AVOID_SYSTEM_POLICIES MOZ_PROXY_BYPASS_PROTECTION
+
 const { XPCOMUtils } = ChromeUtils.import(
   "resource://gre/modules/XPCOMUtils.jsm"
 );
@@ -11,9 +15,11 @@ const { AppConstants } = ChromeUtils.import(
 );
 
 XPCOMUtils.defineLazyModuleGetters(this, {
+#ifndef AVOID_SYSTEM_POLICIES
   WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.jsm",
   macOSPoliciesParser:
     "resource://gre/modules/policies/macOSPoliciesParser.jsm",
+#endif
   Policies: "resource:///modules/policies/Policies.jsm",
   JsonSchemaValidator:
     "resource://gre/modules/components-utils/JsonSchemaValidator.jsm",
@@ -117,11 +123,13 @@ EnterprisePoliciesManager.prototype = {
 
   _chooseProvider() {
     let platformProvider = null;
+#ifndef AVOID_SYSTEM_POLICIES
     if (AppConstants.platform == "win") {
       platformProvider = new WindowsGPOPoliciesProvider();
     } else if (AppConstants.platform == "macosx") {
       platformProvider = new macOSPoliciesProvider();
     }
+#endif
     let jsonProvider = new JSONPoliciesProvider();
     if (platformProvider && platformProvider.hasPolicies) {
       if (jsonProvider.hasPolicies) {
@@ -470,6 +478,7 @@ class JSONPoliciesProvider {
   _getConfigurationFile() {
     let configFile = null;
 
+#ifndef AVOID_SYSTEM_POLICIES
     if (AppConstants.platform == "linux") {
       let systemConfigFile = Cc["@mozilla.org/file/local;1"].createInstance(
         Ci.nsIFile
@@ -482,6 +491,7 @@ class JSONPoliciesProvider {
         return systemConfigFile;
       }
     }
+#endif
 
     try {
       let perUserPath = Services.prefs.getBoolPref(PREF_PER_USER_DIR, false);
@@ -563,6 +573,7 @@ class JSONPoliciesProvider {
   }
 }
 
+#ifndef AVOID_SYSTEM_POLICIES
 class WindowsGPOPoliciesProvider {
   constructor() {
     this._policies = null;
@@ -637,6 +648,7 @@ class macOSPoliciesProvider {
     return this._failed;
   }
 }
+#endif
 
 class CombinedProvider {
   constructor(primaryProvider, secondaryProvider) {
diff --git a/toolkit/components/enterprisepolicies/moz.build b/toolkit/components/enterprisepolicies/moz.build
index 8f7d7d8cfed7..7528f569bb3e 100644
--- a/toolkit/components/enterprisepolicies/moz.build
+++ b/toolkit/components/enterprisepolicies/moz.build
@@ -19,10 +19,12 @@ TEST_DIRS += [
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] != "android":
     EXTRA_COMPONENTS += [
-        'EnterprisePolicies.js',
         'EnterprisePolicies.manifest',
         'EnterprisePoliciesContent.js',
     ]
+    EXTRA_PP_COMPONENTS += [
+        'EnterprisePolicies.js',
+    ]
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
     EXTRA_JS_MODULES.policies += [
diff --git a/toolkit/mozapps/update/UpdateService.jsm b/toolkit/mozapps/update/UpdateService.jsm
index 2c565cecadd7..1fb397373151 100644
--- a/toolkit/mozapps/update/UpdateService.jsm
+++ b/toolkit/mozapps/update/UpdateService.jsm
@@ -3268,6 +3268,14 @@ UpdateService.prototype = {
    * See nsIUpdateService.idl
    */
   get canApplyUpdates() {
+    if (this.disabledByPolicy) {
+      LOG(
+        "UpdateService.canApplyUpdates - unable to apply updates, " +
+          "the option has been disabled by the administrator."
+      );
+      return false;
+    }
+
     return getCanApplyUpdates() && hasUpdateMutex();
   },
 
@@ -3275,6 +3283,14 @@ UpdateService.prototype = {
    * See nsIUpdateService.idl
    */
   get canStageUpdates() {
+    if (this.disabledByPolicy) {
+      LOG(
+        "UpdateService.canStageUpdates - unable to stage updates, " +
+          "the option has been disabled by the administrator."
+      );
+      return false;
+    }
+
     return getCanStageUpdates();
   },
 
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 40025: Remove Mozilla add-on install permissions
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit f212bc28addb3b581e65ea0b1fe2d4dffa65a57d
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Mon Jul 27 18:12:55 2020 +0200
    Bug 40025: Remove Mozilla add-on install permissions
---
 browser/app/permissions | 6 ------
 1 file changed, 6 deletions(-)
diff --git a/browser/app/permissions b/browser/app/permissions
index c50a15acd72b..093c0f6c1bf8 100644
--- a/browser/app/permissions
+++ b/browser/app/permissions
@@ -11,12 +11,6 @@
 origin	uitour	1	https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/
 origin	uitour	1	about:tor
 
-# XPInstall
-origin	install	1	https://addons.mozilla.org
-
 # Remote troubleshooting
 origin	remote-troubleshooting	1	https://support.mozilla.org
 
-# addon install
-origin	install	1	https://private-network.firefox.com
-origin	install	1	https://fpn.firefox.com
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 40166: Disable security.certerrors.mitm.auto_enable_enterprise_roots
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit 61933fd19ac746fd440306d64e404aa658715765
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Fri Oct 9 12:55:35 2020 +0200
    Bug 40166: Disable security.certerrors.mitm.auto_enable_enterprise_roots
---
 browser/app/profile/000-tor-browser.js |  3 +++
 browser/components/BrowserGlue.jsm     | 14 ++++++++++++++
 2 files changed, 17 insertions(+)
diff --git a/browser/app/profile/000-tor-browser.js b/browser/app/profile/000-tor-browser.js
index 2db11b1ea3d7..760c405d06a6 100644
--- a/browser/app/profile/000-tor-browser.js
+++ b/browser/app/profile/000-tor-browser.js
@@ -325,6 +325,9 @@ pref("security.enterprise_roots.enabled", false);
 // Don't ping Mozilla for MitM detection, see bug 32321
 pref("security.certerrors.mitm.priming.enabled", false);
 
+// Don't automatically enable enterprise roots, see bug 40166
+pref("security.certerrors.mitm.auto_enable_enterprise_roots", false);
+
 // Disable the language pack signing check for now on macOS, see #31942
 #ifdef XP_MACOSX
 pref("extensions.langpacks.signatures.required", false);
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index 057a2121533c..3750230a250b 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -1319,6 +1319,20 @@ BrowserGlue.prototype = {
     // handle any UI migration
     this._migrateUI();
 
+    // Clear possibly auto enabled enterprise_roots prefs (see bug 40166)
+    if (
+      !Services.prefs.getBoolPref(
+        "security.certerrors.mitm.auto_enable_enterprise_roots"
+      ) &&
+      Services.prefs.getBoolPref(
+        "security.enterprise_roots.auto-enabled",
+        false
+      )
+    ) {
+      Services.prefs.clearUserPref("security.enterprise_roots.enabled");
+      Services.prefs.clearUserPref("security.enterprise_roots.auto-enabled");
+    }
+
     if (!Services.prefs.prefHasUserValue(PREF_PDFJS_ISDEFAULT_CACHE_STATE)) {
       PdfJs.checkIsDefault(this._isNewProfile);
     }
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0
                            
                          
                          
                            
    
                          
                        
                     
                        
                    
                        
                            
                                
                            
                            [tor-browser/tor-browser-78.13.0esr-10.5-1] Bug 21952: Implement Onion-Location
                        
                        
by sysrqb@torproject.org 04 Aug '21
                    by sysrqb@torproject.org 04 Aug '21
04 Aug '21
                    
                        commit 4c4670f0074d99296d6f94185e12f18313c5341b
Author: Alex Catarineu <acat(a)torproject.org>
Date:   Thu Mar 5 22:16:39 2020 +0100
    Bug 21952: Implement Onion-Location
    
    Whenever a valid Onion-Location HTTP header (or corresponding HTML
    <meta> http-equiv attribute) is found in a document load, we either
    redirect to it (if the user opted-in via preference) or notify the
    presence of an onionsite alternative with a badge in the urlbar.
---
 browser/base/content/browser.js                    |  12 ++
 browser/base/content/browser.xhtml                 |   3 +
 browser/components/BrowserGlue.jsm                 |   9 ++
 .../onionservices/OnionLocationChild.jsm           |  43 ++++++
 .../onionservices/OnionLocationParent.jsm          | 161 +++++++++++++++++++++
 .../content/onionlocation-notification-icons.css   |   5 +
 .../onionservices/content/onionlocation-urlbar.css |  27 ++++
 .../content/onionlocation-urlbar.inc.xhtml         |  10 ++
 .../onionservices/content/onionlocation.svg        |   3 +
 .../content/onionlocationPreferences.inc.xhtml     |  11 ++
 .../content/onionlocationPreferences.js            |  31 ++++
 browser/components/onionservices/jar.mn            |   2 +
 browser/components/onionservices/moz.build         |   2 +
 browser/components/preferences/privacy.inc.xhtml   |   2 +
 browser/components/preferences/privacy.js          |  17 +++
 browser/themes/shared/notification-icons.inc.css   |   2 +
 browser/themes/shared/urlbar-searchbar.inc.css     |   2 +
 dom/base/Document.cpp                              |  34 ++++-
 dom/base/Document.h                                |   2 +
 dom/webidl/Document.webidl                         |   9 ++
 modules/libpref/init/StaticPrefList.yaml           |   5 +
 xpcom/ds/StaticAtoms.py                            |   1 +
 22 files changed, 392 insertions(+), 1 deletion(-)
diff --git a/browser/base/content/browser.js b/browser/base/content/browser.js
index bd5f10cb6f64..04f8752b93f4 100644
--- a/browser/base/content/browser.js
+++ b/browser/base/content/browser.js
@@ -44,6 +44,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
   NetUtil: "resource://gre/modules/NetUtil.jsm",
   NewTabUtils: "resource://gre/modules/NewTabUtils.jsm",
   OpenInTabsUtils: "resource:///modules/OpenInTabsUtils.jsm",
+  OnionLocationParent: "resource:///modules/OnionLocationParent.jsm",
   PageActions: "resource:///modules/PageActions.jsm",
   PageThumbs: "resource://gre/modules/PageThumbs.jsm",
   PanelMultiView: "resource:///modules/PanelMultiView.jsm",
@@ -5422,6 +5423,7 @@ var XULBrowserWindow = {
     Services.obs.notifyObservers(null, "touchbar-location-change", location);
     UpdateBackForwardCommands(gBrowser.webNavigation);
     ReaderParent.updateReaderButton(gBrowser.selectedBrowser);
+    OnionLocationParent.updateOnionLocationBadge(gBrowser.selectedBrowser);
 
     if (!gMultiProcessBrowser) {
       // Bug 1108553 - Cannot rotate images with e10s
@@ -5964,6 +5966,16 @@ const AccessibilityRefreshBlocker = {
 
 var TabsProgressListener = {
   onStateChange(aBrowser, aWebProgress, aRequest, aStateFlags, aStatus) {
+    // Clear OnionLocation UI
+    if (
+      aStateFlags & Ci.nsIWebProgressListener.STATE_START &&
+      aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
+      aRequest &&
+      aWebProgress.isTopLevel
+    ) {
+      OnionLocationParent.onStateChange(aBrowser);
+    }
+
     // Collect telemetry data about tab load times.
     if (
       aWebProgress.isTopLevel &&
diff --git a/browser/base/content/browser.xhtml b/browser/base/content/browser.xhtml
index 4cab5fad6475..c2caecc1a416 100644
--- a/browser/base/content/browser.xhtml
+++ b/browser/base/content/browser.xhtml
@@ -1077,6 +1077,9 @@
                        onclick="FullZoom.reset();"
                        tooltip="dynamic-shortcut-tooltip"
                        hidden="true"/>
+
+#include ../../components/onionservices/content/onionlocation-urlbar.inc.xhtml
+
                 <box id="pageActionSeparator" class="urlbar-page-action"/>
                 <image id="pageActionButton"
                        class="urlbar-icon urlbar-page-action"
diff --git a/browser/components/BrowserGlue.jsm b/browser/components/BrowserGlue.jsm
index d30abff54562..e08e461a27ff 100644
--- a/browser/components/BrowserGlue.jsm
+++ b/browser/components/BrowserGlue.jsm
@@ -539,6 +539,13 @@ let LEGACY_ACTORS = {
       observers: ["keyword-uri-fixup"],
     },
   },
+  OnionLocation: {
+    child: {
+      module: "resource:///modules/OnionLocationChild.jsm",
+      events: { pageshow: {} },
+      messages: ["OnionLocation:Refresh"],
+    },
+  },
 };
 
 if (AppConstants.TOR_BROWSER_UPDATE) {
@@ -713,6 +720,7 @@ XPCOMUtils.defineLazyModuleGetters(this, {
 XPCOMUtils.defineLazyModuleGetters(this, {
   AboutLoginsParent: "resource:///modules/AboutLoginsParent.jsm",
   AsyncPrefs: "resource://gre/modules/AsyncPrefs.jsm",
+  OnionLocationParent: "resource:///modules/OnionLocationParent.jsm",
   PluginManager: "resource:///actors/PluginParent.jsm",
   ReaderParent: "resource:///modules/ReaderParent.jsm",
 });
@@ -816,6 +824,7 @@ const listeners = {
     "AboutLogins:VulnerableLogins": ["AboutLoginsParent"],
     "Reader:FaviconRequest": ["ReaderParent"],
     "Reader:UpdateReaderButton": ["ReaderParent"],
+    "OnionLocation:Set": ["OnionLocationParent"],
   },
 
   observe(subject, topic, data) {
diff --git a/browser/components/onionservices/OnionLocationChild.jsm b/browser/components/onionservices/OnionLocationChild.jsm
new file mode 100644
index 000000000000..1059eb7d5925
--- /dev/null
+++ b/browser/components/onionservices/OnionLocationChild.jsm
@@ -0,0 +1,43 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["OnionLocationChild"];
+
+const { ActorChild } = ChromeUtils.import(
+  "resource://gre/modules/ActorChild.jsm"
+);
+
+class OnionLocationChild extends ActorChild {
+  handleEvent(event) {
+    this.onPageShow(event);
+  }
+
+  onPageShow(event) {
+    if (event.target != this.content.document) {
+      return;
+    }
+    const onionLocationURI = this.content.document.onionLocationURI;
+    if (onionLocationURI) {
+      this.mm.sendAsyncMessage("OnionLocation:Set");
+    }
+  }
+
+  receiveMessage(aMessage) {
+    if (aMessage.name == "OnionLocation:Refresh") {
+      const doc = this.content.document;
+      const docShell = this.mm.docShell;
+      const onionLocationURI = doc.onionLocationURI;
+      const refreshURI = docShell.QueryInterface(Ci.nsIRefreshURI);
+      if (onionLocationURI && refreshURI) {
+        refreshURI.refreshURI(
+          onionLocationURI,
+          doc.nodePrincipal,
+          0,
+          false,
+          true
+        );
+      }
+    }
+  }
+}
diff --git a/browser/components/onionservices/OnionLocationParent.jsm b/browser/components/onionservices/OnionLocationParent.jsm
new file mode 100644
index 000000000000..1c79fc07d215
--- /dev/null
+++ b/browser/components/onionservices/OnionLocationParent.jsm
@@ -0,0 +1,161 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+var EXPORTED_SYMBOLS = ["OnionLocationParent"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { TorStrings } = ChromeUtils.import("resource:///modules/TorStrings.jsm");
+
+// Prefs
+const NOTIFICATION_PREF = "privacy.prioritizeonions.showNotification";
+const PRIORITIZE_ONIONS_PREF = "privacy.prioritizeonions.enabled";
+
+// Element IDs
+const ONIONLOCATION_BOX_ID = "onion-location-box";
+const ONIONLOCATION_BUTTON_ID = "onion-location-button";
+const ONIONLOCATION_LABEL_ID = "onion-label";
+
+// Notification IDs
+const NOTIFICATION_ID = "onion-location";
+const NOTIFICATION_ANCHOR_ID = "onionlocation";
+
+// Strings
+const STRING_ONION_AVAILABLE = TorStrings.onionLocation.onionAvailable;
+const NOTIFICATION_CANCEL_LABEL = TorStrings.onionLocation.notNow;
+const NOTIFICATION_CANCEL_ACCESSKEY = TorStrings.onionLocation.notNowAccessKey;
+const NOTIFICATION_OK_LABEL = TorStrings.onionLocation.alwaysPrioritize;
+const NOTIFICATION_OK_ACCESSKEY =
+  TorStrings.onionLocation.alwaysPrioritizeAccessKey;
+const NOTIFICATION_TITLE = TorStrings.onionLocation.tryThis;
+const NOTIFICATION_DESCRIPTION = TorStrings.onionLocation.description;
+const NOTIFICATION_LEARN_MORE_URL = TorStrings.onionLocation.learnMoreURL;
+
+var OnionLocationParent = {
+  // Listeners are added in BrowserGlue.jsm
+  receiveMessage(aMsg) {
+    switch (aMsg.name) {
+      case "OnionLocation:Set":
+        this.setOnionLocation(aMsg.target);
+        break;
+    }
+  },
+
+  buttonClick(event) {
+    if (event.button != 0) {
+      return;
+    }
+    const win = event.target.ownerGlobal;
+    const browser = win.gBrowser.selectedBrowser;
+    this.redirect(browser);
+  },
+
+  redirect(browser) {
+    browser.messageManager.sendAsyncMessage("OnionLocation:Refresh");
+    this.setDisabled(browser);
+  },
+
+  onStateChange(browser) {
+    delete browser._onionLocation;
+    this.hideNotification(browser);
+  },
+
+  setOnionLocation(browser) {
+    const win = browser.ownerGlobal;
+    browser._onionLocation = true;
+    if (browser === win.gBrowser.selectedBrowser) {
+      this.updateOnionLocationBadge(browser);
+    }
+  },
+
+  hideNotification(browser) {
+    const win = browser.ownerGlobal;
+    if (browser._onionLocationPrompt) {
+      win.PopupNotifications.remove(browser._onionLocationPrompt);
+    }
+  },
+
+  showNotification(browser) {
+    const mustShow = Services.prefs.getBoolPref(NOTIFICATION_PREF, true);
+    if (!mustShow) {
+      return;
+    }
+
+    const win = browser.ownerGlobal;
+    Services.prefs.setBoolPref(NOTIFICATION_PREF, false);
+
+    const mainAction = {
+      label: NOTIFICATION_OK_LABEL,
+      accessKey: NOTIFICATION_OK_ACCESSKEY,
+      callback() {
+        Services.prefs.setBoolPref(PRIORITIZE_ONIONS_PREF, true);
+        OnionLocationParent.redirect(browser);
+        win.openPreferences("privacy-onionservices");
+      },
+    };
+
+    const cancelAction = {
+      label: NOTIFICATION_CANCEL_LABEL,
+      accessKey: NOTIFICATION_CANCEL_ACCESSKEY,
+      callback: () => {},
+    };
+
+    const options = {
+      autofocus: true,
+      persistent: true,
+      removeOnDismissal: false,
+      eventCallback(aTopic) {
+        if (aTopic === "removed") {
+          delete browser._onionLocationPrompt;
+          delete browser.onionpopupnotificationanchor;
+        }
+      },
+      learnMoreURL: NOTIFICATION_LEARN_MORE_URL,
+      displayURI: {
+        hostPort: NOTIFICATION_TITLE, // This is hacky, but allows us to have a title without extra markup/css.
+      },
+      hideClose: true,
+      popupIconClass: "onionlocation-notification-icon",
+    };
+
+    // A hacky way of setting the popup anchor outside the usual url bar icon box
+    // onionlocationpopupnotificationanchor comes from `${ANCHOR_ID}popupnotificationanchor`
+    // From https://searchfox.org/mozilla-esr68/rev/080f9ed47742644d2ff84f7aa0b10aea5c4…
+    browser.onionlocationpopupnotificationanchor = win.document.getElementById(
+      ONIONLOCATION_BUTTON_ID
+    );
+
+    browser._onionLocationPrompt = win.PopupNotifications.show(
+      browser,
+      NOTIFICATION_ID,
+      NOTIFICATION_DESCRIPTION,
+      NOTIFICATION_ANCHOR_ID,
+      mainAction,
+      [cancelAction],
+      options
+    );
+  },
+
+  setEnabled(browser) {
+    const win = browser.ownerGlobal;
+    const label = win.document.getElementById(ONIONLOCATION_LABEL_ID);
+    label.textContent = STRING_ONION_AVAILABLE;
+    const elem = win.document.getElementById(ONIONLOCATION_BOX_ID);
+    elem.removeAttribute("hidden");
+  },
+
+  setDisabled(browser) {
+    const win = browser.ownerGlobal;
+    const elem = win.document.getElementById(ONIONLOCATION_BOX_ID);
+    elem.setAttribute("hidden", true);
+  },
+
+  updateOnionLocationBadge(browser) {
+    if (browser._onionLocation) {
+      this.setEnabled(browser);
+      this.showNotification(browser);
+    } else {
+      this.setDisabled(browser);
+    }
+  },
+};
diff --git a/browser/components/onionservices/content/onionlocation-notification-icons.css b/browser/components/onionservices/content/onionlocation-notification-icons.css
new file mode 100644
index 000000000000..7c8a6d892c6f
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation-notification-icons.css
@@ -0,0 +1,5 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+.onionlocation-notification-icon {
+  display: none;
+}
\ No newline at end of file
diff --git a/browser/components/onionservices/content/onionlocation-urlbar.css b/browser/components/onionservices/content/onionlocation-urlbar.css
new file mode 100644
index 000000000000..91cad5f178d1
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation-urlbar.css
@@ -0,0 +1,27 @@
+/* Copyright (c) 2020, The Tor Project, Inc. */
+
+#onion-location-button {
+  list-style-image: url(chrome://browser/content/onionservices/onionlocation.svg);
+}
+
+#onion-location-box {
+  border-radius: 3px;
+  background-color: #6200A4;
+  padding-left: 5px;
+  padding-right: 5px;
+  color: white;
+  -moz-context-properties: fill;
+  fill: white;
+}
+
+#onion-location-box:hover {
+  background-color: #0060DF !important;
+}
+
+toolbar[brighttext] #onion-location-box {
+  background-color: #9400ff;
+}
+
+toolbar[brighttext] #onion-location-box:hover {
+  background-color: #0060DF !important;
+}
diff --git a/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml b/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml
new file mode 100644
index 000000000000..b612a4236f3c
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation-urlbar.inc.xhtml
@@ -0,0 +1,10 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<hbox id="onion-location-box"
+      class="urlbar-icon-wrapper urlbar-page-action"
+      role="button"
+      hidden="true"
+      onclick="OnionLocationParent.buttonClick(event);">
+  <image id="onion-location-button" role="presentation"/>
+  <hbox id="onion-label-container"><label id="onion-label"/></hbox>
+</hbox>
diff --git a/browser/components/onionservices/content/onionlocation.svg b/browser/components/onionservices/content/onionlocation.svg
new file mode 100644
index 000000000000..37f40ac1812f
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocation.svg
@@ -0,0 +1,3 @@
+<svg width="16" height="16" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <path fill="context-fill" fill-opacity="context-fill-opacity" d="m8.016411 14.54499v-0.969784c3.071908-0.0089 5.559239-2.501304 5.559239-5.575429 0-3.073903-2.487331-5.566336-5.559239-5.575206v-0.9697843c3.607473 0.00909 6.528802 2.935521 6.528802 6.544991 0 3.609691-2.921329 6.536342-6.528802 6.545213zm0-3.394356c1.732661-0.0091 3.135111-1.415756 3.135111-3.150857 0-1.734878-1.402451-3.141542-3.135111-3.150634v-0.9695626c2.268448 0.00887 4.104895 1.849753 4.104895 4.120197 0 2.270666-1.836447 4.111549-4.104895 4.120419zm0-4.846926c0.9294227 0.00887 1.680545 0.7644289 1.680545 1.696069 0 0.9318627-0.7511226 1.687421-1.680545 1.696291zm-8.016411 1.696069c0 4.418473 3.581527 8.000222 8 8.000222 4.418251 0 8-3.581749 8-8.000222 0-4.418251-3.581749-7.999778-8-7.999778-4.418473 0-8 3.581527-8 7.999778z" />
+</svg>
\ No newline at end of file
diff --git a/browser/components/onionservices/content/onionlocationPreferences.inc.xhtml b/browser/components/onionservices/content/onionlocationPreferences.inc.xhtml
new file mode 100644
index 000000000000..c285f403f99b
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocationPreferences.inc.xhtml
@@ -0,0 +1,11 @@
+# Copyright (c) 2020, The Tor Project, Inc.
+
+<groupbox id="onionServicesGroup" data-category="panePrivacy" data-subcategory="onionservices" hidden="true">
+  <label><html:h2 id="onionServicesTitle"></html:h2></label>
+  <label><label class="tail-with-learn-more" id="prioritizeOnionsDesc"></label><label
+  class="learnMore" is="text-link" id="onionServicesLearnMore"></label></label>
+  <radiogroup id="prioritizeOnionsRadioGroup" aria-labelledby="prioritizeOnionsDesc" preference="privacy.prioritizeonions.enabled">
+    <radio id="onionServicesRadioAlways" value="true"/>
+    <radio id="onionServicesRadioAsk" value="false"/>
+  </radiogroup>
+</groupbox>
diff --git a/browser/components/onionservices/content/onionlocationPreferences.js b/browser/components/onionservices/content/onionlocationPreferences.js
new file mode 100644
index 000000000000..aa569b54721c
--- /dev/null
+++ b/browser/components/onionservices/content/onionlocationPreferences.js
@@ -0,0 +1,31 @@
+// Copyright (c) 2020, The Tor Project, Inc.
+
+"use strict";
+
+ChromeUtils.defineModuleGetter(
+  this,
+  "TorStrings",
+  "resource:///modules/TorStrings.jsm"
+);
+
+const OnionLocationPreferences = {
+  init() {
+    document.getElementById("onionServicesTitle").textContent =
+      TorStrings.onionLocation.onionServicesTitle;
+    document.getElementById("prioritizeOnionsDesc").textContent =
+      TorStrings.onionLocation.prioritizeOnionsDescription;
+    const learnMore = document.getElementById("onionServicesLearnMore");
+    learnMore.textContent = TorStrings.onionLocation.learnMore;
+    learnMore.href = TorStrings.onionLocation.learnMoreURL;
+    document.getElementById("onionServicesRadioAlways").label =
+      TorStrings.onionLocation.always;
+    document.getElementById("onionServicesRadioAsk").label =
+      TorStrings.onionLocation.askEverytime;
+  },
+};
+
+Object.defineProperty(this, "OnionLocationPreferences", {
+  value: OnionLocationPreferences,
+  enumerable: true,
+  writable: false,
+});
diff --git a/browser/components/onionservices/jar.mn b/browser/components/onionservices/jar.mn
index 9d6ce88d1841..f45b16dc5d29 100644
--- a/browser/components/onionservices/jar.mn
+++ b/browser/components/onionservices/jar.mn
@@ -7,3 +7,5 @@ browser.jar:
     content/browser/onionservices/onionservices.css                (content/onionservices.css)
     content/browser/onionservices/savedKeysDialog.js               (content/savedKeysDialog.js)
     content/browser/onionservices/savedKeysDialog.xhtml            (content/savedKeysDialog.xhtml)
+    content/browser/onionservices/onionlocationPreferences.js      (content/onionlocationPreferences.js)
+    content/browser/onionservices/onionlocation.svg                (content/onionlocation.svg)
diff --git a/browser/components/onionservices/moz.build b/browser/components/onionservices/moz.build
index e4b6d73f8f40..dfd664df434e 100644
--- a/browser/components/onionservices/moz.build
+++ b/browser/components/onionservices/moz.build
@@ -4,4 +4,6 @@ EXTRA_JS_MODULES += [
     'ExtensionMessaging.jsm',
     'HttpsEverywhereControl.jsm',
     'OnionAliasStore.jsm',
+    'OnionLocationChild.jsm',
+    'OnionLocationParent.jsm',
 ]
diff --git a/browser/components/preferences/privacy.inc.xhtml b/browser/components/preferences/privacy.inc.xhtml
index eb7587afa0e1..6e05405079bf 100644
--- a/browser/components/preferences/privacy.inc.xhtml
+++ b/browser/components/preferences/privacy.inc.xhtml
@@ -14,6 +14,8 @@
   <html:h1 data-l10n-id="privacy-header"/>
 </hbox>
 
+#include ../onionservices/content/onionlocationPreferences.inc.xhtml
+
 <!-- Tracking / Content Blocking -->
 <groupbox id="trackingGroup" data-category="panePrivacy" hidden="true" aria-describedby="contentBlockingDescription">
   <label id="contentBlockingHeader"><html:h2 data-l10n-id="content-blocking-enhanced-tracking-protection"/></label>
diff --git a/browser/components/preferences/privacy.js b/browser/components/preferences/privacy.js
index 41dda96a14de..92f35dc78d12 100644
--- a/browser/components/preferences/privacy.js
+++ b/browser/components/preferences/privacy.js
@@ -90,6 +90,12 @@ XPCOMUtils.defineLazyScriptGetter(
   "chrome://browser/content/securitylevel/securityLevel.js"
 );
 
+XPCOMUtils.defineLazyScriptGetter(
+  this,
+  ["OnionLocationPreferences"],
+  "chrome://browser/content/onionservices/onionlocationPreferences.js"
+);
+
 XPCOMUtils.defineLazyServiceGetter(
   this,
   "listManager",
@@ -158,6 +164,9 @@ Preferences.addAll([
   // Do not track
   { id: "privacy.donottrackheader.enabled", type: "bool" },
 
+  // Onion Location
+  { id: "privacy.prioritizeonions.enabled", type: "bool" },
+
   // Media
   { id: "media.autoplay.default", type: "int" },
 
@@ -300,6 +309,13 @@ var gPrivacyPane = {
     window.addEventListener("unload", unload);
   },
 
+  /**
+   * Show the OnionLocation preferences UI
+   */
+  _initOnionLocation() {
+    OnionLocationPreferences.init();
+  },
+
   /**
    * Whether the prompt to restart Firefox should appear when changing the autostart pref.
    */
@@ -442,6 +458,7 @@ var gPrivacyPane = {
     this._initTrackingProtectionExtensionControl();
     OnionServicesAuthPreferences.init();
     this._initSecurityLevel();
+    this._initOnionLocation();
 
     Services.telemetry.setEventRecordingEnabled("pwmgr", true);
 
diff --git a/browser/themes/shared/notification-icons.inc.css b/browser/themes/shared/notification-icons.inc.css
index 979ae9482244..7aa92d51f4d6 100644
--- a/browser/themes/shared/notification-icons.inc.css
+++ b/browser/themes/shared/notification-icons.inc.css
@@ -415,3 +415,5 @@ html|*#webRTC-previewVideo {
   background: #FFE900 url(chrome://browser/skin/notification-icons/update.svg) no-repeat center;
   border-radius: 50%;
 }
+
+%include ../../components/onionservices/content/onionlocation-notification-icons.css
\ No newline at end of file
diff --git a/browser/themes/shared/urlbar-searchbar.inc.css b/browser/themes/shared/urlbar-searchbar.inc.css
index 0b1f69342995..d3cc6bf7f024 100644
--- a/browser/themes/shared/urlbar-searchbar.inc.css
+++ b/browser/themes/shared/urlbar-searchbar.inc.css
@@ -824,3 +824,5 @@
 .searchbar-search-button:hover:not([addengines=true]) > .searchbar-search-icon-overlay:-moz-locale-dir(rtl) {
   margin-inline: -26px 20px;
 }
+
+%include ../../components/onionservices/content/onionlocation-urlbar.css
diff --git a/dom/base/Document.cpp b/dom/base/Document.cpp
index 132c0ecbfdac..afc872569519 100644
--- a/dom/base/Document.cpp
+++ b/dom/base/Document.cpp
@@ -2542,6 +2542,7 @@ void Document::ResetToURI(nsIURI* aURI, nsILoadGroup* aLoadGroup,
   // mDocumentURI.
   mDocumentBaseURI = nullptr;
   mChromeXHRDocBaseURI = nullptr;
+  mOnionLocationURI = nullptr;
 
   // Check if the current document is the top-level DevTools document.
   // For inner DevTools frames, mIsDevToolsDocument will be set when
@@ -6026,6 +6027,22 @@ void Document::GetHeaderData(nsAtom* aHeaderField, nsAString& aData) const {
   }
 }
 
+static bool IsValidOnionLocation(nsIURI* aDocumentURI,
+                                 nsIURI* aOnionLocationURI) {
+  bool isHttpish;
+  nsAutoCString host;
+  return aDocumentURI && aOnionLocationURI &&
+         NS_SUCCEEDED(aDocumentURI->SchemeIs("https", &isHttpish)) &&
+         isHttpish && NS_SUCCEEDED(aDocumentURI->GetAsciiHost(host)) &&
+         !StringEndsWith(host, NS_LITERAL_CSTRING(".onion")) &&
+         ((NS_SUCCEEDED(aOnionLocationURI->SchemeIs("http", &isHttpish)) &&
+           isHttpish) ||
+          (NS_SUCCEEDED(aOnionLocationURI->SchemeIs("https", &isHttpish)) &&
+           isHttpish)) &&
+         NS_SUCCEEDED(aOnionLocationURI->GetAsciiHost(host)) &&
+         StringEndsWith(host, NS_LITERAL_CSTRING(".onion"));
+}
+
 void Document::SetHeaderData(nsAtom* aHeaderField, const nsAString& aData) {
   if (!aHeaderField) {
     NS_ERROR("null headerField");
@@ -6101,6 +6118,21 @@ void Document::SetHeaderData(nsAtom* aHeaderField, const nsAString& aData) {
       aHeaderField == nsGkAtoms::handheldFriendly) {
     mViewportType = Unknown;
   }
+
+  if (aHeaderField == nsGkAtoms::headerOnionLocation && !aData.IsEmpty()) {
+    nsCOMPtr<nsIURI> onionURI;
+    if (NS_SUCCEEDED(NS_NewURI(getter_AddRefs(onionURI), aData)) &&
+        IsValidOnionLocation(Document::GetDocumentURI(), onionURI)) {
+      if (StaticPrefs::privacy_prioritizeonions_enabled()) {
+        nsCOMPtr<nsIRefreshURI> refresher(mDocumentContainer);
+        if (refresher) {
+          refresher->RefreshURI(onionURI, NodePrincipal(), 0, false, true);
+        }
+      } else {
+        mOnionLocationURI = onionURI;
+      }
+    }
+  }
 }
 
 void Document::TryChannelCharset(nsIChannel* aChannel, int32_t& aCharsetSource,
@@ -10141,7 +10173,7 @@ void Document::RetrieveRelevantHeaders(nsIChannel* aChannel) {
     static const char* const headers[] = {
         "default-style", "content-style-type", "content-language",
         "content-disposition", "refresh", "x-dns-prefetch-control",
-        "x-frame-options",
+        "x-frame-options", "onion-location",
         // add more http headers if you need
         // XXXbz don't add content-location support without reading bug
         // 238654 and its dependencies/dups first.
diff --git a/dom/base/Document.h b/dom/base/Document.h
index 6d06a8c2a8cd..6e80306e94b5 100644
--- a/dom/base/Document.h
+++ b/dom/base/Document.h
@@ -3322,6 +3322,7 @@ class Document : public nsINode,
   void ReleaseCapture() const;
   void MozSetImageElement(const nsAString& aImageElementId, Element* aElement);
   nsIURI* GetDocumentURIObject() const;
+  nsIURI* GetOnionLocationURI() const { return mOnionLocationURI; }
   // Not const because all the fullscreen goop is not const
   const char* GetFullscreenError(CallerType);
   bool FullscreenEnabled(CallerType aCallerType) {
@@ -4194,6 +4195,7 @@ class Document : public nsINode,
   nsCOMPtr<nsIURI> mChromeXHRDocURI;
   nsCOMPtr<nsIURI> mDocumentBaseURI;
   nsCOMPtr<nsIURI> mChromeXHRDocBaseURI;
+  nsCOMPtr<nsIURI> mOnionLocationURI;
 
   // The base domain of the document for third-party checks.
   nsCString mBaseDomain;
diff --git a/dom/webidl/Document.webidl b/dom/webidl/Document.webidl
index 8130db018f47..df3a18eaf266 100644
--- a/dom/webidl/Document.webidl
+++ b/dom/webidl/Document.webidl
@@ -676,3 +676,12 @@ partial interface Document {
   [ChromeOnly, Pure]
   readonly attribute nsIPermissionDelegateHandler permDelegateHandler;
 };
+
+
+/**
+ * Extension to allows chrome JS to know whether the document has a valid
+ * Onion-Location that we could redirect to.
+ */
+partial interface Document {
+  [ChromeOnly] readonly attribute URI? onionLocationURI;
+};
diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/StaticPrefList.yaml
index 1e8dc23fdfd7..5e09aafdbab3 100644
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -8453,6 +8453,11 @@
   value: @IS_NIGHTLY_BUILD@
   mirror: always
 
+- name: privacy.prioritizeonions.enabled
+  type: RelaxedAtomicBool
+  value: false
+  mirror: always
+
 #---------------------------------------------------------------------------
 # Prefs starting with "prompts."
 #---------------------------------------------------------------------------
diff --git a/xpcom/ds/StaticAtoms.py b/xpcom/ds/StaticAtoms.py
index ab5f662575e4..23a5d6f9bb95 100644
--- a/xpcom/ds/StaticAtoms.py
+++ b/xpcom/ds/StaticAtoms.py
@@ -811,6 +811,7 @@ STATIC_ATOMS = [
     Atom("oninputsourceschange","oninputsourceschange"),
     Atom("oninstall", "oninstall"),
     Atom("oninvalid", "oninvalid"),
+    Atom("headerOnionLocation", "onion-location"),
     Atom("onkeydown", "onkeydown"),
     Atom("onkeypress", "onkeypress"),
     Atom("onkeyup", "onkeyup"),
                    
                  
                  
                          
                            
                            1
                            
                          
                          
                            
                            0