mirror of
https://github.com/markqvist/Reticulum.git
synced 2024-12-22 11:50:18 +00:00
Migrated all asymmetric crypto operations to ECIES on Curve25519.
This commit is contained in:
parent
7f5625a526
commit
ce405b9252
@ -39,7 +39,7 @@ def program_setup(configpath, channel=None):
|
||||
|
||||
# We specify a callback that will get called every time
|
||||
# the destination receives data.
|
||||
broadcast_destination.packet_callback(packet_callback)
|
||||
broadcast_destination.set_packet_callback(packet_callback)
|
||||
|
||||
# Everything's ready!
|
||||
# Let's hand over control to the main loop
|
||||
|
@ -52,7 +52,7 @@ def server(configpath):
|
||||
# Tell the destination which function in our program to
|
||||
# run when a packet is received. We do this so we can
|
||||
# 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!
|
||||
# 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.
|
||||
# This will get automatically called when a proof for
|
||||
# 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
|
||||
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.
|
||||
def packet_delivered(receipt):
|
||||
if receipt.status == RNS.PacketReceipt.DELIVERED:
|
||||
rtt = receipt.rtt()
|
||||
rtt = receipt.get_rtt()
|
||||
if (rtt >= 1):
|
||||
rtt = round(rtt, 3)
|
||||
rttstring = str(rtt)+" seconds"
|
||||
|
@ -65,7 +65,7 @@ def server(configpath, path):
|
||||
|
||||
# We configure a function that will get called every time
|
||||
# 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!
|
||||
# Let's Wait for client requests or user input
|
||||
@ -102,7 +102,7 @@ def client_connected(link):
|
||||
if os.path.isdir(serve_path):
|
||||
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
|
||||
data = umsgpack.packb(list_files())
|
||||
@ -114,7 +114,7 @@ def client_connected(link):
|
||||
list_packet = RNS.Packet(link, data)
|
||||
list_receipt = list_packet.send()
|
||||
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)
|
||||
else:
|
||||
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
|
||||
# configure a function that get's called when
|
||||
# the client sends a packet with a file request.
|
||||
link.packet_callback(client_request)
|
||||
link.set_packet_callback(client_request)
|
||||
else:
|
||||
RNS.log("Client connected, but served path no longer exists!", RNS.LOG_ERROR)
|
||||
link.teardown()
|
||||
@ -254,18 +254,18 @@ def client(destination_hexhash, configpath):
|
||||
# We expect any normal data packets on the link
|
||||
# to contain a list of served files, so we set
|
||||
# a callback accordingly
|
||||
link.packet_callback(filelist_received)
|
||||
link.set_packet_callback(filelist_received)
|
||||
|
||||
# We'll also set up functions to inform the
|
||||
# user when the link is established or closed
|
||||
link.link_established_callback(link_established)
|
||||
link.link_closed_callback(link_closed)
|
||||
link.set_link_established_callback(link_established)
|
||||
link.set_link_closed_callback(link_closed)
|
||||
|
||||
# And set the link to automatically begin
|
||||
# downloading advertised resources
|
||||
link.set_resource_strategy(RNS.Link.ACCEPT_ALL)
|
||||
link.resource_started_callback(download_began)
|
||||
link.resource_concluded_callback(download_concluded)
|
||||
link.set_resource_started_callback(download_began)
|
||||
link.set_resource_concluded_callback(download_concluded)
|
||||
|
||||
menu()
|
||||
|
||||
|
@ -44,7 +44,7 @@ def server(configpath):
|
||||
|
||||
# We configure a function that will get called every time
|
||||
# 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!
|
||||
# Let's Wait for client requests or user input
|
||||
@ -76,8 +76,8 @@ def client_connected(link):
|
||||
global latest_client_link
|
||||
|
||||
RNS.log("Client connected")
|
||||
link.link_closed_callback(client_disconnected)
|
||||
link.packet_callback(server_packet_received)
|
||||
link.set_link_closed_callback(client_disconnected)
|
||||
link.set_packet_callback(server_packet_received)
|
||||
latest_client_link = link
|
||||
|
||||
def client_disconnected(link):
|
||||
@ -149,12 +149,12 @@ def client(destination_hexhash, configpath):
|
||||
# We set a callback that will get executed
|
||||
# every time a packet is received over the
|
||||
# link
|
||||
link.packet_callback(client_packet_received)
|
||||
link.set_packet_callback(client_packet_received)
|
||||
|
||||
# We'll also set up functions to inform the
|
||||
# user when the link is established or closed
|
||||
link.link_established_callback(link_established)
|
||||
link.link_closed_callback(link_closed)
|
||||
link.set_link_established_callback(link_established)
|
||||
link.set_link_closed_callback(link_closed)
|
||||
|
||||
# Everything is set up, so let's enter a loop
|
||||
# for the user to interact with the example
|
||||
|
@ -133,8 +133,8 @@ class Destination:
|
||||
|
||||
def announce(self, app_data=None, path_response=False):
|
||||
"""
|
||||
Creates an announce packet for this destination and broadcasts it on
|
||||
all interfaces. Application specific data can be added to the announce.
|
||||
Creates an announce packet for this destination and broadcasts it on all
|
||||
relevant interfaces. Application specific data can be added to the announce.
|
||||
|
||||
:param app_data: *bytes* containing the app_data.
|
||||
: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()
|
||||
|
||||
|
||||
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
|
||||
this destination.
|
||||
@ -181,7 +181,7 @@ class Destination:
|
||||
"""
|
||||
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
|
||||
this destination.
|
||||
@ -190,7 +190,7 @@ class Destination:
|
||||
"""
|
||||
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
|
||||
a packet sent to this destination. Allows control over when and if
|
||||
|
293
RNS/Identity.py
293
RNS/Identity.py
@ -4,14 +4,15 @@ import os
|
||||
import RNS
|
||||
import time
|
||||
import atexit
|
||||
import base64
|
||||
from .vendor import umsgpack as umsgpack
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
from cryptography.hazmat.primitives import hashes
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives.serialization import load_der_public_key
|
||||
from cryptography.hazmat.primitives.serialization import load_der_private_key
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.hazmat.primitives.asymmetric import padding
|
||||
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
||||
from cryptography.hazmat.primitives.asymmetric.x25519 import X25519PrivateKey, X25519PublicKey
|
||||
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
class Identity:
|
||||
"""
|
||||
@ -19,26 +20,29 @@ class Identity:
|
||||
for encryption, decryption, signatures and verification, and is the basis
|
||||
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
|
||||
"""
|
||||
DERKEYSIZE = KEYSIZE+272
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
# Non-configurable constants
|
||||
PADDINGSIZE = 336 # In bits
|
||||
HASHLENGTH = 256 # In bits
|
||||
SIGLENGTH = KEYSIZE
|
||||
|
||||
ENCRYPT_CHUNKSIZE = (KEYSIZE-PADDINGSIZE)//8
|
||||
DECRYPT_CHUNKSIZE = KEYSIZE//8
|
||||
AES_HMAC_OVERHEAD = 58 # In bytes
|
||||
AES128_BLOCKSIZE = 16 # In bytes
|
||||
HASHLENGTH = 256 # In bits
|
||||
SIGLENGTH = KEYSIZE # In bits
|
||||
|
||||
TRUNCATED_HASHLENGTH = 80 # In bits
|
||||
"""
|
||||
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
|
||||
@ -60,7 +64,7 @@ class Identity:
|
||||
RNS.log("Searching for "+RNS.prettyhexrep(destination_hash)+"...", RNS.LOG_EXTREME)
|
||||
if destination_hash in Identity.known_destinations:
|
||||
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.app_data = identity_data[3]
|
||||
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:
|
||||
RNS.log("Validating announce from "+RNS.prettyhexrep(packet.destination_hash), RNS.LOG_DEBUG)
|
||||
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]
|
||||
signature = packet.data[Identity.DERKEYSIZE//8+20:Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8]
|
||||
public_key = packet.data[10:Identity.KEYSIZE//8+10]
|
||||
random_hash = packet.data[Identity.KEYSIZE//8+10:Identity.KEYSIZE//8+20]
|
||||
signature = packet.data[Identity.KEYSIZE//8+20:Identity.KEYSIZE//8+20+Identity.KEYSIZE//8]
|
||||
app_data = b""
|
||||
if len(packet.data) > Identity.DERKEYSIZE//8+20+Identity.KEYSIZE//8:
|
||||
app_data = 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.KEYSIZE//8+20+Identity.KEYSIZE//8:]
|
||||
|
||||
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
|
||||
|
||||
announced_identity = Identity(public_only=True)
|
||||
announced_identity = Identity(create_keys=False)
|
||||
announced_identity.load_public_key(public_key)
|
||||
|
||||
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
|
||||
: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):
|
||||
return identity
|
||||
else:
|
||||
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
|
||||
self.prv = None
|
||||
self.pub = None
|
||||
self.prv_bytes = None
|
||||
self.pub_bytes = None
|
||||
self.hash = None
|
||||
self.hexhash = None
|
||||
self.prv = None
|
||||
self.prv_bytes = None
|
||||
self.sig_prv = None
|
||||
self.sig_prv_bytes = None
|
||||
|
||||
if not public_only:
|
||||
self.pub = None
|
||||
self.pub_bytes = None
|
||||
self.sig_pub = None
|
||||
self.sig_pub_bytes = None
|
||||
|
||||
self.hash = None
|
||||
self.hexhash = None
|
||||
|
||||
if create_keys:
|
||||
self.create_keys()
|
||||
|
||||
def create_keys(self):
|
||||
self.prv = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=Identity.KEYSIZE,
|
||||
backend=default_backend()
|
||||
)
|
||||
self.prv_bytes = self.prv.private_bytes(
|
||||
encoding=serialization.Encoding.DER,
|
||||
format=serialization.PrivateFormat.PKCS8,
|
||||
self.prv = X25519PrivateKey.generate()
|
||||
self.prv_bytes = self.prv.private_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PrivateFormat.Raw,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
self.pub = self.prv.public_key()
|
||||
self.pub_bytes = self.pub.public_bytes(
|
||||
encoding=serialization.Encoding.DER,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
|
||||
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_bytes = self.pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
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()
|
||||
@ -228,13 +263,13 @@ class Identity:
|
||||
"""
|
||||
:returns: The private key as *bytes*
|
||||
"""
|
||||
return self.prv_bytes
|
||||
return self.prv_bytes+self.sig_prv_bytes
|
||||
|
||||
def get_public_key(self):
|
||||
"""
|
||||
: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):
|
||||
"""
|
||||
@ -244,42 +279,53 @@ class Identity:
|
||||
:returns: True if the key was loaded, otherwise False.
|
||||
"""
|
||||
try:
|
||||
self.prv_bytes = prv_bytes
|
||||
self.prv = serialization.load_der_private_key(
|
||||
self.prv_bytes,
|
||||
password=None,
|
||||
backend=default_backend()
|
||||
self.prv_bytes = prv_bytes[:Identity.KEYSIZE//8//2]
|
||||
self.prv = X25519PrivateKey.from_private_bytes(self.prv_bytes)
|
||||
self.sig_prv_bytes = prv_bytes[Identity.KEYSIZE//8//2:]
|
||||
self.sig_prv = Ed25519PrivateKey.from_private_bytes(self.sig_prv_bytes)
|
||||
|
||||
self.pub = self.prv.public_key()
|
||||
self.pub_bytes = self.pub.public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
self.pub = self.prv.public_key()
|
||||
self.pub_bytes = self.pub.public_bytes(
|
||||
encoding=serialization.Encoding.DER,
|
||||
format=serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
|
||||
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()
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
RNS.log("Failed to load identity key", RNS.LOG_ERROR)
|
||||
RNS.log("The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
return False
|
||||
|
||||
def load_public_key(self, key):
|
||||
def load_public_key(self, pub_bytes):
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
try:
|
||||
self.pub_bytes = key
|
||||
self.pub = load_der_public_key(self.pub_bytes, backend=default_backend())
|
||||
self.pub_bytes = pub_bytes[:Identity.KEYSIZE//8//2]
|
||||
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()
|
||||
except Exception as e:
|
||||
RNS.log("Error while loading public key, the contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
|
||||
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()
|
||||
|
||||
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("The contained exception was: "+str(e))
|
||||
|
||||
def get_salt(self):
|
||||
return self.hash
|
||||
|
||||
def get_context(self):
|
||||
return None
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
"""
|
||||
Encrypts information for the identity.
|
||||
|
||||
:param plaintext: The plaintext to be encrypted as *bytes*.
|
||||
:returns: Ciphertext as *bytes*.
|
||||
:raises: *KeyError* if the instance does not hold a public key
|
||||
:returns: Ciphertext token as *bytes*.
|
||||
:raises: *KeyError* if the instance does not hold a public key.
|
||||
"""
|
||||
if self.pub != None:
|
||||
chunksize = Identity.ENCRYPT_CHUNKSIZE
|
||||
chunks = int(math.ceil(len(plaintext)/(float(chunksize))))
|
||||
ephemeral_key = X25519PrivateKey.generate()
|
||||
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes(
|
||||
encoding=serialization.Encoding.Raw,
|
||||
format=serialization.PublicFormat.Raw
|
||||
)
|
||||
|
||||
ciphertext = b"";
|
||||
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:
|
||||
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.
|
||||
|
||||
:param ciphertext: The ciphertext to be decrypted as *bytes*.
|
||||
: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:
|
||||
plaintext = None
|
||||
try:
|
||||
chunksize = Identity.DECRYPT_CHUNKSIZE
|
||||
chunks = int(math.ceil(len(ciphertext)/(float(chunksize))))
|
||||
if len(ciphertext_token) > Identity.KEYSIZE//8//2:
|
||||
plaintext = None
|
||||
try:
|
||||
peer_pub_bytes = ciphertext_token[:Identity.KEYSIZE//8//2]
|
||||
peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
|
||||
|
||||
plaintext = b"";
|
||||
for chunk in range(chunks):
|
||||
start = chunk*chunksize
|
||||
end = (chunk+1)*chunksize
|
||||
if (chunk+1)*chunksize > len(ciphertext):
|
||||
end = len(ciphertext)
|
||||
shared_key = self.prv.exchange(peer_pub)
|
||||
derived_key = derived_key = HKDF(
|
||||
algorithm=hashes.SHA256(),
|
||||
length=32,
|
||||
salt=self.get_salt(),
|
||||
info=self.get_context(),
|
||||
).derive(shared_key)
|
||||
|
||||
plaintext += self.prv.decrypt(
|
||||
ciphertext[start:end],
|
||||
padding.OAEP(
|
||||
mgf=padding.MGF1(algorithm=hashes.SHA1()),
|
||||
algorithm=hashes.SHA1(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
except:
|
||||
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed", RNS.LOG_VERBOSE)
|
||||
|
||||
return plaintext;
|
||||
fernet = Fernet(base64.urlsafe_b64encode(derived_key))
|
||||
ciphertext = ciphertext_token[Identity.KEYSIZE//8//2:]
|
||||
plaintext = fernet.decrypt(base64.urlsafe_b64encode(ciphertext))
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
|
||||
return plaintext;
|
||||
else:
|
||||
RNS.log("Decryption failed because the token size was invalid.", RNS.LOG_DEBUG)
|
||||
return None
|
||||
else:
|
||||
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*.
|
||||
: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:
|
||||
signature = self.prv.sign(
|
||||
message,
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(hashes.SHA256()),
|
||||
salt_length=padding.PSS.MAX_LENGTH
|
||||
),
|
||||
hashes.SHA256()
|
||||
)
|
||||
return signature
|
||||
if self.sig_prv != None:
|
||||
try:
|
||||
return self.sig_prv.sign(message)
|
||||
except Exception as e:
|
||||
RNS.log("The identity "+str(self)+" could not sign the requested message. The contained exception was: "+str(e), RNS.LOG_ERROR)
|
||||
raise e
|
||||
else:
|
||||
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 message: The message to be validated as *bytes*.
|
||||
: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:
|
||||
try:
|
||||
self.pub.verify(
|
||||
signature,
|
||||
message,
|
||||
padding.PSS(
|
||||
mgf=padding.MGF1(hashes.SHA256()),
|
||||
salt_length=padding.PSS.MAX_LENGTH
|
||||
),
|
||||
hashes.SHA256()
|
||||
)
|
||||
self.sig_pub.verify(signature, message)
|
||||
return True
|
||||
except Exception as e:
|
||||
return False
|
||||
|
27
RNS/Link.py
27
RNS/Link.py
@ -33,17 +33,15 @@ class Link:
|
||||
:param peer_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
|
||||
"""
|
||||
|
||||
ECPUBSIZE = 32+32
|
||||
BLOCKSIZE = 16
|
||||
KEYSIZE = 32
|
||||
ECPUBSIZE = 32+32
|
||||
KEYSIZE = 32
|
||||
|
||||
AES_HMAC_OVERHEAD = 58
|
||||
MDU = math.floor((RNS.Reticulum.MDU-AES_HMAC_OVERHEAD)/BLOCKSIZE)*BLOCKSIZE - 1
|
||||
MDU = math.floor((RNS.Reticulum.MDU-RNS.Identity.AES_HMAC_OVERHEAD)/RNS.Identity.AES128_BLOCKSIZE)*RNS.Identity.AES128_BLOCKSIZE - 1
|
||||
|
||||
# TODO: This should not be hardcoded,
|
||||
# but calculated from something like
|
||||
@ -89,11 +87,6 @@ class Link:
|
||||
RNS.Transport.register_link(link)
|
||||
link.last_inbound = time.time()
|
||||
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)
|
||||
return link
|
||||
@ -537,13 +530,13 @@ class Link:
|
||||
except Exception as e:
|
||||
return False
|
||||
|
||||
def link_established_callback(self, callback):
|
||||
def set_link_established_callback(self, callback):
|
||||
self.callbacks.link_established = callback
|
||||
|
||||
def link_closed_callback(self, callback):
|
||||
def set_link_closed_callback(self, 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
|
||||
received over this link.
|
||||
@ -552,7 +545,7 @@ class Link:
|
||||
"""
|
||||
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
|
||||
advertised over this link. If the function returns *True*
|
||||
@ -563,7 +556,7 @@ class Link:
|
||||
"""
|
||||
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
|
||||
transferring over this link.
|
||||
@ -572,7 +565,7 @@ class Link:
|
||||
"""
|
||||
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
|
||||
transferring over this link.
|
||||
|
@ -6,8 +6,16 @@ import RNS
|
||||
|
||||
class Packet:
|
||||
"""
|
||||
The Packet class is used to create packet instances that can be
|
||||
sent over a Reticulum network.
|
||||
The Packet class is used to create packet instances that can be sent
|
||||
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 data: The data payload to be included in the packet as *bytes*.
|
||||
@ -56,14 +64,21 @@ class Packet:
|
||||
|
||||
# This is used to calculate allowable
|
||||
# payload sizes
|
||||
HEADER_MAXSIZE = 23
|
||||
HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
|
||||
MDU = RNS.Reticulum.MDU
|
||||
|
||||
# With an MTU of 500, the maximum RSA-encrypted
|
||||
# amount of data we can send in a single packet
|
||||
# is given by the below calculation; 258 bytes.
|
||||
RSA_MDU = math.floor(MDU/RNS.Identity.DECRYPT_CHUNKSIZE)*RNS.Identity.ENCRYPT_CHUNKSIZE
|
||||
PLAIN_MDU = MDU
|
||||
# TODO: Update this
|
||||
# With an MTU of 500, the maximum of data we can
|
||||
# send in a single encrypted packet is given by
|
||||
# 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
|
||||
"""
|
||||
The maximum size of the payload data in a single unencrypted packet
|
||||
"""
|
||||
|
||||
# TODO: This should be calculated
|
||||
# more intelligently
|
||||
@ -406,7 +421,7 @@ class PacketReceipt:
|
||||
else:
|
||||
return False
|
||||
|
||||
def rtt(self):
|
||||
def get_rtt(self):
|
||||
"""
|
||||
:returns: The round-trip-time in seconds
|
||||
"""
|
||||
@ -439,7 +454,7 @@ class PacketReceipt:
|
||||
"""
|
||||
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.
|
||||
|
||||
@ -449,7 +464,7 @@ class PacketReceipt:
|
||||
|
||||
# Set a function that gets called if the
|
||||
# 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.
|
||||
|
||||
|
@ -17,7 +17,6 @@ class Resource:
|
||||
: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 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 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.
|
||||
@ -134,7 +133,7 @@ class Resource:
|
||||
# Create a resource for transmission to a remote destination
|
||||
# The data passed can be either a bytes-array or a file opened
|
||||
# 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
|
||||
resource_data = None
|
||||
if hasattr(data, "read"):
|
||||
@ -198,7 +197,7 @@ class Resource:
|
||||
self.uncompressed_data = data
|
||||
|
||||
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)
|
||||
self.compressed_data = bz2.compress(self.uncompressed_data)
|
||||
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.
|
||||
"""
|
||||
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.sent_parts
|
||||
self.progress_total_parts = float(self.grand_total_parts)
|
||||
|
@ -108,7 +108,7 @@ class Transport:
|
||||
|
||||
# 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.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_hashes.append(Transport.path_request_destination.hash)
|
||||
|
||||
@ -652,7 +652,7 @@ class Transport:
|
||||
# First, check that the announce is not for a destination
|
||||
# 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):
|
||||
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 = []
|
||||
if packet.destination_hash in Transport.destination_table:
|
||||
random_blobs = Transport.destination_table[packet.destination_hash][4]
|
||||
|
Loading…
Reference in New Issue
Block a user