commit 4deba823ed0da02abd43961c1d027aae4ac07f92
parent cf526eb79d672f212175374af8b333e392aae916
Author: sanketh <me@snkth.com>
Date: Mon, 8 Feb 2021 20:12:44 -0500
TB 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
Diffstat:
7 files changed, 299 insertions(+), 0 deletions(-)
diff --git a/browser/actors/CryptoSafetyChild.sys.mjs b/browser/actors/CryptoSafetyChild.sys.mjs
@@ -0,0 +1,49 @@
+/* -*- 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isCryptoSafetyEnabled",
+ "security.cryptoSafety",
+ true // Defaults to true.
+);
+
+export class CryptoSafetyChild extends JSWindowActorChild {
+ handleEvent(event) {
+ if (
+ !lazy.isCryptoSafetyEnabled ||
+ // Ignore non-HTTP addresses.
+ // We do this before reading the host property since this is not available
+ // for about: pages.
+ !this.document.documentURIObject.schemeIs("http") ||
+ // Ignore onion addresses.
+ this.document.documentURIObject.host.endsWith(".onion") ||
+ (event.type !== "copy" && event.type !== "cut")
+ ) {
+ return;
+ }
+
+ // We send a message to the parent to inspect the clipboard content.
+ // NOTE: We wait until next cycle to allow the event to propagate and fill
+ // the clipboard before being read.
+ // NOTE: Using navigator.clipboard.readText fails with Wayland. See
+ // tor-browser#42702.
+ lazy.setTimeout(() => {
+ this.sendAsyncMessage("CryptoSafety:CopiedText", {
+ host: this.document.documentURIObject.host,
+ });
+ });
+ }
+}
diff --git a/browser/actors/CryptoSafetyParent.sys.mjs b/browser/actors/CryptoSafetyParent.sys.mjs
@@ -0,0 +1,130 @@
+/* -*- 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/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TorDomainIsolator: "resource://gre/modules/TorDomainIsolator.sys.mjs",
+ Bech32Decode: "resource://gre/modules/Bech32Decode.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "CryptoStrings", function () {
+ return new Localization(["toolkit/global/tor-browser.ftl"]);
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ "isCryptoSafetyEnabled",
+ "security.cryptoSafety",
+ 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 (lazy.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_zcash_tz_address/dzr62p5/
+ 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;
+}
+
+export class CryptoSafetyParent extends JSWindowActorParent {
+ async receiveMessage(aMessage) {
+ if (
+ !lazy.isCryptoSafetyEnabled ||
+ aMessage.name !== "CryptoSafety:CopiedText"
+ ) {
+ return;
+ }
+
+ // Read the global clipboard. We assume the contents come from the HTTP
+ // page specified in `aMessage.data.host`.
+ const trans = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ trans.init(null);
+ trans.addDataFlavor("text/plain");
+ Services.clipboard.getData(trans, Ci.nsIClipboard.kGlobalClipboard);
+ let data = {};
+ trans.getTransferData("text/plain", data);
+ data = data?.value.QueryInterface(Ci.nsISupportsString).data;
+
+ let address = data?.replace(/\s+/g, "");
+
+ if (!address || !looksLikeCryptoAddress(address)) {
+ return;
+ }
+
+ if (address.length > 32) {
+ address = `${address.substring(0, 32)}…`;
+ }
+
+ const [titleText, bodyText, reloadText, dismissText] =
+ await lazy.CryptoStrings.formatValues([
+ { id: "crypto-safety-prompt-title" },
+ {
+ id: "crypto-safety-prompt-body",
+ args: { address, host: aMessage.data.host },
+ },
+ { id: "crypto-safety-prompt-reload-button" },
+ { id: "crypto-safety-prompt-dismiss-button" },
+ ]);
+
+ const buttonPressed = Services.prompt.confirmEx(
+ this.browsingContext.topChromeWindow,
+ titleText,
+ bodyText,
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_0 +
+ Services.prompt.BUTTON_TITLE_IS_STRING * Services.prompt.BUTTON_POS_1,
+ reloadText,
+ dismissText,
+ null,
+ null,
+ {}
+ );
+
+ if (buttonPressed === 0) {
+ const { browsingContext } = this.manager;
+ const browser = browsingContext.embedderElement;
+ if (browser) {
+ lazy.TorDomainIsolator.newCircuitForBrowser(
+ browser.ownerGlobal.gBrowser.selectedBrowser
+ );
+ }
+ }
+ }
+}
diff --git a/browser/actors/moz.build b/browser/actors/moz.build
@@ -51,6 +51,8 @@ FINAL_TARGET_FILES.actors += [
"ContentSearchParent.sys.mjs",
"ContextMenuChild.sys.mjs",
"ContextMenuParent.sys.mjs",
+ "CryptoSafetyChild.sys.mjs",
+ "CryptoSafetyParent.sys.mjs",
"DecoderDoctorChild.sys.mjs",
"DecoderDoctorParent.sys.mjs",
"DOMFullscreenChild.sys.mjs",
diff --git a/browser/components/DesktopActorRegistry.sys.mjs b/browser/components/DesktopActorRegistry.sys.mjs
@@ -324,6 +324,23 @@ let JSWINDOWACTORS = {
allFrames: true,
},
+ CryptoSafety: {
+ parent: {
+ esModuleURI: "resource:///actors/CryptoSafetyParent.sys.mjs",
+ },
+
+ child: {
+ esModuleURI: "resource:///actors/CryptoSafetyChild.sys.mjs",
+ group: "browsers",
+ events: {
+ copy: { mozSystemGroup: true },
+ cut: { mozSystemGroup: true },
+ },
+ },
+
+ allFrames: true,
+ },
+
/* Note: this uses the same JSMs as ClickHandler, but because it
* relies on "normal" click events anywhere on the page (not just
* links) and is expensive, and only does something for the
diff --git a/toolkit/content/license.html b/toolkit/content/license.html
@@ -4143,6 +4143,7 @@ product.
<li>Microsoft.UI.Windowing.Core.dll</li>
<li>Microsoft.UI.Windowing.dll</li>
#endif
+ <li><code>toolkit/modules/Bech32Decode.sys.mjs</code></li>
</ul>
See the individual LICENSE files or headers for copyright owners.</p>
</td>
diff --git a/toolkit/modules/Bech32Decode.sys.mjs b/toolkit/modules/Bech32Decode.sys.mjs
@@ -0,0 +1,99 @@
+// 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.
+
+/**
+ * JS module implementation of Bech32 decoding adapted from the reference
+ * implementation https://github.com/sipa/bech32.
+ */
+
+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;
+}
+
+export 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, data: data.slice(0, data.length - 6) };
+}
diff --git a/toolkit/modules/moz.build b/toolkit/modules/moz.build
@@ -153,6 +153,7 @@ EXTRA_JS_MODULES += [
"AppMenuNotifications.sys.mjs",
"AppServicesTracing.sys.mjs",
"AsyncPrefs.sys.mjs",
+ "Bech32Decode.sys.mjs",
"BinarySearch.sys.mjs",
"BrowserTelemetryUtils.sys.mjs",
"BrowserUtils.sys.mjs",