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
# SOFTWARE.
import os
import math
import time
import RNS
from RNS.Cryptography import Fernet
from .vendor import umsgpack as umsgpack
class Callbacks:
def __init__(self):
@ -38,14 +40,14 @@ class Destination:
instances are used both to create outgoing and incoming endpoints. The
destination type will decide if encryption, and what type, is used in
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.
: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 type: ``RNS.Destination.SINGLE``, ``RNS.Destination.GROUP`` or ``RNS.Destination.PLAIN``.
: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
@ -70,6 +72,7 @@ class Destination:
directions = [IN, OUT]
PR_TAG_WINDOW = 30
RATCHET_COUNT = 128
@staticmethod
def expand_name(identity, app_name, *aspects):
@ -137,6 +140,8 @@ class Destination:
self.type = type
self.direction = direction
self.proof_strategy = Destination.PROVE_NONE
self.ratchets = None
self.ratchets_path = None
self.mtu = 0
self.path_responses = {}
@ -170,6 +175,57 @@ class Destination:
"""
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):
"""
@ -211,6 +267,11 @@ class Destination:
destination_hash = self.hash
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 isinstance(self.default_app_data, bytes):
app_data = self.default_app_data

View File

@ -221,6 +221,16 @@ class Identity:
"""
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
def validate_announce(packet, only_validate_signature=False):
try: