From 5252d5d94a808fd478b1ac9d0206d19bd38db483 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Tue, 21 Aug 2018 19:31:02 +1000 Subject: [PATCH] Introduce v3 onion support --- .travis.yml | 2 +- BUILD.md | 4 +- install/requirements-windows.txt | 4 + install/requirements.txt | 4 + onionshare/onion.py | 51 +++++++++--- onionshare/onionkey.py | 126 ++++++++++++++++++++++++++++++ onionshare_gui/server_status.py | 11 ++- onionshare_gui/settings_dialog.py | 9 ++- 8 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 onionshare/onionkey.py 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 e6e54951..57c83438 100644 --- a/BUILD.md +++ b/BUILD.md @@ -11,9 +11,9 @@ cd onionshare Install the needed dependencies: -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` +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; pip3 install pysha3` -For Fedora-like distros: `dnf install -y rpm-build python3-flask python3-stem python3-qt5 python3-pytest nautilus-python tor obfs4` +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; pip3 install pysha3` 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 32b8da4a..611edf3c 100644 --- a/install/requirements-windows.txt +++ b/install/requirements-windows.txt @@ -1,4 +1,5 @@ click==6.7 +cryptography==2.1.4 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 sip==4.19.6 stem==1.6.0 Werkzeug==0.14.1 diff --git a/install/requirements.txt b/install/requirements.txt index c7828080..964030e8 100644 --- a/install/requirements.txt +++ b/install/requirements.txt @@ -1,4 +1,5 @@ click==6.7 +cryptography==2.1.4 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 sip==4.19.6 stem==1.6.0 Werkzeug==0.14.1 diff --git a/onionshare/onion.py b/onionshare/onion.py index 4812842a..73c94600 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 socks from . import common, strings from .settings import Settings @@ -444,20 +446,49 @@ 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') + try: + # is the key a v2 key? + key = onionkey.is_v2_key(self.settings.get('private_key')) + key_type = "RSA1024" + key_content = self.settings.get('private_key') + # 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 + except: + pass + # Assume it was a v3 key + # key_type = "ED25519-V3" + # key_content = self.settings.get('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.3'): + 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": + 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')) @@ -468,7 +499,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..89c781ab --- /dev/null +++ b/onionshare/onionkey.py @@ -0,0 +1,126 @@ +# -*- 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 + + +b = 256 + +def bit(h, i): + return (h[i // 8] >> (i % 8)) & 1 + + +def encodeint(y): + 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 H(m): + return hashlib.sha512(m).digest() + + +def expandSK(sk): + h = H(sk) + 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 encodeint(a) + k + + +def onion_url_from_private_key(private_key): + """ + Derives the public key (.onion hostname) from a v3-style + Onion private key. + """ + private_key = nacl.signing.SigningKey(seed=private_key) + pubkey = bytes(private_key.verify_key) + version = b'\x03' + checksum = hashlib.sha3_256(b".onion checksum" + pubkey + version).digest()[:2] + onion_address = "http://{}.onion".format(base64.b32encode(pubkey + 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) + """ + secretKey = os.urandom(32) + expandedSecretKey = expandSK(secretKey) + private_key = base64.b64encode(expandedSecretKey).decode() + return (private_key, onion_url_from_private_key(secretKey)) + +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_gui/server_status.py b/onionshare_gui/server_status.py index 1562ee10..46310605 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 @@ -88,7 +89,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']) @@ -162,7 +163,13 @@ class ServerStatus(QtWidgets.QWidget): else: self.url_description.setToolTip(strings._('gui_url_label_stay_open', True)) - self.url.setText(self.get_url()) + # Wrap the Onion URL if it's a big v3 one + url_length=len(self.get_url()) + if url_length > 60: + wrapped_onion_url = textwrap.fill(self.get_url(), 50) + self.url.setText(wrapped_onion_url) + else: + self.url.setText(self.get_url()) self.url.show() self.copy_url_button.show() diff --git a/onionshare_gui/settings_dialog.py b/onionshare_gui/settings_dialog.py index 94480205..a41226f6 100644 --- a/onionshare_gui/settings_dialog.py +++ b/onionshare_gui/settings_dialog.py @@ -19,6 +19,7 @@ along with this program. If not, see . """ from PyQt5 import QtCore, QtWidgets, QtGui import sys, platform, datetime, re +from distutils.version import LooseVersion as Version from onionshare import strings, common from onionshare.settings import Settings @@ -64,7 +65,7 @@ class SettingsDialog(QtWidgets.QDialog): self.shutdown_timeout_checkbox.setCheckState(QtCore.Qt.Checked) self.shutdown_timeout_checkbox.setText(strings._("gui_settings_shutdown_timeout_checkbox", True)) - # Whether or not to save the Onion private key for reuse + # Whether or not to save the Onion private key for reuse (persistent URLs) 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)) @@ -421,6 +422,9 @@ class SettingsDialog(QtWidgets.QDialog): self.save_private_key_checkbox.setCheckState(QtCore.Qt.Checked) else: self.save_private_key_checkbox.setCheckState(QtCore.Qt.Unchecked) + # Using persistent URLs with v3 onions is not yet stable + if Version(self.onion.tor_version) >= Version('0.3.2.9'): + self.save_private_key_checkbox.hide() downloads_dir = self.old_settings.get('downloads_dir') self.downloads_dir_lineedit.setText(downloads_dir) @@ -445,6 +449,9 @@ class SettingsDialog(QtWidgets.QDialog): self.hidservauth_copy_button.show() else: self.stealth_checkbox.setCheckState(QtCore.Qt.Unchecked) + # Using Client Auth with v3 onions is not yet possible + if Version(self.onion.tor_version) >= Version('0.3.2.9'): + stealth_group.hide() use_autoupdate = self.old_settings.get('use_autoupdate') if use_autoupdate: