Implemented links

This commit is contained in:
Mark Qvist 2018-04-16 17:13:39 +02:00
parent 52968e8ba5
commit 75e0cb039d
9 changed files with 296 additions and 36 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.DS_Store .DS_Store
*.pyc *.pyc
t.py t.py
t2.py
TODO TODO

View File

@ -1,8 +1,8 @@
header types header types
----------------- -----------------
type 1 00 One byte header, one 10 byte address field type 1 00 Two byte header, one 10 byte address field
type 2 01 One byte header, two 10 byte address fields type 2 01 Two byte header, two 10 byte address fields
type 3 10 Reserved type 3 10 Two byte header, one 10 byte address field, used for link request proofs
type 4 11 Reserved for extended header format type 4 11 Reserved for extended header format

View File

@ -9,6 +9,11 @@ from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
class Callbacks:
def __init__(self):
self.link_established = None
self.packet = None
self.proof = None
class Destination: class Destination:
KEYSIZE = RNS.Identity.KEYSIZE; KEYSIZE = RNS.Identity.KEYSIZE;
@ -59,11 +64,14 @@ class Destination:
if "." in app_name: raise ValueError("Dots can't be used in app names") if "." in app_name: raise ValueError("Dots can't be used in app names")
if not type in Destination.types: raise ValueError("Unknown destination type") if not type in Destination.types: raise ValueError("Unknown destination type")
if not direction in Destination.directions: raise ValueError("Unknown destination direction") if not direction in Destination.directions: raise ValueError("Unknown destination direction")
self.callbacks = Callbacks()
self.type = type self.type = type
self.direction = direction self.direction = direction
self.proof_strategy = Destination.PROVE_NONE self.proof_strategy = Destination.PROVE_NONE
self.mtu = 0 self.mtu = 0
self.links = []
if identity != None and type == Destination.SINGLE: if identity != None and type == Destination.SINGLE:
aspects = aspects+(identity.hexhash,) aspects = aspects+(identity.hexhash,)
@ -87,11 +95,14 @@ class Destination:
return "<"+self.name+"/"+self.hexhash+">" return "<"+self.name+"/"+self.hexhash+">"
def setCallback(self, callback): def link_established_callback(self, callback):
self.callback = callback self.callbacks.link_established = callback
def setProofCallback(self, callback): def packet_callback(self, callback):
self.proofcallback = callback self.callbacks.packet = callback
def proof_callback(self, callback):
self.callbacks.proof = callback
def setProofStrategy(self, proof_strategy): def setProofStrategy(self, proof_strategy):
if not proof_strategy in Destination.proof_strategies: if not proof_strategy in Destination.proof_strategies:
@ -101,9 +112,19 @@ class Destination:
def receive(self, packet): def receive(self, packet):
plaintext = self.decrypt(packet.data) plaintext = self.decrypt(packet.data)
if plaintext != None and self.callback != None: if plaintext != None:
self.callback(plaintext, packet) if packet.packet_type == RNS.Packet.LINKREQUEST:
self.incomingLinkRequest(plaintext, packet)
if packet.packet_type == RNS.Packet.RESOURCE:
if self.callbacks.packet != None:
self.callbacks.packet(plaintext, packet)
def incomingLinkRequest(self, data, packet):
link = RNS.Link.validateRequest(self, data, packet)
if link != None:
RNS.log(str(self)+" accepted link request", RNS.LOG_DEBUG)
self.links.append(link)
def createKeys(self): def createKeys(self):
if self.type == Destination.PLAIN: if self.type == Destination.PLAIN:

View File

@ -14,9 +14,9 @@ from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.hazmat.primitives.asymmetric import padding
class Identity: class Identity:
# Configure key size #KEYSIZE = 1536
KEYSIZE = 1536 KEYSIZE = 1024
DERKEYSIZE = 1808 DERKEYSIZE = KEYSIZE+272
# Padding size, not configurable # Padding size, not configurable
PADDINGSIZE= 336 PADDINGSIZE= 336
@ -223,7 +223,7 @@ class Identity:
) )
) )
except: except:
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed") RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE)
return plaintext; return plaintext;
else: else:

174
RNS/Link.py Normal file
View File

@ -0,0 +1,174 @@
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.fernet import Fernet
import base64
import RNS
import traceback
class LinkCallbacks:
def __init__(self):
self.link_established = None
self.packet = None
self.resource_started = None
self.resource_completed = None
class Link:
CURVE = ec.SECP256R1()
ECPUBSIZE = 91
PENDING = 0x00
ACTIVE = 0x01
@staticmethod
def validateRequest(owner, data, packet):
if len(data) == (Link.ECPUBSIZE):
try:
link = Link(owner = owner, peer_pub_bytes = data[:Link.ECPUBSIZE])
link.setLinkID(packet)
RNS.log("Validating link request "+RNS.prettyhexrep(link.link_id), RNS.LOG_VERBOSE)
link.handshake()
link.attached_interface = packet.receiving_interface
link.prove()
RNS.Transport.registerLink(link)
if link.owner.callbacks.link_established != None:
link.owner.callbacks.link_established(link)
RNS.log("Incoming link request "+str(link)+" accepted", RNS.LOG_VERBOSE)
except Exception as e:
RNS.log("Validating link request failed", RNS.LOG_VERBOSE)
return None
else:
RNS.log("Invalid link request payload size, dropping request", RNS.LOG_VERBOSE)
return None
def __init__(self, destination=None, owner=None, peer_pub_bytes = None):
self.callbacks = LinkCallbacks()
self.status = Link.PENDING
self.type = RNS.Destination.LINK
self.owner = owner
self.destination = destination
self.attached_interface = None
if self.destination == None:
self.initiator = False
else:
self.initiator = True
self.prv = ec.generate_private_key(Link.CURVE, default_backend())
self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.DER,
format=serialization.PublicFormat.SubjectPublicKeyInfo
)
if peer_pub_bytes == None:
self.peer_pub = None
self.peer_pub_bytes = None
else:
self.loadPeer(peer_pub_bytes)
if (self.initiator):
self.request_data = self.pub_bytes
self.packet = RNS.Packet(destination, self.request_data, packet_type=RNS.Packet.LINKREQUEST)
self.packet.pack()
self.setLinkID(self.packet)
RNS.Transport.registerLink(self)
self.packet.send()
RNS.log("Link request "+RNS.prettyhexrep(self.link_id)+" sent to "+str(self.destination), RNS.LOG_VERBOSE)
def loadPeer(self, peer_pub_bytes):
self.peer_pub_bytes = peer_pub_bytes
self.peer_pub = serialization.load_der_public_key(peer_pub_bytes, backend=default_backend())
self.peer_pub.curce = Link.CURVE
def setLinkID(self, packet):
self.link_id = RNS.Identity.truncatedHash(packet.raw)
self.hash = self.link_id
def handshake(self):
self.shared_key = self.prv.exchange(ec.ECDH(), self.peer_pub)
self.derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=self.getSalt(),
info=self.getContext(),
backend=default_backend()
).derive(self.shared_key)
def prove(self):
signed_data = self.link_id+self.pub_bytes
signature = self.owner.identity.sign(signed_data)
proof_data = self.pub_bytes+signature
proof = RNS.Packet(self, proof_data, packet_type=RNS.Packet.PROOF, header_type=RNS.Packet.HEADER_3)
proof.send()
def validateProof(self, packet):
peer_pub_bytes = packet.data[:Link.ECPUBSIZE]
signed_data = self.link_id+peer_pub_bytes
signature = packet.data[Link.ECPUBSIZE:RNS.Identity.KEYSIZE/8+Link.ECPUBSIZE]
if self.destination.identity.validate(signature, signed_data):
self.loadPeer(peer_pub_bytes)
self.handshake()
self.attached_interface = packet.receiving_interface
RNS.Transport.activateLink(self)
if self.callbacks.link_established != None:
self.callbacks.link_established(self)
RNS.log("Link "+str(self)+" established with "+str(self.destination), RNS.LOG_VERBOSE)
else:
RNS.log("Invalid link proof signature received by "+str(self), RNS.LOG_VERBOSE)
def getSalt(self):
return self.link_id
def getContext(self):
return None
def receive(self, packet):
if packet.receiving_interface != self.attached_interface:
RNS.log("Link-associated packet received on unexpected interface! Someone might be trying to manipulate your communication!", RNS.LOG_ERROR)
else:
plaintext = self.decrypt(packet.data)
if (self.callbacks.packet != None):
self.callbacks.packet(plaintext, packet)
def encrypt(self, plaintext):
try:
fernet = Fernet(base64.urlsafe_b64encode(self.derived_key))
ciphertext = base64.urlsafe_b64decode(fernet.encrypt(plaintext))
return ciphertext
except Exception as e:
RNS.log("Encryption on link "+str(self)+" failed. The contained exception was: "+str(e), RNS.LOG_ERROR)
def decrypt(self, ciphertext):
try:
fernet = Fernet(base64.urlsafe_b64encode(self.derived_key))
plaintext = fernet.decrypt(base64.urlsafe_b64encode(ciphertext))
return plaintext
except Exception as e:
RNS.log("Decryption failed on link "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
def link_established_callback(self, callback):
self.callbacks.link_established = callback
def packet_callback(self, callback):
self.callbacks.packet = callback
def resource_started_callback(self, callback):
self.callbacks.resource_started = callback
def resource_completed_callback(self, callback):
self.callbacks.resource_completed = callback
def __str__(self):
return RNS.prettyhexrep(self.link_id)

View File

@ -12,7 +12,7 @@ class Packet:
HEADER_1 = 0x00; # Normal header format HEADER_1 = 0x00; # Normal header format
HEADER_2 = 0x01; # Header format used for link packets in transport HEADER_2 = 0x01; # Header format used for link packets in transport
HEADER_3 = 0x02; # Reserved HEADER_3 = 0x02; # Normal header format, but used to indicate a link request proof
HEADER_4 = 0x03; # Reserved HEADER_4 = 0x03; # Reserved
header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4] header_types = [HEADER_1, HEADER_2, HEADER_3, HEADER_4]
@ -30,7 +30,7 @@ class Packet:
self.transport_id = transport_id self.transport_id = transport_id
self.data = data self.data = data
self.flags = self.getPackedFlags() self.flags = self.getPackedFlags()
self.MTU = self.destination.MTU self.MTU = RNS.Reticulum.MTU
self.raw = None self.raw = None
self.packed = False self.packed = False
@ -45,6 +45,9 @@ class Packet:
self.packet_hash = None self.packet_hash = None
def getPackedFlags(self): def getPackedFlags(self):
if self.header_type == Packet.HEADER_3:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | RNS.Destination.LINK | self.packet_type
else:
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
return packed_flags return packed_flags
@ -58,11 +61,15 @@ class Packet:
else: else:
raise IOError("Packet with header type 2 must have a transport ID") raise IOError("Packet with header type 2 must have a transport ID")
if self.header_type == Packet.HEADER_1:
self.header += self.destination.hash self.header += self.destination.hash
if self.packet_type != Packet.ANNOUNCE: if self.packet_type != Packet.ANNOUNCE:
self.ciphertext = self.destination.encrypt(self.data) self.ciphertext = self.destination.encrypt(self.data)
else: else:
self.ciphertext = self.data self.ciphertext = self.data
if self.header_type == Packet.HEADER_3:
self.header += self.destination.link_id
self.ciphertext = self.data
self.raw = self.header + self.ciphertext self.raw = self.header + self.ciphertext
@ -93,9 +100,10 @@ class Packet:
def send(self): def send(self):
if not self.sent: if not self.sent:
if not self.packed:
self.pack() self.pack()
#RNS.log("Size: "+str(len(self.raw))+" header is "+str(len(self.header))+" payload is "+str(len(self.ciphertext)), RNS.LOG_DEBUG)
RNS.Transport.outbound(self.raw) RNS.Transport.outbound(self)
self.packet_hash = RNS.Identity.fullHash(self.raw) self.packet_hash = RNS.Identity.fullHash(self.raw)
self.sent_at = time.time() self.sent_at = time.time()
self.sent = True self.sent = True

