Introduce v3 onion support

This commit is contained in:
Miguel Jacq 2018-08-21 19:31:02 +10:00
parent f5ccfcf2cc
commit 2de9359629
No known key found for this signature in database
GPG Key ID: EEA4341C6D97A0B6
8 changed files with 195 additions and 16 deletions

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -21,8 +21,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
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

126
onionshare/onionkey.py Normal file
View File

@ -0,0 +1,126 @@
# -*- coding: utf-8 -*-
"""
OnionShare | https://onionshare.org/
Copyright (C) 2017 Micah Lee <micah@micahflee.com>
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 <http://www.gnu.org/licenses/>.
"""
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

View File

@ -18,6 +18,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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()

View File

@ -19,6 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
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: