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': '',