View File

@ -8,7 +8,7 @@ import os.path
import os import os
import RNS import RNS
import traceback #import traceback
class Reticulum: class Reticulum:
MTU = 500 MTU = 500

View File

@ -10,15 +10,23 @@ class Transport:
interfaces = [] interfaces = []
destinations = [] destinations = []
pending_links = []
active_links = []
packet_hashlist = [] packet_hashlist = []
@staticmethod @staticmethod
def outbound(raw): def outbound(packet):
Transport.cacheRaw(raw) Transport.cacheRaw(packet.raw)
for interface in Transport.interfaces: for interface in Transport.interfaces:
if interface.OUT: if interface.OUT:
RNS.log("Transmitting "+str(len(raw))+" bytes via: "+str(interface), RNS.LOG_DEBUG) should_transmit = True
interface.processOutgoing(raw) if packet.destination.type == RNS.Destination.LINK:
if interface != packet.destination.attached_interface:
should_transmit = False
if should_transmit:
RNS.log("Transmitting "+str(len(packet.raw))+" bytes via: "+str(interface), RNS.LOG_DEBUG)
interface.processOutgoing(packet.raw)
@staticmethod @staticmethod
def inbound(raw, interface=None): def inbound(raw, interface=None):
@ -30,12 +38,26 @@ class Transport:
packet = RNS.Packet(None, raw) packet = RNS.Packet(None, raw)
packet.unpack() packet.unpack()
packet.packet_hash = packet_hash packet.packet_hash = packet_hash
packet.receiving_interface = interface
if packet.packet_type == RNS.Packet.ANNOUNCE: if packet.packet_type == RNS.Packet.ANNOUNCE:
if RNS.Identity.validateAnnounce(packet): if RNS.Identity.validateAnnounce(packet):
Transport.cache(packet) Transport.cache(packet)
if packet.packet_type == RNS.Packet.LINKREQUEST:
for destination in Transport.destinations:
if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
packet.destination = destination
destination.receive(packet)
Transport.cache(packet)
if packet.packet_type == RNS.Packet.RESOURCE: if packet.packet_type == RNS.Packet.RESOURCE:
if packet.destination_type == RNS.Destination.LINK:
for link in Transport.active_links:
if link.link_id == packet.destination_hash:
link.receive(packet)
Transport.cache(packet)
else:
for destination in Transport.destinations: for destination in Transport.destinations:
if destination.hash == packet.destination_hash and destination.type == packet.destination_type: if destination.hash == packet.destination_hash and destination.type == packet.destination_type:
packet.destination = destination packet.destination = destination
@ -43,6 +65,13 @@ class Transport:
Transport.cache(packet) Transport.cache(packet)
if packet.packet_type == RNS.Packet.PROOF: if packet.packet_type == RNS.Packet.PROOF:
if packet.header_type == RNS.Packet.HEADER_3:
# This is a link request proof, forward
# to a waiting link request
for link in Transport.pending_links:
if link.link_id == packet.destination_hash:
link.validateProof(packet)
else:
for destination in Transport.destinations: for destination in Transport.destinations:
if destination.hash == packet.destination_hash: if destination.hash == packet.destination_hash:
if destination.proofcallback != None: if destination.proofcallback != None:
@ -55,8 +84,34 @@ class Transport:
if destination.direction == RNS.Destination.IN: if destination.direction == RNS.Destination.IN:
Transport.destinations.append(destination) Transport.destinations.append(destination)
@staticmethod
def registerLink(link):
RNS.log("Registering link "+str(link))
if link.initiator:
Transport.pending_links.append(link)
else:
Transport.active_links.append(link)
@staticmethod
def activateLink(link):
RNS.log("Activating link "+str(link))
if link in Transport.pending_links:
Transport.pending_links.remove(link)
Transport.active_links.append(link)
link.status = RNS.Link.ACTIVE
else:
RNS.log("Attempted to activate a link that was not in the pending table", RNS.LOG_ERROR)
@staticmethod
def shouldCache(packet):
# TODO: Implement sensible rules for which
# packets to cache
return False
@staticmethod @staticmethod
def cache(packet): def cache(packet):
if RNS.Transport.shouldCache(packet):
RNS.Transport.cacheRaw(packet.raw) RNS.Transport.cacheRaw(packet.raw)
@staticmethod @staticmethod

View File

@ -4,6 +4,7 @@ import time
from .Reticulum import Reticulum from .Reticulum import Reticulum
from .Identity import Identity from .Identity import Identity
from .Link import Link
from .Transport import Transport from .Transport import Transport
from .Destination import Destination from .Destination import Destination
from .Packet import Packet from .Packet import Packet