diff --git a/.travis.yml b/.travis.yml index 9010e77a..301f87a7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: - "nightly" # command to install dependencies install: - - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls flake8 + - pip install Flask==0.12 stem==1.5.4 pytest-cov coveralls flake8 pycrypto pynacl cryptography pysha3 before_script: # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics diff --git a/BUILD.md b/BUILD.md index 7d8350a6..f5fbab51 100644 --- a/BUILD.md +++ b/BUILD.md @@ -11,9 +11,11 @@ cd onionshare Install the needed dependencies: -For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-socks python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy` +For Debian-like distros: `apt install -y build-essential fakeroot python3-all python3-stdeb dh-python python3-flask python3-stem python3-pyqt5 python-nautilus python3-pytest tor obfs4proxy python3-cryptography python3-crypto python3-nacl python3-pip python3-socks python3-sha3` -For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 rpm-build` +On some older versions of Debian you may need to install pysha3 with `pip3 install pysha3` if python3-sha3 is not available. + +For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4 python3-pynacl python3-cryptography python3-crypto python3-pip python3-pysocks` After that you can try both the CLI and the GUI version of OnionShare: diff --git a/install/requirements-windows.txt b/install/requirements-windows.txt index 6e03f6e8..65e4196e 100644 --- a/install/requirements-windows.txt +++ b/install/requirements-windows.txt @@ -1,4 +1,5 @@ click==6.7 +cryptography==2.3.1 Flask==0.12.2 future==0.16.0 itsdangerous==0.24 @@ -8,6 +9,9 @@ pefile==2017.11.5 PyInstaller==3.3.1 PyQt5==5.9.2 PySocks==1.6.7 +pynacl==1.2.1 +pycrypto==2.6.1 +pysha3==1.0.2 requests==2.19.1 sip==4.19.6 stem==1.6.0 diff --git a/install/requirements.txt b/install/requirements.txt index ed83b995..95ddb35a 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -1,4 +1,5 @@ click==6.7 +cryptography==2.3.1 Flask==0.12.2 itsdangerous==0.24 Jinja2==2.10 @@ -6,6 +7,9 @@ MarkupSafe==1.0 PyInstaller==3.3.1 PyQt5==5.9.2 PySocks==1.6.7 +pycrypto==2.6.1 +pynacl==1.2.1 +pysha3==1.0.2 requests==2.19.1 sip==4.19.6 stem==1.6.0 diff --git a/onionshare/onion.py b/onionshare/onion.py index 67a32321..a10dc53c 100644 --- a/onionshare/onion.py +++ b/onionshare/onion.py @@ -21,8 +21,10 @@ along with this program. If not, see . from stem.control import Controller from stem import ProtocolError, SocketClosed from stem.connection import MissingPassword, UnreadableCookieFile, AuthenticationFailure -import os, sys, tempfile, shutil, urllib, platform, subprocess, time, shlex +import base64, os, sys, tempfile, shutil, urllib, platform, subprocess, time, shlex +from distutils.version import LooseVersion as Version +from . import onionkey from . import common, strings from .settings import Settings @@ -437,20 +439,48 @@ class Onion(object): basic_auth = None if self.settings.get('private_key'): - key_type = "RSA1024" key_content = self.settings.get('private_key') - self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a saved private key') + # is the key a v2 key? + if onionkey.is_v2_key(key_content): + key_type = "RSA1024" + # The below section is commented out because re-publishing + # a pre-prepared v3 private key is currently unstable in Tor. + # This is fixed upstream but won't reach stable until 0.3.5 + # (expected in December 2018) + # See https://trac.torproject.org/projects/tor/ticket/25552 + # Until then, we will deliberately not work with 'persistent' + # v3 onions, which should not be possible via the GUI settings + # anyway. + # Our ticket: https://github.com/micahflee/onionshare/issues/677 + # + # Assume it was a v3 key + # key_type = "ED25519-V3" + else: + raise TorErrorProtocolError(strings._('error_invalid_private_key')) + self.common.log('Onion', 'Starting a hidden service with a saved private key') else: - key_type = "NEW" - key_content = "RSA1024" - self.common.log('Onion', 'start_onion_service', 'Starting a hidden service with a new private key') + # Work out if we can support v3 onion services, which are preferred + if Version(self.tor_version) >= Version('0.3.2.9') and not self.settings.get('use_legacy_v2_onions'): + key_type = "ED25519-V3" + key_content = onionkey.generate_v3_private_key()[0] + else: + # fall back to v2 onion services + key_type = "RSA1024" + key_content = onionkey.generate_v2_private_key()[0] + self.common.log('Onion', 'Starting a hidden service with a new private key') + + # v3 onions don't yet support basic auth. Our ticket: + # https://github.com/micahflee/onionshare/issues/697 + if key_type == "ED25519-V3" and not self.settings.get('use_legacy_v2_onions'): + basic_auth = None + self.stealth = False try: if basic_auth != None: - res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth, key_type = key_type, key_content=key_content) + res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, basic_auth=basic_auth, key_type=key_type, key_content=key_content) else: # if the stem interface is older than 1.5.0, basic_auth isn't a valid keyword arg - res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, key_type = key_type, key_content=key_content) + res = self.c.create_ephemeral_hidden_service({ 80: port }, await_publication=True, key_type=key_type, key_content=key_content) except ProtocolError: raise TorErrorProtocolError(strings._('error_tor_protocol_error')) @@ -461,7 +491,7 @@ class Onion(object): # A new private key was generated and is in the Control port response. if self.settings.get('save_private_key'): if not self.settings.get('private_key'): - self.settings.set('private_key', res.private_key) + self.settings.set('private_key', key_content) if self.stealth: # Similar to the PrivateKey, the Control port only returns the ClientAuth diff --git a/onionshare/onionkey.py b/onionshare/onionkey.py new file mode 100644 index 00000000..d2c6ad17 --- /dev/null +++ b/onionshare/onionkey.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +""" +OnionShare | https://onionshare.org/ + +Copyright (C) 2017 Micah Lee + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import os +import sys + +import base64 +import hashlib +# Need sha3 if python version is older than 3.6, otherwise +# we can't use hashlib.sha3_256 +if sys.version_info < (3, 6): + import sha3 + +import nacl.signing + +from Crypto.PublicKey import RSA + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa + + +def stem_compatible_base64_blob_from_private_key(private_key_seed: bytes) -> str: + """ + Provides a base64-encoded private key for v3-style Onions. + """ + b = 256 + + def bit(h: bytes, i: int) -> int: + return (h[i // 8] >> (i % 8)) & 1 + + def encode_int(y: int) -> bytes: + bits = [(y >> i) & 1 for i in range(b)] + return b''.join([bytes([(sum([bits[i * 8 + j] << j for j in range(8)]))]) for i in range(b // 8)]) + + def expand_private_key(sk: bytes) -> bytes: + h = hashlib.sha512(sk).digest() + a = 2 ** (b - 2) + sum(2 ** i * bit(h, i) for i in range(3, b - 2)) + k = b''.join([bytes([h[i]]) for i in range(b // 8, b // 4)]) + assert len(k) == 32 + return encode_int(a) + k + + expanded_private_key = expand_private_key(private_key_seed) + return base64.b64encode(expanded_private_key).decode() + + +def onion_url_from_private_key(private_key_seed: bytes) -> str: + """ + Derives the public key (.onion hostname) from a v3-style + Onion private key. + """ + signing_key = nacl.signing.SigningKey(seed=private_key_seed) + public_key = bytes(signing_key.verify_key) + version = b'\x03' + checksum = hashlib.sha3_256(b".onion checksum" + public_key + version).digest()[:2] + onion_address = "http://{}.onion".format(base64.b32encode(public_key + checksum + version).decode().lower()) + return onion_address + + +def generate_v3_private_key(): + """ + Generates a private and public key for use with v3 style Onions. + Returns both the private key as well as the public key (.onion hostname) + """ + private_key_seed = os.urandom(32) + private_key = stem_compatible_base64_blob_from_private_key(private_key_seed) + onion_url = onion_url_from_private_key(private_key_seed) + return (private_key, onion_url) + + +def generate_v2_private_key(): + """ + Generates a private and public key for use with v2 style Onions. + Returns both the serialized private key (compatible with Stem) + as well as the public key (.onion hostname) + """ + # Generate v2 Onion Service private key + private_key = rsa.generate_private_key(public_exponent=65537, + key_size=1024, + backend=default_backend()) + hs_public_key = private_key.public_key() + + # Pre-generate the public key (.onion hostname) + der_format = hs_public_key.public_bytes(encoding=serialization.Encoding.DER, + format=serialization.PublicFormat.PKCS1) + + onion_url = base64.b32encode(hashlib.sha1(der_format).digest()[:-10]).lower().decode() + + # Generate Stem-compatible key content + pem_format = private_key.private_bytes(encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption()) + serialized_key = ''.join(pem_format.decode().split('\n')[1:-2]) + + return (serialized_key, onion_url) + + +def is_v2_key(key): + """ + Helper function for determining if a key is RSA1024 (v2) or not. + """ + try: + # Import the key + key = RSA.importKey(base64.b64decode(key)) + # Is this a v2 Onion key? (1024 bits) If so, we should keep using it. + if key.n.bit_length() == 1024: + return True + else: + return False + except: + return False + diff --git a/onionshare/settings.py b/onionshare/settings.py index ef75be2f..adcfc7a3 100644 --- a/onionshare/settings.py +++ b/onionshare/settings.py @@ -67,6 +67,7 @@ class Settings(object): 'tor_bridges_use_obfs4': False, 'tor_bridges_use_meek_lite_azure': False, 'tor_bridges_use_custom_bridges': '', + 'use_legacy_v2_onions': False, 'save_private_key': False, 'private_key': '', 'public_mode': False, diff --git a/onionshare_gui/server_status.py b/onionshare_gui/server_status.py index e016e8f9..9afceb38 100644 --- a/onionshare_gui/server_status.py +++ b/onionshare_gui/server_status.py @@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ import platform +import textwrap from PyQt5 import QtCore, QtWidgets, QtGui from onionshare import strings @@ -56,6 +57,8 @@ class ServerStatus(QtWidgets.QWidget): self.web = None + self.resizeEvent(None) + # Shutdown timeout layout self.shutdown_timeout_label = QtWidgets.QLabel(strings._('gui_settings_shutdown_timeout', True)) self.shutdown_timeout = QtWidgets.QDateTimeEdit() @@ -88,7 +91,7 @@ class ServerStatus(QtWidgets.QWidget): self.url = QtWidgets.QLabel() self.url.setFont(url_font) self.url.setWordWrap(True) - self.url.setMinimumHeight(60) + self.url.setMinimumHeight(65) self.url.setMinimumSize(self.url.sizeHint()) self.url.setStyleSheet(self.common.css['server_status_url']) @@ -128,6 +131,24 @@ class ServerStatus(QtWidgets.QWidget): self.update() + def resizeEvent(self, event): + """ + When the widget is resized, try and adjust the display of a v3 onion URL. + """ + try: + self.get_url() + url_length=len(self.get_url()) + if url_length > 60: + width = self.frameGeometry().width() + if width < 530: + wrapped_onion_url = textwrap.fill(self.get_url(), 50) + self.url.setText(wrapped_onion_url) + else: + self.url.setText(self.get_url()) + except: + pass + + def shutdown_timeout_reset(self): """ Reset the timeout in the UI after stopping a share diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index c4e6b752..895cde13 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -54,20 +54,106 @@ class SettingsDialog(QtWidgets.QDialog): # General options - # Whether or not to save the Onion private key for reuse (persistent URL mode) - self.save_private_key_checkbox = QtWidgets.QCheckBox() - self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.save_private_key_checkbox.setText(strings._("gui_save_private_key_checkbox", True)) - - # Use a slug + # Use a slug or not ('public mode') self.public_mode_checkbox = QtWidgets.QCheckBox() self.public_mode_checkbox.setCheckState(QtCore.Qt.Unchecked) self.public_mode_checkbox.setText(strings._("gui_settings_public_mode_checkbox", True)) + public_mode_label = QtWidgets.QLabel(strings._("gui_settings_public_mode_details", True)) + public_mode_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + public_mode_label.setOpenExternalLinks(True) + public_mode_label.setMinimumSize(public_mode_label.sizeHint()) + public_mode_layout = QtWidgets.QHBoxLayout() + public_mode_layout.addWidget(self.public_mode_checkbox) + public_mode_layout.addWidget(public_mode_label) + public_mode_layout.addStretch() + public_mode_layout.setContentsMargins(0,0,0,0) + self.public_mode_widget = QtWidgets.QWidget() + self.public_mode_widget.setLayout(public_mode_layout) + + # Whether or not to use a shutdown ('auto-stop') timer + self.shutdown_timeout_checkbox = QtWidgets.QCheckBox() + self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) + self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True)) + shutdown_timeout_label = QtWidgets.QLabel(strings._("gui_settings_shutdown_timeout_details", True)) + shutdown_timeout_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + shutdown_timeout_label.setOpenExternalLinks(True) + shutdown_timeout_label.setMinimumSize(public_mode_label.sizeHint()) + shutdown_timeout_layout = QtWidgets.QHBoxLayout() + shutdown_timeout_layout.addWidget(self.shutdown_timeout_checkbox) + shutdown_timeout_layout.addWidget(shutdown_timeout_label) + shutdown_timeout_layout.addStretch() + shutdown_timeout_layout.setContentsMargins(0,0,0,0) + self.shutdown_timeout_widget = QtWidgets.QWidget() + self.shutdown_timeout_widget.setLayout(shutdown_timeout_layout) + + # Whether or not to use legacy v2 onions + self.use_legacy_v2_onions_checkbox = QtWidgets.QCheckBox() + self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.use_legacy_v2_onions_checkbox.setText(strings._("gui_use_legacy_v2_onions_checkbox", True)) + self.use_legacy_v2_onions_checkbox.clicked.connect(self.use_legacy_v2_onions_checkbox_clicked) + use_legacy_v2_onions_label = QtWidgets.QLabel(strings._("gui_use_legacy_v2_onions_label", True)) + use_legacy_v2_onions_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + use_legacy_v2_onions_label.setOpenExternalLinks(True) + use_legacy_v2_onions_layout = QtWidgets.QHBoxLayout() + use_legacy_v2_onions_layout.addWidget(self.use_legacy_v2_onions_checkbox) + use_legacy_v2_onions_layout.addWidget(use_legacy_v2_onions_label) + use_legacy_v2_onions_layout.addStretch() + use_legacy_v2_onions_layout.setContentsMargins(0,0,0,0) + self.use_legacy_v2_onions_widget = QtWidgets.QWidget() + self.use_legacy_v2_onions_widget.setLayout(use_legacy_v2_onions_layout) + + # Whether or not to save the Onion private key for reuse (persistent URL mode) + self.save_private_key_checkbox = QtWidgets.QCheckBox() + self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.save_private_key_checkbox.setText(strings._("gui_save_private_key_checkbox", True)) + self.save_private_key_checkbox.clicked.connect(self.save_private_key_checkbox_clicked) + save_private_key_label = QtWidgets.QLabel(strings._("gui_save_private_key_label", True)) + save_private_key_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + save_private_key_label.setOpenExternalLinks(True) + save_private_key_layout = QtWidgets.QHBoxLayout() + save_private_key_layout.addWidget(self.save_private_key_checkbox) + save_private_key_layout.addWidget(save_private_key_label) + save_private_key_layout.addStretch() + save_private_key_layout.setContentsMargins(0,0,0,0) + self.save_private_key_widget = QtWidgets.QWidget() + self.save_private_key_widget.setLayout(save_private_key_layout) + + # Stealth + self.stealth_checkbox = QtWidgets.QCheckBox() + self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True)) + self.stealth_checkbox.clicked.connect(self.stealth_checkbox_clicked_connect) + use_stealth_label = QtWidgets.QLabel(strings._("gui_settings_stealth_option_details", True)) + use_stealth_label.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) + use_stealth_label.setOpenExternalLinks(True) + use_stealth_label.setMinimumSize(use_stealth_label.sizeHint()) + use_stealth_layout = QtWidgets.QHBoxLayout() + use_stealth_layout.addWidget(self.stealth_checkbox) + use_stealth_layout.addWidget(use_stealth_label) + use_stealth_layout.addStretch() + use_stealth_layout.setContentsMargins(0,0,0,0) + self.use_stealth_widget = QtWidgets.QWidget() + self.use_stealth_widget.setLayout(use_stealth_layout) + + hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True)) + hidservauth_details.setWordWrap(True) + hidservauth_details.setMinimumSize(hidservauth_details.sizeHint()) + hidservauth_details.hide() + + self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) + self.hidservauth_copy_button.clicked.connect(self.hidservauth_copy_button_clicked) + self.hidservauth_copy_button.hide() # General options layout general_group_layout = QtWidgets.QVBoxLayout() - general_group_layout.addWidget(self.save_private_key_checkbox) - general_group_layout.addWidget(self.public_mode_checkbox) + general_group_layout.addWidget(self.public_mode_widget) + general_group_layout.addWidget(self.shutdown_timeout_widget) + general_group_layout.addWidget(self.use_legacy_v2_onions_widget) + general_group_layout.addWidget(self.save_private_key_widget) + general_group_layout.addWidget(self.use_stealth_widget) + general_group_layout.addWidget(hidservauth_details) + general_group_layout.addWidget(self.hidservauth_copy_button) + general_group = QtWidgets.QGroupBox(strings._("gui_settings_general_label", True)) general_group.setLayout(general_group_layout) @@ -78,15 +164,9 @@ class SettingsDialog(QtWidgets.QDialog): self.close_after_first_download_checkbox.setCheckState(QtCore.Qt.Checked) self.close_after_first_download_checkbox.setText(strings._("gui_settings_close_after_first_download_option", True)) - # Whether or not to use a shutdown timer - self.shutdown_timeout_checkbox = QtWidgets.QCheckBox() - self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) - self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True)) - # Sharing options layout sharing_group_layout = QtWidgets.QVBoxLayout() sharing_group_layout.addWidget(self.close_after_first_download_checkbox) - sharing_group_layout.addWidget(self.shutdown_timeout_checkbox) sharing_group = QtWidgets.QGroupBox(strings._("gui_settings_sharing_label", True)) sharing_group.setLayout(sharing_group_layout) @@ -113,36 +193,6 @@ class SettingsDialog(QtWidgets.QDialog): receiving_group = QtWidgets.QGroupBox(strings._("gui_settings_receiving_label", True)) receiving_group.setLayout(receiving_group_layout) - # Stealth options - - # Stealth - stealth_details = QtWidgets.QLabel(strings._("gui_settings_stealth_option_details", True)) - stealth_details.setWordWrap(True) - stealth_details.setTextInteractionFlags(QtCore.Qt.TextBrowserInteraction) - stealth_details.setOpenExternalLinks(True) - stealth_details.setMinimumSize(stealth_details.sizeHint()) - self.stealth_checkbox = QtWidgets.QCheckBox() - self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) - self.stealth_checkbox.setText(strings._("gui_settings_stealth_option", True)) - - hidservauth_details = QtWidgets.QLabel(strings._('gui_settings_stealth_hidservauth_string', True)) - hidservauth_details.setWordWrap(True) - hidservauth_details.setMinimumSize(hidservauth_details.sizeHint()) - hidservauth_details.hide() - - self.hidservauth_copy_button = QtWidgets.QPushButton(strings._('gui_copy_hidservauth', True)) - self.hidservauth_copy_button.clicked.connect(self.hidservauth_copy_button_clicked) - self.hidservauth_copy_button.hide() - - # Stealth options layout - stealth_group_layout = QtWidgets.QVBoxLayout() - stealth_group_layout.addWidget(stealth_details) - stealth_group_layout.addWidget(self.stealth_checkbox) - stealth_group_layout.addWidget(hidservauth_details) - stealth_group_layout.addWidget(self.hidservauth_copy_button) - stealth_group = QtWidgets.QGroupBox(strings._("gui_settings_stealth_label", True)) - stealth_group.setLayout(stealth_group_layout) - # Automatic updates options # Autoupdate @@ -375,7 +425,6 @@ class SettingsDialog(QtWidgets.QDialog): left_col_layout.addWidget(general_group) left_col_layout.addWidget(sharing_group) left_col_layout.addWidget(receiving_group) - left_col_layout.addWidget(stealth_group) left_col_layout.addWidget(autoupdate_group) left_col_layout.addStretch() @@ -412,11 +461,26 @@ class SettingsDialog(QtWidgets.QDialog): else: self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Unchecked) + use_legacy_v2_onions = self.old_settings.get('use_legacy_v2_onions') + + if use_legacy_v2_onions: + self.save_private_key_widget.show() + self.use_stealth_widget.show() + else: + self.save_private_key_widget.hide() + self.use_stealth_widget.hide() + save_private_key = self.old_settings.get('save_private_key') if save_private_key: self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked) + # Legacy v2 mode is forced on if persistence is enabled + self.use_legacy_v2_onions_checkbox.setEnabled(False) else: self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.use_legacy_v2_onions_checkbox.setEnabled(True) + + if use_legacy_v2_onions or save_private_key: + self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked) downloads_dir = self.old_settings.get('downloads_dir') self.downloads_dir_lineedit.setText(downloads_dir) @@ -436,11 +500,15 @@ class SettingsDialog(QtWidgets.QDialog): use_stealth = self.old_settings.get('use_stealth') if use_stealth: self.stealth_checkbox.setCheckState(QtCore.Qt.Checked) - if save_private_key: + # Legacy v2 mode is forced on if Stealth is enabled + self.use_legacy_v2_onions_checkbox.setEnabled(False) + if save_private_key and self.old_settings.get('hidservauth_string') != "": hidservauth_details.show() self.hidservauth_copy_button.show() else: self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) + if not save_private_key: + self.use_legacy_v2_onions_checkbox.setEnabled(True) use_autoupdate = self.old_settings.get('use_autoupdate') if use_autoupdate: @@ -604,6 +672,39 @@ class SettingsDialog(QtWidgets.QDialog): clipboard = self.qtapp.clipboard() clipboard.setText(self.old_settings.get('hidservauth_string')) + def use_legacy_v2_onions_checkbox_clicked(self, checked): + """ + Show the legacy settings if the legacy mode is enabled. + """ + if checked: + self.save_private_key_widget.show() + self.use_stealth_widget.show() + else: + self.save_private_key_widget.hide() + self.use_stealth_widget.hide() + + def save_private_key_checkbox_clicked(self, checked): + """ + Prevent the v2 legacy mode being switched off if persistence is enabled + """ + if checked: + self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked) + self.use_legacy_v2_onions_checkbox.setEnabled(False) + else: + if not self.stealth_checkbox.isChecked(): + self.use_legacy_v2_onions_checkbox.setEnabled(True) + + def stealth_checkbox_clicked_connect(self, checked): + """ + Prevent the v2 legacy mode being switched off if stealth is enabled + """ + if checked: + self.use_legacy_v2_onions_checkbox.setCheckState(QtCore.Qt.Checked) + self.use_legacy_v2_onions_checkbox.setEnabled(False) + else: + if not self.save_private_key_checkbox.isChecked(): + self.use_legacy_v2_onions_checkbox.setEnabled(True) + def downloads_button_clicked(self): """ Browse for a new downloads directory @@ -790,7 +891,16 @@ class SettingsDialog(QtWidgets.QDialog): settings.set('close_after_first_download', self.close_after_first_download_checkbox.isChecked()) settings.set('shutdown_timeout', self.shutdown_timeout_checkbox.isChecked()) + + # Complicated logic here to force v2 onion mode on or off depending on other settings + if self.use_legacy_v2_onions_checkbox.isChecked(): + use_legacy_v2_onions = True + else: + use_legacy_v2_onions = False + if self.save_private_key_checkbox.isChecked(): + # force the legacy mode on + use_legacy_v2_onions = True settings.set('save_private_key', True) settings.set('private_key', self.old_settings.get('private_key')) settings.set('slug', self.old_settings.get('slug')) @@ -801,6 +911,18 @@ class SettingsDialog(QtWidgets.QDialog): settings.set('slug', '') # Also unset the HidServAuth if we are removing our reusable private key settings.set('hidservauth_string', '') + + if use_legacy_v2_onions: + settings.set('use_legacy_v2_onions', True) + else: + settings.set('use_legacy_v2_onions', False) + # If we are not using legacy mode, but we previously had persistence turned on, force it off! + settings.set('save_private_key', False) + settings.set('private_key', '') + settings.set('slug', '') + # Also unset the HidServAuth if we are removing our reusable private key + settings.set('hidservauth_string', '') + settings.set('downloads_dir', self.downloads_dir_lineedit.text()) settings.set('receive_allow_receiver_shutdown', self.receive_allow_receiver_shutdown_checkbox.isChecked()) settings.set('public_mode', self.public_mode_checkbox.isChecked()) diff --git a/share/locale/en.json b/share/locale/en.json index a3a7f3df..c70ca2eb 100644 --- a/share/locale/en.json +++ b/share/locale/en.json @@ -71,17 +71,16 @@ "error_stealth_not_supported": "To create stealth onion services, you need at least Tor 0.2.9.1-alpha (or Tor Browser 6.5) and at least python3-stem 1.5.0.", "error_ephemeral_not_supported": "OnionShare requires at least Tor 0.2.7.1 and at least python3-stem 1.4.0.", "gui_settings_window_title": "Settings", - "gui_settings_stealth_label": "Stealth (advanced)", - "gui_settings_stealth_option": "Create stealth onion services", - "gui_settings_stealth_option_details": "This makes OnionShare more secure, but also more difficult for the recipient to connect to.
More information.", + "gui_settings_stealth_option": "Create stealth onion services (legacy)", + "gui_settings_stealth_option_details": "(what's this?)", "gui_settings_stealth_hidservauth_string": "You have saved the private key for reuse, so your HidServAuth string is also reused.\nClick below to copy the HidServAuth.", "gui_settings_autoupdate_label": "Check for upgrades", "gui_settings_autoupdate_option": "Notify me when upgrades are available", "gui_settings_autoupdate_timestamp": "Last checked: {}", "gui_settings_autoupdate_timestamp_never": "Never", "gui_settings_autoupdate_check_button": "Check For Upgrades", - "gui_settings_general_label": "General options", - "gui_settings_sharing_label": "Sharing options", + "gui_settings_general_label": "General settings", + "gui_settings_sharing_label": "Sharing settings", "gui_settings_close_after_first_download_option": "Stop sharing after first download", "gui_settings_connection_type_label": "How should OnionShare connect to Tor?", "gui_settings_connection_type_bundled_option": "Use the Tor version that is bundled with OnionShare", @@ -92,7 +91,7 @@ "gui_settings_control_port_label": "Control port", "gui_settings_socket_file_label": "Socket file", "gui_settings_socks_label": "SOCKS port", - "gui_settings_authenticate_label": "Tor authentication options", + "gui_settings_authenticate_label": "Tor authentication settings", "gui_settings_authenticate_no_auth_option": "No authentication, or cookie authentication", "gui_settings_authenticate_password_option": "Password", "gui_settings_password_label": "Password", @@ -110,6 +109,7 @@ "gui_settings_button_cancel": "Cancel", "gui_settings_button_help": "Help", "gui_settings_shutdown_timeout_checkbox": "Use auto-stop timer", + "gui_settings_shutdown_timeout_details": "(what's this?)", "gui_settings_shutdown_timeout": "Stop the share at:", "settings_saved": "Settings saved to {}", "settings_error_unknown": "Can't connect to Tor controller because the settings don't make sense.", @@ -124,6 +124,7 @@ "settings_error_bundled_tor_broken": "OnionShare could not connect to Tor in the background:\n{}", "settings_test_success": "Congratulations, OnionShare can connect to the Tor controller.\n\nTor version: {}\nSupports ephemeral onion services: {}\nSupports stealth onion services: {}", "error_tor_protocol_error": "Could not communicate with the Tor controller.\nIf you're using Whonix, check out https://www.whonix.org/wiki/onionshare to make OnionShare work.", + "error_invalid_private_key": "This private key type is unsupported", "connecting_to_tor": "Connecting to the Tor network", "update_available": "A new version of OnionShare is available. Click here to download it.

Installed version: {}
Latest version: {}", "update_error_check_error": "Error checking for updates: Maybe you're not connected to Tor, or maybe the OnionShare website is down.", @@ -138,7 +139,10 @@ "gui_server_started_after_timeout": "The server started after your chosen auto-timeout.\nPlease start a new share.", "gui_server_timeout_expired": "The chosen timeout has already expired.\nPlease update the timeout and then you may start sharing.", "share_via_onionshare": "Share via OnionShare", - "gui_save_private_key_checkbox": "Use a persistent address\n(unchecking will delete any saved addresses)", + "gui_use_legacy_v2_onions_checkbox": "Use legacy addresses", + "gui_use_legacy_v2_onions_label": "(what's this?)", + "gui_save_private_key_checkbox": "Use a persistent address (legacy)", + "gui_save_private_key_label": "(what's this?)", "gui_share_url_description": "Anyone with this link can download your files using the Tor Browser: ", "gui_receive_url_description": "Anyone with this link can upload files to your computer using the Tor Browser: ", "gui_url_label_persistent": "This share will not expire automatically unless a timer is set.

Every share will have the same address (to use one-time addresses, disable persistence in Settings)", @@ -166,11 +170,12 @@ "receive_mode_received_file": "Received file: {}", "gui_mode_share_button": "Share Files", "gui_mode_receive_button": "Receive Files", - "gui_settings_receiving_label": "Receiving options", + "gui_settings_receiving_label": "Receiving settings", "gui_settings_downloads_label": "Save files to", "gui_settings_downloads_button": "Browse", "gui_settings_receive_allow_receiver_shutdown_checkbox": "Receive mode can be stopped by the sender", - "gui_settings_public_mode_checkbox": "OnionShare is open to the public\n(don't prevent people from guessing the OnionShare address)", + "gui_settings_public_mode_checkbox": "Public mode", + "gui_settings_public_mode_details": "(what's this?)", "systray_close_server_title": "OnionShare Server Closed", "systray_close_server_message": "A user closed the server", "systray_page_loaded_title": "OnionShare Page Loaded", diff --git a/stdeb.cfg b/stdeb.cfg index 334502c0..e190fe8b 100644 --- a/stdeb.cfg +++ b/stdeb.cfg @@ -2,5 +2,5 @@ Package3: onionshare Depends3: python3-flask, python3-stem, python3-pyqt5, python-nautilus, tor, obfs4proxy Build-Depends: python3-pytest, python3-flask, python3-stem, python3-pyqt5 -Suite: xenial -X-Python3-Version: >= 3.4 +Suite: bionic +X-Python3-Version: >= 3.6 diff --git a/test/test_onionshare_settings.py b/test/test_onionshare_settings.py index 1521492d..1f1ef528 100644 --- a/test/test_onionshare_settings.py +++ b/test/test_onionshare_settings.py @@ -59,6 +59,7 @@ class TestSettings: 'tor_bridges_use_obfs4': False, 'tor_bridges_use_meek_lite_azure': False, 'tor_bridges_use_custom_bridges': '', + 'use_legacy_v2_onions': False, 'save_private_key': False, 'private_key': '', 'slug': '',