Implemented ratchet generation, rotation and persistence

This commit is contained in:
Mark Qvist 2024-09-04 12:02:55 +02:00
parent 2bf75f60bc
commit 54eaff203f
2 changed files with 73 additions and 2 deletions

View File

@ -20,11 +20,13 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE. # SOFTWARE.
import os
import math import math
import time import time
import RNS import RNS
from RNS.Cryptography import Fernet from RNS.Cryptography import Fernet
from .vendor import umsgpack as umsgpack
class Callbacks: class Callbacks:
def __init__(self): def __init__(self):
@ -38,14 +40,14 @@ class Destination:
instances are used both to create outgoing and incoming endpoints. The instances are used both to create outgoing and incoming endpoints. The
destination type will decide if encryption, and what type, is used in destination type will decide if encryption, and what type, is used in
communication with the endpoint. A destination can also announce its communication with the endpoint. A destination can also announce its
presence on the network, which will also distribute necessary keys for presence on the network, which will distribute necessary keys for
encrypted communication with it. encrypted communication with it.
:param identity: An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing. :param identity: An instance of :ref:`RNS.Identity<api-identity>`. Can hold only public keys for an outgoing destination, or holding private keys for an ingoing.
:param direction: ``RNS.Destination.IN`` or ``RNS.Destination.OUT``. :param direction: ``RNS.Destination.IN`` or ``RNS.Destination.OUT``.
:param type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``. :param type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``.
:param app_name: A string specifying the app name. :param app_name: A string specifying the app name.
:param \*aspects: Any non-zero number of string arguments. :param \\*aspects: Any non-zero number of string arguments.
""" """
# Constants # Constants
@ -70,6 +72,7 @@ class Destination:
directions = [IN, OUT] directions = [IN, OUT]
PR_TAG_WINDOW = 30 PR_TAG_WINDOW = 30
RATCHET_COUNT = 128
@staticmethod @staticmethod
def expand_name(identity, app_name, *aspects): def expand_name(identity, app_name, *aspects):
@ -137,6 +140,8 @@ class Destination:
self.type = type self.type = type
self.direction = direction self.direction = direction
self.proof_strategy = Destination.PROVE_NONE self.proof_strategy = Destination.PROVE_NONE
self.ratchets = None
self.ratchets_path = None
self.mtu = 0 self.mtu = 0
self.path_responses = {} self.path_responses = {}
@ -170,6 +175,57 @@ class Destination:
""" """
return "<"+self.name+"/"+self.hexhash+">" return "<"+self.name+"/"+self.hexhash+">"
def enable_ratchets(self, ratchets_path):
if ratchets_path != None:
if os.path.isfile(ratchets_path):
try:
ratchets_file = open(ratchets_path, "rb")
persisted_data = umsgpack.unpackb(ratchets_file.read())
if "signature" in persisted_data and "ratchets" in persisted_data:
if self.identity.validate(persisted_data["signature"], persisted_data["ratchets"]):
self.ratchets = umsgpack.unpackb(persisted_data["ratchets"])
self.ratchets_path = ratchets_path
else:
raise KeyError("Invalid ratchet file signature")
except Exception as e:
self.ratchets = None
self.ratchets_path = None
raise OSError("Could not read ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
else:
RNS.log("No existing ratchet data found, initialising new ratchet file for "+str(self), RNS.LOG_DEBUG)
self.ratchets = []
self.ratchets_path = ratchets_path
self.persist_ratchets()
RNS.log("Enabled ratchets on "+str(self), RNS.LOG_DEBUG) # TODO: Remove
return True
else:
raise ValueError("No ratchet file path specified for "+str(self))
def persist_ratchets(self):
try:
packed_ratchets = umsgpack.packb(self.ratchets)
persisted_data = {"signature": self.sign(packed_ratchets), "ratchets": packed_ratchets}
ratchets_file = open(self.ratchets_path, "wb")
ratchets_file.write(umsgpack.packb(persisted_data))
ratchets_file.close()
except Exception as e:
self.ratchets = None
self.ratchets_path = None
raise OSError("Could not write ratchet file contents for "+str(self)+". The contained exception was: "+str(e), RNS.LOG_ERROR)
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()
self.ratchets.insert(0, new_ratchet)
if len (self.ratchets) > Destination.RATCHET_COUNT:
self.ratchets = self.ratchets[:Destination.RATCHET_COUNT]
self.persist_ratchets()
else:
raise SystemError("Cannot rotate ratchet on "+str(self)+", ratchets are not enabled")
def announce(self, app_data=None, path_response=False, attached_interface=None, tag=None, send=True): def announce(self, app_data=None, path_response=False, attached_interface=None, tag=None, send=True):
""" """
@ -211,6 +267,11 @@ class Destination:
destination_hash = self.hash destination_hash = self.hash
random_hash = RNS.Identity.get_random_hash()[0:5]+int(time.time()).to_bytes(5, "big") random_hash = RNS.Identity.get_random_hash()[0:5]+int(time.time()).to_bytes(5, "big")
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
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

View File

@ -221,6 +221,16 @@ class Identity:
""" """
return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8)) return Identity.truncated_hash(os.urandom(Identity.TRUNCATED_HASHLENGTH//8))
@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()
@staticmethod @staticmethod
def validate_announce(packet, only_validate_signature=False): def validate_announce(packet, only_validate_signature=False):
try: try: