Migrated all asymmetric crypto operations to ECIES on Curve25519.

This commit is contained in:
Mark Qvist 2021-05-20 15:31:38 +02:00
parent 7f5625a526
commit ce405b9252
10 changed files with 231 additions and 185 deletions

View File

@ -39,7 +39,7 @@ def program_setup(configpath, channel=None):
# We specify a callback that will get called every time # We specify a callback that will get called every time
# the destination receives data. # the destination receives data.
broadcast_destination.packet_callback(packet_callback) broadcast_destination.set_packet_callback(packet_callback)
# Everything's ready! # Everything's ready!
# Let's hand over control to the main loop # Let's hand over control to the main loop

View File

@ -52,7 +52,7 @@ def server(configpath):
# Tell the destination which function in our program to # Tell the destination which function in our program to
# run when a packet is received. We do this so we can # run when a packet is received. We do this so we can
# print a log message when the server receives a request # print a log message when the server receives a request
echo_destination.packet_callback(server_callback) echo_destination.set_packet_callback(server_callback)
# Everything's ready! # Everything's ready!
# Let's Wait for client requests or user input # Let's Wait for client requests or user input
@ -175,7 +175,7 @@ def client(destination_hexhash, configpath, timeout=None):
# We can then set a delivery callback on the receipt. # We can then set a delivery callback on the receipt.
# This will get automatically called when a proof for # This will get automatically called when a proof for
# this specific packet is received from the destination. # this specific packet is received from the destination.
packet_receipt.delivery_callback(packet_delivered) packet_receipt.set_delivery_callback(packet_delivered)
# Tell the user that the echo request was sent # Tell the user that the echo request was sent
RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash)) RNS.log("Sent echo request to "+RNS.prettyhexrep(request_destination.hash))
@ -189,7 +189,7 @@ def client(destination_hexhash, configpath, timeout=None):
# receives a proof packet. # receives a proof packet.
def packet_delivered(receipt): def packet_delivered(receipt):
if receipt.status == RNS.PacketReceipt.DELIVERED: if receipt.status == RNS.PacketReceipt.DELIVERED:
rtt = receipt.rtt() rtt = receipt.get_rtt()
if (rtt >= 1): if (rtt >= 1):
rtt = round(rtt, 3) rtt = round(rtt, 3)
rttstring = str(rtt)+" seconds" rttstring = str(rtt)+" seconds"

View File

@ -65,7 +65,7 @@ def server(configpath, path):
# We configure a function that will get called every time # We configure a function that will get called every time
# a new client creates a link to this destination. # a new client creates a link to this destination.
server_destination.link_established_callback(client_connected) server_destination.set_link_established_callback(client_connected)
# Everything's ready! # Everything's ready!
# Let's Wait for client requests or user input # Let's Wait for client requests or user input
@ -102,7 +102,7 @@ def client_connected(link):
if os.path.isdir(serve_path): if os.path.isdir(serve_path):
RNS.log("Client connected, sending file list...") RNS.log("Client connected, sending file list...")
link.link_closed_callback(client_disconnected) link.set_link_closed_callback(client_disconnected)
# We pack a list of files for sending in a packet # We pack a list of files for sending in a packet
data = umsgpack.packb(list_files()) data = umsgpack.packb(list_files())
@ -114,7 +114,7 @@ def client_connected(link):
list_packet = RNS.Packet(link, data) list_packet = RNS.Packet(link, data)
list_receipt = list_packet.send() list_receipt = list_packet.send()
list_receipt.set_timeout(APP_TIMEOUT) list_receipt.set_timeout(APP_TIMEOUT)
list_receipt.delivery_callback(list_delivered) list_receipt.set_delivery_callback(list_delivered)
list_receipt.timeout_callback(list_timeout) list_receipt.timeout_callback(list_timeout)
else: else:
RNS.log("Too many files in served directory!", RNS.LOG_ERROR) RNS.log("Too many files in served directory!", RNS.LOG_ERROR)
@ -125,7 +125,7 @@ def client_connected(link):
# open until the client requests a file. We'll # open until the client requests a file. We'll
# configure a function that get's called when # configure a function that get's called when
# the client sends a packet with a file request. # the client sends a packet with a file request.
link.packet_callback(client_request) link.set_packet_callback(client_request)
else: else:
RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR) RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR)
link.teardown() link.teardown()
@ -254,18 +254,18 @@ def client(destination_hexhash, configpath):
# We expect any normal data packets on the link # We expect any normal data packets on the link
# to contain a list of served files, so we set # to contain a list of served files, so we set
# a callback accordingly # a callback accordingly
link.packet_callback(filelist_received) link.set_packet_callback(filelist_received)
# We'll also set up functions to inform the # We'll also set up functions to inform the
# user when the link is established or closed # user when the link is established or closed
link.link_established_callback(link_established) link.set_link_established_callback(link_established)
link.link_closed_callback(link_closed) link.set_link_closed_callback(link_closed)
# And set the link to automatically begin # And set the link to automatically begin
# downloading advertised resources # downloading advertised resources
link.set_resource_strategy(RNS.Link.ACCEPT_ALL) link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
link.resource_started_callback(download_began) link.set_resource_started_callback(download_began)
link.resource_concluded_callback(download_concluded) link.set_resource_concluded_callback(download_concluded)
menu() menu()

