11from core .requests import http_request
22from logic .storage import save_account_data
33from logic .pfs import send_new_ephemeral_keys
4+ from core .trad_crypto import sha3_512
45from core .crypto import *
56from core .constants import *
67from base64 import b64decode , b64encode
1112logger = logging .getLogger (__name__ )
1213
1314
14- def generate_and_send_pads (user_data , user_data_lock , contact_id : str , contact_kyber_key , our_private_key , ui_queue ) -> bool :
15+ def generate_and_send_pads (user_data , user_data_lock , contact_id : str , ui_queue ) -> bool :
1516 with user_data_lock :
1617 server_url = user_data ["server_url" ]
1718 auth_token = user_data ["token" ]
18- replay_protection_number = user_data ["contacts" ][contact_id ]["our_pads" ]["replay_protection_number" ]
19-
20- ciphertext_blob , pads = generate_kyber_shared_secrets (contact_kyber_key )
21-
22- if not replay_protection_number :
23- # 1 because at this point, replay_protection_number is None.
24- replay_protection_number = randomize_replay_protection_number (1 )
25- else :
19+
20+ contact_kyber_public_key = user_data ["contacts" ][contact_id ]["ephemeral_keys" ]["contact_public_key" ]
21+ our_lt_private_key = user_data ["contacts" ][contact_id ]["lt_sign_keys" ]["our_keys" ]["private_key" ]
2622
27- # This +1 is needed to ensure it at least increments by 1
28- replay_protection_number += 1
29- replay_protection_number = randomize_replay_protection_number (replay_protection_number )
3023
31- json_inner_payload = json .dumps ({
32- "ciphertext_blob" : b64encode (ciphertext_blob ).decode (),
33- "replay_protection_number" : replay_protection_number
34- })
24+ ciphertext_blob , pads = generate_kyber_shared_secrets (contact_kyber_public_key )
3525
36- inner_payload_signature = create_signature ("Dilithium5" , json_inner_payload . encode ( "utf-8" ), our_private_key )
37- inner_payload_signature = b64encode (inner_payload_signature ).decode ()
26+ otp_batch_signature = create_signature ("Dilithium5" , ciphertext_blob , our_lt_private_key )
27+ otp_batch_signature = b64encode (otp_batch_signature ).decode ()
3828
3929 payload = {
40- "json_payload " : json_inner_payload ,
41- "payload_signature " : inner_payload_signature ,
30+ "otp_hashchain_ciphertext " : b64encode ( ciphertext_blob ). decode () ,
31+ "otp_hashchain_signature " : otp_batch_signature ,
4232 "recipient" : contact_id
4333 }
4434 try :
@@ -49,8 +39,8 @@ def generate_and_send_pads(user_data, user_data_lock, contact_id: str, contact_k
4939
5040 # We update & save only at the end, so if request fails, we do not desync our state.
5141 with user_data_lock :
52- user_data ["contacts" ][contact_id ]["our_pads" ]["pads" ] = pads
53- user_data ["contacts" ][contact_id ]["our_pads" ]["replay_protection_number " ] = replay_protection_number
42+ user_data ["contacts" ][contact_id ]["our_pads" ]["pads" ] = pads [ 64 :]
43+ user_data ["contacts" ][contact_id ]["our_pads" ]["hash_chain " ] = pads [: 64 ]
5444
5545 save_account_data (user_data , user_data_lock )
5646
@@ -108,7 +98,9 @@ def send_message_processor(user_data, user_data_lock, contact_id: str, message:
10898 # ephemeral key exchanges always get processed before messages do.
10999 # Which means if we generate and send pads with contact's, we would be using his old key, which would get overriden by the request, even if we send pads first
110100 # This is because of our server archiecture which prioritizes PFS requests before messages.
111- #
101+ #
102+ # Another note, that means after batch ends, and rotation time comes, you won't be able to send messages until other contact is online.
103+ # This will change in a future update
112104 if rotation_counter == rotate_at :
113105 logger .info ("We are rotating our ephemeral keys for contact (%s)" , contact_id )
114106 ui_queue .put ({"type" : "showinfo" , "title" : "Perfect Forward Secrecy" , "message" : f"We are rotating our ephemeral keys for contact ({ contact_id [:32 ]} )" })
@@ -117,7 +109,7 @@ def send_message_processor(user_data, user_data_lock, contact_id: str, message:
117109 save_account_data (user_data , user_data_lock )
118110 return False
119111
120- if not generate_and_send_pads (user_data , user_data_lock , contact_id , contact_kyber_public_key , our_lt_private_key , ui_queue ):
112+ if not generate_and_send_pads (user_data , user_data_lock , contact_id , ui_queue ):
121113 return False
122114
123115
@@ -128,12 +120,17 @@ def send_message_processor(user_data, user_data_lock, contact_id: str, message:
128120
129121 logger .debug ("Incremented rotation_counter by 1. (%d)" , rotation_counter )
130122
131-
123+
132124 with user_data_lock :
133- replay_protection_number = user_data ["contacts" ][contact_id ]["our_pads" ]["replay_protection_number" ]
125+ our_hash_chain = user_data ["contacts" ][contact_id ]["our_pads" ]["hash_chain" ]
126+
134127
135128 message_encoded = message .encode ("utf-8" )
136129
130+ next_hash_chain = sha3_512 (our_hash_chain + message_encoded )
131+
132+ message_encoded = next_hash_chain + message_encoded
133+
137134 message_otp_padding_length = max (0 , OTP_PADDING_LIMIT - OTP_PADDING_LENGTH - len (message_encoded ))
138135
139136 if (len (message_encoded ) + OTP_PADDING_LENGTH + message_otp_padding_length ) > len (our_pads ):
@@ -152,44 +149,22 @@ def send_message_processor(user_data, user_data_lock, contact_id: str, message:
152149 message_encrypted = otp_encrypt_with_padding (message_encoded , message_otp_pad , padding_limit = message_otp_padding_length )
153150 message_encrypted = b64encode (message_encrypted ).decode ()
154151
155- # Unlike in other functions, we truncate pads here and update replay_protection_number regardless of request being successful or not
152+ # Unlike in other functions, we truncate pads here and compute the next hash chain regardless of request being successful or not
156153 # because a malicious server could make our requests fail to force us to re-use the same pad for our next message
157154 # which would break all of our security
158155 with user_data_lock :
159- user_data ["contacts" ][contact_id ]["our_pads" ]["pads" ] = user_data ["contacts" ][contact_id ]["our_pads" ]["pads" ][len (message_encoded ) + OTP_PADDING_LENGTH + message_otp_padding_length :]
160-
161- replay_protection_number = user_data ["contacts" ][contact_id ]["our_pads" ]["replay_protection_number" ]
162-
163- # This ensures the replay counter always gets incremented by at very least 1
164- replay_protection_number += 1
165-
166- # This helps obfsucate how many total messages were sent incase the request is intercepted
167- # Adversaries might be able to come with a modest guess of how many total messages were sent
168- # but never actually the exact amount, this provides some form of plausible deniability.
169- replay_protection_number = randomize_replay_protection_number (replay_protection_number )
170-
171- user_data ["contacts" ][contact_id ]["our_pads" ]["replay_protection_number" ] = replay_protection_number
172-
173-
156+ user_data ["contacts" ][contact_id ]["our_pads" ]["pads" ] = user_data ["contacts" ][contact_id ]["our_pads" ]["pads" ][len (message_encoded ) + OTP_PADDING_LENGTH + message_otp_padding_length :]
157+ user_data ["contacts" ][contact_id ]["our_pads" ]["hash_chain" ] = next_hash_chain
174158
175159 save_account_data (user_data , user_data_lock )
176160
177-
178- json_inner_payload = json .dumps ({
179- "message_encrypted" : message_encrypted ,
180- "replay_protection_number" : replay_protection_number
181- })
182-
183- json_inner_payload_signature = create_signature ("Dilithium5" , json_inner_payload .encode ("utf-8" ), our_lt_private_key )
184- json_inner_payload_signature = b64encode (json_inner_payload_signature ).decode ()
185-
186- payload = {
187- "json_payload" : json_inner_payload ,
188- "payload_signature" : json_inner_payload_signature ,
189- "recipient" : contact_id
190- }
191161 try :
192- response = http_request (f"{ server_url } /messages/send_message" , "POST" , payload = payload , auth_token = auth_token )
162+ response = http_request (f"{ server_url } /messages/send_message" , "POST" , payload = {
163+ "message_encrypted" : message_encrypted ,
164+ "recipient" : contact_id
165+ },
166+ auth_token = auth_token
167+ )
193168 except :
194169 ui_queue .put ({"type" : "showerror" , "title" : "Error" , "message" : "Failed to send our message to the server" })
195170 return False
@@ -211,7 +186,7 @@ def messages_data_handler(user_data, user_data_lock, user_data_copied, ui_queue,
211186
212187
213188 if not user_data_copied ["contacts" ][contact_id ]["lt_sign_key_smp" ]["verified" ]:
214- logger .warning ("Contact long-term signing key is not verified.. it is possible that this is a MiTM attack by the server , we ignoring this Message for now." )
189+ logger .warning ("Contact long-term signing key is not verified.. it is possible that this is a MiTM attack, we ignoring this message for now." )
215190 return
216191
217192
@@ -225,67 +200,60 @@ def messages_data_handler(user_data, user_data_lock, user_data_copied, ui_queue,
225200 logger .debug ("Received a new message of type: %s" , message ["msg_type" ])
226201
227202 if message ["msg_type" ] == "new_otp_batch" :
228- payload_signature = b64decode (message ["payload_signature" ], validate = True )
229- valid_signature = verify_signature ("Dilithium5" , message ["json_payload" ].encode ("utf-8" ), payload_signature , contact_public_key )
203+ otp_hashchain_signature = b64decode (message ["otp_hashchain_signature" ], validate = True )
204+ otp_hashchain_ciphertext = b64decode (message ["otp_hashchain_ciphertext" ], validate = True )
205+
206+ valid_signature = verify_signature ("Dilithium5" , otp_hashchain_ciphertext , otp_hashchain_signature , contact_public_key )
230207 if not valid_signature :
231- logger .debug ("Invalid OTP batch signature.. possible MiTM ?" )
208+ logger .debug ("Invalid OTP_hashchain_ciphertext signature.. possible MiTM ?" )
232209 return
233210
234- json_payload = json .loads (message ["json_payload" ])
235-
236- ciphertext_blob = b64decode (json_payload ["ciphertext_blob" ], validate = True )
237- replay_protection_number = int (json_payload ["replay_protection_number" ])
238-
239211 our_kyber_key = user_data_copied ["contacts" ][contact_id ]["ephemeral_keys" ]["our_keys" ]["private_key" ]
240212
241213 try :
242- contact_pads = decrypt_kyber_shared_secrets (ciphertext_blob , our_kyber_key )
214+ contact_pads = decrypt_kyber_shared_secrets (otp_hashchain_ciphertext , our_kyber_key )
243215 except :
244216 logger .debug ("Failed to decrypt shared_secrets, possible MiTM?" )
245217 return
246218
219+
247220 with user_data_lock :
248- user_data ["contacts" ][contact_id ]["contact_pads" ]["pads" ] = contact_pads
249- user_data ["contacts" ][contact_id ]["contact_pads" ]["replay_protection_number " ] = replay_protection_number
221+ user_data ["contacts" ][contact_id ]["contact_pads" ]["pads" ] = contact_pads [ 64 :]
222+ user_data ["contacts" ][contact_id ]["contact_pads" ]["hash_chain " ] = contact_pads [: 64 ]
250223
251- logger .info ("Saved contact (%s) new batch of One-Time-Pads" , contact_id )
224+ logger .info ("Saved contact (%s) new batch of One-Time-Pads and hash chain seed " , contact_id )
252225
253226 save_account_data (user_data , user_data_lock )
254227
255228 elif message ["msg_type" ] == "new_message" :
256- payload_signature = b64decode (message ["payload_signature" ], validate = True )
257- valid_signature = verify_signature ("Dilithium5" , message ["json_payload" ].encode ("utf-8" ), payload_signature , contact_public_key )
258- if not valid_signature :
259- logger .debug ("Invalid new message signature.. possible MiTM ?" )
260- return
261-
262- json_payload = json .loads (message ["json_payload" ])
263- message_encrypted = b64decode (json_payload ["message_encrypted" ], validate = True )
264- replay_protection_number = int (json_payload ["replay_protection_number" ])
229+ message_encrypted = b64decode (message ["message_encrypted" ], validate = True )
265230
266231 with user_data_lock :
267- contact_pads = user_data ["contacts" ][contact_id ]["contact_pads" ]["pads" ]
268- contact_replay_protection_number = user_data ["contacts" ][contact_id ]["contact_pads" ]["replay_protection_number" ]
269-
232+ contact_pads = user_data ["contacts" ][contact_id ]["contact_pads" ]["pads" ]
233+ contact_hash_chain = user_data ["contacts" ][contact_id ]["contact_pads" ]["hash_chain" ]
270234
271235 if (not contact_pads ) or (len (message_encrypted ) > len (contact_pads )):
272236 logger .warning ("Message payload is larger than our local pads for the contact, we are skipping this message.." )
273237 return
274238
275- if (not contact_replay_protection_number ) or (replay_protection_number <= contact_replay_protection_number ):
276- logger .warning ("Message replay_protection_number is equal or smaller than our saved replay_protection_number, this could be a possible replay attack, skipping this message..." )
277- return
278-
279-
280239 message_decrypted = otp_decrypt_with_padding (message_encrypted , contact_pads [:len (message_encrypted )])
281-
282240 # immediately truncate the pads
283241 contact_pads = contact_pads [len (message_encrypted ):]
284242
243+ hash_chain = message_decrypted [:64 ]
244+ message_decrypted = message_decrypted [64 :]
245+
246+ next_hash_chain = sha3_512 (contact_hash_chain + message_decrypted )
247+
248+ if next_hash_chain != hash_chain :
249+ logger .warning ("Message hash chain did not match, this could be a possible replay attack, or a failed tampering attempt. Skipping this message..." )
250+ return
251+
252+
285253 # and immediately save the new pads and replay protection number
286254 with user_data_lock :
287- user_data ["contacts" ][contact_id ]["contact_pads" ]["pads" ] = contact_pads
288- user_data ["contacts" ][contact_id ]["contact_pads" ]["replay_protection_number " ] = replay_protection_number
255+ user_data ["contacts" ][contact_id ]["contact_pads" ]["pads" ] = contact_pads
256+ user_data ["contacts" ][contact_id ]["contact_pads" ]["hash_chain " ] = next_hash_chain
289257
290258 save_account_data (user_data , user_data_lock )
291259
0 commit comments