Skip to content

Commit 0e93cee

Browse files
committed
refactor: Add SMP warnings for low-entropy answers
1 parent bdb29a5 commit 0e93cee

File tree

3 files changed

+48
-27
lines changed

3 files changed

+48
-27
lines changed

logic/smp.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,8 @@ def initiate_smp(user_data: dict, user_data_lock: threading.Lock, contact_id: st
7373
response = http_request(f"{server_url}/data/send", "POST", metadata = {
7474
"recipient": contact_id
7575
}, blob = SMP_TYPE + kem_public_key, auth_token = auth_token)
76-
except Exception:
77-
raise ValueError("Could not connect to server")
76+
except Exception as e:
77+
raise ValueError("Could not connect to server: " + str(e))
7878

7979
response = json.loads(response.decode())
8080
if (not ("status" in response)) or response["status"] != "success":

logic/utils.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
import traceback
22

3+
def check_str_high_entropy(s: str) -> bool:
4+
# strings under 8 characters long are insecure no matter the language.
5+
if len(s) < 8:
6+
return False
7+
8+
# if string is not all ascii, just assume it has enough entropy.
9+
if not s.ascii():
10+
return True
11+
12+
# Check if string is all lowercase or uppercase
13+
if s.lower() == s or s.upper() == s:
14+
return False
15+
16+
# if all digits
17+
if s.isdigit():
18+
return False
19+
20+
# if doesn't contain digits
21+
if not any(c.isdigit() for c in s):
22+
return False
23+
24+
# Check for special characters, spaces, etc
25+
if not any(not c.isalnum() for c in s):
26+
return False
27+
28+
return True
29+
330

431
def thread_failsafe_wrapper(target, stop_flag, ui_queue, *args, **kwargs):
532
try:

ui/smp_setup_window.py

Lines changed: 19 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import tkinter as tk
22
from tkinter import messagebox
3-
from ui.utils import *
3+
from ui.utils import (
4+
enhanced_entry
5+
)
6+
from logic.utils import (
7+
check_str_high_entropy
8+
)
49
from logic.smp import initiate_smp
510
from core.constants import (
611
SMP_QUESTION_MAX_LEN
@@ -29,17 +34,15 @@ def __init__(self, master, contact_id):
2934
tk.Label(self, text="Question:", fg="white", bg="black", anchor="w").pack(fill="x", padx=20)
3035
self.question_entry = tk.Entry(self, width=50)
3136
self.question_entry.pack(padx=20, pady=(0, 10))
32-
self.question_entry.focus()
37+
# self.question_entry.focus()
3338

3439
tk.Label(self, text="Answer:", fg="white", bg="black", anchor="w").pack(fill="x", padx=20)
3540
self.answer_entry = tk.Entry(self, width=50)
3641
self.answer_entry.pack(padx=20, pady=(0, 20))
3742

38-
# TODO: Figure out why enhanced_entry bricks the question and answer entry
39-
# enhanced_entry(self.question_entry, placeholder="I.e. Where did we meet last Thursday ?")
40-
# enhanced_entry(self.answer_entry, placeholder="I.e. Central Park")
43+
enhanced_entry(self.question_entry, placeholder="I.e. What's our secret passphrase ?")
44+
enhanced_entry(self.answer_entry, placeholder="I.e. Horse Whale Australia Dog Times Lake")
4145

42-
# Send button
4346
tk.Button(
4447
self,
4548
text="Send Verification Request",
@@ -70,39 +73,30 @@ def submit(self):
7073
messagebox.showerror("Error", "Question must not contain the answer!")
7174
return
7275

76+
if question.lower() in answer.lower():
77+
messagebox.showerror("Error", "Answer must not contain any part of the question!")
78+
return
7379

7480
if len(question) > SMP_QUESTION_MAX_LEN:
7581
messagebox.showerror("Error", f"Question must be under {SMP_QUESTION_MAX_LEN} characters long.")
7682

77-
78-
# This is just unacceptable, 4 characaters is the bare minimum.
79-
# Given our argon2id parameters, we have calculated the worst case scenario of an adversary with
80-
# many 128-core CPU clusters, and 1000s of machines available, with custom-optimizied cracking-rigs
81-
# and we concluded that as of 2025, would still require a couple minutes - couple seconds short of
82-
# a minute to crack the answer which might *just* be enough time if both users are online and the
83-
# server is not malicious. If the server is malicious, it could delay the process, which means that
84-
# powerful adversary would have cracked the answer to your question and possibly fed you spoofed keys
85-
#
86-
# We don't actually enforce high-entropy answers because we know users will try to bypass the limit
87-
# by picking long-length but low-entropy answers, or worse, put / hint the answer in the question
88-
# Instead, we opted for an approach of allowing the user to choose low-entropy answers but give
89-
# them 2 warnings to ensure they understand the risks.
90-
#
91-
9283
if len(answer) <= 3:
9384
messagebox.showerror("Error", "Answer must be at least 4 characters long!")
9485
return
9586

96-
if len(answer) <= 5:
87+
if len(answer) <= 5 or not check_str_high_entropy(answer):
9788
# Even though we enforce SMP, sometime a user might want to add someone whom our user don't have a out-of-band channel to communicate with
9889
# allowing the user to set a low-entropy answer gives user the opportunity to do so
9990
# But we still warn the user twice about the importance of the answer's entropy in context of SMP verification
100-
if messagebox.askyesno("Warning", "Answer is less than 6 characters long, this is unsafe and not recommended, do you want to proceed anyway ?"):
91+
if messagebox.askyesno("Warning", "Answer does not contain good entropy, this is unsafe and not recommended, do you want to proceed anyway ?"):
10192
if not messagebox.askyesno("Warning", "If the server is malicious, low-entropy answer potentially undermines encryption. To reduce risks only proceed if you are certain that the contact is online right now, are you absolutely sure?"):
10293
return
10394
else:
10495
return
10596

106-
initiate_smp(self.master.user_data, self.master.user_data_lock, self.contact_id, question, answer)
107-
97+
try:
98+
initiate_smp(self.master.user_data, self.master.user_data_lock, self.contact_id, question, answer)
99+
except Exception as e:
100+
messagebox.showerror("Error", e)
101+
108102
self.destroy()

0 commit comments

Comments
 (0)