tor-browser

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

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:
Abrowser/actors/CryptoSafetyChild.sys.mjs | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Abrowser/actors/CryptoSafetyParent.sys.mjs | 130+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbrowser/actors/moz.build | 2++
Mbrowser/components/DesktopActorRegistry.sys.mjs | 17+++++++++++++++++
Mtoolkit/content/license.html | 1+
Atoolkit/modules/Bech32Decode.sys.mjs | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtoolkit/modules/moz.build | 1+
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",