From b8e7742a9430e4852f57fd2f713a5935053a34c2 Mon Sep 17 00:00:00 2001 From: chadsec1 Date: Sat, 6 Sep 2025 21:31:42 +0300 Subject: [PATCH 1/4] feat: federation support & SMP adjustments --- core/constants.py | 1 + core/crypto.py | 11 +++++------ core/requests.py | 2 +- logic/authentication.py | 7 +++++-- logic/background_worker.py | 18 ++++++++++++------ logic/smp.py | 10 ++++++++++ main.py | 2 +- ui/add_contact_prompt.py | 9 +++++---- ui/connect_window.py | 4 +++- ui/password_window.py | 4 +++- 10 files changed, 46 insertions(+), 22 deletions(-) diff --git a/core/constants.py b/core/constants.py index 3c9858e..98388c0 100644 --- a/core/constants.py +++ b/core/constants.py @@ -10,6 +10,7 @@ PFS_TYPE = b"\x01" MSG_TYPE = b"\x02" +COLDWIRE_DATA_SEP = b"\0" COLDWIRE_LEN_OFFSET = 3 # network defaults (seconds & bytes) diff --git a/core/crypto.py b/core/crypto.py index 82c4865..4c506ee 100644 --- a/core/crypto.py +++ b/core/crypto.py @@ -14,7 +14,6 @@ import oqs import secrets -from typing import Tuple from core.constants import ( OTP_PAD_SIZE, OTP_MAX_RANDOM_PAD, @@ -62,7 +61,7 @@ def verify_signature(algorithm: str, message: bytes, signature: bytes, public_ke with oqs.Signature(algorithm) as verifier: return verifier.verify(message, signature[:ALGOS_BUFFER_LIMITS[algorithm]["SIGN_LEN"]], public_key[:ALGOS_BUFFER_LIMITS[algorithm]["PK_LEN"]]) -def generate_sign_keys(algorithm: str = ML_DSA_87_NAME) -> Tuple[bytes, bytes]: +def generate_sign_keys(algorithm: str = ML_DSA_87_NAME) -> tuple[bytes, bytes]: """ Generates a new post-quantum signature keypair. @@ -77,7 +76,7 @@ def generate_sign_keys(algorithm: str = ML_DSA_87_NAME) -> Tuple[bytes, bytes]: private_key = signer.export_secret_key() return private_key, public_key -def otp_encrypt_with_padding(plaintext: bytes, key: bytes) -> Tuple[bytes, bytes]: +def otp_encrypt_with_padding(plaintext: bytes, key: bytes) -> tuple[bytes, bytes]: """ Encrypts plaintext using a one-time pad with random or bucket padding. @@ -151,7 +150,7 @@ def one_time_pad(plaintext: bytes, key: bytes) -> bytes: key = key[len(otpd_plaintext):] return otpd_plaintext, key -def generate_kem_keys(algorithm: str) -> Tuple[bytes, bytes]: +def generate_kem_keys(algorithm: str) -> tuple[bytes, bytes]: """ Generates a KEM keypair. @@ -166,7 +165,7 @@ def generate_kem_keys(algorithm: str) -> Tuple[bytes, bytes]: private_key = kem.export_secret_key() return private_key, public_key -def encap_shared_secret(public_key: bytes, algorithm: str) -> Tuple[bytes, bytes]: +def encap_shared_secret(public_key: bytes, algorithm: str) -> tuple[bytes, bytes]: """ Derive a KEM shared secret from a public key. @@ -226,7 +225,7 @@ def decrypt_shared_secrets(ciphertext_blob: bytes, private_key: bytes, algorithm return shared_secrets #[:otp_pad_size] -def generate_shared_secrets(public_key: bytes, algorithm: str = None, size: int = OTP_PAD_SIZE) -> Tuple[bytes, bytes]: +def generate_shared_secrets(public_key: bytes, algorithm: str = None, size: int = OTP_PAD_SIZE) -> tuple[bytes, bytes]: """ Generates many shared secrets via `algorithm` encapsulation in chunks. diff --git a/core/requests.py b/core/requests.py index 7f1e0e6..81c3998 100644 --- a/core/requests.py +++ b/core/requests.py @@ -99,7 +99,7 @@ def http_request(url: str, method: str, auth_token: str = None, metadata: dict = body = b"" if metadata is not None: - body += encode_field("metadata", json.dumps({"metadata": metadata}), boundary, CRLF) + body += encode_field("metadata", json.dumps(metadata), boundary, CRLF) if blob is not None: body += encode_file("blob", "blob.bin", blob, boundary, CRLF) diff --git a/logic/authentication.py b/logic/authentication.py index fb8b815..15d23fd 100644 --- a/logic/authentication.py +++ b/logic/authentication.py @@ -44,8 +44,11 @@ def authenticate_account(user_data: dict) -> dict: raise ValueError("Could not connect to server! Are you sure your proxy settings are valid ?") else: raise ValueError("Could not connect to server! Are you sure the URL is valid ?") - - response = json.loads(response.decode()) + + try: + response = json.loads(response.decode()) + except Exception as e: + raise ValueError("Error while parsing server JSON response: ") if not 'challenge' in response: raise ValueError("Server did not give authenticatation challenge! Are you sure this is a Coldwire server ?") diff --git a/logic/background_worker.py b/logic/background_worker.py index 12cd115..78090db 100644 --- a/logic/background_worker.py +++ b/logic/background_worker.py @@ -86,13 +86,19 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag): logger.debug("Received data: %s", str(message)[:3000]) # Sanity check universal message fields - if (not "sender" in message) or (not message["sender"].isdigit()) or (len(message["sender"]) != 16): - 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...") - - if "sender" in message: - logger.debug("Impossible condition's sender is: %s", message["sender"]) + if (not "sender" in message): + 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...") + continue + if message["sender"].isdigit() and len(message["sender"]) != 16: + 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 same-server sender (%s)...", message["sender"]) continue + + if (not message["sender"].isdigit()): + split = message["sender"].split("@") + if (len(split) != 2) or (not split[0].isdigit()): + 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 federated-server sender (%s)...", message["sender"]) + continue sender = message["sender"] blob = message["blob"] @@ -115,10 +121,10 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag): try: blob_plaintext = decrypt_xchacha20poly1305(chacha_key, blob[:XCHACHA20POLY1305_NONCE_LEN], blob[XCHACHA20POLY1305_NONCE_LEN:]) except Exception as e: - logger.debug("Failed to decrypt blob from contact (%s) probably due to invalid nonce: %s", sender, str(e)) if contact_next_strand_nonce is None: raise Exception("Unable to decrypt apparent SMP request due to missing contact strand nonce.") + 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)) blob_plaintext = decrypt_xchacha20poly1305(chacha_key, contact_next_strand_nonce, blob) except Exception as e: diff --git a/logic/smp.py b/logic/smp.py index 631fd50..7973bf4 100644 --- a/logic/smp.py +++ b/logic/smp.py @@ -244,6 +244,10 @@ def smp_step_3(user_data: dict, user_data_lock: threading.Lock, contact_id: str, def smp_step_4_request_answer(user_data, user_data_lock, contact_id, smp_plaintext, ui_queue) -> None: + with user_data_lock: + our_nonce = b64decode(user_data["contacts"][contact_id]["lt_sign_key_smp"]["our_nonce"]) + + contact_new_strand_nonce = smp_plaintext[:XCHACHA20POLY1305_NONCE_LEN] 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 question = smp_plaintext[SMP_NONCE_LENGTH + XCHACHA20POLY1305_NONCE_LEN + SMP_PROOF_LENGTH + ML_DSA_87_PK_LEN:].decode("utf-8") + if our_nonce == contact_nonce: + logger.warning("SMP Verification failed at step 4: Contact nonce is the same as our nonce!") + smp_failure_notify_contact(user_data, user_data_lock, contact_id, ui_queue) + return + + with user_data_lock: user_data["contacts"][contact_id]["lt_sign_key_smp"]["question"] = question diff --git a/main.py b/main.py index 8e212e4..c5a54ab 100644 --- a/main.py +++ b/main.py @@ -25,7 +25,7 @@ def setup_logging(debug: bool) -> None: logger.addHandler(handler) def parse_args(): - parser = argparse.ArgumentParser(description="Coldwire - Post-Quantum secure messenger") + parser = argparse.ArgumentParser(description="Coldwire - Ultra-Paranoid, Post-Quantum Secure Messenger") parser.add_argument("--debug", action="store_true", help="Enable debug logging") return parser.parse_args() diff --git a/ui/add_contact_prompt.py b/ui/add_contact_prompt.py index e61a07f..7554472 100644 --- a/ui/add_contact_prompt.py +++ b/ui/add_contact_prompt.py @@ -42,18 +42,19 @@ def __init__(self, master): def add_contact(self): contact_id = self.entry.get().strip() - if not (contact_id.isdigit() and len(contact_id) == 16): + """if not (contact_id.isdigit() and len(contact_id) == 16): self.status.config(text="Invalid User ID", fg="red") return + """ if contact_id == self.master.user_data["user_id"]: self.status.config(text="You cannot add yourself", fg="red") return try: - if not check_if_contact_exists(self.master.user_data, self.master.user_data_lock, contact_id): - logger.error("[BUG] This should never execute, because the server should return a 40X error code and that should cause an exception..") - return + # if not check_if_contact_exists(self.master.user_data, self.master.user_data_lock, contact_id): + # logger.error("[BUG] This should never execute, because the server should return a 40X error code and that should cause an exception..") + # return save_contact(self.master.user_data, self.master.user_data_lock, contact_id) save_account_data(self.master.user_data, self.master.user_data_lock) diff --git a/ui/connect_window.py b/ui/connect_window.py index 691a0cf..ea6be0a 100644 --- a/ui/connect_window.py +++ b/ui/connect_window.py @@ -1,4 +1,6 @@ -from ui.utils import * +from ui.utils import ( + enhanced_entry +) from ui.password_window import PasswordWindow from logic.storage import save_account_data from logic.authentication import authenticate_account diff --git a/ui/password_window.py b/ui/password_window.py index bf253c0..f03612a 100644 --- a/ui/password_window.py +++ b/ui/password_window.py @@ -1,6 +1,8 @@ import tkinter as tk from tkinter import messagebox -from ui.utils import * +from ui.utils import ( + enhanced_entry +) class PasswordWindow(tk.Toplevel): def __init__(self, master, callback): From 5309452797745a30ef5587bd38a9f6af1627958f Mon Sep 17 00:00:00 2001 From: chadsec1 Date: Sat, 6 Sep 2025 21:56:02 +0300 Subject: [PATCH 2/4] fix: local identifier verification --- logic/get_user.py | 28 ---------------------------- logic/user.py | 21 +++++++++++++++++++++ ui/add_contact_prompt.py | 20 ++++++++------------ 3 files changed, 29 insertions(+), 40 deletions(-) delete mode 100644 logic/get_user.py diff --git a/logic/get_user.py b/logic/get_user.py deleted file mode 100644 index 144026b..0000000 --- a/logic/get_user.py +++ /dev/null @@ -1,28 +0,0 @@ -from core.requests import http_request -import json - -def check_if_contact_exists(user_data: dict, user_data_lock, contact_id: str) -> bool: - with user_data_lock: - url = user_data["server_url"] - auth_token = user_data["token"] - - try: - response = http_request(f"{url}/get_user?user_id={contact_id}", "GET", auth_token = auth_token) - except: - raise ValueError("Could not connect to server, try again") - - response = json.loads(response.decode()) - - if not "status" in response: - raise ValueError("Server gave a malformed response") - - if response["status"] == "failure": - if not 'error' in response: - raise ValueError("Server gave a malformed response") - else: - raise ValueError(response["error"][:1024]) - - if response["status"] == "success": - return True - - return False diff --git a/logic/user.py b/logic/user.py index 78c4255..da27bad 100644 --- a/logic/user.py +++ b/logic/user.py @@ -8,3 +8,24 @@ def build_initial_user_data() -> dict: "ignore_new_contacts_smp": False, } } + + +def validate_identifier(identifier) -> bool: + if identifier.isdigit() and len(identifier) == 16: + return True + + + split = identifier.split("@") + if len(split) != 2: + return False + + if not split[0].isdigit(): + return False + + # Max domain length is 253 bytes + if len(split[1] > 253): + return False + + + return True + diff --git a/ui/add_contact_prompt.py b/ui/add_contact_prompt.py index 7554472..dfdfa7d 100644 --- a/ui/add_contact_prompt.py +++ b/ui/add_contact_prompt.py @@ -1,6 +1,6 @@ from tkinter import messagebox from ui.utils import * -from logic.get_user import check_if_contact_exists +from logic.user import validate_identifier from logic.contacts import save_contact from logic.storage import save_account_data import tkinter as tk @@ -51,18 +51,14 @@ def add_contact(self): self.status.config(text="You cannot add yourself", fg="red") return - try: - # if not check_if_contact_exists(self.master.user_data, self.master.user_data_lock, contact_id): - # logger.error("[BUG] This should never execute, because the server should return a 40X error code and that should cause an exception..") - # return - - save_contact(self.master.user_data, self.master.user_data_lock, contact_id) - save_account_data(self.master.user_data, self.master.user_data_lock) - except ValueError as e: - self.status.config(text=e, fg="red") - logger.error("Error occured while adding new contact (%s): %s ", contact_id, e) + if not validate_identifier(contact_id): + logger.debug("Identifier is invalid.") + self.status.config(text = "Invalid identifier", fg="red") return - + + save_contact(self.master.user_data, self.master.user_data_lock, contact_id) + save_account_data(self.master.user_data, self.master.user_data_lock) + self.master.new_contact(contact_id) self.destroy() From 1d3e15742c03f51559ae4109b422124a84b19fd6 Mon Sep 17 00:00:00 2001 From: chadsec1 Date: Sat, 6 Sep 2025 21:59:50 +0300 Subject: [PATCH 3/4] fix: local identifier verification --- logic/background_worker.py | 11 +++-------- ui/add_contact_prompt.py | 4 ++-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/logic/background_worker.py b/logic/background_worker.py index 78090db..555c7fd 100644 --- a/logic/background_worker.py +++ b/logic/background_worker.py @@ -2,6 +2,7 @@ from logic.smp import smp_unanswered_questions, smp_data_handler from logic.pfs import pfs_data_handler, update_ephemeral_keys from logic.message import messages_data_handler +from logic.user import validate_identifier from core.constants import ( LONGPOLL_MIN, LONGPOLL_MAX, @@ -90,15 +91,9 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag): 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...") continue - if message["sender"].isdigit() and len(message["sender"]) != 16: - 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 same-server sender (%s)...", message["sender"]) + if not validate_identifier(message["sender"]): + 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"]) continue - - if (not message["sender"].isdigit()): - split = message["sender"].split("@") - if (len(split) != 2) or (not split[0].isdigit()): - 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 federated-server sender (%s)...", message["sender"]) - continue sender = message["sender"] blob = message["blob"] diff --git a/ui/add_contact_prompt.py b/ui/add_contact_prompt.py index dfdfa7d..db58c7b 100644 --- a/ui/add_contact_prompt.py +++ b/ui/add_contact_prompt.py @@ -20,7 +20,7 @@ def __init__(self, master): self.entry = tk.Entry(self, font=("Helvetica", 12), bg="gray15", fg="white", insertbackground="white") self.entry.pack(pady=5) self.entry.focus() - enhanced_entry(self.entry, placeholder="I.e. 1234567890123456") + enhanced_entry(self.entry, placeholder="I.e. 1234567890123456, 1234567890123456@example.com") self.status = tk.Label(self, text="", fg="gray", bg="black", font=("Helvetica", 10)) self.status.pack(pady=(5, 0)) @@ -62,5 +62,5 @@ def add_contact(self): self.master.new_contact(contact_id) self.destroy() - messagebox.showinfo("Added", "Added the user to your contact list") + messagebox.showinfo("Added", f"Added the `{contact_id}` to your contact list.") From 2d3372fce7d5868b4d321742a58064d4e0d89bac Mon Sep 17 00:00:00 2001 From: chadsec1 Date: Sat, 6 Sep 2025 22:01:14 +0300 Subject: [PATCH 4/4] refactor: include server-sided errors --- logic/authentication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/logic/authentication.py b/logic/authentication.py index 15d23fd..2ddc5c7 100644 --- a/logic/authentication.py +++ b/logic/authentication.py @@ -48,7 +48,7 @@ def authenticate_account(user_data: dict) -> dict: try: response = json.loads(response.decode()) except Exception as e: - raise ValueError("Error while parsing server JSON response: ") + raise ValueError("Error while parsing server JSON response: " + str(e)) if not 'challenge' in response: raise ValueError("Server did not give authenticatation challenge! Are you sure this is a Coldwire server ?")