/tcp.py (0b4efe4dcdf1263c9f2dd18d16ef257ea4016da2) (11836 bytes) (mode 100644) (type blob)

#!/usr/bin/python3

import binascii
import select
import socket
import struct
import nacl.bindings
import nacl.utils

import util

from nacl.public import PrivateKey, PublicKey, Box

from exceptions import *

class SerializationError(ToxException):
    pass
class ParseError(ToxException):
    pass

def h(b):
    return binascii.hexlify(b)

class TcpPacket:
    """
    Holds generic packet to be sent via TCP
    """
    # Class variable holding callbacks: def callback(packet) will be called
    # if this packet is received
    listeners = {}

    def __init__(self):
        self.bytes = b""

    def send(self, socket=None):
        """
        Serialize and send packet via socket
        """
        serialized_b = self.serialize()
        print(binascii.hexlify(serialized_b))
        if socket is None:
            socket = self.relay.get_socket()
        socket.send(serialized_b)
        print ("sent %d bytes" % len(serialized_b))

    def encrypt(self, data):
        """
        Identity function - to be overridden
        """
        return data

    def serialize(self):
        """
        Return wire-ready serialized packet
        """
        serialized_b = self.encrypt(self.bytes)
        if len(serialized_b) >= 2**16:
            raise SerializationError('TCP packet too long: %d' % len(serialized_b))

        length_b = struct.pack(">H", len(serialized_b))
        return length_b + serialized_b

    def notify_listeners(self):
        if self.__class__ in self.listeners:
            for listener in self.listeners[self.__class__]:
                listener(self)

    @classmethod
    def register_listener(cls, listener):
        if cls not in TcpPacket.listeners:
            TcpPacket.listeners[cls] = []
        TcpPacket.listeners[cls].append(listener)
        print (cls, listener)


class EncryptedPacket(TcpPacket):
    def __init__(self, relay):
        """
        Relay needed for decrypting the incoming packet
        """
        self.relay = relay

    def encrypt(self, payload):
        """
        Encrypt with outgoing_public_key, sign with relay_private_key
        """
        relay = self.relay
        combined = nacl.bindings.crypto_box_beforenm(bytes(relay.get_outgoing_public_key()), bytes(relay.get_private_key()))
        nonce = relay.get_outgoing_nonce()
        print ("combined key", binascii.hexlify(combined))
        print ("outgoing nonce", h(nonce))
        encrypted = nacl.bindings.crypto_box_afternm(payload, nonce, combined)
        return encrypted

class TcpPingPacket(EncryptedPacket):
    """
    Ping packet (0x04)
    """
    packet_id = 0x04
    def __init__(self, relay=None):
        super().__init__(relay)
        self.bytes = b"\x04puretoxc"

    @classmethod
    def from_bytes(cls, packet_bytes):
        self = cls()
        assert (packet_bytes[0] == self.packet_id), "First byte not equal to packet id"
        self.bytes = packet_bytes
        self.ping_message = packet_bytes[1:]

        return self

class TcpPongPacket(EncryptedPacket):
    """
    Pong packet (0x05)
    """
    packet_id = 0x05
    def __init__(self, relay=None):
        super().__init__(relay)

    @classmethod
    def from_bytes(cls, packet_bytes):
        self = TcpPongPacket()
        assert (packet_bytes[0] == self.packet_id), "First byte does not equal to packet id"
        self.pong_message = packet_bytes[1:]

        self.bytes = packet_bytes

        return self

    def __repr__(self):
        return "<TcpPongPacket(%s)>" % self.pong_message

class TcpRoutingRequestPacket(EncryptedPacket):
    """
    Routing request (0x00)
    """
    packet_id = 0x00
    def __init__(self, relay=None):
        super().__init__(relay)
        

