mirror of
https://github.com/markqvist/Reticulum.git
synced 2024-10-01 03:15:44 -04:00
Implemented ratchets
This commit is contained in:
parent
c32086c6f1
commit
a11f14e75f
@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@ -72,7 +72,7 @@ class Destination:
|
||||
directions = [IN, OUT]
|
||||
|
||||
PR_TAG_WINDOW = 30
|
||||
RATCHET_COUNT = 128
|
||||
RATCHET_COUNT = 256
|
||||
|
||||
@staticmethod
|
||||
def expand_name(identity, app_name, *aspects):
|
||||
@ -219,7 +219,7 @@ class Destination:
|
||||
def rotate_ratchets(self):
|
||||
if self.ratchets != None:
|
||||
RNS.log("Rotating ratchets for "+str(self), RNS.LOG_DEBUG) # TODO: Remove
|
||||
new_ratchet = RNS.Identity.generate_ratchet()
|
||||
new_ratchet = RNS.Identity._generate_ratchet()
|
||||
self.ratchets.insert(0, new_ratchet)
|
||||
if len (self.ratchets) > Destination.RATCHET_COUNT:
|
||||
self.ratchets = self.ratchets[:Destination.RATCHET_COUNT]
|
||||
@ -241,6 +241,7 @@ class Destination:
|
||||
if self.direction != Destination.IN:
|
||||
raise TypeError("Only IN destination types can be announced")
|
||||
|
||||
ratchet = b""
|
||||
now = time.time()
|
||||
stale_responses = []
|
||||
for entry_tag in self.path_responses:
|
||||
@ -269,8 +270,8 @@ class Destination:
|
||||
|
||||
if self.ratchets != None:
|
||||
self.rotate_ratchets()
|
||||
ratchet_pub = RNS.Identity.ratchet_public_bytes(self.ratchets[0])
|
||||
RNS.log(f"Including {len(ratchet_pub)*8}-bit ratchet {RNS.hexrep(ratchet_pub)} in announce", RNS.LOG_DEBUG) # TODO: Remove
|
||||
ratchet = RNS.Identity._ratchet_public_bytes(self.ratchets[0])
|
||||
RNS.log(f"Including {len(ratchet)*8}-bit ratchet {RNS.hexrep(ratchet)} in announce", RNS.LOG_DEBUG) # TODO: Remove
|
||||
|
||||
if app_data == None and self.default_app_data != None:
|
||||
if isinstance(self.default_app_data, bytes):
|
||||
@ -280,13 +281,12 @@ class Destination:
|
||||
if isinstance(returned_app_data, bytes):
|
||||
app_data = returned_app_data
|
||||
|
||||
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash
|
||||
signed_data = self.hash+self.identity.get_public_key()+self.name_hash+random_hash+ratchet
|
||||
if app_data != None:
|
||||
signed_data += app_data
|
||||
|
||||
signature = self.identity.sign(signed_data)
|
||||
|
||||
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+signature
|
||||
announce_data = self.identity.get_public_key()+self.name_hash+random_hash+ratchet+signature
|
||||
|
||||
if app_data != None:
|
||||
announce_data += app_data
|
||||
@ -298,8 +298,13 @@ class Destination:
|
||||
else:
|
||||
announce_context = RNS.Packet.NONE
|
||||
|
||||
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context, attached_interface = attached_interface)
|
||||
if ratchet:
|
||||
context_flag = RNS.Packet.FLAG_SET
|
||||
else:
|
||||
context_flag = RNS.Packet.FLAG_UNSET
|
||||
|
||||
announce_packet = RNS.Packet(self, announce_data, RNS.Packet.ANNOUNCE, context = announce_context,
|
||||
attached_interface = attached_interface, context_flag=context_flag)
|
||||
if send:
|
||||
announce_packet.send()
|
||||
else:
|
||||
@ -485,7 +490,7 @@ class Destination:
|
||||
return plaintext
|
||||
|
||||
if self.type == Destination.SINGLE and self.identity != None:
|
||||
return self.identity.encrypt(plaintext)
|
||||
return self.identity.encrypt(plaintext, ratchet=RNS.Identity.get_ratchet(self.hash))
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
if hasattr(self, "prv") and self.prv != None:
|
||||
@ -510,7 +515,7 @@ class Destination:
|
||||
return ciphertext
|
||||
|
||||
if self.type == Destination.SINGLE and self.identity != None:
|
||||
return self.identity.decrypt(ciphertext)
|
||||
return self.identity.decrypt(ciphertext, ratchets=self.ratchets)
|
||||
|
||||
if self.type == Destination.GROUP:
|
||||
if hasattr(self, "prv") and self.prv != None:
|
||||
|
161
RNS/Identity.py
161
RNS/Identity.py
@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@ -52,6 +52,9 @@ class Identity:
|
||||
X25519 key size in bits. A complete key is the concatenation of a 256 bit encryption key, and a 256 bit signing key.
|
||||
"""
|
||||
|
||||
RATCHETSIZE = 256
|
||||
RATCHET_EXPIRY = 60*60*24*30
|
||||
|
||||
# Non-configurable constants
|
||||
FERNET_OVERHEAD = RNS.Cryptography.Fernet.FERNET_OVERHEAD
|
||||
AES128_BLOCKSIZE = 16 # In bytes
|
||||
@ -67,6 +70,7 @@ class Identity:
|
||||
|
||||
# Storage
|
||||
known_destinations = {}
|
||||
known_ratchets = {}
|
||||
|
||||
@staticmethod
|
||||
def remember(packet_hash, destination_hash, public_key, app_data = None):
|
||||
@ -222,29 +226,102 @@ class Identity:
|
||||
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
|
||||
|
||||
@staticmethod
|
||||
def generate_ratchet():
|
||||
def _ratchet_public_bytes(ratchet):
|
||||
return X25519PrivateKey.from_private_bytes(ratchet).public_key().public_bytes()
|
||||
|
||||
@staticmethod
|
||||
def _generate_ratchet():
|
||||
ratchet_prv = X25519PrivateKey.generate()
|
||||
ratchet_pub = ratchet_prv.public_key()
|
||||
return ratchet_prv.private_bytes()
|
||||
|
||||
@staticmethod
|
||||
def ratchet_public_bytes(ratchet):
|
||||
return X25519PrivateKey.from_private_bytes(ratchet).public_key().public_bytes()
|
||||
def _remember_ratchet(destination_hash, ratchet):
|
||||
RNS.log(f"Remembering ratchet {RNS.hexrep(ratchet)} for {RNS.prettyhexrep(destination_hash)}", RNS.LOG_DEBUG) # TODO: Remove
|
||||
try:
|
||||
Identity.known_ratchets[destination_hash] = ratchet
|
||||
hexhash = RNS.hexrep(destination_hash, delimit=False)
|
||||
ratchet_data = {"ratchet": ratchet, "received": time.time()}
|
||||
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
|
||||
if not os.path.isdir(ratchetdir):
|
||||
os.makedirs(ratchetdir)
|
||||
|
||||
outpath = f"{ratchetdir}/{hexhash}.out"
|
||||
finalpath = f"{ratchetdir}/{hexhash}"
|
||||
ratchet_file = open(outpath, "wb")
|
||||
ratchet_file.write(umsgpack.packb(ratchet_data))
|
||||
ratchet_file.close()
|
||||
os.rename(outpath, finalpath)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"Could not persist ratchet for {RNS.prettyhexrep(destination_hash)} to storage.", RNS.LOG_ERROR)
|
||||
RNS.log(f"The contained exception was: {e}")
|
||||
RNS.trace_exception(e)
|
||||
|
||||
@staticmethod
|
||||
def get_ratchet(destination_hash):
|
||||
if not destination_hash in Identity.known_ratchets:
|
||||
RNS.log(f"Trying to load ratchet for {RNS.prettyhexrep(destination_hash)} from storage") # TODO: Remove
|
||||
ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
|
||||
hexhash = RNS.hexrep(destination_hash, delimit=False)
|
||||
ratchet_path = f"{ratchetdir}/hexhash"
|
||||
if os.path.isfile(ratchet_path):
|
||||
try:
|
||||
ratchet_file = open(ratchet_path, "rb")
|
||||
ratchet_data = umsgpack.unpackb(ratchets_file.read())
|
||||
if time.time() < ratchet_data["received"]+Identity.RATCHET_EXPIRY and len(ratchet_data["ratchet"]) == Identity.RATCHETSIZE//8:
|
||||
Identity.known_ratchets[destination_hash] = ratchet_data["ratchet"]
|
||||
else:
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
RNS.log(f"An error occurred while loading ratchet data for {RNS.prettyhexrep(destination_hash)} from storage.", RNS.LOG_ERROR)
|
||||
RNS.log(f"The contained exception was: {e}", RNS.LOG_ERROR)
|
||||
return None
|
||||
|
||||
if destination_hash in Identity.known_ratchets:
|
||||
return Identity.known_ratchets[destination_hash]
|
||||
else:
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def validate_announce(packet, only_validate_signature=False):
|
||||
try:
|
||||
if packet.packet_type == RNS.Packet.ANNOUNCE:
|
||||
keysize = Identity.KEYSIZE//8
|
||||
ratchetsize = Identity.RATCHETSIZE//8
|
||||
name_hash_len = Identity.NAME_HASH_LENGTH//8
|
||||
sig_len = Identity.SIGLENGTH//8
|
||||
destination_hash = packet.destination_hash
|
||||
public_key = packet.data[:Identity.KEYSIZE//8]
|
||||
name_hash = packet.data[Identity.KEYSIZE//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8]
|
||||
random_hash = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10]
|
||||
signature = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10:Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8]
|
||||
app_data = b""
|
||||
if len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
|
||||
app_data = packet.data[Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:]
|
||||
|
||||
signed_data = destination_hash+public_key+name_hash+random_hash+app_data
|
||||
# Get public key bytes from announce
|
||||
public_key = packet.data[:keysize]
|
||||
|
||||
# If the packet context flag is set,
|
||||
# this announce contains a new ratchet
|
||||
if packet.context_flag == RNS.Packet.FLAG_SET:
|
||||
name_hash = packet.data[keysize:keysize+name_hash_len ]
|
||||
random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
|
||||
ratchet = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+ratchetsize]
|
||||
signature = packet.data[keysize+name_hash_len+10+ratchetsize:keysize+name_hash_len+10+ratchetsize+sig_len]
|
||||
app_data = b""
|
||||
if len(packet.data) > keysize+name_hash_len+10+sig_len+ratchetsize:
|
||||
app_data = packet.data[keysize+name_hash_len+10+sig_len+ratchetsize:]
|
||||
|
||||
# If the packet context flag is not set,
|
||||
# this announce does not contain a ratchet
|
||||
else:
|
||||
ratchet = b""
|
||||
name_hash = packet.data[keysize:keysize+name_hash_len]
|
||||
random_hash = packet.data[keysize+name_hash_len:keysize+name_hash_len+10]
|
||||
signature = packet.data[keysize+name_hash_len+10:keysize+name_hash_len+10+sig_len]
|
||||
app_data = b""
|
||||
if len(packet.data) > keysize+name_hash_len+10+sig_len:
|
||||
app_data = packet.data[keysize+name_hash_len+10+sig_len:]
|
||||
|
||||
signed_data = destination_hash+public_key+name_hash+random_hash+ratchet+app_data
|
||||
|
||||
if not len(packet.data) > Identity.KEYSIZE//8+Identity.NAME_HASH_LENGTH//8+10+Identity.SIGLENGTH//8:
|
||||
app_data = None
|
||||
@ -291,6 +368,9 @@ class Identity:
|
||||
else:
|
||||
RNS.log("Valid announce for "+RNS.prettyhexrep(destination_hash)+" "+str(packet.hops)+" hops away, received on "+str(packet.receiving_interface)+signal_str, RNS.LOG_EXTREME)
|
||||
|
||||
if ratchet:
|
||||
Identity._remember_ratchet(destination_hash, ratchet)
|
||||
|
||||
return True
|
||||
|
||||
else:
|
||||
@ -479,7 +559,7 @@ class Identity:
|
||||
def get_context(self):
|
||||
return None
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
def encrypt(self, plaintext, ratchet=None):
|
||||
"""
|
||||
Encrypts information for the identity.
|
||||
|
||||
@ -491,7 +571,13 @@ class Identity:
|
||||
ephemeral_key = X25519PrivateKey.generate()
|
||||
ephemeral_pub_bytes = ephemeral_key.public_key().public_bytes()
|
||||
|
||||
shared_key = ephemeral_key.exchange(self.pub)
|
||||
if ratchet != None:
|
||||
RNS.log(f"Encrypting with ratchet {RNS.hexrep(ratchet)}", RNS.LOG_DEBUG) # TODO: Remove
|
||||
target_public_key = X25519PublicKey.from_public_bytes(ratchet)
|
||||
else:
|
||||
target_public_key = self.pub
|
||||
|
||||
shared_key = ephemeral_key.exchange(target_public_key)
|
||||
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
@ -509,7 +595,7 @@ class Identity:
|
||||
raise KeyError("Encryption failed because identity does not hold a public key")
|
||||
|
||||
|
||||
def decrypt(self, ciphertext_token):
|
||||
def decrypt(self, ciphertext_token, ratchets=None):
|
||||
"""
|
||||
Decrypts information for the identity.
|
||||
|
||||
@ -523,19 +609,40 @@ class Identity:
|
||||
try:
|
||||
peer_pub_bytes = ciphertext_token[:Identity.KEYSIZE//8//2]
|
||||
peer_pub = X25519PublicKey.from_public_bytes(peer_pub_bytes)
|
||||
|
||||
shared_key = self.prv.exchange(peer_pub)
|
||||
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
fernet = Fernet(derived_key)
|
||||
ciphertext = ciphertext_token[Identity.KEYSIZE//8//2:]
|
||||
plaintext = fernet.decrypt(ciphertext)
|
||||
|
||||
if ratchets:
|
||||
for ratchet in ratchets:
|
||||
try:
|
||||
ratchet_prv = X25519PrivateKey.from_private_bytes(ratchet)
|
||||
shared_key = ratchet_prv.exchange(peer_pub)
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
fernet = Fernet(derived_key)
|
||||
plaintext = fernet.decrypt(ciphertext)
|
||||
RNS.log(f"Decrypted with ratchet {RNS.hexrep(ratchet_prv.public_key().public_bytes())}", RNS.LOG_DEBUG) # TODO: Remove
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
# RNS.log("Decryption using this ratchet failed", RNS.LOG_DEBUG) # TODO: Remove
|
||||
|
||||
if plaintext == None:
|
||||
shared_key = self.prv.exchange(peer_pub)
|
||||
derived_key = RNS.Cryptography.hkdf(
|
||||
length=32,
|
||||
derive_from=shared_key,
|
||||
salt=self.get_salt(),
|
||||
context=self.get_context(),
|
||||
)
|
||||
|
||||
fernet = Fernet(derived_key)
|
||||
plaintext = fernet.decrypt(ciphertext)
|
||||
|
||||
except Exception as e:
|
||||
RNS.log("Decryption by "+RNS.prettyhexrep(self.hash)+" failed: "+str(e), RNS.LOG_DEBUG)
|
||||
|
@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@ -83,6 +83,10 @@ class Packet:
|
||||
LRRTT = 0xFE # Packet is a link request round-trip time measurement
|
||||
LRPROOF = 0xFF # Packet is a link request proof
|
||||
|
||||
# Context flag values
|
||||
FLAG_SET = 0x01
|
||||
FLAG_UNSET = 0x00
|
||||
|
||||
# This is used to calculate allowable
|
||||
# payload sizes
|
||||
HEADER_MAXSIZE = RNS.Reticulum.HEADER_MAXSIZE
|
||||
@ -102,7 +106,9 @@ class Packet:
|
||||
|
||||
TIMEOUT_PER_HOP = RNS.Reticulum.DEFAULT_PER_HOP_TIMEOUT
|
||||
|
||||
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST, header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True):
|
||||
def __init__(self, destination, data, packet_type = DATA, context = NONE, transport_type = RNS.Transport.BROADCAST,
|
||||
header_type = HEADER_1, transport_id = None, attached_interface = None, create_receipt = True, context_flag=FLAG_UNSET):
|
||||
|
||||
if destination != None:
|
||||
if transport_type == None:
|
||||
transport_type = RNS.Transport.BROADCAST
|
||||
@ -111,6 +117,7 @@ class Packet:
|
||||
self.packet_type = packet_type
|
||||
self.transport_type = transport_type
|
||||
self.context = context
|
||||
self.context_flag = context_flag
|
||||
|
||||
self.hops = 0;
|
||||
self.destination = destination
|
||||
@ -142,9 +149,10 @@ class Packet:
|
||||
|
||||
def get_packed_flags(self):
|
||||
if self.context == Packet.LRPROOF:
|
||||
packed_flags = (self.header_type << 6) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | self.packet_type
|
||||
packed_flags = (self.header_type << 6) | (self.context_flag << 5) | (self.transport_type << 4) | (RNS.Destination.LINK << 2) | 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.context_flag << 5) | (self.transport_type << 4) | (self.destination.type << 2) | self.packet_type
|
||||
|
||||
return packed_flags
|
||||
|
||||
def pack(self):
|
||||
@ -216,7 +224,8 @@ class Packet:
|
||||
self.hops = self.raw[1]
|
||||
|
||||
self.header_type = (self.flags & 0b01000000) >> 6
|
||||
self.transport_type = (self.flags & 0b00110000) >> 4
|
||||
self.context_flag = (self.flags & 0b00100000) >> 5
|
||||
self.transport_type = (self.flags & 0b00010000) >> 4
|
||||
self.destination_type = (self.flags & 0b00001100) >> 2
|
||||
self.packet_type = (self.flags & 0b00000011)
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
|
@ -1,6 +1,6 @@
|
||||
# MIT License
|
||||
#
|
||||
# Copyright (c) 2016-2023 Mark Qvist / unsigned.io and contributors.
|
||||
# Copyright (c) 2016-2024 Mark Qvist / unsigned.io and contributors.
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
@ -403,7 +403,8 @@ class Transport:
|
||||
header_type = RNS.Packet.HEADER_2,
|
||||
transport_type = Transport.TRANSPORT,
|
||||
transport_id = Transport.identity.hash,
|
||||
attached_interface = attached_interface
|
||||
attached_interface = attached_interface,
|
||||
context_flag = packet.context_flag,
|
||||
)
|
||||
|
||||
new_packet.hops = announce_entry[4]
|
||||
|
Loading…
Reference in New Issue
Block a user