Skip to content

Commit 8cc6c9d

Browse files
committed
feat: add strandlock protocol to smp and messages
1 parent fcc5d57 commit 8cc6c9d

File tree

5 files changed

+108
-41
lines changed

5 files changed

+108
-41
lines changed

core/trad_crypto.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def derive_key_argon2id(password: bytes, salt: bytes = None, output_length: int
6767
), salt
6868

6969

70-
def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, counter: int = None, counter_safety: int = 2 ** 32) -> tuple[bytes, bytes]:
70+
def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, nonce: bytes = None, counter: int = None, counter_safety: int = 2 ** 32) -> tuple[bytes, bytes]:
7171
"""
7272
Encrypt plaintext using ChaCha20Poly1305.
7373
@@ -83,7 +83,9 @@ def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, counter: int = None,
8383
- nonce: The randomly generated AES-GCM nonce.
8484
- ciphertext: The encrypted data including the authentication tag.
8585
"""
86-
nonce = secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN)
86+
if nonce is None:
87+
nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
88+
8789
if counter is not None:
8890
if counter > counter_safety:
8991
raise ValueError("ChaCha counter has overflowen")

logic/contacts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ def save_contact(user_data: dict, user_data_lock, contact_id: str) -> None:
8787
}
8888
},
8989
"our_strand_key": None,
90-
"contact_strand_key": None,
90+
"our_next_strand_nonce": None,
91+
"contact_next_strand_key": None,
92+
"contact_strand_nonce": None,
9193
"our_pads": {
9294
"hash_chain": None,
9395
"pads": None

logic/message.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ def generate_and_send_pads(user_data, user_data_lock, contact_id: str, ui_queue)
7272

7373
otp_batch_signature = create_signature(ML_DSA_87_NAME, kyber_ciphertext_blob + mceliece_ciphertext_blob, our_lt_private_key)
7474

75-
hash_chain_seed = secrets.token_bytes(MESSAGE_HASH_CHAIN_LEN)
75+
hash_chain_seed = sha3_512(secrets.token_bytes(MESSAGE_HASH_CHAIN_LEN))
76+
7677
ciphertext_nonce, ciphertext_blob = encrypt_xchacha20poly1305(
7778
our_strand_key,
7879
b"\x00" + hash_chain_seed + otp_batch_signature + kyber_ciphertext_blob + mceliece_ciphertext_blob
@@ -90,9 +91,12 @@ def generate_and_send_pads(user_data, user_data_lock, contact_id: str, ui_queue)
9091

9192
pads, _ = one_time_pad(kyber_shared_secrets, mceliece_shared_secrets)
9293

94+
95+
our_strand_key = sha3_512(pads[:32])[:32]
96+
9397
# We update & save only at the end, so if request fails, we do not desync our state.
9498
with user_data_lock:
95-
user_data["contacts"][contact_id]["our_strand_key"] = pads[:32]
99+
user_data["contacts"][contact_id]["our_strand_key"] = our_strand_key
96100
user_data["contacts"][contact_id]["our_pads"]["pads"] = pads[32:]
97101

98102
user_data["contacts"][contact_id]["our_pads"]["hash_chain"] = hash_chain_seed
@@ -294,7 +298,7 @@ def messages_data_handler(user_data: dict, user_data_lock, user_data_copied: dic
294298
return
295299

296300
contact_pads, _ = one_time_pad(contact_kyber_pads, contact_mceliece_pads)
297-
contact_strand_key = contact_pads[:32]
301+
contact_strand_key = sha3_512(contact_pads[:32])[:32]
298302
contact_pads = contact_pads[32:]
299303

300304

logic/smp.py

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -110,14 +110,18 @@ def smp_step_2(user_data: dict, user_data_lock, contact_id: str, message: dict,
110110

111111
signing_private_key, signing_public_key = generate_sign_keys()
112112

113-
our_nonce = secrets.token_bytes(SMP_NONCE_LENGTH)
113+
our_nonce = sha3_512(secrets.token_bytes(SMP_NONCE_LENGTH))[:SMP_NONCE_LENGTH]
114114

115115
key_ciphertext, chacha_key = encap_shared_secret(contact_kem_public_key, ML_KEM_1024_NAME)
116116
chacha_key = sha3_512(chacha_key)[:32]
117117

118+
our_next_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
119+
contact_next_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
120+
121+
118122
ciphertext_nonce, ciphertext_blob = encrypt_xchacha20poly1305(
119123
chacha_key,
120-
signing_public_key + our_nonce,
124+
signing_public_key + our_nonce + our_next_strand_nonce + contact_next_strand_nonce,
121125
counter = 2
122126
)
123127

@@ -146,6 +150,9 @@ def smp_step_2(user_data: dict, user_data_lock, contact_id: str, message: dict,
146150
user_data["contacts"][contact_id]["lt_sign_keys"]["our_keys"]["public_key"] = signing_public_key
147151

148152
user_data["contacts"][contact_id]["lt_sign_key_smp"]["smp_step"] = 4
153+
154+
user_data["contacts"][contact_id]["our_next_strand_nonce"] = our_next_strand_nonce
155+
user_data["contacts"][contact_id]["contact_next_strand_nonce"] = contact_next_strand_nonce
149156

150157

151158
def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str, message: dict, ui_queue: queue.Queue()) -> None:
@@ -173,9 +180,12 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str,
173180
)
174181

175182
contact_signing_public_key = smp_plaintext[:ML_DSA_87_PK_LEN]
176-
contact_nonce = smp_plaintext[ML_DSA_87_PK_LEN:]
183+
contact_nonce = smp_plaintext[ML_DSA_87_PK_LEN: ML_DSA_87_PK_LEN + SMP_NONCE_LENGTH]
177184

178-
our_nonce = secrets.token_bytes(SMP_NONCE_LENGTH)
185+
contact_next_strand_nonce = smp_plaintext[ML_DSA_87_PK_LEN + SMP_NONCE_LENGTH: ML_DSA_87_PK_LEN + SMP_NONCE_LENGTH + XCHACHA20POLY1305_NONCE_LEN]
186+
our_next_strand_nonce = smp_plaintext[ML_DSA_87_PK_LEN + SMP_NONCE_LENGTH + XCHACHA20POLY1305_NONCE_LEN:]
187+
188+
our_nonce = sha3_512(secrets.token_bytes(SMP_NONCE_LENGTH))[:SMP_NONCE_LENGTH]
179189

180190
signing_private_key, signing_public_key = generate_sign_keys()
181191

@@ -190,17 +200,19 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str,
190200
our_proof = hmac.new(answer_secret, our_proof, hashlib.sha3_512).digest()
191201

192202
logger.debug("Our proof of contact (%s) public-key fingerprint: %s", contact_id, our_proof)
203+
193204

194-
ciphertext_nonce, ciphertext_blob = encrypt_xchacha20poly1305(
205+
our_new_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
206+
_, ciphertext_blob = encrypt_xchacha20poly1305(
195207
chacha_key,
196-
signing_public_key + our_nonce + our_proof + question.encode("utf-8"),
197-
counter = 3
208+
our_new_strand_nonce + signing_public_key + our_nonce + our_proof + question.encode("utf-8"),
209+
nonce = our_next_strand_nonce
198210
)
199211

200212

201213
try:
202214
http_request(f"{server_url}/smp/step", "POST", payload = {
203-
"ciphertext_blob": b64encode(ciphertext_nonce + ciphertext_blob).decode(),
215+
"ciphertext_blob": b64encode(ciphertext_blob).decode(),
204216
"recipient": contact_id
205217

206218
}, auth_token=auth_token)
@@ -214,34 +226,51 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str,
214226
with user_data_lock:
215227
user_data["contacts"][contact_id]["lt_sign_keys"]["contact_public_key"] = contact_signing_public_key
216228

217-
user_data["contacts"][contact_id]["lt_sign_key_smp"]["contact_nonce"] = b64encode(contact_nonce).decode()
218-
user_data["contacts"][contact_id]["lt_sign_key_smp"]["our_nonce"] = b64encode(our_nonce).decode()
219-
user_data["contacts"][contact_id]["lt_sign_key_smp"]["tmp_key"] = b64encode(chacha_key).decode()
229+
user_data["contacts"][contact_id]["lt_sign_key_smp"]["contact_nonce"] = b64encode(contact_nonce).decode()
230+
user_data["contacts"][contact_id]["lt_sign_key_smp"]["our_nonce"] = b64encode(our_nonce).decode()
231+
user_data["contacts"][contact_id]["lt_sign_key_smp"]["tmp_key"] = b64encode(chacha_key).decode()
220232

221233
user_data["contacts"][contact_id]["lt_sign_keys"]["our_keys"]["private_key"] = signing_private_key
222234
user_data["contacts"][contact_id]["lt_sign_keys"]["our_keys"]["public_key"] = signing_public_key
223235

236+
user_data["contacts"][contact_id]["our_next_strand_nonce"] = our_new_strand_nonce
237+
user_data["contacts"][contact_id]["contact_next_strand_nonce"] = contact_next_strand_nonce
238+
239+
224240
user_data["contacts"][contact_id]["lt_sign_key_smp"]["smp_step"] = 5
225241

226242

227243
def smp_step_4_request_answer(user_data, user_data_lock, contact_id, message, ui_queue) -> None:
228244
with user_data_lock:
229245
tmp_key = b64decode(user_data["contacts"][contact_id]["lt_sign_key_smp"]["tmp_key"])
230246

247+
our_next_strand_nonce = user_data["contacts"][contact_id]["our_next_strand_nonce"]
248+
contact_next_strand_nonce = user_data["contacts"][contact_id]["contact_next_strand_nonce"]
249+
250+
231251
ciphertext_blob = b64decode(message["ciphertext_blob"], validate = True)
232-
smp_plaintext = decrypt_xchacha20poly1305(tmp_key, ciphertext_blob[:XCHACHA20POLY1305_NONCE_LEN], ciphertext_blob[XCHACHA20POLY1305_NONCE_LEN:])
252+
253+
254+
smp_plaintext = decrypt_xchacha20poly1305(tmp_key, contact_next_strand_nonce, ciphertext_blob)
255+
256+
contact_new_strand_nonce = smp_plaintext[:XCHACHA20POLY1305_NONCE_LEN]
233257

234-
contact_signing_public_key = smp_plaintext[:ML_DSA_87_PK_LEN]
235-
contact_nonce = b64encode(smp_plaintext[ML_DSA_87_PK_LEN : SMP_NONCE_LENGTH + ML_DSA_87_PK_LEN]).decode()
236-
contact_proof = b64encode(smp_plaintext[SMP_NONCE_LENGTH + ML_DSA_87_PK_LEN : SMP_NONCE_LENGTH + SMP_PROOF_LENGTH + ML_DSA_87_PK_LEN]).decode()
237-
question = smp_plaintext[SMP_NONCE_LENGTH + SMP_PROOF_LENGTH + ML_DSA_87_PK_LEN:].decode("utf-8")
258+
contact_signing_public_key = smp_plaintext[XCHACHA20POLY1305_NONCE_LEN : ML_DSA_87_PK_LEN + XCHACHA20POLY1305_NONCE_LEN]
259+
260+
contact_nonce = b64encode(smp_plaintext[XCHACHA20POLY1305_NONCE_LEN + ML_DSA_87_PK_LEN : SMP_NONCE_LENGTH + ML_DSA_87_PK_LEN + XCHACHA20POLY1305_NONCE_LEN]).decode()
261+
262+
contact_proof = b64encode(smp_plaintext[XCHACHA20POLY1305_NONCE_LEN + SMP_NONCE_LENGTH + ML_DSA_87_PK_LEN : SMP_NONCE_LENGTH + SMP_PROOF_LENGTH + ML_DSA_87_PK_LEN + XCHACHA20POLY1305_NONCE_LEN]).decode()
263+
264+
question = smp_plaintext[SMP_NONCE_LENGTH + XCHACHA20POLY1305_NONCE_LEN + SMP_PROOF_LENGTH + ML_DSA_87_PK_LEN:].decode("utf-8")
265+
238266

239267

240268
with user_data_lock:
241269
user_data["contacts"][contact_id]["lt_sign_key_smp"]["question"] = question
242270
user_data["contacts"][contact_id]["lt_sign_key_smp"]["tmp_proof"] = contact_proof
243271
# user_data["contacts"][contact_id]["lt_sign_key_smp"]["smp_step"] = 5
244272

273+
user_data["contacts"][contact_id]["contact_next_strand_nonce"] = contact_new_strand_nonce
245274

246275
user_data["contacts"][contact_id]["lt_sign_key_smp"]["contact_nonce"] = contact_nonce
247276

@@ -269,6 +298,8 @@ def smp_step_4_answer_provided(user_data, user_data_lock, contact_id, answer, ui
269298

270299
our_signing_public_key = user_data["contacts"][contact_id]["lt_sign_keys"]["our_keys"]["public_key"]
271300

301+
our_next_strand_nonce = user_data["contacts"][contact_id]["our_next_strand_nonce"]
302+
272303
tmp_key = b64decode(user_data["contacts"][contact_id]["lt_sign_key_smp"]["tmp_key"])
273304

274305
answer = normalize_answer(answer)
@@ -286,10 +317,11 @@ def smp_step_4_answer_provided(user_data, user_data_lock, contact_id, answer, ui
286317
logger.debug("SMP Proof sent to us: %s", contact_proof)
287318
logger.debug("Our compute message: %s", our_proof)
288319

320+
289321
# Verify Contact's version of our public-key fingerprint matches our actual public-key fingerprint
290322
# We compare using compare_digest to prevent timing analysis by avoiding content-based short circuiting behaviour
291323
if not hmac.compare_digest(our_proof, contact_proof):
292-
logger.warning("SMP Verification failed")
324+
logger.warning("SMP Verification failed at step 4")
293325
smp_failure_notify_contact(user_data, user_data_lock, contact_id, ui_queue)
294326
return
295327

@@ -300,19 +332,21 @@ def smp_step_4_answer_provided(user_data, user_data_lock, contact_id, answer, ui
300332
our_proof = contact_nonce + our_nonce + contact_key_fingerprint
301333
our_proof = hmac.new(answer_secret, our_proof, hashlib.sha3_512).digest()
302334

303-
our_strand_key = secrets.token_bytes(32)
304-
contact_strand_key = secrets.token_bytes(32)
305335

306-
ciphertext_nonce, ciphertext_blob = encrypt_xchacha20poly1305(
336+
our_strand_key = sha3_512(secrets.token_bytes(32))[:32]
337+
contact_strand_key = sha3_512(secrets.token_bytes(32))[:32]
338+
339+
our_new_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
340+
_, ciphertext_blob = encrypt_xchacha20poly1305(
307341
tmp_key,
308-
our_proof + our_strand_key + contact_strand_key,
309-
counter = 4
342+
our_new_strand_nonce + our_proof + our_strand_key + contact_strand_key,
343+
nonce = our_next_strand_nonce
310344
)
311345

312346

313347
try:
314348
http_request(f"{server_url}/smp/step", "POST", payload = {
315-
"ciphertext_blob": b64encode(ciphertext_nonce + ciphertext_blob).decode(),
349+
"ciphertext_blob": b64encode(ciphertext_blob).decode(),
316350
"recipient": contact_id
317351
}, auth_token=auth_token)
318352
except Exception:
@@ -326,10 +360,11 @@ def smp_step_4_answer_provided(user_data, user_data_lock, contact_id, answer, ui
326360

327361
with user_data_lock:
328362
user_data["contacts"][contact_id]["lt_sign_key_smp"]["answer"] = answer
329-
user_data["contacts"][contact_id]["our_strand_key"] = our_strand_key
330-
user_data["contacts"][contact_id]["contact_strand_key"] = contact_strand_key
331363

364+
user_data["contacts"][contact_id]["our_next_strand_nonce"] = our_new_strand_nonce
332365

366+
user_data["contacts"][contact_id]["our_strand_key"] = our_strand_key
367+
user_data["contacts"][contact_id]["contact_strand_key"] = contact_strand_key
333368

334369

335370

@@ -347,7 +382,8 @@ def smp_step_5(user_data, user_data_lock, contact_id, message, ui_queue) -> None
347382
contact_nonce = b64decode(user_data["contacts"][contact_id]["lt_sign_key_smp"]["contact_nonce"], validate=True)
348383

349384
tmp_key = b64decode(user_data["contacts"][contact_id]["lt_sign_key_smp"]["tmp_key"])
350-
385+
contact_next_strand_nonce = user_data["contacts"][contact_id]["contact_next_strand_nonce"]
386+
351387

352388
our_key_fingerprint = sha3_512(our_signing_public_key + our_kem_public_key)
353389

@@ -360,25 +396,32 @@ def smp_step_5(user_data, user_data_lock, contact_id, message, ui_queue) -> None
360396
our_proof = hmac.new(answer_secret, our_proof, hashlib.sha3_512).digest()
361397

362398
ciphertext_blob = b64decode(message["ciphertext_blob"], validate = True)
363-
smp_plaintext = decrypt_xchacha20poly1305(tmp_key, ciphertext_blob[:XCHACHA20POLY1305_NONCE_LEN], ciphertext_blob[XCHACHA20POLY1305_NONCE_LEN:])
364399

365-
contact_proof = smp_plaintext[:SMP_PROOF_LENGTH]
366-
contact_strand_key = smp_plaintext[SMP_PROOF_LENGTH : SMP_PROOF_LENGTH + 32]
367-
our_strand_key = smp_plaintext[SMP_PROOF_LENGTH + 32:]
400+
401+
smp_plaintext = decrypt_xchacha20poly1305(tmp_key, contact_next_strand_nonce, ciphertext_blob)
368402

403+
contact_new_strand_nonce = smp_plaintext[:XCHACHA20POLY1305_NONCE_LEN]
404+
405+
contact_proof = smp_plaintext[XCHACHA20POLY1305_NONCE_LEN : SMP_PROOF_LENGTH + XCHACHA20POLY1305_NONCE_LEN]
406+
407+
contact_strand_key = smp_plaintext[XCHACHA20POLY1305_NONCE_LEN + SMP_PROOF_LENGTH : XCHACHA20POLY1305_NONCE_LEN + SMP_PROOF_LENGTH + 32]
408+
our_strand_key = smp_plaintext[XCHACHA20POLY1305_NONCE_LEN + SMP_PROOF_LENGTH + 32:]
369409

370410
logger.debug("SMP Proof sent to us: %s", contact_proof)
371411
logger.debug("Our compute message: %s", our_proof)
372412

373413

414+
374415
# Verify Contact's version of our public-key fingerprint matches our actual public-key fingerprint
375416
# We compare using compare_digest to prevent timing analysis by avoiding content-based short circuiting behaviour
376417
if not hmac.compare_digest(our_proof, contact_proof):
377-
logger.warning("SMP Verification failed")
418+
logger.warning("SMP Verification failed at step 5")
378419
smp_failure_notify_contact(user_data, user_data_lock, contact_id, ui_queue)
379420
return
380421

381422
with user_data_lock:
423+
user_data["contacts"][contact_id]["contact_next_strand_nonce"] = contact_new_strand_nonce
424+
382425
user_data["contacts"][contact_id]["our_strand_key"] = our_strand_key
383426
user_data["contacts"][contact_id]["contact_strand_key"] = contact_strand_key
384427

@@ -480,6 +523,12 @@ def smp_data_handler(user_data, user_data_lock, user_data_copied, ui_queue, mess
480523
except Exception:
481524
smp_step = 2
482525

526+
527+
if "failure" in message:
528+
# Delete SMP state for contact
529+
smp_failure(user_data, user_data_lock, contact_id, ui_queue)
530+
return
531+
483532
# Check if we don't have this contact saved
484533
if contact_id not in user_data_copied["contacts"]:
485534
# We assume it has to be step 1 because the contact did not exist before
@@ -508,10 +557,6 @@ def smp_data_handler(user_data, user_data_lock, user_data_copied, ui_queue, mess
508557
elif smp_step == 2:
509558
smp_step_2(user_data, user_data_lock, contact_id, message, ui_queue)
510559

511-
elif "failure" in message:
512-
# Delete SMP state for contact
513-
smp_failure(user_data, user_data_lock, contact_id, ui_queue)
514-
515560
elif smp_step == 3:
516561
if (not user_data_copied["contacts"][contact_id]["lt_sign_key_smp"]["pending_verification"]):
517562
logger.error("Contact (%s) is not pending verification, yet they sent us a SMP request. Ignoring it.", contact_id)

logic/storage.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ def load_account_data(password = None) -> dict:
9191
except TypeError:
9292
pass
9393

94+
try:
95+
user_data["contacts"][contact_id]["our_next_strand_nonce"] = b64decode(user_data["contacts"][contact_id]["our_next_strand_nonce"], validate=True)
96+
user_data["contacts"][contact_id]["contact_next_strand_nonce"] = b64decode(user_data["contacts"][contact_id]["contact_next_strand_nonce"], validate=True)
97+
except TypeError:
98+
pass
99+
100+
94101

95102
try:
96103
user_data["contacts"][contact_id]["our_pads"]["pads"] = b64decode(user_data["contacts"][contact_id]["our_pads"]["pads"], validate=True)
@@ -179,6 +186,13 @@ def save_account_data(user_data: dict, user_data_lock, password = None) -> None:
179186
except TypeError:
180187
pass
181188

189+
try:
190+
user_data["contacts"][contact_id]["our_next_strand_nonce"] = b64encode(user_data["contacts"][contact_id]["our_next_strand_nonce"]).decode()
191+
user_data["contacts"][contact_id]["contact_next_strand_nonce"] = b64encode(user_data["contacts"][contact_id]["contact_next_strand_nonce"]).decode()
192+
except TypeError:
193+
pass
194+
195+
182196

183197
try:
184198
user_data["contacts"][contact_id]["our_pads"]["pads"] = b64encode(user_data["contacts"][contact_id]["our_pads"]["pads"]).decode()

0 commit comments

Comments
 (0)