View File

@ -44,7 +44,7 @@ def server(configpath):
# We configure a function that will get called every time # We configure a function that will get called every time
# a new client creates a link to this destination. # a new client creates a link to this destination.
server_destination.link_established_callback(client_connected) server_destination.set_link_established_callback(client_connected)
# Everything's ready! # Everything's ready!
# Let's Wait for client requests or user input # Let's Wait for client requests or user input
@ -76,8 +76,8 @@ def client_connected(link):
global latest_client_link global latest_client_link
RNS.log("Client connected") RNS.log("Client connected")
link.link_closed_callback(client_disconnected) link.set_link_closed_callback(client_disconnected)
link.packet_callback(server_packet_received) link.set_packet_callback(server_packet_received)
latest_client_link = link latest_client_link = link
def client_disconnected(link): def client_disconnected(link):
@ -149,12 +149,12 @@ def client(destination_hexhash, configpath):
# We set a callback that will get executed # We set a callback that will get executed
# every time a packet is received over the # every time a packet is received over the
# link # link
link.packet_callback(client_packet_received) link.set_packet_callback(client_packet_received)
# We'll also set up functions to inform the # We'll also set up functions to inform the
# user when the link is established or closed # user when the link is established or closed
link.link_established_callback(link_established) link.set_link_established_callback(link_established)
link.link_closed_callback(link_closed) link.set_link_closed_callback(link_closed)
# Everything is set up, so let's enter a loop # Everything is set up, so let's enter a loop
# for the user to interact with the example # for the user to interact with the example

View File

@ -133,8 +133,8 @@ class Destination:
def announce(self, app_data=None, path_response=False): def announce(self, app_data=None, path_response=False):
""" """
Creates an announce packet for this destination and broadcasts it on Creates an announce packet for this destination and broadcasts it on all
all interfaces. Application specific data can be added to the announce. relevant interfaces. Application specific data can be added to the announce.
:param app_data: *bytes* containing the app_data. :param app_data: *bytes* containing the app_data.
:param path_response: Internal flag used by :ref:`RNS.Transport<api-transport>`. Ignore. :param path_response: Internal flag used by :ref:`RNS.Transport<api-transport>`. Ignore.
@ -172,7 +172,7 @@ class Destination:
RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send() RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context).send()
def link_established_callback(self, callback): def set_link_established_callback(self, callback):
""" """
Registers a function to be called when a link has been established to Registers a function to be called when a link has been established to
this destination. this destination.
@ -181,7 +181,7 @@ class Destination:
""" """
self.callbacks.link_established = callback self.callbacks.link_established = callback
def packet_callback(self, callback): def set_packet_callback(self, callback):
""" """
Registers a function to be called when a packet has been received by Registers a function to be called when a packet has been received by
this destination. this destination.
@ -190,7 +190,7 @@ class Destination:
""" """
self.callbacks.packet = callback self.callbacks.packet = callback
def proof_requested_callback(self, callback): def set_proof_requested_callback(self, callback):
""" """
Registers a function to be called when a proof has been requested for Registers a function to be called when a proof has been requested for
a packet sent to this destination. Allows control over when and if a packet sent to this destination. Allows control over when and if

View File

@ -4,14 +4,15 @@ import os
import RNS import RNS
import time import time
import atexit import atexit
import base64
from .vendor import umsgpack as umsgpack from .vendor import umsgpack as umsgpack
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.serialization import load_der_public_key from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
from cryptography.hazmat.primitives.serialization import load_der_private_key from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives.asymmetric import padding from cryptography.fernet import Fernet
class Identity: class Identity:
""" """
@ -19,26 +20,29 @@ class Identity:
for encryption, decryption, signatures and verification, and is the basis for encryption, decryption, signatures and verification, and is the basis
for all encrypted communication over Reticulum networks. for all encrypted communication over Reticulum networks.
:param public_only: Specifies whether this destination only holds a public key. :param create_keys: Specifies whether new encryption and signing keys should be generated.
""" """
KEYSIZE = 1024
CURVE = "Curve25519"
""" """
RSA key size in bits. The curve used for Elliptic Curve DH key exchanges
"""
KEYSIZE = 256*2
"""
X25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
""" """
DERKEYSIZE = KEYSIZE+272
# Non-configurable constants # Non-configurable constants
PADDINGSIZE = 336 # In bits AES_HMAC_OVERHEAD = 58 # In bytes
AES128_BLOCKSIZE = 16 # In bytes
HASHLENGTH = 256 # In bits HASHLENGTH = 256 # In bits
SIGLENGTH = KEYSIZE SIGLENGTH = KEYSIZE # In bits
ENCRYPT_CHUNKSIZE = (KEYSIZE-PADDINGSIZE)//8
DECRYPT_CHUNKSIZE = KEYSIZE//8
TRUNCATED_HASHLENGTH = 80 # In bits TRUNCATED_HASHLENGTH = 80 # In bits
""" """
Constant specifying the truncated hash length (in bits) used by Reticulum Constant specifying the truncated hash length (in bits) used by Reticulum
for addressable hashes. Non-configurable. for addressable hashes and other purposes. Non-configurable.
""" """
# Storage # Storage
@ -60,7 +64,7 @@ class Identity:
RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME) RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME)
if destination_hash in Identity.known_destinations: if destination_hash in Identity.known_destinations:
identity_data = Identity.known_destinations[destination_hash] identity_data = Identity.known_destinations[destination_hash]
identity = Identity(public_only=True) identity = Identity(create_keys=False)
identity.load_public_key(identity_data[2]) identity.load_public_key(identity_data[2])
identity.app_data = identity_data[3] identity.app_data = identity_data[3]
RNS.log("Found "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME) RNS.log("Found "+RNS.prettyhexrep(destination_hash)+" in known destinations", RNS.LOG_EXTREME)
@ -145,19 +149,19 @@ class Identity:
if packet.packet_type == RNS.Packet.ANNOUNCE: if packet.packet_type == RNS.Packet.ANNOUNCE:
RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG) RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG)
destination_hash = packet.destination_hash destination_hash = packet.destination_hash
public_key = packet.data[10:Identity.DERKEYSIZE//8+10] public_key = packet.data[10:Identity.KEYSIZE//8+10]
random_hash = packet.data[Identity.DERKEYSIZE//8+10:Identity.DERKEYSIZE//8+20] random_hash = packet.data[Identity.KEYSIZE//8+10:Identity.KEYSIZE//8+20]
signature = packet.data[Identity.DERKEYSIZE//8+20:Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8] signature = packet.data[Identity.KEYSIZE//8+20:Identity.KEYSIZE//8+20+Identity.KEYSIZE//8]
app_data = b"" app_data = b""
if len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8: if len(packet.data) > Identity.KEYSIZE//8+20+Identity.KEYSIZE//8:
app_data = packet.data[Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:] app_data = packet.data[Identity.KEYSIZE//8+20+Identity.KEYSIZE//8:]
signed_data = destination_hash+public_key+random_hash+app_data signed_data = destination_hash+public_key+random_hash+app_data
if not len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8: if not len(packet.data) > Identity.KEYSIZE//8+20+Identity.KEYSIZE//8:
app_data = None app_data = None
announced_identity = Identity(public_only=True) announced_identity = Identity(create_keys=False)
announced_identity.load_public_key(public_key) announced_identity.load_public_key(public_key)
if announced_identity.pub != None and announced_identity.validate(signature, signed_data): if announced_identity.pub != None and announced_identity.validate(signature, signed_data):
@ -184,40 +188,71 @@ class Identity:
:param path: The full path to the saved :ref:`RNS.Identity<api-identity>` data :param path: The full path to the saved :ref:`RNS.Identity<api-identity>` data
:returns: A :ref:`RNS.Identity<api-identity>` instance, or *None* if the loaded data was invalid. :returns: A :ref:`RNS.Identity<api-identity>` instance, or *None* if the loaded data was invalid.
""" """
identity = Identity(public_only=True) identity = Identity(create_keys=False)
if identity.load(path): if identity.load(path):
return identity return identity
else: else:
return None return None
@staticmethod
def from_bytes(prv_bytes):
"""
Create a new :ref:`RNS.Identity<api-identity>` instance from *bytes* of private key.
Can be used to load previously created and saved identities into Reticulum.
def __init__(self,public_only=False): :param prv_bytes: The *bytes* of private a saved private key. **HAZARD!** Never not use this to generate a new key by feeding random data in prv_bytes.
:returns: A :ref:`RNS.Identity<api-identity>` instance, or *None* if the *bytes* data was invalid.
"""
identity = Identity(create_keys=False)
if identity.load_private_key(prv_bytes):
return identity
else:
return None
def __init__(self,create_keys=True):
# Initialize keys to none # Initialize keys to none
self.prv = None self.prv = None
self.pub = None
self.prv_bytes = None self.prv_bytes = None
self.sig_prv = None
self.sig_prv_bytes = None
self.pub = None
self.pub_bytes = None self.pub_bytes = None
self.sig_pub = None
self.sig_pub_bytes = None
self.hash = None self.hash = None
self.hexhash = None self.hexhash = None
if not public_only: if create_keys:
self.create_keys() self.create_keys()
def create_keys(self): def create_keys(self):
self.prv = rsa.generate_private_key( self.prv = X25519PrivateKey.generate()
public_exponent=65537,
key_size=Identity.KEYSIZE,
backend=default_backend()
)
self.prv_bytes = self.prv.private_bytes( self.prv_bytes = self.prv.private_bytes(
encoding=serialization.Encoding.DER, encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.PKCS8, format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption() encryption_algorithm=serialization.NoEncryption()
) )
self.sig_prv = Ed25519PrivateKey.generate()
self.sig_prv_bytes = self.sig_prv.private_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PrivateFormat.Raw,
encryption_algorithm=serialization.NoEncryption()
)
self.pub = self.prv.public_key() self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes( self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.DER, encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.Raw
)
self.sig_pub = self.sig_prv.public_key()
self.sig_pub_bytes = self.sig_pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
) )
self.update_hashes() self.update_hashes()
@ -228,13 +263,13 @@ class Identity:
""" """
:returns: The private key as *bytes* :returns: The private key as *bytes*
""" """
return self.prv_bytes return self.prv_bytes+self.sig_prv_bytes
def get_public_key(self): def get_public_key(self):
""" """
:returns: The public key as *bytes* :returns: The public key as *bytes*
""" """
return self.pub_bytes return self.pub_bytes+self.sig_pub_bytes
def load_private_key(self, prv_bytes): def load_private_key(self, prv_bytes):
""" """
@ -244,42 +279,53 @@ class Identity:
:returns: True if the key was loaded, otherwise False. :returns: True if the key was loaded, otherwise False.
""" """
try: try:
self.prv_bytes = prv_bytes self.prv_bytes = prv_bytes[:Identity.KEYSIZE//8//2]
self.prv = serialization.load_der_private_key( self.prv = X25519PrivateKey.from_private_bytes(self.prv_bytes)
self.prv_bytes, self.sig_prv_bytes = prv_bytes[Identity.KEYSIZE//8//2:]
password=None, self.sig_prv = Ed25519PrivateKey.from_private_bytes(self.sig_prv_bytes)
backend=default_backend()
)
self.pub = self.prv.public_key() self.pub = self.prv.public_key()
self.pub_bytes = self.pub.public_bytes( self.pub_bytes = self.pub.public_bytes(
encoding=serialization.Encoding.DER, encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.SubjectPublicKeyInfo format=serialization.PublicFormat.Raw
) )
self.sig_pub = self.sig_prv.public_key()
self.sig_pub_bytes = self.sig_pub.public_bytes(
encoding=serialization.Encoding.Raw,
format=serialization.PublicFormat.Raw
)
self.update_hashes() self.update_hashes()
return True return True
except Exception as e: except Exception as e:
raise e
RNS.log("Failed to load identity key", RNS.LOG_ERROR) RNS.log("Failed to load identity key", RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
return False return False
def load_public_key(self, key): def load_public_key(self, pub_bytes):
""" """
Load a public key into the instance. Load a public key into the instance.
:param prv_bytes: The public key as *bytes*. :param pub_bytes: The public key as *bytes*.
:returns: True if the key was loaded, otherwise False. :returns: True if the key was loaded, otherwise False.
""" """
try: try:
self.pub_bytes = key self.pub_bytes = pub_bytes[:Identity.KEYSIZE//8//2]
self.pub = load_der_public_key(self.pub_bytes, backend=default_backend()) self.sig_pub_bytes = pub_bytes[Identity.KEYSIZE//8//2:]
self.pub = X25519PublicKey.from_public_bytes(self.pub_bytes)
self.sig_pub = Ed25519PublicKey.from_public_bytes(self.sig_pub_bytes)
self.update_hashes() self.update_hashes()
except Exception as e: except Exception as e:
RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR) RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR)
def update_hashes(self): def update_hashes(self):
self.hash = Identity.truncated_hash(self.pub_bytes) self.hash = Identity.truncated_hash(self.get_public_key())
self.hexhash = self.hash.hex() self.hexhash = self.hash.hex()
def to_file(self, path): def to_file(self, path):
@ -310,71 +356,78 @@ class Identity:
RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR) RNS.log("Error while loading identity from "+str(path), RNS.LOG_ERROR)
RNS.log("The contained exception was: "+str(e)) RNS.log("The contained exception was: "+str(e))
def get_salt(self):
return self.hash
def get_context(self):
return None
def encrypt(self, plaintext): def encrypt(self, plaintext):
""" """
Encrypts information for the identity. Encrypts information for the identity.
:param plaintext: The plaintext to be encrypted as *bytes*. :param plaintext: The plaintext to be encrypted as *bytes*.
:returns: Ciphertext as *bytes*. :returns: Ciphertext token as *bytes*.
:raises: *KeyError* if the instance does not hold a public key :raises: *KeyError* if the instance does not hold a public key.
""" """
if self.pub != None: if self.pub != None:
chunksize = Identity.ENCRYPT_CHUNKSIZE ephemeral_key = X25519PrivateKey.generate()
chunks = int(math.ceil(len(plaintext)/(float(chunksize)))) ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes(
encoding=serialization.Encoding.Raw,
ciphertext = b""; format=serialization.PublicFormat.Raw
for chunk in range(chunks):
start = chunk*chunksize
end = (chunk+1)*chunksize
if (chunk+1)*chunksize > len(plaintext):
end = len(plaintext)
ciphertext += self.pub.encrypt(
plaintext[start:end],
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
) )
)
return ciphertext shared_key = ephemeral_key.exchange(self.pub)
derived_key = derived_key = HKDF(
algorithm=hashes.SHA256(),
length=32,
salt=self.get_salt(),
info=self.get_context(),
).derive(shared_key)
fernet = Fernet(base64.urlsafe_b64encode(derived_key))
ciphertext = base64.urlsafe_b64decode(fernet.encrypt(plaintext))
token = ephemeral_pub_bytes+ciphertext
return token
else: else:
raise KeyError("Encryption failed because identity does not hold a public key") raise KeyError("Encryption failed because identity does not hold a public key")
def decrypt(self, ciphertext): def decrypt(self, ciphertext_token):
""" """
Decrypts information for the identity. Decrypts information for the identity.
:param ciphertext: The ciphertext to be decrypted as *bytes*. :param ciphertext: The ciphertext to be decrypted as *bytes*.
:returns: Plaintext as *bytes*, or *None* if decryption fails. :returns: Plaintext as *bytes*, or *None* if decryption fails.
:raises: *KeyError* if the instance does not hold a private key :raises: *KeyError* if the instance does not hold a private key.
""" """
if self.prv != None: if self.prv != None:
if len(ciphertext_token) > Identity.KEYSIZE//8//2:
plaintext = None plaintext = None
try: try:
chunksize = Identity.DECRYPT_CHUNKSIZE peer_pub_bytes = ciphertext_token[:Identity.KEYSIZE//8//2]
chunks = int(math.ceil(len(ciphertext)/(float(chunksize)))) peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
plaintext = b""; shared_key = self.prv.exchange(peer_pub)
for chunk in range(chunks): derived_key = derived_key = HKDF(
start = chunk*chunksize algorithm=hashes.SHA256(),
end = (chunk+1)*chunksize length=32,
if (chunk+1)*chunksize > len(ciphertext): salt=self.get_salt(),
end = len(ciphertext) info=self.get_context(),
).derive(shared_key)
plaintext += self.prv.decrypt( fernet = Fernet(base64.urlsafe_b64encode(derived_key))
ciphertext[start:end], ciphertext = ciphertext_token[Identity.KEYSIZE//8//2:]
padding.OAEP( plaintext = fernet.decrypt(base64.urlsafe_b64encode(ciphertext))
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(), except Exception as e:
label=None RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)
)
)
except:
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE)
return plaintext; return plaintext;
else:
RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG)
return None
else: else:
raise KeyError("Decryption failed because identity does not hold a private key") raise KeyError("Decryption failed because identity does not hold a private key")
@ -385,18 +438,14 @@ class Identity:
:param message: The message to be signed as *bytes*. :param message: The message to be signed as *bytes*.
:returns: Signature as *bytes*. :returns: Signature as *bytes*.
:raises: *KeyError* if the instance does not hold a private key :raises: *KeyError* if the instance does not hold a private key.
""" """
if self.prv != None: if self.sig_prv != None:
signature = self.prv.sign( try:
message, return self.sig_prv.sign(message)
padding.PSS( except Exception as e:
mgf=padding.MGF1(hashes.SHA256()), RNS.log("The identity "+str(self)+" could not sign the requested message. The contained exception was: "+str(e), RNS.LOG_ERROR)
salt_length=padding.PSS.MAX_LENGTH raise e
),
hashes.SHA256()
)
return signature
else: else:
raise KeyError("Signing failed because identity does not hold a private key") raise KeyError("Signing failed because identity does not hold a private key")
@ -407,19 +456,11 @@ class Identity:
:param signature: The signature to be validated as *bytes*. :param signature: The signature to be validated as *bytes*.
:param message: The message to be validated as *bytes*. :param message: The message to be validated as *bytes*.
:returns: True if the signature is valid, otherwise False. :returns: True if the signature is valid, otherwise False.
:raises: *KeyError* if the instance does not hold a public key :raises: *KeyError* if the instance does not hold a public key.
""" """
if self.pub != None: if self.pub != None:
try: try:
self.pub.verify( self.sig_pub.verify(signature, message)
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
return True return True
except Exception as e: except Exception as e:
return False return False

View File

@ -33,17 +33,15 @@ class Link:
:param peer_pub_bytes: Internal use, ignore this argument. :param peer_pub_bytes: Internal use, ignore this argument.
:param peer_sig_pub_bytes: Internal use, ignore this argument. :param peer_sig_pub_bytes: Internal use, ignore this argument.
""" """
CURVE = "Curve25519" CURVE = RNS.Identity.CURVE
""" """
The curve used for Elliptic Curve DH key exchanges The curve used for Elliptic Curve DH key exchanges
""" """
ECPUBSIZE = 32+32 ECPUBSIZE = 32+32
BLOCKSIZE = 16
KEYSIZE = 32 KEYSIZE = 32
AES_HMAC_OVERHEAD = 58 MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.AES_HMAC_OVERHEAD)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
MDU = math.floor((RNS.Reticulum.MDU-AES_HMAC_OVERHEAD)/BLOCKSIZE)*BLOCKSIZE - 1
# TODO: This should not be hardcoded, # TODO: This should not be hardcoded,
# but calculated from something like # but calculated from something like
@ -90,11 +88,6 @@ class Link:
link.last_inbound = time.time() link.last_inbound = time.time()
link.start_watchdog() link.start_watchdog()
# TODO: Why was link_established callback here? Seems weird
# to call this before RTT packet has been received
#if self.owner.callbacks.link_established != None:
# self.owner.callbacks.link_established(link)
RNS.log("Incoming link request "+str(link)+" accepted", RNS.LOG_VERBOSE) RNS.log("Incoming link request "+str(link)+" accepted", RNS.LOG_VERBOSE)
return link return link
@ -537,13 +530,13 @@ class Link:
except Exception as e: except Exception as e:
return False return False
def link_established_callback(self, callback): def set_link_established_callback(self, callback):
self.callbacks.link_established = callback self.callbacks.link_established = callback
def link_closed_callback(self, callback): def set_link_closed_callback(self, callback):
self.callbacks.link_closed = callback self.callbacks.link_closed = callback
def packet_callback(self, callback): def set_packet_callback(self, callback):
""" """
Registers a function to be called when a packet has been Registers a function to be called when a packet has been
received over this link. received over this link.
@ -552,7 +545,7 @@ class Link:
""" """
self.callbacks.packet = callback self.callbacks.packet = callback
def resource_callback(self, callback): def set_resource_callback(self, callback):
""" """
Registers a function to be called when a resource has been Registers a function to be called when a resource has been
advertised over this link. If the function returns *True* advertised over this link. If the function returns *True*
@ -563,7 +556,7 @@ class Link:
""" """
self.callbacks.resource = callback self.callbacks.resource = callback
def resource_started_callback(self, callback): def set_resource_started_callback(self, callback):
""" """
Registers a function to be called when a resource has begun Registers a function to be called when a resource has begun
transferring over this link. transferring over this link.
@ -572,7 +565,7 @@ class Link:
""" """
self.callbacks.resource_started = callback self.callbacks.resource_started = callback
def resource_concluded_callback(self, callback): def set_resource_concluded_callback(self, callback):
""" """
Registers a function to be called when a resource has concluded Registers a function to be called when a resource has concluded
transferring over this link. transferring over this link.

View File

@ -6,8 +6,16 @@ import RNS
class Packet: class Packet:
""" """
The Packet class is used to create packet instances that can be The Packet class is used to create packet instances that can be sent
sent over a Reticulum network. over a Reticulum network. Packets to will automatically be encrypted if
they are adressed to a ``RNS.Destination.SINGLE`` destination,
``RNS.Destination.GROUP`` destination or a :ref:`RNS.Link<api-link>`.
For ``RNS.Destination.GROUP`` destinations, Reticulum will use the
pre-shared key configured for the destination.
For ``RNS.Destination.SINGLE`` destinations and :ref:`RNS.Link<api-link>`
destinations, reticulum will use ephemeral keys, and offers **Forward Secrecy**.
:param destination: A :ref:`RNS.Destination<api-destination>` instance to which the packet will be sent. :param destination: A :ref:`RNS.Destination<api-destination>` instance to which the packet will be sent.
:param data: The data payload to be included in the packet as *bytes*. :param data: The data payload to be included in the packet as *bytes*.
@ -56,14 +64,21 @@ class Packet:
# This is used to calculate allowable # This is used to calculate allowable
# payload sizes # payload sizes
HEADER_MAXSIZE = 23 HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
MDU = RNS.Reticulum.MDU MDU = RNS.Reticulum.MDU
# With an MTU of 500, the maximum RSA-encrypted # TODO: Update this
# amount of data we can send in a single packet # With an MTU of 500, the maximum of data we can
# is given by the below calculation; 258 bytes. # send in a single encrypted packet is given by
RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE # the below calculation; 383 bytes.
ENCRYPTED_MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.AES_HMAC_OVERHEAD-RNS.Identity.KEYSIZE//16)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
"""
The maximum size of the payload data in a single encrypted packet
"""
PLAIN_MDU = MDU PLAIN_MDU = MDU
"""
The maximum size of the payload data in a single unencrypted packet
"""
# TODO: This should be calculated # TODO: This should be calculated
# more intelligently # more intelligently
@ -406,7 +421,7 @@ class PacketReceipt:
else: else:
return False return False
def rtt(self): def get_rtt(self):
""" """
:returns: The round-trip-time in seconds :returns: The round-trip-time in seconds
""" """
@ -439,7 +454,7 @@ class PacketReceipt:
""" """
self.timeout = float(timeout) self.timeout = float(timeout)
def delivery_callback(self, callback): def set_delivery_callback(self, callback):
""" """
Sets a function that gets called if a successfull delivery has been proven. Sets a function that gets called if a successfull delivery has been proven.
@ -449,7 +464,7 @@ class PacketReceipt:
# Set a function that gets called if the # Set a function that gets called if the
# delivery times out # delivery times out
def timeout_callback(self, callback): def set_timeout_callback(self, callback):
""" """
Sets a function that gets called if the delivery times out. Sets a function that gets called if the delivery times out.

View File

@ -17,7 +17,6 @@ class Resource:
:param link: The :ref:`RNS.Link<api-link>` instance on which to transfer the data. :param link: The :ref:`RNS.Link<api-link>` instance on which to transfer the data.
:param advertise: Whether to automatically advertise the resource. Can be *True* or *False*. :param advertise: Whether to automatically advertise the resource. Can be *True* or *False*.
:param auto_compress: Whether to auto-compress the resource. Can be *True* or *False*. :param auto_compress: Whether to auto-compress the resource. Can be *True* or *False*.
:param auto_compress: Whether the resource must be compressed. Can be *True* or *False*. Used for debugging, will disappear in the future.
:param callback: A *callable* with the signature *callback(resource)*. Will be called when the resource transfer concludes. :param callback: A *callable* with the signature *callback(resource)*. Will be called when the resource transfer concludes.
:param progress_callback: A *callable* with the signature *callback(resource)*. Will be called whenever the resource transfer progress is updated. :param progress_callback: A *callable* with the signature *callback(resource)*. Will be called whenever the resource transfer progress is updated.
:param segment_index: Internal use, ignore. :param segment_index: Internal use, ignore.
@ -134,7 +133,7 @@ class Resource:
# Create a resource for transmission to a remote destination # Create a resource for transmission to a remote destination
# The data passed can be either a bytes-array or a file opened # The data passed can be either a bytes-array or a file opened
# in binary read mode. # in binary read mode.
def __init__(self, data, link, advertise=True, auto_compress=True, must_compress=False, callback=None, progress_callback=None, segment_index = 1, original_hash = None): def __init__(self, data, link, advertise=True, auto_compress=True, callback=None, progress_callback=None, segment_index = 1, original_hash = None):
data_size = None data_size = None
resource_data = None resource_data = None
if hasattr(data, "read"): if hasattr(data, "read"):
@ -198,7 +197,7 @@ class Resource:
self.uncompressed_data = data self.uncompressed_data = data
compression_began = time.time() compression_began = time.time()
if must_compress or (auto_compress and len(self.uncompressed_data) < Resource.AUTO_COMPRESS_MAX_SIZE): if (auto_compress and len(self.uncompressed_data) < Resource.AUTO_COMPRESS_MAX_SIZE):
RNS.log("Compressing resource data...", RNS.LOG_DEBUG) RNS.log("Compressing resource data...", RNS.LOG_DEBUG)
self.compressed_data = bz2.compress(self.uncompressed_data) self.compressed_data = bz2.compress(self.uncompressed_data)
RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_DEBUG) RNS.log("Compression completed in "+str(round(time.time()-compression_began, 3))+" seconds", RNS.LOG_DEBUG)
@ -748,8 +747,6 @@ class Resource:
:returns: The current progress of the resource transfer as a *float* between 0.0 and 1.0. :returns: The current progress of the resource transfer as a *float* between 0.0 and 1.0.
""" """
if self.initiator: if self.initiator:
# TODO: Remove
# progress = self.sent_parts / len(self.parts)
self.processed_parts = (self.segment_index-1)*math.ceil(Resource.MAX_EFFICIENT_SIZE/Resource.SDU) self.processed_parts = (self.segment_index-1)*math.ceil(Resource.MAX_EFFICIENT_SIZE/Resource.SDU)
self.processed_parts += self.sent_parts self.processed_parts += self.sent_parts
self.progress_total_parts = float(self.grand_total_parts) self.progress_total_parts = float(self.grand_total_parts)

View File

@ -108,7 +108,7 @@ class Transport:
# Create transport-specific destinations # Create transport-specific destinations
Transport.path_request_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request") Transport.path_request_destination = RNS.Destination(None, RNS.Destination.IN, RNS.Destination.PLAIN, Transport.APP_NAME, "path", "request")
Transport.path_request_destination.packet_callback(Transport.path_request_handler) Transport.path_request_destination.set_packet_callback(Transport.path_request_handler)
Transport.control_destinations.append(Transport.path_request_destination) Transport.control_destinations.append(Transport.path_request_destination)
Transport.control_hashes.append(Transport.path_request_destination.hash) Transport.control_hashes.append(Transport.path_request_destination.hash)
@ -652,7 +652,7 @@ class Transport:
# First, check that the announce is not for a destination # First, check that the announce is not for a destination
# local to this system, and that hops are less than the max # local to this system, and that hops are less than the max
if (not any(packet.destination_hash == d.hash for d in Transport.destinations) and packet.hops < Transport.PATHFINDER_M+1): if (not any(packet.destination_hash == d.hash for d in Transport.destinations) and packet.hops < Transport.PATHFINDER_M+1):
random_blob = packet.data[RNS.Identity.DERKEYSIZE//8+10:RNS.Identity.DERKEYSIZE//8+20] random_blob = packet.data[RNS.Identity.KEYSIZE//8+10:RNS.Identity.KEYSIZE//8+20]
random_blobs = [] random_blobs = []
if packet.destination_hash in Transport.destination_table: if packet.destination_hash in Transport.destination_table:
random_blobs = Transport.destination_table[packet.destination_hash][4] random_blobs = Transport.destination_table[packet.destination_hash][4]