1+ """
2+ logic/pfs.py
3+ -----------------
4+ Handles Perfect Forward Secrecy (PFS) ephemeral keys exchange and rotation for contacts.
5+
6+ Handles:
7+ - Generates and rotates ephemeral (one-time use) ML-KEM-1024 keys and (medium-term) Classic McEliece keys.
8+ - Uses per-contact hash chains to prevent replay attacks, and verifies authenticity using ML-DSA-87.
9+ - Sends and receives signed ephemeral keys using long-term signing keys.
10+ - Updates local account storage with new key material after successful exchange.
11+ """
12+
113from core .requests import http_request
214from logic .storage import save_account_data
315from core .crypto import (
2032import copy
2133import json
2234import logging
35+ import threading
36+ import queue
2337
2438logger = logging .getLogger (__name__ )
2539
2640
27- def send_new_ephemeral_keys (user_data , user_data_lock , contact_id , ui_queue ) -> None :
41+ def send_new_ephemeral_keys (user_data : dict , user_data_lock : threading .Lock , contact_id : str , ui_queue : queue .Queue ) -> None :
42+ """
43+ Generate and send fresh ephemeral keys to a contact.
44+
45+ - Maintains a per-contact hash chain for signing key material.
46+ - Generates new Kyber1024 keys every call.
47+ - Optionally rotates McEliece keys if rotation threshold is reached.
48+ - Signs all key material with the long-term signing key.
49+ - Sends to the server using an authenticated HTTP request.
50+ - If successful, stores new keys in `user_data["tmp"]` for later update.
51+
52+ Args:
53+ user_data (dict): Shared user account state.
54+ user_data_lock (threading.Lock): Lock protecting shared state.
55+ contact_id (str): Target contact's ID.
56+ ui_queue (queue.Queue): UI queue for showing error messages.
57+ """
58+
2859 with user_data_lock :
2960 user_data_copied = copy .deepcopy (user_data )
30-
31- rotation_counter = user_data [ "contacts" ][ contact_id ][ "ephemeral_keys" ][ "our_keys" ][ CLASSIC_MCELIECE_8_F_NAME ][ "rotation_counter" ]
32- rotate_at = user_data [ "contacts" ][ contact_id ][ "ephemeral_keys" ][ "our_keys" ][ CLASSIC_MCELIECE_8_F_NAME ][ "rotate_at " ]
61+
62+ server_url = user_data_copied [ "server_url" ]
63+ auth_token = user_data_copied [ "token " ]
3364
34- server_url = user_data [ "server_url" ]
35- auth_token = user_data [ "token " ]
65+ rotation_counter = user_data_copied [ "contacts" ][ contact_id ][ "ephemeral_keys" ][ "our_keys" ][ CLASSIC_MCELIECE_8_F_NAME ][ "rotation_counter" ]
66+ rotate_at = user_data_copied [ "contacts" ][ contact_id ][ "ephemeral_keys" ][ "our_keys" ][ CLASSIC_MCELIECE_8_F_NAME ][ "rotate_at " ]
3667
3768 lt_sign_private_key = user_data_copied ["contacts" ][contact_id ]["lt_sign_keys" ]["our_keys" ]["private_key" ]
3869
@@ -48,7 +79,7 @@ def send_new_ephemeral_keys(user_data, user_data_lock, contact_id, ui_queue) ->
4879 # We continue the hash chain
4980 our_hash_chain = sha3_512 (our_hash_chain )
5081
51- # Generate new Kyber1024 keys for us
82+ # Generate new ML-KEM-1024 keys for us
5283 kyber_private_key , kyber_public_key = generate_kem_keys (ML_KEM_1024_NAME )
5384 publickeys_hashchain = our_hash_chain + kyber_public_key
5485
@@ -100,7 +131,18 @@ def send_new_ephemeral_keys(user_data, user_data_lock, contact_id, ui_queue) ->
100131
101132
102133
103- def update_ephemeral_keys (user_data , user_data_lock ) -> None :
134+ def update_ephemeral_keys (user_data : dict , user_data_lock : threading .Lock ) -> None :
135+ """
136+ Commit newly generated ephemeral keys to permanent storage.
137+
138+ - Moves keys from `user_data["tmp"]` into `user_data["contacts"]`.
139+ - Clears temporary key buffers after update.
140+ - Saves account state to disk.
141+
142+ Args:
143+ user_data (dict): Shared user account state.
144+ user_data_lock (threading.Lock): Lock protecting shared state.
145+ """
104146 with user_data_lock :
105147 new_ml_kem_keys = user_data ["tmp" ]["new_ml_kem_keys" ]
106148 new_code_kem_keys = user_data ["tmp" ]["new_code_kem_keys" ]
@@ -126,24 +168,42 @@ def update_ephemeral_keys(user_data, user_data_lock) -> None:
126168
127169
128170
129- def pfs_data_handler (user_data , user_data_lock , user_data_copied , ui_queue , message ) -> None :
171+ def pfs_data_handler (user_data : dict , user_data_lock : threading .Lock , user_data_copied : dict , ui_queue : queue .Queue , message : dict ) -> None :
172+ """
173+ Handle incoming PFS (Perfect Forward Secrecy) key messages from contacts.
174+
175+ - Validates the contact exists and their signing key is verified.
176+ - Verifies the signature on the ephemeral keys + hash chain.
177+ - Updates stored contact ephemeral keys and hash chains.
178+ - If we don't have keys yet for this contact, triggers sending ours.
179+ - Saves updated account state to disk.
180+
181+ Args:
182+ user_data (dict): Shared user account state.
183+ user_data_lock (threading.Lock): Lock protecting shared state.
184+ user_data_copied (dict): A read-only copy of user_data for consistency.
185+ ui_queue (queue.Queue): UI queue for notifications/errors.
186+ message (dict): Incoming PFS message from the server.
187+
188+ Returns:
189+ None
190+ """
130191 contact_id = message ["sender" ]
131192
132193 if contact_id not in user_data_copied ["contacts" ]:
133- logger .error ("Contact is missing , maybe we (or they) are not synced? Not sure, but we will ignore this PFS request for now " )
134- logger .debug ("Our saved contacts: %s" , json . dumps (user_data_copied ["contacts" ], indent = 2 ))
194+ logger .error ("Contact is not saved. , maybe we (or they) are not synced? Ignoring this PFS message. " )
195+ logger .debug ("Our saved contacts: %s" , str (user_data_copied ["contacts" ]))
135196 return
136197
137- # Contact's main long-term public signing key
198+ # Contact's per-contact signing public- key
138199 contact_lt_public_key = user_data_copied ["contacts" ][contact_id ]["lt_sign_keys" ]["contact_public_key" ]
139200
140-
141201 if not contact_lt_public_key :
142202 logger .error ("Contact long-term signing key is missing... 0 clue how we reached here, but we aint continuing.." )
143203 return
144204
145205 if not user_data_copied ["contacts" ][contact_id ]["lt_sign_key_smp" ]["verified" ]:
146- logger .error ("Contact long-term signing key is not verified! it is possible that this is a MiTM attack, we ignoring this PFS for now ." )
206+ logger .error ("Contact long-term signing key is not verified! We will ignore this PFS message ." )
147207 return
148208
149209 contact_hashchain_signature = b64decode (message ["hashchain_signature" ], validate = True )
@@ -170,7 +230,7 @@ def pfs_data_handler(user_data, user_data_lock, user_data_copied, ui_queue, mess
170230 contact_last_hash_chain = sha3_512 (contact_last_hash_chain )
171231
172232 if contact_last_hash_chain != contact_hash_chain :
173- logger .error ("Contact hash chain does not match our computed hash chain, we are skipping this PFS message..." )
233+ logger .error ("Contact keys hash chain does not match our computed hash chain! Skipping this PFS message..." )
174234 return
175235
176236 contact_kyber_public_key = contact_publickeys_hashchain [KEYS_HASH_CHAIN_LEN : ALGOS_BUFFER_LIMITS [ML_KEM_1024_NAME ]["PK_LEN" ] + KEYS_HASH_CHAIN_LEN ]
@@ -195,8 +255,8 @@ def pfs_data_handler(user_data, user_data_lock, user_data_copied, ui_queue, mess
195255 new_code_kem_keys = user_data ["tmp" ]["new_code_kem_keys" ]
196256
197257 if (our_kyber_private_key is None or our_mceliece_private_key is None ) and ((contact_id not in new_ml_kem_keys ) and (contact_id not in new_code_kem_keys )):
198- send_new_ephemeral_keys (user_data , user_data_lock , contact_id , ui_queue )
199258 logger .info ("We are sending the contact (%s) our ephemeral keys because we didnt do it before." , contact_id )
259+ send_new_ephemeral_keys (user_data , user_data_lock , contact_id , ui_queue )
200260
201261 save_account_data (user_data , user_data_lock )
202262
0 commit comments