class TcpClientHelloPacket(TcpPacket):
    """
    The TCP packet sent just after opening the connection
    """
    def __init__(self, relay, session, identity):
        # The nonce used in hello packet only
        self.nonce = bytes(session.get_nonce())

        self.bytes = b""
        self.bytes += bytes(session.get_public_key())
        self.bytes += self.nonce
        self.bytes += self.get_payload(relay, session, identity)

        self.relay = relay

    def get_payload(self, relay, session, identity):
        payload = bytes(relay.get_incoming_public_key())
        payload += bytes(relay.get_outgoing_nonce_without_incrementing())

        combined = nacl.bindings.crypto_box_beforenm(bytes(relay.get_dht_public_key()), bytes(session.get_private_key()))
        print ("combined key", binascii.hexlify(combined))
        print ("relay incoming nonce", h(relay.get_outgoing_nonce_without_incrementing()))
        print ("self nonce", h(self.nonce))
        encrypted = nacl.bindings.crypto_box_afternm(payload, self.nonce, combined)
        print(encrypted)

        return bytes(encrypted)

    def serialize(self):
        """
        The hello packet doesn't add length header
        """
        return self.bytes

class TcpServerHelloPacket(TcpPacket):
    """
    TCP packet received in response to TcpClientHelloPacket
    """
    def parse(self, blob):
        if len(blob) != 24+72:
            raise ParseError("Packet length %d instead of %d" % (len(blob), 24+72))

        self.nonce = blob[:24]
        self.encrypted_payload = blob[24:]

    def decrypt_payload(self, relay, session):
        combined = nacl.bindings.crypto_box_beforenm(bytes(relay.get_dht_public_key()), bytes(session.get_private_key()))
        plaintext = nacl.bindings.crypto_box_open_afternm(self.encrypted_payload, self.nonce, combined)
        if len(plaintext) != 32+24:
            raise ParseError("Plaintext length %d instead of %d" % (len(plaintext), 32+24))
        self.relay_public_key = plaintext[:32]
        self.base_nonce = plaintext[32:32+24]
        


class TcpRelay:
    """
    Represents a connection to TCP relay
    """
    def __init__(self, hostname, port, public_key):
        """
        public_key is a hex-encoded string containing relay's public DHT key
        """
        self.hostname = hostname
        self.port = int(port)
        self.dht_public_key = PublicKey(binascii.unhexlify(public_key))
        self.tcp_incoming_nonce = None
        # temporary key that will be used for encryption during the connection and will be discarded after
        self.private_key = PrivateKey.generate()

        self.tcp_outgoing_nonce = nacl.utils.random(Box.NONCE_SIZE)
        # a temporary public key tied to this connection
        self.outgoing_public_key = None

        # The socked used to connect to this relay
        self._socket = None

    def connect(self):
        """
        Establish a TCP connection
        """
        self._socket = socket.socket()
        self._socket.connect((self.hostname, self.port))
        return self._socket

    def get_socket(self):
        """
        Return a socket to TCP relay, connect if necessary
        """
        if self._socket is not None:
            return self._socket
        return self.connect()

    def get_client_hello_packet(self, session, identity):
        return TcpClientHelloPacket(self, session, identity)

    def get_incoming_nonce(self):
        """
        Return nonce for decrypting incoming packets (TCP relay -> me), incrementing the nonce
        """
        nonce = self.tcp_incoming_nonce
        self.increment_incoming_nonce()
        return nonce

    def increment_incoming_nonce(self):
        self.tcp_incoming_nonce = util.increment_nonce(self.tcp_incoming_nonce)

    def get_incoming_public_key(self):
        return self.private_key.public_key

    def get_private_key(self):
        return self.private_key

    def get_dht_public_key(self):
        """
        Return relay's DHT public key
        """
        return self.dht_public_key

    def set_incoming_nonce(self, nonce):
        self.tcp_incoming_nonce = nonce

    def set_outgoing_public_key(self, key):
        """
        Set the ephemeral relay's public key with which this connection is encrypted
        """
        self.tcp_outgoing_public_key = PublicKey(key)

    def get_outgoing_nonce(self):
        """
        Return the nonce used for encrypting outgoing packets (me -> TCP relay)
        Increments the nonce.
        """
        rv = self.tcp_outgoing_nonce
        self.increment_outgoing_nonce()
        return rv

    def get_outgoing_nonce_without_incrementing(self):
        return self.tcp_outgoing_nonce

    def increment_outgoing_nonce(self):
        self.tcp_outgoing_nonce = util.increment_nonce(self.tcp_outgoing_nonce)

    def get_outgoing_public_key(self):
        return self.tcp_outgoing_public_key

    def negotiate_forward_secret_keys(self, session, identity):
        """
        Given our session (DHT key) and identity (long term key), send
        hello packets to the TCP relay and agree on nonce and ephemeral
        keys.
        This is a high-level function.
        """
        hello_packet = self.get_client_hello_packet(session, identity)
        hello_packet.send()

        sock = self.get_socket()
        received = sock.recv(4096)

        packet = TcpServerHelloPacket()
        packet.parse(received)
        packet.decrypt_payload(self, session)
        print ("relay key", h(packet.relay_public_key))
        print ("base nonce", h(packet.base_nonce))
        self.set_incoming_nonce(packet.base_nonce)
        self.set_outgoing_public_key(packet.relay_public_key)
        self._socket.setblocking(0)

class IncomingStreamHandler:
    """
    Split incoming TCP stream into packets
    """
    def __init__(self, relay):
        """
        sock - tcp socket to read from
        relay - TcpRelay instance needed for decryption
        """
        self.relay = relay
        self.socket = relay.get_socket()
        self.buf = b""
        self.packet_types = {}
        self.build_packet_types_table()

    def tick(self):
        """
        To be called in a loop. Reads bytes from the stream, parses them
        """
        ready_to_read, _, _ = select.select([self.socket], [], [], 0)
        if self.socket not in ready_to_read:
            return

        self.buf += self.socket.recv(4096)

        packets = []
        while True:
            packet_ciphertext = self.try_build_packet()
            if not packet_ciphertext:
                break

            packet_cleartext = self.decrypt(packet_ciphertext)
            packet = self.parse_packet(packet_cleartext)
            if packet:
                packets.append(packet)

        return packets
            
    def decrypt(self, ciphertext):
        combined = nacl.bindings.crypto_box_beforenm(bytes(self.relay.get_outgoing_public_key()), bytes(self.relay.get_private_key()))
        nonce = self.relay.get_incoming_nonce()
        print ("IncomingStreamHandler.decrypt:")
        print ("combined key", binascii.hexlify(combined))
        print ("incoming nonce", h(nonce))
        plaintext = nacl.bindings.crypto_box_open_afternm(ciphertext, nonce, combined)
        
        return plaintext

    def try_build_packet(self):
        length = int.from_bytes(self.buf[:2], byteorder='big')
        try:
            packet = self.buf[2:2+length]
            self.buf = self.buf[2+length:]
            return packet
        except IndexError:
            return None

    def build_packet_types_table(self):
        for cls in EncryptedPacket.__subclasses__():
            self.packet_types[cls.packet_id] = cls

        print (self.packet_types)

    def parse_packet(self, packet_bytes):
        print (packet_bytes)
        packet_id = packet_bytes[0]
        if packet_id in self.packet_types:
            packet = self.packet_types[packet_id].from_bytes(packet_bytes)
            packet.notify_listeners()

            return packet
        else:
            print("Unknown packet type %d" % packet_id)


Mode Type Size Ref File
100755 blob 1521 99a0c93e02ec61a69bfe227726e31684885d6f36 crap.py
100644 blob 63 2b8db81f939b6621684a6fa5d7d00477a7de401a exceptions.py
100644 blob 523 fe0c50cb32adfaf523a254ee49d73a2a16cb91bb identity.py
100644 blob 594 ef6304573432cb08f54ea4e0df4dbee00affab83 session.py
100644 blob 11836 0b4efe4dcdf1263c9f2dd18d16ef257ea4016da2 tcp.py
100644 blob 229 12c1beccb7852f14fe670785de765cd7e923eacc util.py
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/gdr/PurePyTox

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/gdr/PurePyTox

Clone this repository using git:
git clone git://git.rocketgit.com/user/gdr/PurePyTox

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main