Skip to content

Commit 023f5c8

Browse files
committed
refactor: significantly improve availability
1 parent cb0d54e commit 023f5c8

File tree

10 files changed

+179
-170
lines changed

10 files changed

+179
-170
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ __pycache__/
22
account.coldwire
33
*.swp
44
*.swo
5+
*.swn
56
TODO.md
67
TODO.txt

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Here are some features that we have decided against implementing after thoughtfu
4040
- Voice, and video calls.
4141
- Voice messages
4242
- Compression support
43-
- Metadata-rich features (avatars, vanity server-side usernames, bios, delievery receipts, read receipts, online status, last seen status, user-created server authentication passwords)
43+
- Metadata-rich features (avatars, vanity server-side usernames, about me, delievery receipts, read receipts, online status, last seen status, user-created server authentication passwords)
4444
- Account recovery
4545
- Persistent chat history
4646
- Any "convenience" features that could impact security and or privacy (clickable URLs, keyboard hotkeys, keyboard shortscuts beyond the basic CTRL-C CTRL-V)

core/crypto.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ def decrypt_shared_secrets(ciphertext_blob: bytes, private_key: bytes, algorithm
217217
while len(shared_secrets) < size:
218218
ciphertext = ciphertext_blob[cursor:cursor + cipher_size]
219219
if len(ciphertext) != cipher_size:
220-
raise ValueError(f"Ciphertext of {algorithm} blob is malformed or incomplete ({len(ciphertext)})")
220+
raise ValueError(f"Ciphertext of {algorithm} blob is malformed or incomplete ({len(ciphertext)})")
221221

222222
shared_secret = kem.decap_secret(ciphertext)
223223
shared_secrets += shared_secret

core/trad_crypto.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def derive_key_argon2id(password: bytes, salt: bytes = None, output_length: int
6969
), salt
7070

7171

72-
def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, nonce: bytes = None, counter: int = None, counter_safety: int = 255, max_padding: int = XCHACHA20POLY1305_MAX_RANODM_PAD) -> tuple[bytes, bytes]:
72+
def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, nonce: bytes = None, max_padding: int = XCHACHA20POLY1305_MAX_RANODM_PAD) -> tuple[bytes, bytes]:
7373
"""
7474
Encrypt plaintext using XChaCha20Poly1305.
7575
@@ -79,8 +79,6 @@ def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, nonce: bytes = None,
7979
key: A 32-byte XChaCha20Poly1305 key.
8080
plaintext: Data to encrypt.
8181
nonce: An (optional) nonce to be used.
82-
counter: an (optional) number to add to nonce
83-
counter_safety: an (optional) max counter number, to prevent counter overflow.
8482
max_padding: an (optional) maximum padding limit number to message. Cannot be larger than what `XCHACHA20POLY1305_MAX_RANODM_PAD` could store. Set to 0 for no padding.
8583
Returns:
8684
A tuple (nonce, ciphertext) where:
@@ -90,12 +88,6 @@ def encrypt_xchacha20poly1305(key: bytes, plaintext: bytes, nonce: bytes = None,
9088
if nonce is None:
9189
nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
9290

93-
if counter is not None:
94-
if counter > counter_safety:
95-
raise ValueError("ChaCha counter has overflowen")
96-
97-
nonce = nonce[:XCHACHA20POLY1305_NONCE_LEN - 1] + counter.to_bytes(1, "big")
98-
9991
if max_padding < 0:
10092
raise ValueError(f"Max_padding is less than 0! ({max_padding})")
10193

logic/background_worker.py

Lines changed: 35 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@
1010
SMP_TYPES,
1111
PFS_TYPES,
1212
MSG_TYPES,
13-
XCHACHA20POLY1305_NONCE_LEN
13+
XCHACHA20POLY1305_NONCE_LEN,
14+
ML_KEM_1024_NAME,
15+
ML_KEM_1024_CT_LEN
16+
)
17+
from core.crypto import (
18+
random_number_range,
19+
decap_shared_secret
1420
)
15-
from core.crypto import random_number_range
1621
from core.trad_crypto import (
1722
decrypt_xchacha20poly1305
1823
)
@@ -114,48 +119,43 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag):
114119
acks["acks"].append(ack_id)
115120

116121
with user_data_lock:
122+
try:
123+
user_data["contacts"][sender]["locked"] = True
124+
except Exception:
125+
pass
126+
117127
user_data_copied = copy.deepcopy(user_data)
118128

119129
# Everything from here is not validated by server
120130

121131
blob_plaintext = None
122132

123133
if sender in user_data_copied["contacts"]:
124-
chacha_key = user_data["contacts"][sender]["lt_sign_key_smp"]["tmp_key"]
125-
contact_next_strand_nonce = user_data["contacts"][sender]["contact_next_strand_nonce"]
126134

127-
if chacha_key is not None:
128-
chacha_key = b64decode(user_data["contacts"][sender]["lt_sign_key_smp"]["tmp_key"])
135+
contact_next_strand_key = user_data_copied["contacts"][sender]["contact_next_strand_key"]
136+
contact_next_strand_nonce = user_data_copied["contacts"][sender]["contact_next_strand_nonce"]
129137

138+
if contact_next_strand_key and contact_next_strand_nonce:
130139
try:
131-
try:
132-
blob_plaintext = decrypt_xchacha20poly1305(chacha_key, blob[:XCHACHA20POLY1305_NONCE_LEN], blob[XCHACHA20POLY1305_NONCE_LEN:])
133-
except Exception as e:
134-
if contact_next_strand_nonce is None:
135-
raise Exception("Unable to decrypt apparent SMP request due to missing contact strand nonce.")
140+
blob_plaintext = decrypt_xchacha20poly1305(contact_next_strand_key, contact_next_strand_nonce, blob)
141+
142+
contact_next_strand_key = blob_plaintext[:32]
143+
contact_next_strand_nonce = blob_plaintext[32:32 + XCHACHA20POLY1305_NONCE_LEN]
144+
blob_plaintext = blob_plaintext[32 + XCHACHA20POLY1305_NONCE_LEN:]
136145

137-
logger.debug("Failed to decrypt blob from contact (%s) probably due to invalid nonce: %s, we will try decrypting using strand nonce", sender, str(e))
138-
blob_plaintext = decrypt_xchacha20poly1305(chacha_key, contact_next_strand_nonce, blob)
146+
with user_data_lock:
147+
user_data["contacts"][sender]["contact_next_strand_key"] = contact_next_strand_key
148+
user_data["contacts"][sender]["contact_next_strand_nonce"] = contact_next_strand_nonce
139149

140150
except Exception as e:
141-
logger.error("Failed to decrypt blob from contact (%s), we just going to treat blob as plaintext. Error: %s", sender, str(e))
142-
blob_plaintext = blob
151+
logger.error("Unable to decrypt incoming data from contact (%s). Error: %s", sender, str(e))
152+
153+
continue
143154
else:
144-
chacha_key = user_data["contacts"][sender]["contact_strand_key"]
145-
146-
if (chacha_key is None) and (contact_next_strand_nonce is None):
147-
# just assume at this point that it's not encrypted.
148-
blob_plaintext = blob
149-
else:
150-
# Under known laws of physics, this should never fail. Unless the contact is acting funny on purpose / invalid implementation of Coldwire + strandlock protocol.
151-
try:
152-
blob_plaintext = decrypt_xchacha20poly1305(chacha_key, contact_next_strand_nonce, blob)
153-
except Exception as e:
154-
logger.error(
155-
"Failed to decrypt blob from contact (%s)"
156-
"We dont know what caused this except maybe a re-SMP verification. error: %s", sender, str(e)
157-
)
158-
blob_plaintext = blob
155+
# just assume at this point that it's not encrypted.
156+
logger.debug("Contact (%s) does not have a strand key and or a strand nonce! We will just gonna assume blob_plaintext = blob", sender)
157+
blob_plaintext = blob
158+
159159
else:
160160
logger.debug("Contact (%s) not saved.. we just gonna assume blob_plaintext = blob", sender)
161161
blob_plaintext = blob
@@ -180,10 +180,9 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag):
180180
sender
181181
)
182182

183-
# *Sigh* I had to put this here because if we rotate before finishing reading all of the messages
184-
# we would overwrite our own key.
185-
# TODO: We need to keep the last used key and use it when decapsulation with new key gives invalid output
186-
# because it might actually take some time for our keys to be uploaded to server + other servers, and to the contact.
187-
#
188-
# update_ephemeral_keys(user_data, user_data_lock)
189183

184+
with user_data_lock:
185+
try:
186+
user_data["contacts"][sender]["locked"] = False
187+
except Exception:
188+
pass

logic/contacts.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def save_contact(user_data: dict, user_data_lock, contact_id: str) -> None:
4141

4242
user_data["contacts"][contact_id] = {
4343
"nickname": None,
44+
"locked": False,
4445
"lt_sign_keys": {
4546
"contact_public_key": None,
4647
"our_keys": {
@@ -60,7 +61,6 @@ def save_contact(user_data: dict, user_data_lock, contact_id: str) -> None:
6061
"contact_nonce": None,
6162
"smp_step": None,
6263
"tmp_proof": None,
63-
"tmp_key": None,
6464
"contact_kem_public_key": None,
6565
"our_kem_keys": {
6666
"private_key": None,
@@ -97,8 +97,8 @@ def save_contact(user_data: dict, user_data_lock, contact_id: str) -> None:
9797

9898
}
9999
},
100-
"our_strand_key": None,
101-
"contact_strand_key": None,
100+
"our_next_strand_key": None,
101+
"contact_next_strand_key": None,
102102
"our_next_strand_nonce": None,
103103
"contact_next_strand_nonce": None,
104104
"our_pads": None,

logic/message.py

Lines changed: 32 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def generate_and_send_pads(user_data, user_data_lock, contact_id: str, ui_queue)
6262
contact_mceliece_public_key = user_data["contacts"][contact_id]["ephemeral_keys"]["contact_public_keys"][CLASSIC_MCELIECE_8_F_NAME]
6363
our_lt_private_key = user_data["contacts"][contact_id]["lt_sign_keys"]["our_keys"]["private_key"]
6464

65-
our_strand_key = user_data["contacts"][contact_id]["our_strand_key"]
65+
our_next_strand_key = user_data["contacts"][contact_id]["our_next_strand_key"]
6666

6767
our_next_strand_nonce = user_data["contacts"][contact_id]["our_next_strand_nonce"]
6868

@@ -78,10 +78,13 @@ def generate_and_send_pads(user_data, user_data_lock, contact_id: str, ui_queue)
7878

7979
otp_batch_signature = create_signature(ML_DSA_87_NAME, kyber_ciphertext_blob + mceliece_ciphertext_blob, our_lt_private_key)
8080

81-
our_new_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
81+
# Here, the strandkey is actually just added to make messages structure uniform and easier to process in implementations
82+
# once contact receives this, he will save this new random key, then, process the batch, and save the new key derived from the batch.
83+
84+
new_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
8285
_, ciphertext_blob = encrypt_xchacha20poly1305(
83-
our_strand_key,
84-
MSG_TYPES["MSG_BATCH"] + our_new_strand_nonce + otp_batch_signature + kyber_ciphertext_blob + mceliece_ciphertext_blob + xchacha_shared_secrets,
86+
our_next_strand_key,
87+
sha3_512(secrets.token_bytes(32))[:32] + new_strand_nonce + MSG_TYPES["MSG_BATCH"] + otp_batch_signature + kyber_ciphertext_blob + mceliece_ciphertext_blob + xchacha_shared_secrets,
8588
nonce = our_next_strand_nonce
8689
)
8790

@@ -98,17 +101,19 @@ def generate_and_send_pads(user_data, user_data_lock, contact_id: str, ui_queue)
98101
ui_queue.put({"type": "showerror", "title": "Error", "message": "Failed to send our one-time-pads key batch to the server"})
99102
return False
100103

104+
# XOR shared secrets together for hybrid encryption
101105
pads, _ = one_time_pad(kyber_shared_secrets, mceliece_shared_secrets)
102106
pads, _ = one_time_pad(pads, xchacha_shared_secrets)
103107

104-
105-
our_strand_key = pads[:32]
108+
# Derive key from pad + truncate it.
109+
new_strand_key = pads[:32]
110+
otp_pads = pads[32:]
106111

107112
# We update & save only at the end, so if request fails, we do not desync our state.
108113
with user_data_lock:
109-
user_data["contacts"][contact_id]["our_next_strand_nonce"] = our_new_strand_nonce
110-
user_data["contacts"][contact_id]["our_strand_key"] = our_strand_key
111-
user_data["contacts"][contact_id]["our_pads"] = pads[32:]
114+
user_data["contacts"][contact_id]["our_next_strand_nonce"] = new_strand_nonce
115+
user_data["contacts"][contact_id]["our_next_strand_key"] = new_strand_key
116+
user_data["contacts"][contact_id]["our_pads"] = otp_pads
112117

113118

114119
save_account_data(user_data, user_data_lock)
@@ -139,6 +144,8 @@ def send_message_processor(user_data, user_data_lock, contact_id: str, message:
139144

140145
our_pads = user_data["contacts"][contact_id]["our_pads"]
141146

147+
if user_data["contacts"][contact_id]["locked"]:
148+
return
142149

143150

144151
if contact_kyber_public_key is None or contact_mceliece_public_key is None:
@@ -189,21 +196,23 @@ def send_message_processor(user_data, user_data_lock, contact_id: str, message:
189196
# because a malicious server could make our requests fail to force us to re-use the same pad for our next message
190197
# which would break all of our security
191198

192-
our_new_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
199+
new_strand_key = sha3_512(secrets.token_bytes(32))[:32]
200+
new_strand_nonce = sha3_512(secrets.token_bytes(XCHACHA20POLY1305_NONCE_LEN))[:XCHACHA20POLY1305_NONCE_LEN]
193201

194202
with user_data_lock:
195203
user_data["contacts"][contact_id]["our_pads"] = user_data["contacts"][contact_id]["our_pads"][len(message_encrypted):]
196204

197-
our_strand_key = user_data["contacts"][contact_id]["our_strand_key"]
205+
our_next_strand_key = user_data["contacts"][contact_id]["our_next_strand_key"]
206+
user_data["contacts"][contact_id]["our_next_strand_key"] = new_strand_key
198207

199208
our_next_strand_nonce = user_data["contacts"][contact_id]["our_next_strand_nonce"]
200-
user_data["contacts"][contact_id]["our_next_strand_nonce"] = our_new_strand_nonce
209+
user_data["contacts"][contact_id]["our_next_strand_nonce"] = new_strand_nonce
201210

202211
save_account_data(user_data, user_data_lock)
203212

204213
_, ciphertext_blob = encrypt_xchacha20poly1305(
205-
our_strand_key,
206-
MSG_TYPES["MSG_NEW"] + our_new_strand_nonce + message_encrypted,
214+
our_next_strand_key,
215+
new_strand_key + new_strand_nonce + MSG_TYPES["MSG_NEW"] + message_encrypted,
207216
nonce = our_next_strand_nonce
208217
)
209218

@@ -251,7 +260,7 @@ def messages_data_handler(user_data: dict, user_data_lock, user_data_copied: dic
251260
logger.error("Contact (%s) per-contact ML-DSA-87 public key is missing! Skipping message..", contact_id)
252261
return
253262

254-
if user_data_copied["contacts"][contact_id]["contact_strand_key"] is None:
263+
if user_data_copied["contacts"][contact_id]["contact_next_strand_key"] is None:
255264
logger.error("Contact (%s) strand key key is missing! Skipping message...", contact_id)
256265
return
257266

@@ -262,14 +271,14 @@ def messages_data_handler(user_data: dict, user_data_lock, user_data_copied: dic
262271

263272
# /32 because KEM shared_secret is 32 bytes, /64 because sha3_512 output is 64 bytes
264273

265-
if len(msgs_plaintext) != ( (ML_KEM_1024_CT_LEN + CLASSIC_MCELIECE_8_F_CT_LEN) * (OTP_PAD_SIZE // 32)) + (64 * (OTP_PAD_SIZE // 64)) + ML_DSA_87_SIGN_LEN + XCHACHA20POLY1305_NONCE_LEN + 1:
274+
if len(msgs_plaintext) != ( (ML_KEM_1024_CT_LEN + CLASSIC_MCELIECE_8_F_CT_LEN) * (OTP_PAD_SIZE // 32)) + (64 * (OTP_PAD_SIZE // 64)) + ML_DSA_87_SIGN_LEN + 1:
266275
logger.error("Contact (%s) gave us a otp batch message request with malformed strand plaintext length (%d)", contact_id, len(msgs_plaintext))
267276
return
268277

269-
otp_hashchain_signature = msgs_plaintext[1 + XCHACHA20POLY1305_NONCE_LEN: ML_DSA_87_SIGN_LEN + XCHACHA20POLY1305_NONCE_LEN + 1]
270-
otp_hashchain_ciphertext = msgs_plaintext[ML_DSA_87_SIGN_LEN + XCHACHA20POLY1305_NONCE_LEN + 1: ML_DSA_87_SIGN_LEN + XCHACHA20POLY1305_NONCE_LEN + 1 + ((ML_KEM_1024_CT_LEN + CLASSIC_MCELIECE_8_F_CT_LEN) * (OTP_PAD_SIZE // 32))]
278+
otp_hashchain_signature = msgs_plaintext[1: ML_DSA_87_SIGN_LEN + 1]
279+
otp_hashchain_ciphertext = msgs_plaintext[ML_DSA_87_SIGN_LEN + 1: ML_DSA_87_SIGN_LEN + 1 + ((ML_KEM_1024_CT_LEN + CLASSIC_MCELIECE_8_F_CT_LEN) * (OTP_PAD_SIZE // 32))]
271280

272-
xchacha_pads = msgs_plaintext[ML_DSA_87_SIGN_LEN + XCHACHA20POLY1305_NONCE_LEN + 1 + ((ML_KEM_1024_CT_LEN + CLASSIC_MCELIECE_8_F_CT_LEN) * (OTP_PAD_SIZE // 32)):]
281+
xchacha_pads = msgs_plaintext[ML_DSA_87_SIGN_LEN + 1 + ((ML_KEM_1024_CT_LEN + CLASSIC_MCELIECE_8_F_CT_LEN) * (OTP_PAD_SIZE // 32)):]
273282

274283
try:
275284
valid_signature = verify_signature(ML_DSA_87_NAME, otp_hashchain_ciphertext, otp_hashchain_signature, contact_public_key)
@@ -298,14 +307,14 @@ def messages_data_handler(user_data: dict, user_data_lock, user_data_copied: dic
298307
contact_pads, _ = one_time_pad(contact_kyber_pads, contact_mceliece_pads)
299308
contact_pads, _ = one_time_pad(contact_pads, xchacha_pads)
300309

301-
contact_strand_key = contact_pads[:32]
310+
contact_next_strand_key = contact_pads[:32]
302311
contact_pads = contact_pads[32:]
303312

304313

305314
with user_data_lock:
306315
user_data["contacts"][contact_id]["contact_pads"] = contact_pads
307316

308-
user_data["contacts"][contact_id]["contact_strand_key"] = contact_strand_key
317+
user_data["contacts"][contact_id]["contact_next_strand_key"] = contact_next_strand_key
309318

310319
user_data["contacts"][contact_id]["ephemeral_keys"]["our_keys"][CLASSIC_MCELIECE_8_F_NAME]["rotation_counter"] += 1
311320

@@ -331,12 +340,12 @@ def messages_data_handler(user_data: dict, user_data_lock, user_data_copied: dic
331340
elif bytes([msgs_plaintext[0]]) == MSG_TYPES["MSG_NEW"]:
332341
logger.debug("Received a new message from contact (%s).", contact_id)
333342

334-
if len(msgs_plaintext) < OTP_MAX_BUCKET + XCHACHA20POLY1305_NONCE_LEN + 1:
343+
if len(msgs_plaintext) < OTP_MAX_BUCKET + 1:
335344
logger.error("Contact (%s) gave us a message request with malformed strand plaintext length (%d)", contact_id, len(msgs_plaintext))
336345
return
337346

338347

339-
message_encrypted = msgs_plaintext[XCHACHA20POLY1305_NONCE_LEN + 1:]
348+
message_encrypted = msgs_plaintext[1:]
340349

341350

342351
with user_data_lock:
@@ -378,5 +387,3 @@ def messages_data_handler(user_data: dict, user_data_lock, user_data_copied: dic
378387
logger.error("Received unknown message type (%d)", msgs_plaintext[0])
379388
return
380389

381-
with user_data_lock:
382-
user_data["contacts"][contact_id]["contact_next_strand_nonce"] = msgs_plaintext[1: XCHACHA20POLY1305_NONCE_LEN + 1]

0 commit comments

Comments
 (0)