Skip to content
This repository was archived by the owner on Mar 10, 2026. It is now read-only.

Commit b8e7742

Browse files
committed
feat: federation support & SMP adjustments
1 parent f8e3031 commit b8e7742

File tree

10 files changed

+46
-22
lines changed

10 files changed

+46
-22
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: ")
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: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,19 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag):
8686
logger.debug("Received data: %s", str(message)[:3000])
8787

8888
# 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"])
89+
if (not "sender" in message):
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 sender...")
91+
continue
9492

93+
if message["sender"].isdigit() and len(message["sender"]) != 16:
94+
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"])
9595
continue
96+
97+
if (not message["sender"].isdigit()):
98+
split = message["sender"].split("@")
99+
if (len(split) != 2) or (not split[0].isdigit()):
100+
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"])
101+
continue
96102

97103
sender = message["sender"]
98104
blob = message["blob"]
@@ -115,10 +121,10 @@ def background_worker(user_data, user_data_lock, ui_queue, stop_flag):
115121
try:
116122
blob_plaintext = decrypt_xchacha20poly1305(chacha_key, blob[:XCHACHA20POLY1305_NONCE_LEN], blob[XCHACHA20POLY1305_NONCE_LEN:])
117123
except Exception as e:
118-
logger.debug("Failed to decrypt blob from contact (%s) probably due to invalid nonce: %s", sender, str(e))
119124
if contact_next_strand_nonce is None:
120125
raise Exception("Unable to decrypt apparent SMP request due to missing contact strand nonce.")
121126

127+
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))
122128
blob_plaintext = decrypt_xchacha20poly1305(chacha_key, contact_next_strand_nonce, blob)
123129

124130
except Exception as e:

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

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: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@ 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

5354
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
55+
# if not check_if_contact_exists(self.master.user_data, self.master.user_data_lock, contact_id):
56+
# logger.error("[BUG] This should never execute, because the server should return a 40X error code and that should cause an exception..")
57+
# return
5758

5859
save_contact(self.master.user_data, self.master.user_data_lock, contact_id)
5960
save_account_data(self.master.user_data, self.master.user_data_lock)

ui/connect_window.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
from ui.utils import *
1+
from ui.utils import (
2+
enhanced_entry
3+
)
24
from ui.password_window import PasswordWindow
35
from logic.storage import save_account_data
46
from logic.authentication import authenticate_account

ui/password_window.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import tkinter as tk
22
from tkinter import messagebox
3-
from ui.utils import *
3+
from ui.utils import (
4+
enhanced_entry
5+
)
46

57
class PasswordWindow(tk.Toplevel):
68
def __init__(self, master, callback):

0 commit comments

Comments
 (0)