From d4be4078216b095873968f983202b112866e52d9 Mon Sep 17 00:00:00 2001 From: Mark Qvist Date: Tue, 20 Mar 2018 12:32:41 +0100 Subject: [PATCH] Echo example --- FPE/Destination.py | 37 ++++-- FPE/FlexPE.py | 33 ++--- FPE/Identity.py | 60 ++++++--- FPE/Interfaces/UdpInterface.py | 9 +- FPE/Packet.py | 35 +++++- FPE/Transport.py | 44 ++++++- FPE/Utilities/Echo.py | 222 +++++++++++++++++++++++++++++++++ FPE/__init__.py | 5 + t.py | 3 +- 9 files changed, 386 insertions(+), 62 deletions(-) create mode 100644 FPE/Utilities/Echo.py diff --git a/FPE/Destination.py b/FPE/Destination.py index e40e174..160a448 100755 --- a/FPE/Destination.py +++ b/FPE/Destination.py @@ -15,12 +15,17 @@ class Destination: PADDINGSIZE= FPE.Identity.PADDINGSIZE; # Constants - SINGLE = 0x00; - GROUP = 0x01; - PLAIN = 0x02; - LINK = 0x03; + SINGLE = 0x00 + GROUP = 0x01 + PLAIN = 0x02 + LINK = 0x03 types = [SINGLE, GROUP, PLAIN, LINK] + PROVE_NONE = 0x21 + PROVE_APP = 0x22 + PROVE_ALL = 0x23 + proof_strategies = [PROVE_NONE, PROVE_APP, PROVE_ALL] + IN = 0x11; OUT = 0x12; directions = [IN, OUT] @@ -56,20 +61,24 @@ class Destination: if not direction in Destination.directions: raise ValueError("Unknown destination direction") self.type = type self.direction = direction + self.proof_strategy = Destination.PROVE_NONE self.mtu = 0 - if identity == None: + if identity != None and type == Destination.SINGLE: + aspects = aspects+(identity.hexhash,) + + if identity == None and direction == Destination.IN: identity = Identity() - identity.createKeys() + aspects = aspects+(identity.hexhash,) self.identity = identity - aspects = aspects+(identity.hexhash,) self.name = Destination.getDestinationName(app_name, *aspects) self.hash = Destination.getDestinationHash(app_name, *aspects) self.hexhash = self.hash.encode("hex_codec") self.callback = None + self.proofcallback = None FPE.Transport.registerDestination(self) @@ -81,11 +90,19 @@ class Destination: def setCallback(self, callback): self.callback = callback + def setProofCallback(self, callback): + self.proofcallback = callback - def receive(self, data): - plaintext = self.decrypt(data) + def setProofStrategy(self, proof_strategy): + if not proof_strategy in Destination.proof_strategies: + raise TypeError("Unsupported proof strategy") + else: + self.proof_strategy = proof_strategy + + def receive(self, packet): + plaintext = self.decrypt(packet.data) if plaintext != None and self.callback != None: - self.callback(plaintext, self) + self.callback(plaintext, packet) def createKeys(self): diff --git a/FPE/FlexPE.py b/FPE/FlexPE.py index 698081c..93c9828 100755 --- a/FPE/FlexPE.py +++ b/FPE/FlexPE.py @@ -1,7 +1,7 @@ from Interfaces import * import ConfigParser -import jsonpickle from vendor.configobj import ConfigObj +import atexit import struct import array import os.path @@ -13,16 +13,12 @@ class FlexPE: MTU = 500 router = None config = None - destinations = [] - interfaces = [] + configdir = os.path.expanduser("~")+"/.flexpe" configpath = "" storagepath = "" cachepath = "" - # TODO: Move this to Transport - packetlist = [] - def __init__(self,configdir=None): if configdir != None: FlexPE.configdir = configdir @@ -48,23 +44,14 @@ class FlexPE: FPE.Identity.loadKnownDestinations() FlexPE.router = self - @staticmethod - def addDestination(destination): - destination.MTU = FlexPE.MTU - FlexPE.destinations.append(destination) - - @staticmethod - def outbound(raw): - for interface in FlexPE.interfaces: - if interface.OUT: - FPE.log("Transmitting via: "+str(interface), FPE.LOG_DEBUG) - interface.processOutgoing(raw) + atexit.register(FPE.Identity.exitHandler) def applyConfig(self): - for option in self.config["logging"]: - value = self.config["logging"][option] - if option == "loglevel": - FPE.loglevel = int(value) + if "logging" in self.config: + for option in self.config["logging"]: + value = self.config["logging"][option] + if option == "loglevel": + FPE.loglevel = int(value) for name in self.config["interfaces"]: c = self.config["interfaces"][name] @@ -82,7 +69,7 @@ class FlexPE: interface.OUT = True interface.name = name - FlexPE.interfaces.append(interface) + FPE.Transport.interfaces.append(interface) if c["type"] == "SerialInterface": interface = SerialInterface.SerialInterface( @@ -98,7 +85,7 @@ class FlexPE: interface.OUT = True interface.name = name - FlexPE.interfaces.append(interface) + FPE.Transport.interfaces.append(interface) except Exception as e: FPE.log("The interface \""+name+"\" could not be created. Check your configuration file for errors!", FPE.LOG_ERROR) diff --git a/FPE/Identity.py b/FPE/Identity.py index 33f7d8d..87e0f90 100644 --- a/FPE/Identity.py +++ b/FPE/Identity.py @@ -37,13 +37,23 @@ class Identity: self.createKeys() @staticmethod - def remember(hash, public_key, app_data = None): - FPE.log("Remembering "+FPE.hexrep(hash, False), FPE.LOG_VERBOSE) - Identity.known_destinations[hash] = [time.time(), public_key, app_data] + def remember(packet_hash, destination_hash, public_key, app_data = None): + FPE.log("Remembering "+FPE.prettyhexrep(destination_hash), FPE.LOG_VERBOSE) + Identity.known_destinations[destination_hash] = [time.time(), packet_hash, public_key, app_data] + @staticmethod - def recall(identity): - pass + def recall(destination_hash): + FPE.log("Searching for "+FPE.prettyhexrep(destination_hash)+"...", FPE.LOG_DEBUG) + if destination_hash in Identity.known_destinations: + identity_data = Identity.known_destinations[destination_hash] + identity = Identity(public_only=True) + identity.loadPublicKey(identity_data[2]) + FPE.log("Found "+FPE.prettyhexrep(destination_hash)+" in known destinations", FPE.LOG_DEBUG) + return identity + else: + FPE.log("Could not find "+FPE.prettyhexrep(destination_hash)+" in known destinations", FPE.LOG_DEBUG) + return None @staticmethod def saveKnownDestinations(): @@ -80,7 +90,7 @@ class Identity: @staticmethod def validateAnnounce(packet): if packet.packet_type == FPE.Packet.ANNOUNCE: - FPE.log("Validating announce from "+FPE.hexrep(packet.destination_hash), FPE.LOG_VERBOSE) + FPE.log("Validating announce from "+FPE.prettyhexrep(packet.destination_hash), FPE.LOG_VERBOSE) destination_hash = packet.destination_hash public_key = packet.data[10:Identity.DERKEYSIZE/8+10] random_hash = packet.data[Identity.DERKEYSIZE/8+10:Identity.DERKEYSIZE/8+20] @@ -96,11 +106,19 @@ class Identity: if announced_identity.validate(signature, signed_data): FPE.log("Announce is valid", FPE.LOG_VERBOSE) - FPE.Identity.remember(destination_hash, public_key) + FPE.Identity.remember(FPE.Identity.fullHash(packet.raw), destination_hash, public_key) + FPE.log("Stored valid announce from "+FPE.prettyhexrep(destination_hash), FPE.LOG_INFO) + del announced_identity + return True else: FPE.log("Announce is invalid", FPE.LOG_VERBOSE) + del announced_identity + return False + + @staticmethod + def exitHandler(): + Identity.saveKnownDestinations() - del announced_identity def createKeys(self): self.prv = rsa.generate_private_key( @@ -119,10 +137,9 @@ class Identity: format=serialization.PublicFormat.SubjectPublicKeyInfo ) - self.hash = Identity.truncatedHash(self.pub_bytes) - self.hexhash = self.hash.encode("hex_codec") + self.updateHashes() - FPE.log("Identity keys created, private length is "+str(len(self.prv_bytes))+" public length is "+str(len(self.pub_bytes)), FPE.LOG_INFO) + FPE.log("Identity keys created for "+FPE.prettyhexrep(self.hash), FPE.LOG_INFO) def getPrivateKey(self): return self.prv_bytes @@ -138,10 +155,16 @@ class Identity: encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo ) + self.updateHashes() def loadPublicKey(self, key): self.pub_bytes = key self.pub = load_der_public_key(self.pub_bytes, backend=default_backend()) + self.updateHashes() + + def updateHashes(self): + self.hash = Identity.truncatedHash(self.pub_bytes) + self.hexhash = self.hash.encode("hex_codec") def saveIdentity(self): pass @@ -150,7 +173,7 @@ class Identity: pass def encrypt(self, plaintext): - if self.prv != None: + if self.pub != None: chunksize = (Identity.KEYSIZE-Identity.PADDINGSIZE)/8 chunks = int(math.ceil(len(plaintext)/(float(chunksize)))) # TODO: Remove debug output print("Plaintext size is "+str(len(plaintext))+", with "+str(chunks)+" chunks") @@ -175,7 +198,7 @@ class Identity: # TODO: Remove debug output print("Plaintext encrypted, ciphertext length is "+str(len(ciphertext))+" bytes.") return ciphertext else: - raise KeyError("Encryption failed because identity does not hold a private key") + raise KeyError("Encryption failed because identity does not hold a public key") def decrypt(self, ciphertext): @@ -238,11 +261,12 @@ class Identity: else: raise KeyError("Signature validation failed because identity does not hold a public key") + def prove(self, packet, destination): + proof_data = packet.packet_hash + self.sign(packet.packet_hash) + proof = FPE.Packet(destination, proof_data, FPE.Packet.PROOF) + proof.send() + + def getRandomHash(self): return self.truncatedHash(os.urandom(10)) - -def identityExithandler(): - Identity.saveKnownDestinations() - -atexit.register(identityExithandler) \ No newline at end of file diff --git a/FPE/Interfaces/UdpInterface.py b/FPE/Interfaces/UdpInterface.py index bb29ad9..52472d2 100755 --- a/FPE/Interfaces/UdpInterface.py +++ b/FPE/Interfaces/UdpInterface.py @@ -3,6 +3,7 @@ import SocketServer import threading import socket import sys +import FPE class UdpInterface(Interface): bind_ip = None @@ -36,9 +37,13 @@ class UdpInterface(Interface): def processIncoming(self, data): + # TODO: remove + #FPE.log("IN: "+FPE.prettyhexrep(data)) self.owner.inbound(data) def processOutgoing(self,data): + # TODO: remove + #FPE.log("OUT: "+FPE.prettyhexrep(" "+data)) udp_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) udp_socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) udp_socket.sendto(data, (self.forward_ip, self.forward_port)) @@ -52,5 +57,7 @@ class UdpInterfaceHandler(SocketServer.BaseRequestHandler): def handle(self): if (UdpInterfaceHandler.interface != None): - data = self.request[0].strip() + # TODO: remove + #FPE.log("Datagram contents: "+FPE.prettyhexrep(self.request[0])) + data = self.request[0] UdpInterfaceHandler.interface.processIncoming(data) \ No newline at end of file diff --git a/FPE/Packet.py b/FPE/Packet.py index 58e3311..5c95591 100755 --- a/FPE/Packet.py +++ b/FPE/Packet.py @@ -1,4 +1,5 @@ import struct +import time import FPE class Packet: @@ -29,14 +30,19 @@ class Packet: self.transport_id = transport_id self.data = data self.flags = self.getPackedFlags() + self.MTU = self.destination.MTU self.raw = None self.packed = False self.sent = False - self.MTU = self.destination.MTU + self.fromPacked = False else: - self.raw = data - self.packed = True + self.raw = data + self.packed = True + self.fromPacked = True + + self.sent_at = None + self.packet_hash = None def getPackedFlags(self): packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type @@ -90,6 +96,8 @@ class Packet: self.pack() FPE.log("Size: "+str(len(self.raw))+" header is "+str(len(self.header))+" payload is "+str(len(self.ciphertext)), FPE.LOG_DEBUG) FPE.Transport.outbound(self.raw) + self.packet_hash = FPE.Identity.fullHash(self.raw) + self.sent_at = time.time() self.sent = True else: raise IOError("Packet was already sent") @@ -98,4 +106,23 @@ class Packet: if self.sent: Transport.outbound(self.raw) else: - raise IOError("Packet was not sent yet") \ No newline at end of file + raise IOError("Packet was not sent yet") + + def prove(self, destination): + if self.fromPacked and self.destination: + if self.destination.identity and self.destination.identity.prv: + self.destination.identity.prove(self, destination) + + def validateProofPacket(self, proof_packet): + return self.validateProof(proof_packet.data) + + def validateProof(self, proof): + proof_hash = proof[:32] + signature = proof[32:] + if proof_hash == self.packet_hash: + return self.destination.identity.validate(signature, proof_hash) + else: + return False + + + diff --git a/FPE/Transport.py b/FPE/Transport.py index f474d1d..e28cfe4 100755 --- a/FPE/Transport.py +++ b/FPE/Transport.py @@ -8,11 +8,17 @@ class Transport: TUNNEL = 0x03; types = [BROADCAST, TRANSPORT, RELAY, TUNNEL] + interfaces = [] + destinations = [] packet_hashlist = [] @staticmethod def outbound(raw): - FPE.FlexPE.outbound(raw) + Transport.cacheRaw(raw) + for interface in Transport.interfaces: + if interface.OUT: + FPE.log("Transmitting via: "+str(interface), FPE.LOG_DEBUG) + interface.processOutgoing(raw) @staticmethod def inbound(raw): @@ -22,15 +28,43 @@ class Transport: Transport.packet_hashlist.append(packet_hash) packet = FPE.Packet(None, raw) packet.unpack() + packet.packet_hash = packet_hash if packet.packet_type == FPE.Packet.ANNOUNCE: - FPE.Identity.validateAnnounce(packet) + if FPE.Identity.validateAnnounce(packet): + Transport.cache(packet) if packet.packet_type == FPE.Packet.RESOURCE: - for destination in FlexPE.destinations: + for destination in Transport.destinations: if destination.hash == packet.destination_hash and destination.type == packet.destination_type: - destination.receive(packet.data) + packet.destination = destination + destination.receive(packet) + Transport.cache(packet) + + if packet.packet_type == FPE.Packet.PROOF: + for destination in Transport.destinations: + if destination.hash == packet.destination_hash: + if destination.proofcallback != None: + destination.proofcallback(packet) + # TODO: add universal proof handling @staticmethod def registerDestination(destination): - FPE.FlexPE.addDestination(destination) \ No newline at end of file + destination.MTU = FPE.FlexPE.MTU + if destination.direction == FPE.Destination.IN: + Transport.destinations.append(destination) + + @staticmethod + def cache(packet): + FPE.Transport.cacheRaw(packet.raw) + + @staticmethod + def cacheRaw(raw): + try: + file = open(FPE.FlexPE.cachepath+"/"+FPE.hexrep(FPE.Identity.fullHash(raw), delimit=False), "w") + file.write(raw) + file.close() + FPE.log("Wrote packet "+FPE.prettyhexrep(FPE.Identity.fullHash(raw))+" to cache", FPE.LOG_DEBUG) + except Exception as e: + FPE.log("Error writing packet to cache", FPE.LOG_ERROR) + FPE.log("The contained exception was: "+str(e)) \ No newline at end of file diff --git a/FPE/Utilities/Echo.py b/FPE/Utilities/Echo.py new file mode 100644 index 0000000..ae73112 --- /dev/null +++ b/FPE/Utilities/Echo.py @@ -0,0 +1,222 @@ +import argparse +import time +import FPE + +# Let's define an app name. We'll use this for all +# destinations we create. Since this echo example +# is part of a range of example utilities, we'll put +# them all within the app namespace "example_utilities" +APP_NAME = "example_utilitites" + +# This initialisation is executed when the users chooses +# to run as a server +def server(configpath): + # We must first initialise FlexPE + fpe = FPE.FlexPE(configpath) + + # Randomly create a new identity for our echo server + server_identity = FPE.Identity() + + # We create a destination that clients can query. We want + # to be able to verify echo replies to our clients, so we + # create a "single" destination that can receive encrypted + # messages. This way the client can send a request and be + # certain that no-one else than this destination was able + # to read it. + echo_destination = FPE.Destination(server_identity, FPE.Destination.IN, FPE.Destination.SINGLE, APP_NAME, "echo", "request") + + # Tell the destination which function in our program to + # run when a packet is received. + echo_destination.setCallback(serverCallback) + + # Everything's ready! + # Let's Wait for client requests or user input + announceLoop(echo_destination) + + +def announceLoop(destination): + # Let the user know that everything is ready + FPE.log("Echo server running, hit enter to send announce (Ctrl-C to quit)") + + # We enter a loop that runs until the users exits. + # If the user just hits enter, we will announce our server + # destination on the network, which will let clients know + # how to create messages directed towards it. + while True: + entered = raw_input() + destination.announce() + FPE.log("Sent announce from "+FPE.prettyhexrep(destination.hash)) + + +def serverCallback(message, packet): + # We have received am echo request from a client! When + # a client sends a request, it will include the hash of + # it's identity in the message. Since we know that the + # client has created a listening destination using this + # identity hash, we can construct an outgoing destination + # to direct our response to. The hash is sent in binary + # format, so we encode it as printable hexadecimal first, + # since aspect names need to in printable text. + client_identity_hexhash = message.encode("hex_codec") + + # We can now create a destination that will let us reach + # the client which send the echo request. + reply_destination = FPE.Destination(None, FPE.Destination.OUT, FPE.Destination.PLAIN, APP_NAME, "echo", "reply", client_identity_hexhash) + + # Let's encode the reply destination hash in a readable + # way, so we can output some info to the user. + reply_destination_hexhash = reply_destination.hash.encode("hex_codec") + + # Tell the user that we received an echo request, and + # that we are going to send a reply to the requester. + FPE.log("Received packet from <"+reply_destination_hexhash+">, sending reply") + + # To let the client know that we got the echo request, + # we will use the "proof" functions of FlexPE. In most + # applications, the proving of packets will occur fully + # automatically, but in some cases like this, it can be + # beneficial to use the functions manually, since it + # neatly provides functionality that can unequivocally + # prove the receipt of the request to the client. + # + # Using the proof functionality is very simple, we just + # need to call the "prove" method on the packet we wish + # to prove, and specify which destination it should be + # directed to. + packet.prove(reply_destination) + + +# We need a global list to hold sent echo requests +sent_requests = [] +# This initialisation is executed when the users chooses +# to run as a client +def client(destination_hexhash, configpath): + # We need a binary representation of the destination + # hash that was entered on the command line + try: + if len(destination_hexhash) != 20: + raise ValueError("Destination length is invalid, must be 20 hexadecimal characters (10 bytes)") + destination_hash = destination_hexhash.decode("hex") + except: + FPE.log("Invalid destination entered. Check your input!") + exit() + + # We must first initialise FlexPE + fpe = FPE.FlexPE() + + # Randomly create a new identity for our echo server + client_identity = FPE.Identity() + + # Let's set up a destination for replies to our echo + # requests. This destination will be used by the server + # to direct replies to. We're going to use a "plain" + # destination, so the server can send replies back + # without knowing any public keys of the client. In this + # case, such a design is benificial, since any client + # can send echo requests directly to the server, without + # first having to announce it's destination, or include + # public keys in the echo request + # + # We will use the destination naming convention of: + # example_utilities.echo.reply. + # where the last part is a hex representation of the hash + # of our "client_identity". We need to include this to + # create a unique destination for the server to respond to. + # If we had used a "single" destination, something equivalent + # to this process would have happened automatically. + reply_destination = FPE.Destination(client_identity, FPE.Destination.IN, FPE.Destination.PLAIN, APP_NAME, "echo", "reply", client_identity.hexhash) + + # Since we are only expecting packets of the "proof" + # type to reach our reply destination, we just set the + # proof callback (and in this case not the normal + # message callback) + reply_destination.setProofCallback(clientProofCallback) + + # Tell the user that the client is ready! + FPE.log("Echo client "+FPE.prettyhexrep(reply_destination.hash)+" ready, hit enter to send echo request (Ctrl-C to quit)") + + # We enter a loop that runs until the user exits. + # If the user hits enter, we will try to send an + # echo request to the destination specified on the + # command line. + while True: + raw_input() + # To address the server, we need to know it's public + # key, so we check if FlexPE knows this destination. + # This is done by calling the "recall" method of the + # Identity module. If the destination is known, it will + # return an Identity instance that can be used in + # outgoing destinations. + server_identity = FPE.Identity.recall(destination_hash) + if server_identity != None: + # We got the correct identity instance from the + # recall method, so let's create an outgoing + # destination. We use the naming convention: + # example_utilities.echo.request + # Since this is a "single" destination, the identity + # hash will be automatically added to the end of + # the name. + request_destination = FPE.Destination(server_identity, FPE.Destination.OUT, FPE.Destination.SINGLE, APP_NAME, "echo", "request") + + # The destination is ready, so let's create a packet. + # We set the destination to the request_destination + # that was just created, and the only data we add + # is the identity hash of our client identity. + # Including that information will let the server + # create a destination to send replies to. + echo_request = FPE.Packet(request_destination, client_identity.hash) + + # Send the packet! + echo_request.send() + + # Add the request to our list of sent packets + sent_requests.append(echo_request) + + # Tell the user that the echo request was sent + FPE.log("Sent echo request to "+FPE.prettyhexrep(request_destination.hash)) + else: + # If we do not know this destination, tell the + # user to wait for an announce to arrive. + FPE.log("Destination is not yet known. Wait for an announce to arrive.") + +def clientProofCallback(proof_packet): + now = time.time() + for unproven_packet in sent_requests: + if unproven_packet.packet_hash == proof_packet.data[:32]: + if unproven_packet.validateProofPacket(proof_packet): + rtt = now - unproven_packet.sent_at + if (rtt >= 1): + rtt = round(rtt, 3) + rttstring = str(rtt)+" seconds" + else: + rtt = round(rtt*1000, 3) + rttstring = str(rtt)+" milliseconds" + FPE.log( + "Valid echo reply, proved by "+FPE.prettyhexrep(unproven_packet.destination.hash)+ + ", round-trip time was "+rttstring + ) + else: + FPE.log("Proof invalid") + + + +if __name__ == "__main__": + try: + parser = argparse.ArgumentParser(description="Simple echo server and client utility") + parser.add_argument("-s", "--server", action="store_true", help="wait for incoming packets from clients") + parser.add_argument("--config", action="store", default=None, help="path to alternative FlexPE config directory", type=str) + parser.add_argument("destination", nargs="?", default=None, help="hexadecimal hash of the server destination", type=str) + args = parser.parse_args() + + if args.server: + configarg=None + if args.config: + configarg = args.config + server(configarg) + else: + configarg=None + if args.config: + configarg = args.config + client(args.destination, configarg) + except KeyboardInterrupt: + exit() \ No newline at end of file diff --git a/FPE/__init__.py b/FPE/__init__.py index 3a92e21..7a36cc3 100755 --- a/FPE/__init__.py +++ b/FPE/__init__.py @@ -67,4 +67,9 @@ def hexrep(data, delimit=True): if not delimit: delimiter = "" hexrep = delimiter.join("{:02x}".format(ord(c)) for c in data) + return hexrep + +def prettyhexrep(data): + delimiter = "" + hexrep = "<"+delimiter.join("{:02x}".format(ord(c)) for c in data)+">" return hexrep \ No newline at end of file diff --git a/t.py b/t.py index 9014ce2..34812ab 100755 --- a/t.py +++ b/t.py @@ -17,6 +17,7 @@ fpe = FlexPE() identity = Identity() d1=Destination(identity, Destination.IN, Destination.SINGLE, "messenger", "user") +#d1.setProofStrategy(Destination.PROVE_ALL) d1.setCallback(testCallback) msg="" @@ -28,7 +29,7 @@ pl = len(d1.identity.pub_bytes) d1.announce() p1=Packet(d1, msg) -#p1.send() +p1.send() # p2=Packet(d2,"Test af msg") # p2.send()