Skip to content

Commit 43089ec

Browse files
authored
Merge pull request #9 from Freedom-Club-Sec/feat/federation-support
feat: federation support & SMP adjustments
2 parents e572364 + 2d3372f commit 43089ec

File tree

12 files changed

+69
-61
lines changed

12 files changed

+69
-61
lines changed

core/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
PFS_TYPE = b"\x01"
1111
MSG_TYPE = b"\x02"
1212

13+
COLDWIRE_DATA_SEP = b"\0"
1314
COLDWIRE_LEN_OFFSET = 3
1415

1516
# network defaults (seconds & bytes)

core/crypto.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import oqs
1616
import secrets
17-
from typing import Tuple
1817
from core.constants import (
1918
OTP_PAD_SIZE,
2019
OTP_MAX_RANDOM_PAD,
@@ -62,7 +61,7 @@ def verify_signature(algorithm: str, message: bytes, signature: bytes, public_ke
6261
with oqs.Signature(algorithm) as verifier:
6362
return verifier.verify(message, signature[:ALGOS_BUFFER_LIMITS[algorithm]["SIGN_LEN"]], public_key[:ALGOS_BUFFER_LIMITS[algorithm]["PK_LEN"]])
6463

65-
def generate_sign_keys(algorithm: str = ML_DSA_87_NAME) -> Tuple[bytes, bytes]:
64+
def generate_sign_keys(algorithm: str = ML_DSA_87_NAME) -> tuple[bytes, bytes]:
6665
"""
6766
Generates a new post-quantum signature keypair.
6867
@@ -77,7 +76,7 @@ def generate_sign_keys(algorithm: str = ML_DSA_87_NAME) -> Tuple[bytes, bytes]:
7776
private_key = signer.export_secret_key()
7877
return private_key, public_key
7978

80-
def otp_encrypt_with_padding(plaintext: bytes, key: bytes) -> Tuple[bytes, bytes]:
79+
def otp_encrypt_with_padding(plaintext: bytes, key: bytes) -> tuple[bytes, bytes]:
8180
"""
8281
Encrypts plaintext using a one-time pad with random or bucket padding.
8382
@@ -151,7 +150,7 @@ def one_time_pad(plaintext: bytes, key: bytes) -> bytes:
151150
key = key[len(otpd_plaintext):]
152151
return otpd_plaintext, key
153152

154-
def generate_kem_keys(algorithm: str) -> Tuple[bytes, bytes]:
153+
def generate_kem_keys(algorithm: str) -> tuple[bytes, bytes]:
155154
"""
156155
Generates a KEM keypair.
157156
@@ -166,7 +165,7 @@ def generate_kem_keys(algorithm: str) -> Tuple[bytes, bytes]:
166165
private_key = kem.export_secret_key()
167166
return private_key, public_key
168167

169-
def encap_shared_secret(public_key: bytes, algorithm: str) -> Tuple[bytes, bytes]:
168+
def encap_shared_secret(public_key: bytes, algorithm: str) -> tuple[bytes, bytes]:
170169
"""
171170
Derive a KEM shared secret from a public key.
172171
@@ -226,7 +225,7 @@ def decrypt_shared_secrets(ciphertext_blob: bytes, private_key: bytes, algorithm
226225

227226
return shared_secrets #[:otp_pad_size]
228227

229-
def generate_shared_secrets(public_key: bytes, algorithm: str = None, size: int = OTP_PAD_SIZE) -> Tuple[bytes, bytes]:
228+
def generate_shared_secrets(public_key: bytes, algorithm: str = None, size: int = OTP_PAD_SIZE) -> tuple[bytes, bytes]:
230229
"""
231230
Generates many shared secrets via `algorithm` encapsulation in chunks.
232231

core/requests.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def http_request(url: str, method: str, auth_token: str = None, metadata: dict =
9999
body = b""
100100

101101
if metadata is not None:
102-
body += encode_field("metadata", json.dumps({"metadata": metadata}), boundary, CRLF)
102+
body += encode_field("metadata", json.dumps(metadata), boundary, CRLF)
103103

104104
if blob is not None:
105105
body += encode_file("blob", "blob.bin", blob, boundary, CRLF)

logic/authentication.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,11 @@ def authenticate_account(user_data: dict) -> dict:
4444
raise ValueError("Could not connect to server! Are you sure your proxy settings are valid ?")
4545
else:
4646
raise ValueError("Could not connect to server! Are you sure the URL is valid ?")
47-
48-
response = json.loads(response.decode())
47+
48+
try:
49+
response = json.loads(response.decode())
50+
except Exception as e:
51+
raise ValueError("Error while parsing server JSON response: " + str(e))
4952

5053
if not 'challenge' in response:
5154
raise ValueError("Server did not give authenticatation challenge! Are you sure this is a Coldwire server ?")

logic/background_worker.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from logic.smp import smp_unanswered_questions, smp_data_handler
33
from logic.pfs import pfs_data_handler, update_ephemeral_keys
44
from logic.message import messages_data_handler
5+
from logic.user import validate_identifier
56
from core.constants import (
67
LONGPOLL_MIN,
78
LONGPOLL_MAX,
@@ -86,12 +87,12 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag):
8687
logger.debug("Received data: %s", str(message)[:3000])
8788

8889
# Sanity check universal message fields
89-
if (not "sender" in message) or (not message["sender"].isdigit()) or (len(message["sender"]) != 16):
90-
logger.error("Impossible condition, either you have discovered a bug in Coldwire, or the server is attempting to denial-of-service you. Skipping data message with no (or malformed) sender...")
91-
92-
if "sender" in message:
93-
logger.debug("Impossible condition's sender is: %s", message["sender"])
90+
if (not "sender" in message):
91+
logger.error("Impossible condition, either you have discovered a bug in Coldwire, or the server is attempting to denial-of-service you. Skipping data message with no sender...")
92+
continue
9493

94+
if not validate_identifier(message["sender"]):
95+
logger.error("Impossible condition, either you have discovered a bug in Coldwire, or the server is attempting to denial-of-service you. Skipping data message with malformed sender identifier (%s)...", message["sender"])
9596
continue
9697

9798
sender = message["sender"]
@@ -115,10 +116,10 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag):
115116
try:
116117
blob_plaintext = decrypt_xchacha20poly1305(chacha_key, blob[:XCHACHA20POLY1305_NONCE_LEN], blob[XCHACHA20POLY1305_NONCE_LEN:])
117118
except Exception as e:
118-
logger.debug("Failed to decrypt blob from contact (%s) probably due to invalid nonce: %s", sender, str(e))
119119
if contact_next_strand_nonce is None:
120120
raise Exception("Unable to decrypt apparent SMP request due to missing contact strand nonce.")
121121

122+
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))
122123
blob_plaintext = decrypt_xchacha20poly1305(chacha_key, contact_next_strand_nonce, blob)
123124

124125
except Exception as e:

logic/get_user.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

logic/smp.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,10 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str,
244244

245245

246246
def smp_step_4_request_answer(user_data, user_data_lock, contact_id, smp_plaintext, ui_queue) -> None:
247+
with user_data_lock:
248+
our_nonce = b64decode(user_data["contacts"][contact_id]["lt_sign_key_smp"]["our_nonce"])
249+
250+
247251
contact_new_strand_nonce = smp_plaintext[:XCHACHA20POLY1305_NONCE_LEN]
248252

249253
contact_signing_public_key = smp_plaintext[XCHACHA20POLY1305_NONCE_LEN : ML_DSA_87_PK_LEN + XCHACHA20POLY1305_NONCE_LEN]
@@ -254,6 +258,12 @@ def smp_step_4_request_answer(user_data, user_data_lock, contact_id, smp_plainte
254258

255259
question = smp_plaintext[SMP_NONCE_LENGTH + XCHACHA20POLY1305_NONCE_LEN + SMP_PROOF_LENGTH + ML_DSA_87_PK_LEN:].decode("utf-8")
256260

261+
if our_nonce == contact_nonce:
262+
logger.warning("SMP Verification failed at step 4: Contact nonce is the same as our nonce!")
263+
smp_failure_notify_contact(user_data, user_data_lock, contact_id, ui_queue)
264+
return
265+
266+
257267

258268
with user_data_lock:
259269
user_data["contacts"][contact_id]["lt_sign_key_smp"]["question"] = question

logic/user.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,24 @@ def build_initial_user_data() -> dict:
88
"ignore_new_contacts_smp": False,
99
}
1010
}
11+
12+
13+
def validate_identifier(identifier) -> bool:
14+
if identifier.isdigit() and len(identifier) == 16:
15+
return True
16+
17+
18+
split = identifier.split("@")
19+
if len(split) != 2:
20+
return False
21+
22+
if not split[0].isdigit():
23+
return False
24+
25+
# Max domain length is 253 bytes
26+
if len(split[1] > 253):
27+
return False
28+
29+
30+
return True
31+

main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def setup_logging(debug: bool) -> None:
2525
logger.addHandler(handler)
2626

2727
def parse_args():
28-
parser = argparse.ArgumentParser(description="Coldwire - Post-Quantum secure messenger")
28+
parser = argparse.ArgumentParser(description="Coldwire - Ultra-Paranoid, Post-Quantum Secure Messenger")
2929
parser.add_argument("--debug", action="store_true", help="Enable debug logging")
3030
return parser.parse_args()
3131

ui/add_contact_prompt.py

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from tkinter import messagebox
22
from ui.utils import *
3-
from logic.get_user import check_if_contact_exists
3+
from logic.user import validate_identifier
44
from logic.contacts import save_contact
55
from logic.storage import save_account_data
66
import tkinter as tk
@@ -20,7 +20,7 @@ def __init__(self, master):
2020
self.entry = tk.Entry(self, font=("Helvetica", 12), bg="gray15", fg="white", insertbackground="white")
2121
self.entry.pack(pady=5)
2222
self.entry.focus()
23-
enhanced_entry(self.entry, placeholder="I.e. 1234567890123456")
23+
enhanced_entry(self.entry, placeholder="I.e. 1234567890123456, [email protected]")
2424

2525
self.status = tk.Label(self, text="", fg="gray", bg="black", font=("Helvetica", 10))
2626
self.status.pack(pady=(5, 0))
@@ -42,28 +42,25 @@ def __init__(self, master):
4242

4343
def add_contact(self):
4444
contact_id = self.entry.get().strip()
45-
if not (contact_id.isdigit() and len(contact_id) == 16):
45+
"""if not (contact_id.isdigit() and len(contact_id) == 16):
4646
self.status.config(text="Invalid User ID", fg="red")
4747
return
48+
"""
4849

4950
if contact_id == self.master.user_data["user_id"]:
5051
self.status.config(text="You cannot add yourself", fg="red")
5152
return
5253

53-
try:
54-
if not check_if_contact_exists(self.master.user_data, self.master.user_data_lock, contact_id):
55-
logger.error("[BUG] This should never execute, because the server should return a 40X error code and that should cause an exception..")
56-
return
57-
58-
save_contact(self.master.user_data, self.master.user_data_lock, contact_id)
59-
save_account_data(self.master.user_data, self.master.user_data_lock)
60-
except ValueError as e:
61-
self.status.config(text=e, fg="red")
62-
logger.error("Error occured while adding new contact (%s): %s ", contact_id, e)
54+
if not validate_identifier(contact_id):
55+
logger.debug("Identifier is invalid.")
56+
self.status.config(text = "Invalid identifier", fg="red")
6357
return
64-
58+
59+
save_contact(self.master.user_data, self.master.user_data_lock, contact_id)
60+
save_account_data(self.master.user_data, self.master.user_data_lock)
61+
6562

6663
self.master.new_contact(contact_id)
6764
self.destroy()
68-
messagebox.showinfo("Added", "Added the user to your contact list")
65+
messagebox.showinfo("Added", f"Added the `{contact_id}` to your contact list.")
6966

0 commit comments

Comments
 (0)