tfc-mirror/src/relay/onion.py

231 lines
8.2 KiB
Python

#!/usr/bin/env python3.6
# -*- coding: utf-8 -*-
"""
TFC - Onion-routed, endpoint secure messaging system
Copyright (C) 2013-2019 Markus Ottela
This file is part of TFC.
TFC 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.
TFC 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 TFC. If not, see <https://www.gnu.org/licenses/>.
"""
import base64
import hashlib
import os
import random
import shlex
import socket
import subprocess
import tempfile
import time
from multiprocessing import Queue
from typing import Any, Dict
import nacl.signing
import stem.control
import stem.process
from src.common.encoding import pub_key_to_onion_address
from src.common.exceptions import CriticalError
from src.common.output import m_print, rp_print
from src.common.statics import *
def get_available_port(min_port: int, max_port: int) -> str:
"""Find a random available port within the given range."""
with socket.socket() as temp_sock:
while True:
try:
temp_sock.bind(('127.0.0.1', random.randint(min_port, max_port)))
break
except OSError:
pass
_, port = temp_sock.getsockname() # type: Any, str
return port
class Tor(object):
"""Tor class manages the starting and stopping of Tor client."""
def __init__(self) -> None:
self.tor_process = None # type: Any
self.controller = None # type: Any
def connect(self, port: str) -> None:
"""Launch Tor as a subprocess."""
tor_data_directory = tempfile.TemporaryDirectory()
tor_control_socket = os.path.join(tor_data_directory.name, 'control_socket')
if not os.path.isfile('/usr/bin/tor'):
raise CriticalError("Check that Tor is installed.")
while True:
try:
self.tor_process = stem.process.launch_tor_with_config(
config={'DataDirectory': tor_data_directory.name,
'SocksPort': str(port),
'ControlSocket': tor_control_socket,
'AvoidDiskWrites': '1',
'Log': 'notice stdout',
'GeoIPFile': '/usr/share/tor/geoip',
'GeoIPv6File ': '/usr/share/tor/geoip6'},
tor_cmd='/usr/bin/tor')
break
except OSError:
pass # Tor timed out. Try again.
start_ts = time.monotonic()
self.controller = stem.control.Controller.from_socket_file(path=tor_control_socket)
self.controller.authenticate()
while True:
time.sleep(0.1)
try:
response = self.controller.get_info("status/bootstrap-phase")
except stem.SocketClosed:
raise CriticalError("Tor socket closed.")
res_parts = shlex.split(response)
summary = res_parts[4].split('=')[1]
if summary == 'Done':
tor_version = self.controller.get_version().version_str.split(' (')[0]
rp_print(f"Setup 70% - Tor {tor_version} is now running", bold=True)
break
if time.monotonic() - start_ts > 15:
start_ts = time.monotonic()
self.controller = stem.control.Controller.from_socket_file(path=tor_control_socket)
self.controller.authenticate()
def stop(self) -> None:
"""Stop the Tor subprocess."""
if self.tor_process:
self.tor_process.terminate()
time.sleep(0.1)
if not self.tor_process.poll():
self.tor_process.kill()
def stem_compatible_ed25519_key_from_private_key(private_key: bytes) -> str:
"""Tor's custom encoding format for v3 Onion Service private keys.
This code is based on Tor's testing code at
https://github.com/torproject/tor/blob/8e84968ffbf6d284e8a877ddcde6ded40b3f5681/src/test/ed25519_exts_ref.py#L48
"""
b = 256
def bit(h: bytes, i: int) -> int:
"""\
Output (i % 8 + 1) right-most bit of (i // 8) right-most byte
of the digest.
"""
return (h[i // 8] >> (i % 8)) & 1
def encode_int(y: int) -> bytes:
"""Encode integer to 32-byte bytestring (little-endian format)."""
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:
"""Expand private key to base64 blob."""
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) == ONION_SERVICE_PRIVATE_KEY_LENGTH
return encode_int(a) + k
expanded_private_key = expand_private_key(private_key)
return base64.b64encode(expanded_private_key).decode()
def kill_background_tor() -> None:
"""Kill any open TFC-related Tor instances left open.
Copies of Tor might stay open in cases where the user has closed the
application from Terminator's close window ((x) button).
"""
try:
pids = subprocess.check_output("ps aux |grep '[t]fc/tor' | awk '{print $2}' 2>/dev/null", shell=True)
for pid in pids.split(b'\n'):
subprocess.Popen("kill {}".format(int(pid)), shell=True).wait()
except ValueError:
pass
def onion_service(queues: Dict[bytes, 'Queue']) -> None:
"""Manage the Tor Onion Service and control Tor via stem."""
kill_background_tor()
rp_print("Setup 0% - Waiting for Onion Service configuration...", bold=True)
while queues[ONION_KEY_QUEUE].qsize() == 0:
time.sleep(0.1)
private_key, c_code = queues[ONION_KEY_QUEUE].get() # type: bytes, bytes
public_key_user = bytes(nacl.signing.SigningKey(seed=private_key).verify_key)
onion_addr_user = pub_key_to_onion_address(public_key_user)
try:
rp_print("Setup 10% - Launching Tor...", bold=True)
tor_port = get_available_port(1000, 65535)
tor = Tor()
tor.connect(tor_port)
except (EOFError, KeyboardInterrupt):
return
try:
rp_print("Setup 75% - Launching Onion Service...", bold=True)
key_data = stem_compatible_ed25519_key_from_private_key(private_key)
response = tor.controller.create_ephemeral_hidden_service(ports={80: 5000},
key_type='ED25519-V3',
key_content=key_data,
await_publication=True)
rp_print("Setup 100% - Onion Service is now published.", bold=True)
m_print(["Your TFC account is:",
onion_addr_user, '',
f"Onion Service confirmation code (to Transmitter): {c_code.hex()}"], box=True)
# Allow the client to start looking for contacts at this point.
queues[TOR_DATA_QUEUE].put((tor_port, onion_addr_user))
except (KeyboardInterrupt, stem.SocketClosed):
tor.stop()
return
while True:
try:
time.sleep(0.1)
if queues[ONION_KEY_QUEUE].qsize() > 0:
queues[ONION_KEY_QUEUE].get() # Discard re-sent private keys
if queues[ONION_CLOSE_QUEUE].qsize() > 0:
command = queues[ONION_CLOSE_QUEUE].get()
queues[EXIT_QUEUE].put(command)
tor.controller.remove_hidden_service(response.service_id)
tor.stop()
break
except (EOFError, KeyboardInterrupt):
pass
except stem.SocketClosed:
tor.controller.remove_hidden_service(response.service_id)
tor.stop()
break