4646logger = logging .getLogger (__name__ )
4747
4848
49+ def send_pfs_ack (user_data : dict , user_data_lock : threading .Lock , contact_id : str , ui_queue : queue .Queue ) -> None :
50+ with user_data_lock :
51+ server_url = user_data ["server_url" ]
52+ auth_token = user_data ["token" ]
53+ session_headers = user_data ["tmp" ]["session_headers" ]
54+
55+
56+ our_next_strand_nonce = user_data ["contacts" ][contact_id ]["our_next_strand_nonce" ]
57+ our_strand_key = user_data ["contacts" ][contact_id ]["our_strand_key" ]
58+
59+
60+ our_new_strand_nonce = sha3_512 (secrets .token_bytes (XCHACHA20POLY1305_NONCE_LEN ))[:XCHACHA20POLY1305_NONCE_LEN ]
61+ _ , ciphertext_blob = encrypt_xchacha20poly1305 (
62+ our_strand_key ,
63+ PFS_TYPE + b"\x01 " + our_new_strand_nonce ,
64+ nonce = our_next_strand_nonce
65+ )
66+
67+ try :
68+ http_request (f"{ server_url } /data/send" , "POST" , metadata = {
69+ "recipient" : contact_id
70+ },
71+ blob = ciphertext_blob ,
72+ headers = session_headers ,
73+ auth_token = auth_token
74+ )
75+ except Exception :
76+ ui_queue .put ({"type" : "showerror" , "title" : "Error" , "message" : "Failed to send our ephemeral keys to the server" })
77+ return
78+
79+ # We update at the very end to ensure if any of previous steps fail, we do not desync our state
80+ with user_data_lock :
81+ user_data ["contacts" ][contact_id ]["our_next_strand_nonce" ] = our_new_strand_nonce
82+
83+
84+
85+
4986def send_new_ephemeral_keys (user_data : dict , user_data_lock : threading .Lock , contact_id : str , ui_queue : queue .Queue ) -> None :
5087 """
5188 Generate, encrypt, and send fresh ephemeral keys to a contact.
@@ -70,12 +107,12 @@ def send_new_ephemeral_keys(user_data: dict, user_data_lock: threading.Lock, con
70107 # we put here because it could've change between time copy finished copying.
71108
72109 our_next_strand_nonce = user_data ["contacts" ][contact_id ]["our_next_strand_nonce" ]
73-
110+ our_strand_key = user_data ["contacts" ][contact_id ]["our_strand_key" ]
111+
74112 server_url = user_data_copied ["server_url" ]
75113 auth_token = user_data_copied ["token" ]
76114 session_headers = user_data_copied ["tmp" ]["session_headers" ]
77115
78- our_strand_key = user_data_copied ["contacts" ][contact_id ]["our_strand_key" ]
79116
80117 rotation_counter = user_data_copied ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["rotation_counter" ]
81118 rotate_at = user_data_copied ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["rotate_at" ]
@@ -111,7 +148,7 @@ def send_new_ephemeral_keys(user_data: dict, user_data_lock: threading.Lock, con
111148 our_new_strand_nonce = sha3_512 (secrets .token_bytes (XCHACHA20POLY1305_NONCE_LEN ))[:XCHACHA20POLY1305_NONCE_LEN ]
112149 _ , ciphertext_blob = encrypt_xchacha20poly1305 (
113150 our_strand_key ,
114- PFS_TYPE + our_new_strand_nonce + publickeys_hashchain_signature + publickeys_hashchain ,
151+ PFS_TYPE + b" \x00 " + our_new_strand_nonce + publickeys_hashchain_signature + publickeys_hashchain ,
115152 nonce = our_next_strand_nonce ,
116153 max_padding = 1024
117154 )
@@ -133,60 +170,24 @@ def send_new_ephemeral_keys(user_data: dict, user_data_lock: threading.Lock, con
133170 with user_data_lock :
134171 user_data ["contacts" ][contact_id ]["our_next_strand_nonce" ] = our_new_strand_nonce
135172
136- user_data ["tmp" ]["new_ml_kem_keys" ][contact_id ] = {
137- "private_key" : kyber_private_key ,
138- "public_key" : kyber_public_key
139- }
140-
141- if rotate_mceliece :
142- user_data ["tmp" ]["new_code_kem_keys" ][contact_id ] = {
143- "private_key" : mceliece_private_key ,
144- "public_key" : mceliece_public_key
145- }
146-
147- user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["rotation_counter" ] = 0
148- user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["rotate_at" ] = CLASSIC_MCELIECE_8_F_ROTATE_AT
149173
174+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["private_key" ] = kyber_private_key
175+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["public_key" ] = kyber_public_key
150176
151177
178+ if rotate_mceliece :
179+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["private_key" ] = mceliece_private_key
180+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["public_key" ] = mceliece_public_key
152181
153- user_data ["contacts" ][contact_id ]["lt_sign_keys" ]["our_hash_chain" ] = our_hash_chain
154-
155-
156-
157- def update_ephemeral_keys (user_data : dict , user_data_lock : threading .Lock ) -> None :
158- """
159- Commit newly generated ephemeral keys to permanent storage.
160-
161- - Moves keys from `user_data["tmp"]` into `user_data["contacts"]`.
162- - Clears temporary key buffers after update.
163- - Saves account state to disk.
164182
165- Args:
166- user_data (dict): Shared user account state.
167- user_data_lock (threading.Lock): Lock protecting shared state.
168- """
169- with user_data_lock :
170- new_ml_kem_keys = user_data ["tmp" ]["new_ml_kem_keys" ]
171- new_code_kem_keys = user_data ["tmp" ]["new_code_kem_keys" ]
183+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["rotation_counter" ] = 0
184+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["rotate_at" ] = CLASSIC_MCELIECE_8_F_ROTATE_AT
172185
173- for contact_id , v in new_ml_kem_keys .items ():
174- with user_data_lock :
175- user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][ML_KEM_1024_NAME ]["private_key" ] = v ["private_key" ]
176- user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][ML_KEM_1024_NAME ]["public_key" ] = v ["public_key" ]
177186
178- for contact_id , v in new_code_kem_keys .items ():
179- with user_data_lock :
180- user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["private_key" ] = v ["private_key" ]
181- user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["public_key" ] = v ["public_key" ]
182187
183188
184- with user_data_lock :
185- user_data ["tmp" ]["new_ml_kem_keys" ] = {}
186- user_data ["tmp" ]["new_code_kem_keys" ] = {}
189+ user_data ["contacts" ][contact_id ]["lt_sign_keys" ]["our_hash_chain" ] = our_hash_chain
187190
188-
189- save_account_data (user_data , user_data_lock )
190191
191192
192193
@@ -206,6 +207,7 @@ def pfs_data_handler(user_data: dict, user_data_lock: threading.Lock, user_data_
206207 user_data_lock (threading.Lock): Lock protecting shared state.
207208 user_data_copied (dict): A read-only copy of user_data for consistency.
208209 ui_queue (queue.Queue): UI queue for notifications/errors.
210+ contact_id (str): Sender ID.
209211 pfs_plaintext (bytes): Decrypted Incoming PFS plaintext from the server.
210212
211213 Returns:
@@ -232,6 +234,33 @@ def pfs_data_handler(user_data: dict, user_data_lock: threading.Lock, user_data_
232234 logger .error ("Contact (%s) strand key key is missing! Skipping message..." , contact_id )
233235 return
234236
237+ if pfs_plaintext [0 ] == 1 :
238+ logger .info ("Received acknowlegement of PFS keys from contact %s" , contact_id )
239+ with user_data_lock :
240+ user_data ["contacts" ][contact_id ]["contact_next_strand_nonce" ] = pfs_plaintext [1 :]
241+
242+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][ML_KEM_1024_NAME ]["private_key" ] = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["private_key" ]
243+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][ML_KEM_1024_NAME ]["public_key" ] = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["public_key" ]
244+
245+ if user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["private_key" ]:
246+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["private_key" ] = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["private_key" ]
247+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["public_key" ] = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["public_key" ]
248+
249+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["private_key" ] = None
250+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["public_key" ] = None
251+
252+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["private_key" ] = None
253+ user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["public_key" ] = None
254+
255+ save_account_data (user_data , user_data_lock )
256+ return
257+
258+ elif pfs_plaintext [0 ] == 0 :
259+ pfs_plaintext = pfs_plaintext [1 :]
260+ else :
261+ logger .error ("Skipping unknown PFS of type (%d) from contact (%s)" , pfs_plaintext [0 ], contact_id )
262+ return
263+
235264
236265 if (
237266 (len (pfs_plaintext ) < ML_KEM_1024_PK_LEN + ML_DSA_87_SIGN_LEN + KEYS_HASH_CHAIN_LEN )
@@ -283,6 +312,10 @@ def pfs_data_handler(user_data: dict, user_data_lock: threading.Lock, user_data_
283312 logger .info ("contact (%s) has rotated their Kyber keys" , contact_id )
284313
285314
315+ logger .info ("We are acknowledging contact's new PFS keys" )
316+ send_pfs_ack (user_data , user_data_lock , contact_id , ui_queue )
317+
318+
286319 with user_data_lock :
287320 user_data ["contacts" ][contact_id ]["contact_next_strand_nonce" ] = contact_next_strand_nonce
288321
@@ -292,10 +325,11 @@ def pfs_data_handler(user_data: dict, user_data_lock: threading.Lock, user_data_
292325 our_kyber_private_key = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][ML_KEM_1024_NAME ]["private_key" ]
293326 our_mceliece_private_key = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["private_key" ]
294327
295- new_ml_kem_keys = user_data ["tmp" ]["new_ml_kem_keys" ]
296- new_code_kem_keys = user_data ["tmp" ]["new_code_kem_keys" ]
297328
298- 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 )):
329+ staged_kem_private_key = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][ML_KEM_1024_NAME ]["private_key" ]
330+ staged_code_private_key = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["staged_keys" ][CLASSIC_MCELIECE_8_F_NAME ]["private_key" ]
331+
332+ if (our_kyber_private_key is None or our_mceliece_private_key is None ) and ((staged_kem_private_key is None ) and (staged_code_private_key is None )):
299333 logger .info ("We are sending the contact (%s) our ephemeral keys because we didnt do it before." , contact_id )
300334 send_new_ephemeral_keys (user_data , user_data_lock , contact_id , ui_queue )
301335
0 commit comments