Added automatic ratchet reload if required ratchet is unavailable

This commit is contained in:
Mark Qvist 2024-09-08 17:48:25 +02:00
parent 3a580e74de
commit a072a5b074
2 changed files with 58 additions and 32 deletions

View File

@ -23,6 +23,7 @@
import os import os
import math import math
import time import time
import threading
import RNS import RNS
from RNS.Cryptography import Fernet from RNS.Cryptography import Fernet
@ -152,6 +153,7 @@ class Destination:
self.ratchets = None self.ratchets = None
self.ratchets_path = None self.ratchets_path = None
self.ratchet_interval = Destination.RATCHET_INTERVAL self.ratchet_interval = Destination.RATCHET_INTERVAL
self.ratchet_file_lock = threading.Lock()
self.retained_ratchets = Destination.RATCHET_COUNT self.retained_ratchets = Destination.RATCHET_COUNT
self.latest_ratchet_time = None self.latest_ratchet_time = None
self.latest_ratchet_id = None self.latest_ratchet_id = None
@ -196,6 +198,7 @@ class Destination:
def _persist_ratchets(self): def _persist_ratchets(self):
try: try:
with self.ratchet_file_lock:
packed_ratchets = umsgpack.packb(self.ratchets) packed_ratchets = umsgpack.packb(self.ratchets)
persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets} persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets}
ratchets_file = open(self.ratchets_path, "wb") ratchets_file = open(self.ratchets_path, "wb")
@ -267,9 +270,6 @@ class Destination:
self.rotate_ratchets() self.rotate_ratchets()
ratchet = RNS.Identity._ratchet_public_bytes(self.ratchets[0]) ratchet = RNS.Identity._ratchet_public_bytes(self.ratchets[0])
# TODO: Remove at some point
RNS.log(f"Including ratchet {RNS.prettyhexrep(RNS.Identity.truncated_hash(ratchet))} in announce", RNS.LOG_EXTREME)
if app_data == None and self.default_app_data != None: if app_data == None and self.default_app_data != None:
if isinstance(self.default_app_data, bytes): if isinstance(self.default_app_data, bytes):
app_data = self.default_app_data app_data = self.default_app_data
@ -417,24 +417,9 @@ class Destination:
if link != None: if link != None:
self.links.append(link) self.links.append(link)
def enable_ratchets(self, ratchets_path): def _reload_ratchets(self, ratchets_path):
"""
Enables ratchets on the destination. When ratchets are enabled, Reticulum will automatically rotate
the keys used to encrypt packets to this destination, and include the latest ratchet key in announces.
Enabling ratchets on a destination will provide forward secrecy for packets sent to that destination,
even when sent outside a ``Link``. The normal Reticulum ``Link`` establishment procedure already performs
its own ephemeral key exchange for each link establishment, which means that ratchets are not necessary
to provide forward secrecy for links.
Enabling ratchets will have a small impact on announce size, adding 32 bytes to every sent announce.
:param ratchets_path: The path to a file to store ratchet data in.
:returns: True if the operation succeeded, otherwise False.
"""
if ratchets_path != None:
self.latest_ratchet_time = 0
if os.path.isfile(ratchets_path): if os.path.isfile(ratchets_path):
with self.ratchet_file_lock:
try: try:
ratchets_file = open(ratchets_path, "rb") ratchets_file = open(ratchets_path, "rb")
persisted_data = umsgpack.unpackb(ratchets_file.read()) persisted_data = umsgpack.unpackb(ratchets_file.read())
@ -455,6 +440,25 @@ class Destination:
self.ratchets_path = ratchets_path self.ratchets_path = ratchets_path
self._persist_ratchets() self._persist_ratchets()
def enable_ratchets(self, ratchets_path):
"""
Enables ratchets on the destination. When ratchets are enabled, Reticulum will automatically rotate
the keys used to encrypt packets to this destination, and include the latest ratchet key in announces.
Enabling ratchets on a destination will provide forward secrecy for packets sent to that destination,
even when sent outside a ``Link``. The normal Reticulum ``Link`` establishment procedure already performs
its own ephemeral key exchange for each link establishment, which means that ratchets are not necessary
to provide forward secrecy for links.
Enabling ratchets will have a small impact on announce size, adding 32 bytes to every sent announce.
:param ratchets_path: The path to a file to store ratchet data in.
:returns: True if the operation succeeded, otherwise False.
"""
if ratchets_path != None:
self.latest_ratchet_time = 0
self._reload_ratchets(ratchets_path)
# TODO: Remove at some point # TODO: Remove at some point
RNS.log("Ratchets enabled on "+str(self), RNS.LOG_DEBUG) RNS.log("Ratchets enabled on "+str(self), RNS.LOG_DEBUG)
return True return True
@ -568,6 +572,7 @@ class Destination:
if self.type == Destination.SINGLE and self.identity != None: if self.type == Destination.SINGLE and self.identity != None:
selected_ratchet = RNS.Identity.get_ratchet(self.hash) selected_ratchet = RNS.Identity.get_ratchet(self.hash)
if selected_ratchet:
self.latest_ratchet_id = RNS.Identity.truncated_hash(selected_ratchet) self.latest_ratchet_id = RNS.Identity.truncated_hash(selected_ratchet)
return self.identity.encrypt(plaintext, ratchet=selected_ratchet) return self.identity.encrypt(plaintext, ratchet=selected_ratchet)
@ -592,7 +597,28 @@ class Destination:
return ciphertext return ciphertext
if self.type == Destination.SINGLE and self.identity != None: if self.type == Destination.SINGLE and self.identity != None:
return self.identity.decrypt(ciphertext, ratchets=self.ratchets, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self) if self.ratchets:
decrypted = None
try:
decrypted = self.identity.decrypt(ciphertext, ratchets=self.ratchets, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
except:
decrypted = None
if not decrypted:
try:
RNS.log(f"Decryption with ratchets failed on {self}, reloading ratchets from storage and retrying", RNS.LOG_ERROR)
self._reload_ratchets(self.ratchets_path)
decrypted = self.identity.decrypt(ciphertext, ratchets=self.ratchets, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
except Exception as e:
RNS.log(f"Decryption still failing after ratchet reload. The contained exception was: {e}", RNS.LOG_ERROR)
raise e
RNS.log("Decryption succeeded after ratchet reload", RNS.LOG_NOTICE)
return decrypted
else:
return self.identity.decrypt(ciphertext, ratchets=None, enforce_ratchets=self.__enforce_ratchets, ratchet_id_receiver=self)
if self.type == Destination.GROUP: if self.type == Destination.GROUP:
if hasattr(self, "prv") and self.prv != None: if hasattr(self, "prv") and self.prv != None:

View File

@ -324,11 +324,11 @@ class Identity:
if not destination_hash in Identity.known_ratchets: if not destination_hash in Identity.known_ratchets:
ratchetdir = RNS.Reticulum.storagepath+"/ratchets" ratchetdir = RNS.Reticulum.storagepath+"/ratchets"
hexhash = RNS.hexrep(destination_hash, delimit=False) hexhash = RNS.hexrep(destination_hash, delimit=False)
ratchet_path = f"{ratchetdir}/hexhash" ratchet_path = f"{ratchetdir}/{hexhash}"
if os.path.isfile(ratchet_path): if os.path.isfile(ratchet_path):
try: try:
ratchet_file = open(ratchet_path, "rb") ratchet_file = open(ratchet_path, "rb")
ratchet_data = umsgpack.unpackb(ratchets_file.read()) ratchet_data = umsgpack.unpackb(ratchet_file.read())
if time.time() < ratchet_data["received"]+Identity.RATCHET_EXPIRY and len(ratchet_data["ratchet"]) == Identity.RATCHETSIZE//8: 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"] Identity.known_ratchets[destination_hash] = ratchet_data["ratchet"]
else: else: