From 2f128f9a1a6eabe3a3d2bafb5496738d69ece834 Mon Sep 17 00:00:00 2001 From: MOJ1403 Date: Mon, 23 Mar 2026 19:29:24 +0330 Subject: [PATCH] avalin proge --- App_GUI.py | 249 +++ Crypto_Engine.py | 162 ++ DB_Handler.py | 134 ++ GSM_Manager.py | 218 +++ __pycache__/App_GUI.cpython-313.pyc | Bin 0 -> 16176 bytes __pycache__/Crypto_Engine.cpython-313.pyc | Bin 0 -> 7938 bytes __pycache__/DB_Handler.cpython-313.pyc | Bin 0 -> 8208 bytes __pycache__/GSM_Manager.cpython-313.pyc | Bin 0 -> 10368 bytes __pycache__/main.cpython-313.pyc | Bin 0 -> 11073 bytes _bad_strings.txt | 7 + _question_strings.txt | 6 + _string_tokens.txt | 643 +++++++ a.ps1 | 44 + main.py | 276 +++ requirements.txt | 5 + secure_sms/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 165 bytes .../__pycache__/controller.cpython-313.pyc | Bin 0 -> 9207 bytes .../__pycache__/database.cpython-313.pyc | Bin 0 -> 22442 bytes secure_sms/__pycache__/gsm.cpython-313.pyc | Bin 0 -> 9640 bytes secure_sms/__pycache__/models.cpython-313.pyc | Bin 0 -> 2225 bytes .../__pycache__/protocol.cpython-313.pyc | Bin 0 -> 4767 bytes .../__pycache__/security.cpython-313.pyc | Bin 0 -> 9041 bytes .../__pycache__/services.cpython-313.pyc | Bin 0 -> 23383 bytes secure_sms/__pycache__/ui.cpython-313.pyc | Bin 0 -> 84520 bytes secure_sms/controller.py | 137 ++ secure_sms/database.py | 410 +++++ secure_sms/gsm.py | 154 ++ secure_sms/models.py | 53 + secure_sms/protocol.py | 94 + secure_sms/security.py | 133 ++ secure_sms/services.py | 407 +++++ secure_sms/ui.py | 1509 +++++++++++++++++ secure_sms_v2.db | Bin 0 -> 45056 bytes 34 files changed, 4642 insertions(+) create mode 100644 App_GUI.py create mode 100644 Crypto_Engine.py create mode 100644 DB_Handler.py create mode 100644 GSM_Manager.py create mode 100644 __pycache__/App_GUI.cpython-313.pyc create mode 100644 __pycache__/Crypto_Engine.cpython-313.pyc create mode 100644 __pycache__/DB_Handler.cpython-313.pyc create mode 100644 __pycache__/GSM_Manager.cpython-313.pyc create mode 100644 __pycache__/main.cpython-313.pyc create mode 100644 _bad_strings.txt create mode 100644 _question_strings.txt create mode 100644 _string_tokens.txt create mode 100644 a.ps1 create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 secure_sms/__init__.py create mode 100644 secure_sms/__pycache__/__init__.cpython-313.pyc create mode 100644 secure_sms/__pycache__/controller.cpython-313.pyc create mode 100644 secure_sms/__pycache__/database.cpython-313.pyc create mode 100644 secure_sms/__pycache__/gsm.cpython-313.pyc create mode 100644 secure_sms/__pycache__/models.cpython-313.pyc create mode 100644 secure_sms/__pycache__/protocol.cpython-313.pyc create mode 100644 secure_sms/__pycache__/security.cpython-313.pyc create mode 100644 secure_sms/__pycache__/services.cpython-313.pyc create mode 100644 secure_sms/__pycache__/ui.cpython-313.pyc create mode 100644 secure_sms/controller.py create mode 100644 secure_sms/database.py create mode 100644 secure_sms/gsm.py create mode 100644 secure_sms/models.py create mode 100644 secure_sms/protocol.py create mode 100644 secure_sms/security.py create mode 100644 secure_sms/services.py create mode 100644 secure_sms/ui.py create mode 100644 secure_sms_v2.db diff --git a/App_GUI.py b/App_GUI.py new file mode 100644 index 0000000..0170012 --- /dev/null +++ b/App_GUI.py @@ -0,0 +1,249 @@ +import customtkinter as ctk + +# GUI Configuration +ctk.set_appearance_mode("Dark") +ctk.set_default_color_theme("blue") + +class AppGUI(ctk.CTk): + def __init__(self, controller): + super().__init__() + self.controller = controller + + self.title("Secure SMS - Raspberry Pi") + self.geometry("800x480") # Typical Pi touchscreen resolution + + # Current state + self.current_contact = None + + self.setup_ui() + self.load_contacts() + + def setup_ui(self): + # Grid Layout (1x2) - Sidebar | Main Content + self.grid_rowconfigure(0, weight=1) + self.grid_columnconfigure(1, weight=1) + + # --- Sidebar --- + self.sidebar_frame = ctk.CTkFrame(self, width=200, corner_radius=0) + self.sidebar_frame.grid(row=0, column=0, sticky="nsew") + self.sidebar_frame.grid_rowconfigure(4, weight=1) + + self.logo_label = ctk.CTkLabel(self.sidebar_frame, text="Secure SMS", font=ctk.CTkFont(size=20, weight="bold")) + self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10)) + + self.contacts_scrollable_frame = ctk.CTkScrollableFrame(self.sidebar_frame, label_text="Contacts") + self.contacts_scrollable_frame.grid(row=1, column=0, padx=10, pady=10, sticky="nsew", rowspan=4) + + # Settings / Add Contact Button + self.settings_btn = ctk.CTkButton(self.sidebar_frame, text="Settings / Add Contact", command=self.open_settings) + self.settings_btn.grid(row=5, column=0, padx=10, pady=20) + + # --- Main Chat Area --- + self.main_frame = ctk.CTkFrame(self, corner_radius=0) + self.main_frame.grid(row=0, column=1, sticky="nsew") + self.main_frame.grid_rowconfigure(1, weight=1) + self.main_frame.grid_columnconfigure(0, weight=1) + + # Chat Header + self.chat_header_var = ctk.StringVar(value="Select a contact") + self.chat_header = ctk.CTkLabel(self.main_frame, textvariable=self.chat_header_var, font=ctk.CTkFont(size=18, weight="bold")) + self.chat_header.grid(row=0, column=0, padx=20, pady=10, sticky="w") + + # Secured Status Label Indicator + self.header_secure_status = ctk.CTkLabel(self.main_frame, text="Current Mode: Normal", text_color="yellow") + self.header_secure_status.grid(row=0, column=0, padx=20, pady=10, sticky="e") + + # Messages Display + self.msg_display = ctk.CTkTextbox(self.main_frame, state="disabled", wrap="word") + self.msg_display.grid(row=1, column=0, padx=20, pady=(0, 10), sticky="nsew") + self.msg_display.tag_config('sent_normal', foreground='lightgray', justify='right') + self.msg_display.tag_config('sent_secure', foreground='lightgreen', justify='right') + self.msg_display.tag_config('recv_normal', foreground='white', justify='left') + self.msg_display.tag_config('recv_secure', foreground='mediumspringgreen', justify='left') + self.msg_display.tag_config('system', foreground='red', justify='center') + + # Bottom Input Area + self.input_frame = ctk.CTkFrame(self.main_frame, height=50) + self.input_frame.grid(row=2, column=0, padx=20, pady=10, sticky="nsew") + self.input_frame.grid_columnconfigure(0, weight=1) + + self.msg_entry = ctk.CTkEntry(self.input_frame, placeholder_text="Type a message...") + self.msg_entry.grid(row=0, column=0, padx=(10, 5), pady=10, sticky="nsew") + self.msg_entry.bind("", lambda e: self.send_message()) + + # Toggle Secure SMS + self.secure_mode_var = ctk.StringVar(value="off") + self.secure_switch = ctk.CTkSwitch( + self.input_frame, + text="Secure", + command=self.toggle_mode, + variable=self.secure_mode_var, + onvalue="on", + offvalue="off" + ) + self.secure_switch.grid(row=0, column=1, padx=5, pady=10) + + self.send_btn = ctk.CTkButton(self.input_frame, text="Send", width=80, command=self.send_message) + self.send_btn.grid(row=0, column=2, padx=(5, 10), pady=10) + + def toggle_mode(self): + mode = "Secure" if self.secure_mode_var.get() == "on" else "Normal" + color = "mediumspringgreen" if mode == "Secure" else "yellow" + self.header_secure_status.configure(text=f"Current Mode: {mode}", text_color=color) + + def load_contacts(self): + # Clear sidebar list + for widget in self.contacts_scrollable_frame.winfo_children(): + widget.destroy() + + contacts = self.controller.get_contacts() + for idx, contact in enumerate(contacts): + # contact = (name, phone, pubkey) + name, phone, _ = contact + btn = ctk.CTkButton( + self.contacts_scrollable_frame, + text=f"{name}\n{phone}", + command=lambda p=phone: self.select_contact(p), + fg_color="transparent", + border_width=2, + text_color=("gray10", "#DCE4EE") + ) + btn.grid(row=idx, column=0, padx=5, pady=5, sticky="ew") + + def select_contact(self, phone): + self.current_contact = self.controller.get_contact_info(phone) + name = self.current_contact[0] + self.chat_header_var.set(f"Chat with {name} ({phone})") + + # Determine if we can do secure + has_key = self.current_contact[2] is not None + if not has_key: + self.secure_switch.deselect() + self.secure_switch.configure(state="disabled") + self.toggle_mode() + self.append_system_msg("This contact has no public key. Secure mode disabled.") + else: + self.secure_switch.configure(state="normal") + + self.refresh_messages() + + def refresh_messages(self): + self.msg_display.configure(state="normal") + self.msg_display.delete("1.0", "end") + + if self.current_contact: + messages = self.controller.get_messages(self.current_contact[1]) + for msg in messages: + # msg = (id, phone, text, date, is_secure, status) + text = msg[2] + date = msg[3] + is_secure = msg[4] + status = msg[5] # sent / recv + + tag = f"{status}_{'secure' if is_secure else 'normal'}" + prefix = "🔒 " if is_secure else "" + + display_text = f"[{date}]\n{prefix}{text}\n\n" + self.msg_display.insert("end", display_text, tag) + + self.msg_display.configure(state="disabled") + self.msg_display.yview("end") + + def append_system_msg(self, text): + self.msg_display.configure(state="normal") + self.msg_display.insert("end", f"--- {text} ---\n\n", "system") + self.msg_display.configure(state="disabled") + self.msg_display.yview("end") + + def send_message(self): + if not self.current_contact: + return + + text = self.msg_entry.get().strip() + if not text: + return + + is_secure = self.secure_mode_var.get() == "on" + phone = self.current_contact[1] + + # Ask controller to send + self.msg_entry.delete(0, 'end') + self.append_system_msg("Sending...") + self.update() # force UI refresh safely + + success = self.controller.send_message(phone, text, is_secure) + if success: + self.refresh_messages() + else: + self.append_system_msg("Failed to send SMS!") + + def open_settings(self): + SettingsWindow(self, self.controller) + +class SettingsWindow(ctk.CTkToplevel): + def __init__(self, master, controller): + super().__init__(master) + self.controller = controller + self.title("Settings") + self.geometry("500x500") + + # Bring to front + self.attributes('-topmost', 1) + + self.grid_columnconfigure(0, weight=1) + + # My Public Key + lbl1 = ctk.CTkLabel(self, text="My Public Key (Share this to chat securely):") + lbl1.grid(row=0, column=0, padx=20, pady=(20, 5), sticky="w") + + self.my_key_box = ctk.CTkTextbox(self, height=100) + self.my_key_box.grid(row=1, column=0, padx=20, pady=5, sticky="ew") + self.my_key_box.insert("1.0", self.controller.get_my_public_key()) + self.my_key_box.configure(state="disabled") + + # Add / Update Contact + lbl_add = ctk.CTkLabel(self, text="Add/Update Contact:", font=ctk.CTkFont(weight="bold")) + lbl_add.grid(row=2, column=0, padx=20, pady=(30, 5), sticky="w") + + self.name_entry = ctk.CTkEntry(self, placeholder_text="Contact Name") + self.name_entry.grid(row=3, column=0, padx=20, pady=5, sticky="ew") + + self.phone_entry = ctk.CTkEntry(self, placeholder_text="Phone Number (+989...)") + self.phone_entry.grid(row=4, column=0, padx=20, pady=5, sticky="ew") + + lbl2 = ctk.CTkLabel(self, text="Contact's Public Key (Optional but required for secure SMS):") + lbl2.grid(row=5, column=0, padx=20, pady=5, sticky="w") + + self.contact_key_box = ctk.CTkTextbox(self, height=100) + self.contact_key_box.grid(row=6, column=0, padx=20, pady=5, sticky="ew") + + self.save_btn = ctk.CTkButton(self, text="Save Contact", command=self.save_contact) + self.save_btn.grid(row=7, column=0, padx=20, pady=20) + + def save_contact(self): + name = self.name_entry.get().strip() + phone = self.phone_entry.get().strip() + pubkey = self.contact_key_box.get("1.0", 'end-1c').strip() + + if not name or not phone: + return + + if not pubkey: + pubkey = None + + self.controller.save_contact(name, phone, pubkey) + self.master.load_contacts() + self.destroy() + +if __name__ == "__main__": + # Mock Controller for standalone GUI test + class MockController: + def get_contacts(self): return [("Alice", "+1234", "key"), ("Bob", "+5678", None)] + def get_contact_info(self, phone): return ("Alice", phone, "key") if phone == "+1234" else ("Bob", phone, None) + def get_messages(self, phone): return [(1, phone, "Hi", "2023-01-01", 0, "recv"), (2, phone, "Secret", "2023-01-01", 1, "sent")] + def get_my_public_key(self): return "MY_PEM_MOCK_DATA" + def send_message(self, *args): return True + def save_contact(self, *args): pass + + app = AppGUI(MockController()) + app.mainloop() diff --git a/Crypto_Engine.py b/Crypto_Engine.py new file mode 100644 index 0000000..e9a507c --- /dev/null +++ b/Crypto_Engine.py @@ -0,0 +1,162 @@ +import os +import base64 +from cryptography.hazmat.primitives.asymmetric import x25519 +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +# Constants +PRIVATE_KEY_FILE = "private_key.pem" +PUBLIC_KEY_FILE = "public_key.pem" + +class CryptoEngine: + def __init__(self): + self.private_key = None + self.public_key = None + self.load_or_generate_keys() + + def load_or_generate_keys(self): + """Loads keys from disk or generates a new pair if they don't exist.""" + if os.path.exists(PRIVATE_KEY_FILE) and os.path.exists(PUBLIC_KEY_FILE): + with open(PRIVATE_KEY_FILE, "rb") as f: + self.private_key = serialization.load_pem_private_key( + f.read(), password=None + ) + with open(PUBLIC_KEY_FILE, "rb") as f: + self.public_key = serialization.load_pem_public_key(f.read()) + else: + self.generate_keypair() + + def generate_keypair(self): + """Generates a new X25519 keypair and saves it to disk.""" + self.private_key = x25519.X25519PrivateKey.generate() + self.public_key = self.private_key.public_key() + + # Save private key + with open(PRIVATE_KEY_FILE, "wb") as f: + f.write(self.private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + )) + + # Save public key + with open(PUBLIC_KEY_FILE, "wb") as f: + f.write(self.public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + )) + + def get_my_public_key_pem(self): + """Returns my public key as string.""" + return self.public_key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo + ).decode('utf-8') + + def encrypt_message(self, message: str, peer_public_key_pem: str) -> str: + """ + Encrypts a message using X25519 exchange + HKDF + AESGCM. + Returns a base64 encoded string containing ephemeral public key, IV, and ciphertext. + """ + if not peer_public_key_pem: + raise ValueError("Peer public key is empty.") + + peer_public_key = serialization.load_pem_public_key(peer_public_key_pem.encode('utf-8')) + + # Generate an ephemeral keypair for this message to provide forward secrecy + ephemeral_private_key = x25519.X25519PrivateKey.generate() + ephemeral_public_key = ephemeral_private_key.public_key() + ephemeral_pub_bytes = ephemeral_public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw + ) + + # Perform key exchange + shared_key = ephemeral_private_key.exchange(peer_public_key) + + # Derive a symmetric key using HKDF + derived_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=b'sms-secure-encryption' + ).derive(shared_key) + + # Encrypt with AES-GCM + aesgcm = AESGCM(derived_key) + nonce = os.urandom(12) + ciphertext = aesgcm.encrypt(nonce, message.encode('utf-8'), None) + + # Construct final payload: [Ephemeral Pub Key (32)] + [Nonce (12)] + [Ciphertext + Tag] + payload = ephemeral_pub_bytes + nonce + ciphertext + + # We prepend a marker to easily identify secure messages + marker = "SEC:" + b64_payload = base64.b64encode(payload).decode('ascii') + + return marker + b64_payload + + def decrypt_message(self, secure_payload: str) -> str: + """ + Decrypts a secure payload. + Assumes it starts with 'SEC:'. + """ + if not secure_payload.startswith("SEC:"): + raise ValueError("Not a secure message format.") + + b64_payload = secure_payload[4:] + try: + payload = base64.b64decode(b64_payload) + except Exception: + raise ValueError("Invalid Base64 payload.") + + if len(payload) < 32 + 12 + 16: # Length of pub key + nonce + tag + raise ValueError("Payload too short.") + + ephemeral_pub_bytes = payload[:32] + nonce = payload[32:44] + ciphertext = payload[44:] + + ephemeral_public_key = x25519.X25519PublicKey.from_public_bytes(ephemeral_pub_bytes) + + # Key exchange + shared_key = self.private_key.exchange(ephemeral_public_key) + + # Derive symmetric key + derived_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=b'sms-secure-encryption' + ).derive(shared_key) + + # Decrypt + aesgcm = AESGCM(derived_key) + try: + plaintext = aesgcm.decrypt(nonce, ciphertext, None) + return plaintext.decode('utf-8') + except Exception as e: + raise ValueError("Decryption failed. Invalid key or message tampered.") from e + +if __name__ == "__main__": + # Test + alice = CryptoEngine() + alice_pub = alice.get_my_public_key_pem() + + bob = CryptoEngine() + bob_pub = bob.get_my_public_key_pem() + + msg = "This is a highly secret message!" + + # Alice sends to Bob + encrypted = alice.encrypt_message(msg, bob_pub) + print("Encrypted:", encrypted) + + # Bob decrypts + decrypted = bob.decrypt_message(encrypted) + print("Decrypted:", decrypted) + + assert msg == decrypted + print("Crypto Engine OK.") diff --git a/DB_Handler.py b/DB_Handler.py new file mode 100644 index 0000000..7f43392 --- /dev/null +++ b/DB_Handler.py @@ -0,0 +1,134 @@ +import sqlite3 +from datetime import datetime +import os + +DB_FILE = "sms_app.db" + +class DBHandler: + def __init__(self, db_path=DB_FILE): + self.db_path = db_path + self._initialize_db() + + def _get_connection(self): + return sqlite3.connect(self.db_path) + + def _initialize_db(self): + with self._get_connection() as conn: + cursor = conn.cursor() + # Create contacts table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS contacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + phone TEXT UNIQUE NOT NULL, + public_key TEXT + ) + ''') + # Create messages table + cursor.execute(''' + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone TEXT NOT NULL, + text TEXT NOT NULL, + date TEXT NOT NULL, + is_secure INTEGER NOT NULL, + status TEXT NOT NULL, + FOREIGN KEY(phone) REFERENCES contacts(phone) + ) + ''') + # Create settings table for our own keypair if needed (though usually we save keys to files) + cursor.execute(''' + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT + ) + ''') + conn.commit() + + # --- Contacts Methods --- + def add_contact(self, name, phone, public_key=None): + with self._get_connection() as conn: + cursor = conn.cursor() + try: + cursor.execute('INSERT INTO contacts (name, phone, public_key) VALUES (?, ?, ?)', + (name, phone, public_key)) + conn.commit() + return True + except sqlite3.IntegrityError: + return False # Phone number already exists + + def update_contact_key(self, phone, public_key): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute('UPDATE contacts SET public_key = ? WHERE phone = ?', (public_key, phone)) + conn.commit() + + def get_contact(self, phone): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT name, phone, public_key FROM contacts WHERE phone = ?', (phone,)) + return cursor.fetchone() + + def get_all_contacts(self): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT name, phone, public_key FROM contacts') + return cursor.fetchall() + + def set_contact_name_if_not_exists(self, phone, name): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT phone FROM contacts WHERE phone = ?', (phone,)) + if not cursor.fetchone(): + cursor.execute('INSERT INTO contacts (name, phone, public_key) VALUES (?, ?, ?)', (name, phone, None)) + conn.commit() + + # --- Messages Methods --- + def add_message(self, phone, text, is_secure, status): + date_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + INSERT INTO messages (phone, text, date, is_secure, status) + VALUES (?, ?, ?, ?, ?) + ''', (phone, text, date_str, int(is_secure), status)) + conn.commit() + return cursor.lastrowid + + def get_messages_for_contact(self, phone): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute(''' + SELECT id, phone, text, date, is_secure, status + FROM messages + WHERE phone = ? + ORDER BY date ASC + ''', (phone,)) + return cursor.fetchall() + + def delete_message(self, msg_id): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute('DELETE FROM messages WHERE id = ?', (msg_id,)) + conn.commit() + + # --- Settings Methods --- + def get_setting(self, key): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute('SELECT value FROM settings WHERE key = ?', (key,)) + row = cursor.fetchone() + return row[0] if row else None + + def set_setting(self, key, value): + with self._get_connection() as conn: + cursor = conn.cursor() + cursor.execute('INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)', (key, value)) + conn.commit() + +if __name__ == "__main__": + db = DBHandler("test_sms.db") + db.add_contact("Alice", "+1234567890", "PUBLIC_KEY_CONTENT") + db.add_message("+1234567890", "Hello Alice!", is_secure=False, status="sent") + print("Database OK.") + os.remove("test_sms.db") diff --git a/GSM_Manager.py b/GSM_Manager.py new file mode 100644 index 0000000..5bed997 --- /dev/null +++ b/GSM_Manager.py @@ -0,0 +1,218 @@ +import serial +import time +import threading +import re + +class GSMManager: + def __init__(self, port='COM1', baudrate=115200, message_callback=None): + """ + message_callback: function(sender_number, message_text) + called when a new SMS is fully read. + """ + self.port = port + self.baudrate = baudrate + self.serial_conn = None + self.is_running = False + self.read_thread = None + self.message_callback = message_callback + + # Lock for thread-safe AT command execution + self.lock = threading.Lock() + + def connect(self): + try: + self.serial_conn = serial.Serial( + port=self.port, + baudrate=self.baudrate, + timeout=1 # 1 second timeout for readline + ) + self.is_running = True + + # Setup Modem + self.send_at_cmd('AT') + self.send_at_cmd('AT+CMGF=1') # Text Mode + self.send_at_cmd('AT+CNMI=2,1,0,0,0') # Route URC to TE for +CMTI + + # Start Read Thread + self.read_thread = threading.Thread(target=self._read_loop, daemon=True) + self.read_thread.start() + + return True + except Exception as e: + print(f"Failed to connect GSM: {e}") + return False + + def disconnect(self): + self.is_running = False + if self.serial_conn and self.serial_conn.is_open: + self.serial_conn.close() + if self.read_thread: + self.read_thread.join(timeout=2) + + def send_at_cmd(self, cmd, expected_resp='OK', timeout=2): + """Send an AT command safely and wait for a response.""" + with self.lock: + if not self.serial_conn or not self.serial_conn.is_open: + return False, "Not connected" + + # Flush input buffer to clear old data + self.serial_conn.reset_input_buffer() + + full_cmd = cmd + '\r\n' + self.serial_conn.write(full_cmd.encode('ascii')) + + start_time = time.time() + response_lines = [] + + while time.time() - start_time < timeout: + if self.serial_conn.in_waiting: + line = self.serial_conn.readline().decode('ascii', errors='ignore').strip() + if line: + response_lines.append(line) + if expected_resp in line or 'ERROR' in line: + break + else: + time.sleep(0.05) + + return expected_resp in "\n".join(response_lines), response_lines + + def send_sms(self, phone_number, message): + """ + Sends an SMS. For long messages, splits them up. + (Note: Quectel M66 supports up to 160 chars in 7-bit text mode natively. + For simplicity, we chunk it to 150 chars max per SMS if it exceeds). + """ + chunk_size = 140 # Safe limit for standard ASCII/Base64 + chunks = [message[i:i+chunk_size] for i in range(0, len(message), chunk_size)] + + for idx, chunk in enumerate(chunks): + # If multiple chunks, we can prepend (1/2) kind of indicator for normal texts + # But for base64 secure chunks, they might need stitching. + # For simplicity, we just send them sequentially. + # In a production resilient app, multipart PDU mode is preferred. + + success = self._send_single_sms(phone_number, chunk) + if not success: + return False + + if len(chunks) > 1: + time.sleep(2) # Add some delay between multi-part sends + + return True + + def _send_single_sms(self, phone_number, text): + with self.lock: + try: + self.serial_conn.reset_input_buffer() + + # Start SMS prompt + self.serial_conn.write(f'AT+CMGS="{phone_number}"\r\n'.encode('ascii')) + + # Wait for '>' + start_time = time.time() + prompt_ready = False + while time.time() - start_time < 2: + if self.serial_conn.in_waiting: + char = self.serial_conn.read().decode('ascii', errors='ignore') + if char == '>': + prompt_ready = True + break + time.sleep(0.05) + + if not prompt_ready: + print("SMS prompt '>' not received.") + # Send ESC just in case + self.serial_conn.write(chr(27).encode('ascii')) + return False + + # Write text and send Ctrl+Z (ASCII 26) + self.serial_conn.write(text.encode('ascii') + chr(26).encode('ascii')) + + # Wait for +CMGS and OK + start_time = time.time() + success = False + response_lines = [] + while time.time() - start_time < 10: # SMS sending can take time + if self.serial_conn.in_waiting: + line = self.serial_conn.readline().decode('ascii', errors='ignore').strip() + if line: + response_lines.append(line) + if 'OK' in line: + success = True + break + elif 'ERROR' in line: + break + else: + time.sleep(0.1) + + return success + except Exception as e: + print(f"Error sending SMS: {e}") + return False + + def _read_loop(self): + """Background thread continuously reading URCs like +CMTI""" + while self.is_running: + try: + if self.serial_conn and self.serial_conn.in_waiting: + # We only read if we can acquire the lock without waiting, + # otherwise it means AT command is in progress and it handles reading. + # Wait, AT command reading in `send_at_cmd` reads synchronously. URCs might mix. + # Standard practice for URCs: the AT command sender ignores unexpected URCs, + # OR we have a dedicated thread that reads everything and routes URCs vs command responses. + # For simplicity, we try to grab the lock. If locked, AT comm is happening. + if self.lock.acquire(blocking=False): + try: + line = self.serial_conn.readline().decode('ascii', errors='ignore').strip() + if line.startswith('+CMTI:'): + # Incoming message: +CMTI: "SM",1 + match = re.search(r'\+CMTI:\s*".*?",(\d+)', line) + if match: + msg_index = int(match.group(1)) + # Process it in a new thread or queue to avoid blocking this loop + threading.Thread(target=self.process_incoming_sms, args=(msg_index,), daemon=True).start() + finally: + self.lock.release() + except Exception as e: + pass + time.sleep(0.1) + + def process_incoming_sms(self, index): + """Reads the SMS payload from memory and deletes it.""" + # read_sms uses AT command, so it uses the lock + # wait a bit ensuring any ongoing AT comm finishes + time.sleep(1) + + success, response = self.send_at_cmd(f'AT+CMGR={index}', expected_resp='OK', timeout=3) + if success: + sender = "Unknown" + text_lines = [] + is_text_block = False + + for line in response: + if line.startswith('+CMGR:'): + # +CMGR: "REC UNREAD","+989123456789",,"23/05/26,10:30:00+14" + parts = line.split(',') + if len(parts) >= 2: + sender = parts[1].strip('"') + is_text_block = True + elif is_text_block and line not in ['OK', 'ERROR'] and not line.startswith('+CMGR:'): + text_lines.append(line) + + full_text = "\n".join(text_lines) + + if self.message_callback: + self.message_callback(sender, full_text) + + # Delete message to free space + self.send_at_cmd(f'AT+CMGD={index}') + +if __name__ == "__main__": + def on_sms(sender, text): + print(f"\n[NEW SMS] From: {sender}\nText:{text}\n") + + gsm = GSMManager(port='COM3') + print("Testing locally...") + # This will obviously fail without actual M66 module on COM3, but acts as boilerplate check. + # gsm.connect() + # gsm.disconnect() diff --git a/__pycache__/App_GUI.cpython-313.pyc b/__pycache__/App_GUI.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f15e825f21a9cf1e31d444f4793c389a3b0988b4 GIT binary patch literal 16176 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 R_M2zFHTbAnaMtlf`aen@X=VTb literal 0 HcmV?d00001 diff --git a/__pycache__/Crypto_Engine.cpython-313.pyc b/__pycache__/Crypto_Engine.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1862346d7698135dc3e8bd05a8be8dbdb0563901 GIT binary patch literal 7938 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 literal 0 HcmV?d00001 diff --git a/__pycache__/GSM_Manager.cpython-313.pyc b/__pycache__/GSM_Manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6c621a9126f01010a5cada3b40d99b4d0e709ac GIT binary patch literal 10368 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 literal 0 HcmV?d00001 diff --git a/_bad_strings.txt b/_bad_strings.txt new file mode 100644 index 0000000..00e3ead --- /dev/null +++ b/_bad_strings.txt @@ -0,0 +1,7 @@ +57: ? +276: ????? +278: ????? +280: ???? +281: ??? +283: ??? +284: ????? \ No newline at end of file diff --git a/_question_strings.txt b/_question_strings.txt new file mode 100644 index 0000000..dba873c --- /dev/null +++ b/_question_strings.txt @@ -0,0 +1,6 @@ +276|????? +278|????? +280|???? +281|??? +283|??? +284|????? \ No newline at end of file diff --git a/_string_tokens.txt b/_string_tokens.txt new file mode 100644 index 0000000..a7f8c04 --- /dev/null +++ b/_string_tokens.txt @@ -0,0 +1,643 @@ +15|light +16|blue +19|#175B4B +20|#0E4236 +21|#DFF1E8 +22|#E8A04D +23|#C97E2D +24|#F5EFE7 +25|#FFFDFC +26|#FBF7F2 +27|#FFFCF8 +28|#16312A +29|#6B7A77 +30|#B6465F +31|#9A6C3C +32|#E5DCCE +33|#E7DED3 +34|#FFF8F0 +35|#F2E7D8 +37|#1B5A4A +38|#245E4E +39|DejaVu Sans +40|DejaVu Sans +41|[\\u0600-\\u06FF] +43|fa +44|\u0636 +44|\u0635 +44|\u062b +44|\u0642 +44|\u0641 +44|\u063a +44|\u0639 +44|\u0647 +44|\u062e +44|\u062d +45|\u0634 +45|\u0633 +45|\u06cc +45|\u0628 +45|\u0644 +45|\u0627 +45|\u062a +45|\u0646 +45|\u0645 +45|\u06a9 +46|123 +46|\u0638 +46|\u0637 +46|\u0632 +46|\u0631 +46|\u0630 +46|\u062f +46|\u067e +46|\u0648 +46|\u067e\u0627\u06a9 +47|English +47|\u060c +47|\u0641\u0627\u0635\u0644\u0647 +47|\u062a\u0627\u06cc\u06cc\u062f +49|en +50|q +50|w +50|e +50|r +50|t +50|y +50|u +50|i +50|o +50|p +51|a +51|s +51|d +51|f +51|g +51|h +51|j +51|k +51|l +52|123 +52|z +52|x +52|c +52|v +52|b +52|n +52|m +52|Back +53|\u0641\u0627\u0631\u0633\u06cc +53|. +53|Space +53|Enter +55|numeric +56|1 +56|2 +56|3 +56|4 +56|5 +56|6 +56|7 +56|8 +56|9 +56|0 +57|+ +57|- +57|/ +57|@ +57|_ +57|. +57|: +57|( +57|) +57|? +58|\u0641\u0627\u0631\u0633\u06cc +58|English +58|Back +59|\u0628\u0633\u062a\u0646 +59|\u0641\u0627\u0635\u0644\u0647 +59|\u062a\u0627\u06cc\u06cc\u062f +63|\u0641\u0627\u0631\u0633\u06cc +63|fa +64|English +64|en +65|123 +65|numeric +66|\u067e\u0627\u06a9 +66|backspace +67|\u0628\u0633\u062a\u0646 +67|close +83|text +84|text +84|text +85|placeholder_text +86|placeholder_text +86|placeholder_text +87|label_text +88|label_text +88|label_text +102|corner_radius +111|justify +111|right +112|fg_color +113|border_color +114|corner_radius +140|fa +144|SECURE_SMS_WINDOWED +144|0 +144|1 +145|\u067e\u06cc\u0627\u0645\u200c\u0631\u0633\u0627\u0646 \u0627\u0645\u0646 \u0635\u0628\u0627 +146|800x480 +150|*Cursor +150|none +152| +152|+ +161|-topmost +162|-fullscreen +164|800x480+0+0 +169|none +176|_entry +176|_textbox +184|master +191|master +199|master +211|fa +213|title +214|layout +215|multiline +216|submit +221| +221|+ +222| +222|+ +234|layout +271|title +272|\u0648\u0631\u0648\u062f\u06cc +276|????? +277|Space +278|????? +279|Enter +280|???? +281|??? +282|Back +283|??? +284|????? +285|English +286|123 +290|\u062a\u0627\u06cc\u06cc\u062f +290|Enter +291|white +292|ABC +292|\u0641\u0627 +292|\u06f1\u06f2\u06f3 +292|123 +292|English +292|\u0641\u0627\u0631\u0633\u06cc +293|#3A2514 +294|\u062d\u0630\u0641 +294|\u0628\u0633\u062a\u0646 +294|Back +294|\u067e\u0627\u06a9 +295|#E4D4C2 +296|#F2E6D8 +299|fa +299|en +299|numeric +301|white +301|#3A2514 +302|backspace +303|#E4D4C2 +304|#F4EFE9 +304|#E8D8C7 +307|fa +307|en +307|numeric +309|backspace +311|close +328|bold +330|ew +336|fa +337|transparent +338|ew +349|bold +351|ew +354|ABC +354|English +355|en +357|\u0641\u0627 +357|\u0641\u0627\u0631\u0633\u06cc +358|fa +360|\u06f1\u06f2\u06f3 +360|123 +361|numeric +363|\u0641\u0627\u0635\u0644\u0647 +363|Space +364| +366|\u062d\u0630\u0641 +366|Back +366|\u067e\u0627\u06a9 +369|\u0628\u0633\u062a\u0646 +372|\u062a\u0627\u06cc\u06cc\u062f +372|Enter +384|sel +387|insert +388|insert +392|sel.first +392|sel.last +395|insert +407|sel +410|insert +410|> +410|1.0 +411|insert-1c +412|insert +416|sel.first +416|sel.last +421|insert +433|submit +437|multiline +438|\n +448|nsew +453|ew +463|ew +466|transparent +467|ew +472|\u06a9\u06cc\u0628\u0648\u0631\u062f \u0644\u0645\u0633\u06cc +474|bold +475|e +478|\u0628\u0633\u062a\u0646 +482|#E4D4C2 +484|bold +486|e +490| +494|e +496|transparent +497|ew +500|transparent +501|ew +522|center +523|\u0631\u0627\u0647\u200c\u0627\u0646\u062f\u0627\u0632\u06cc \u0627\u0645\u0646 \u0628\u0631\u0646\u0627\u0645\u0647 +523|\u0648\u0631\u0648\u062f \u0628\u0647 \u0628\u0631\u0646\u0627\u0645\u0647 +525|\u06cc\u06a9 \u0631\u0645\u0632 \u0627\u0635\u0644\u06cc \u062a\u0639\u06cc\u06cc\u0646 \u06a9\u0646 \u062a\u0627 \u06a9\u0644\u06cc\u062f\u0647\u0627 \u0648 \u062f\u0627\u062f\u0647\u200c\u0647\u0627\u06cc \u062d\u0633\u0627\u0633 \u062f\u0627\u062e\u0644 \u062f\u06cc\u062a\u0627\u0628\u06cc\u0633 \u0628\u0647 \u0635\u0648\u0631\u062a \u0631\u0645\u0632\u200c\u0634\u062f\u0647 \u0646\u06af\u0647\u200c\u062f\u0627\u0631\u06cc \u0634\u0648\u0646\u062f. +527|\u0628\u0631\u0627\u06cc \u0648\u0631\u0648\u062f\u060c \u0631\u0645\u0632 \u0627\u0635\u0644\u06cc \u0628\u0631\u0646\u0627\u0645\u0647 \u0631\u0627 \u0648\u0627\u0631\u062f \u06a9\u0646. +532|bold +539|right +545|\u0631\u0645\u0632 \u0627\u0635\u0644\u06cc +546|* +550|x +555|\u062a\u06a9\u0631\u0627\u0631 \u0631\u0645\u0632 +556|* +560|x +563| +568|\u0634\u0631\u0648\u0639 \u0628\u0631\u0646\u0627\u0645\u0647 +568|\u0648\u0631\u0648\u062f +576|bold +577|x +578| +579|\u0631\u0645\u0632 \u0627\u0635\u0644\u06cc +579|en +581| +582|\u062a\u06a9\u0631\u0627\u0631 \u0631\u0645\u0632 +582|en +587|\u0631\u0645\u0632 \u0628\u0627\u06cc\u062f \u062d\u062f\u0627\u0642\u0644 \u06f6 \u06a9\u0627\u0631\u0627\u06a9\u062a\u0631 \u0628\u0627\u0634\u062f. +592|\u062a\u06a9\u0631\u0627\u0631 \u0631\u0645\u0632 \u0628\u0627 \u0631\u0645\u0632 \u0627\u0635\u0644\u06cc \u06cc\u06a9\u0633\u0627\u0646 \u0646\u06cc\u0633\u062a. +600|\u0631\u0645\u0632 \u0648\u0627\u0631\u062f \u0634\u062f\u0647 \u062f\u0631\u0633\u062a \u0646\u06cc\u0633\u062a. +608|nsew +613|\xd8\xb5\xd8\xa8\xd8\xa7 +614|white +615|bold +616|e +619|\u067e\u06cc\u0627\u0645\u200c\u0631\u0633\u0627\u0646 \u0627\u0645\u0646 \u0648 \u0633\u0627\u062f\u0647 \u0628\u0631\u0627\u06cc \u06a9\u0627\u0631\u0628\u0631 \u063a\u06cc\u0631 \u0641\u0646\u06cc +620|#D5E8E1 +622|e +626| +628|#2E7D62 +629|white +630|bold +634|e +636|transparent +637|ew +641|\u0645\u062e\u0627\u0637\u0628 \u062c\u062f\u06cc\u062f +644|#3A2514 +647|bold +648|ew +651|\u062a\u0646\u0638\u06cc\u0645\u0627\u062a +653|#F4EFE9 +654|#15302B +655|#ECE1D5 +657|bold +658|ew +665|#3B7B66 +667|ew +671|\u0645\u062e\u0627\u0637\u0628 \u062c\u062f\u06cc\u062f +672|white +673|bold +674|e +677|\u0646\u0627\u0645 \u0645\u062e\u0627\u0637\u0628 +681|ew +684|\u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06cc\u0644 +688|ew +691| +692|#FDE68A +695|e +696|transparent +697|ew +701|\u0630\u062e\u06cc\u0631\u0647 +703|#3A2514 +706|bold +708|ew +711|\xd8\xa8\xd8\xb3\xd8\xaa\xd9\u2020 +712|#F4EFE9 +714|#ECE1D5 +716|bold +718|ew +719| +720|\u0646\u0627\u0645 \u0645\u062e\u0627\u0637\u0628 +720|fa +723|\u0634\u0645\u0627\u0631\u0647 \u0645\u0648\u0628\u0627\u06cc\u0644 +724|numeric +731|\u0645\u062e\u0627\u0637\u0628\u200c\u0647\u0627 +732|bold +733|transparent +735|nsew +738|nsew +743|ew +748|\u06cc\u06a9 \u0645\u062e\u0627\u0637\u0628 \u0631\u0627 \u0627\u0646\u062a\u062e\u0627\u0628 \u06a9\u0646 +750|bold +752|e +755|\u062f\u0631 \u0627\u06cc\u0646\u062c\u0627 \u0641\u0642\u0637 \u062f\u0648 \u062d\u0627\u0644\u062a \u062f\u0627\u0631\u06cc: \u0639\u0627\u062f\u06cc \u06cc\u0627 \u0627\u0645\u0646 +759|e +762|\xd8\xb9\xd8\xa7\xd8\xaf\xdb\u0152 +768|bold +770|e +772|transparent +773|nsew +785|word +787|nsew +788|out +788|right +789|in +789|right +790|system +790|#8A5C2E +790|right +791|disabled +794|nsew +798|\u067e\u0631\u0648\u0641\u0627\u06cc\u0644 \u0645\u062e\u0627\u0637\u0628 +800|bold +801|e +804|\u0646\u0627\u0645 \u0645\u062e\u0627\u0637\u0628 +806|bold +808|e +811|\u0634\u0645\u0627\u0631\u0647 +815|e +818|\u0628\u0631\u0627\u06cc \u0627\u06cc\u0646 \u0645\u062e\u0627\u0637\u0628 \u0647\u0646\u0648\u0632 \u062d\u0627\u0644\u062a \u0627\u0645\u0646 \u0641\u0639\u0627\u0644 \u0646\u0634\u062f\u0647 \u0627\u0633\u062a. +820|right +824|e +827|\u0641\u0639\u0627\u0644\u200c\u0633\u0627\u0632\u06cc \u0627\u0631\u062a\u0628\u0627\u0637 \u0627\u0645\u0646 +830|bold +834|ew +837|\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u062d\u0627\u0644\u062a \u0639\u0627\u062f\u06cc +838|#F4EFE9 +840|#ECE1D5 +841|bold +845|ew +848|ew +856|word +858|ew +859|transparent +860|ns +863| +870|\xd8\xa7\xd8\xb1\xd8\xb3\xd8\xa7\xd9\u201e +872|#3A2514 +874|bold +879| +880|\u0645\u062a\u0646 \u067e\u06cc\u0627\u0645 +880|fa +889|nsew +906|connected +907|port +907|#2E7D62 +909|port +909|#9A6C3C +918|\u0647\u0646\u0648\u0632 \u0645\u062e\u0627\u0637\u0628\u06cc \u0627\u0636\u0627\u0641\u0647 \u0646\u0634\u062f\u0647 \u0627\u0633\u062a. +919|#D5E8E1 +921|e +927|\u0622\u0645\u0627\u062f\u0647 \u06af\u0641\u062a\u06af\u0648 +928|e +932|#F7EFE5 +932|#2A6956 +933|#F0E3D5 +933|#32745F +934|white +937|ew +946|\u06cc\u06a9 \u0645\u062e\u0627\u0637\u0628 \u0631\u0627 \u0627\u0646\u062a\u062e\u0627\u0628 \u06a9\u0646 +947|\u062f\u0631 \u0627\u06cc\u0646\u062c\u0627 \u0641\u0642\u0637 \u062f\u0648 \u062d\u0627\u0644\u062a \u062f\u0627\u0631\u06cc: \u0639\u0627\u062f\u06cc \u06cc\u0627 \u0627\u0645\u0646 +948|\xd8\xb9\xd8\xa7\xd8\xaf\xdb\u0152 +949|\u0646\u0627\u0645 \u0645\u062e\u0627\u0637\u0628 +950|\u0634\u0645\u0627\u0631\u0647 +951|\u0628\u0631\u0627\u06cc \u0634\u0631\u0648\u0639\u060c \u06cc\u06a9 \u0645\u062e\u0627\u0637\u0628 \u0627\u0632 \u0633\u062a\u0648\u0646 \u0633\u0645\u062a \u0631\u0627\u0633\u062a \u0627\u0646\u062a\u062e\u0627\u0628 \u06a9\u0646. +960|pending +961|\xd8\xaf\xd8\xb1 \xd8\xa7\xd9\u2020\xd8\xaa\xd8\xb8\xd8\xa7\xd8\xb1 +961|#FCEBD7 +961|#9A6C3C +962|\u062f\u0631\u062e\u0648\u0627\u0633\u062a \u0627\u0631\u062a\u0628\u0627\u0637 \u0627\u0645\u0646 \u0627\u0631\u0633\u0627\u0644 \u0634\u062f\u0647 \u0648 \u0628\u0631\u0646\u0627\u0645\u0647 \u0645\u0646\u062a\u0638\u0631 \u067e\u0627\u0633\u062e \u0637\u0631\u0641 \u0645\u0642\u0627\u0628\u0644 \u0627\u0633\u062a. +963|secure +964|\u0627\u0645\u0646 +964|#D9F5E8 +964|#0F8A5F +965|\u0627\u0631\u062a\u0628\u0627\u0637 \u0627\u0645\u0646 \u0641\u0639\u0627\u0644 \u0627\u0633\u062a. \u0647\u0631 \u0632\u0645\u0627\u0646 \u0628\u062e\u0648\u0627\u0647\u06cc \u0645\u06cc\u200c\u062a\u0648\u0627\u0646\u06cc \u0628\u0647 \u062d\u0627\u0644\u062a \u0639\u0627\u062f\u06cc \u0628\u0631\u06af\u0631\u062f\u06cc. +967|\xd8\xb9\xd8\xa7\xd8\xaf\xdb\u0152 +969|\u06a9\u0644\u06cc\u062f \u0627\u06cc\u0646 \u0645\u062e\u0627\u0637\u0628 \u0622\u0645\u0627\u062f\u0647 \u0627\u0633\u062a. \u0627\u06af\u0631 \u0628\u062e\u0648\u0627\u0647\u06cc \u0645\u06cc\u200c\u062a\u0648\u0627\u0646\u06cc \u062f\u0648\u0628\u0627\u0631\u0647 \u0627\u0631\u062a\u0628\u0627\u0637 \u0627\u0645\u0646 \u0631\u0627 \u0641\u0639\u0627\u0644 \u06a9\u0646\u06cc. +971|\u0628\u0631\u0627\u06cc \u0627\u0645\u0646 \u0634\u062f\u0646 \u06af\u0641\u062a\u06af\u0648\u060c \u0641\u0642\u0637 \u0631\u0648\u06cc \u062f\u06a9\u0645\u0647 \u0641\u0639\u0627\u0644\u200c\u0633\u0627\u0632\u06cc \u0627\u0631\u062a\u0628\u0627\u0637 \u0627\u0645\u0646 \u0628\u0632\u0646. +972|normal +973|normal +973|secure +973|pending +973|disabled +977|normal +978|1.0 +978|end +980|end +980|\n\u0647\u0646\u0648\u0632 \u067e\u06cc\u0627\u0645\u06cc \u062b\u0628\u062a \u0646\u0634\u062f\u0647 \u0627\u0633\u062a.\n +980|system +982|system +983|end +983|system +985|\xd8\xb4\xd9\u2026\xd8\xa7 +985|out +985|\xd9\u2026\xd8\xae\xd8\xa7\xd8\xb7\xd8\xa8 +986| | \u0627\u0645\u0646 +986|secure +986| +987|out +987|out +987|in +989|end +993|disabled +994|end +998|\u0627\u0648\u0644 \u06cc\u06a9 \u0645\u062e\u0627\u0637\u0628 \u0631\u0627 \u0627\u0646\u062a\u062e\u0627\u0628 \u06a9\u0646. +1000|1.0 +1000|end-1c +1002|\u0645\u062a\u0646 \u067e\u06cc\u0627\u0645 \u062e\u0627\u0644\u06cc \u0627\u0633\u062a. +1006|1.0 +1006|end +1007|\xd8\xa7\xd8\xb1\xd8\xb3\xd8\xa7\xd9\u201e \xd8\xb4\xd8\xaf. +1007|sent +1007|\u062f\u0631 \u062d\u0627\u0644\u062a \u0622\u0641\u0644\u0627\u06cc\u0646\u060c \u067e\u06cc\u0627\u0645 \u0628\u0647 \u0635\u0648\u0631\u062a \u0634\u0628\u06cc\u0647\u200c\u0633\u0627\u0632\u06cc \u062b\u0628\u062a \u0634\u062f. +1019|\u062f\u0631\u062e\u0648\u0627\u0633\u062a \u0627\u0631\u062a\u0628\u0627\u0637 \u0627\u0645\u0646 \u0627\u0631\u0633\u0627\u0644 \u0634\u062f. +1019|sent +1019|\u062f\u0631 \u062d\u0627\u0644\u062a \u0622\u0641\u0644\u0627\u06cc\u0646\u060c \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u0627\u0645\u0646 \u0628\u0647 \u0635\u0648\u0631\u062a \u0645\u062d\u0644\u06cc \u062b\u0628\u062a \u0634\u062f. +1023|\u0627\u0631\u0633\u0627\u0644 \u062f\u0631\u062e\u0648\u0627\u0633\u062a \u0627\u0645\u0646 \u0646\u0627\u0645\u0648\u0641\u0642 \u0628\u0648\u062f. +1032|\u06af\u0641\u062a\u06af\u0648 \u0628\u0647 \u062d\u0627\u0644\u062a \u0639\u0627\u062f\u06cc \u0628\u0631\u06af\u0634\u062a. +1032|sent +1032|\u062f\u0631 \u062d\u0627\u0644\u062a \u0622\u0641\u0644\u0627\u06cc\u0646\u060c \u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u0639\u0627\u062f\u06cc \u0645\u062d\u0644\u06cc \u062b\u0628\u062a \u0634\u062f. +1036|\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u062d\u0627\u0644\u062a \u0639\u0627\u062f\u06cc \u0627\u0646\u062c\u0627\u0645 \u0646\u0634\u062f. +1043| +1044|end +1045|end +1052| +1058|\u0646\u0627\u0645 \u0648 \u0634\u0645\u0627\u0631\u0647 \u0647\u0631 \u062f\u0648 \u0644\u0627\u0632\u0645 \u0647\u0633\u062a\u0646\u062f. +1062|\u0645\u062e\u0627\u0637\u0628 \u0630\u062e\u06cc\u0631\u0647 \u0634\u062f. +1063|end +1064|end +1072|\u062a\u0646\u0638\u06cc\u0645\u0627\u062a +1073|\u0647\u0645\u0647 \u0628\u062e\u0634\u200c\u0647\u0627 \u062f\u0627\u062e\u0644 \u0647\u0645\u06cc\u0646 \u067e\u0646\u062c\u0631\u0647 \u0628\u0627\u0632 \u0645\u06cc\u200c\u0634\u0648\u0646\u062f \u062a\u0627 \u0628\u0631\u0627\u06cc \u0646\u0645\u0627\u06cc\u0634\u06af\u0631 \u06f7 \u0627\u06cc\u0646\u0686\u06cc \u0633\u0627\u062f\u0647 \u0648 \u0642\u0627\u0628\u0644 \u0644\u0645\u0633 \u0628\u0645\u0627\u0646\u0646\u062f. +1075|x +1078|both +1083|\u0648\u0636\u0639\u06cc\u062a \u0641\u0639\u0644\u06cc \u062f\u0633\u062a\u06af\u0627\u0647 +1085|bold +1086|e +1089|\u0645\u062a\u0635\u0644 +1089|connected +1089|\u0622\u0641\u0644\u0627\u06cc\u0646 +1089|port +1089|baudrate +1092|right +1094|e +1097|\u0648\u0631\u0648\u062f \u0628\u0647 \u067e\u0646\u0644 \u0627\u062f\u0645\u06cc\u0646 +1100|bold +1103|x +1106|\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u06af\u0641\u062a\u06af\u0648 +1107|#F1E7DB +1109|#E8D8C7 +1110|bold +1113|x +1131|transparent +1135|\xd8\xa8\xd8\xb3\xd8\xaa\xd9\u2020 +1138|#F1E7DB +1140|#E8D8C7 +1141|bold +1143|e +1148|bold +1149|e +1155|right +1157|e +1163|\u0648\u0631\u0648\u062f \u0627\u062f\u0645\u06cc\u0646 +1164|\u062c\u0632\u0626\u06cc\u0627\u062a \u0641\u0646\u06cc\u060c \u0644\u0627\u06af\u200c\u0647\u0627 \u0648 \u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u0627\u0645\u0646\u06cc\u062a\u06cc \u0641\u0642\u0637 \u0628\u0639\u062f \u0627\u0632 \u0648\u0631\u0648\u062f \u0627\u062f\u0645\u06cc\u0646 \u0646\u0645\u0627\u06cc\u0634 \u062f\u0627\u062f\u0647 \u0645\u06cc\u200c\u0634\u0648\u0646\u062f. +1166|x +1169|both +1172|\u0631\u0645\u0632 \u0627\u0635\u0644\u06cc \u0631\u0627 \u0648\u0627\u0631\u062f \u06a9\u0646 +1174|bold +1175|e +1178|\u0631\u0645\u0632 \u0627\u0635\u0644\u06cc +1179|* +1183|x +1184| +1185|e +1189|\u0631\u0645\u0632 \u0627\u062f\u0645\u06cc\u0646 \u0635\u062d\u06cc\u062d \u0646\u06cc\u0633\u062a. +1195|\u0648\u0631\u0648\u062f \u0628\u0647 \u067e\u0646\u0644 \u0627\u062f\u0645\u06cc\u0646 +1198|bold +1201|x +1204|\u0628\u0627\u0632\u06af\u0634\u062a \u0628\u0647 \u062a\u0646\u0638\u06cc\u0645\u0627\u062a +1205|#F1E7DB +1207|#E8D8C7 +1208|bold +1211|x +1212|\u0631\u0645\u0632 \u0627\u0635\u0644\u06cc \u0627\u062f\u0645\u06cc\u0646 +1212|en +1217|\u067e\u0646\u0644 \u0627\u062f\u0645\u06cc\u0646 +1218|\u0627\u06cc\u0646 \u0628\u062e\u0634 \u0628\u0631\u0627\u06cc \u0646\u0645\u0627\u06cc\u0634\u06af\u0631 \u06a9\u0648\u0686\u06a9 \u062e\u0644\u0627\u0635\u0647 \u0634\u062f\u0647 \u062a\u0627 \u0627\u0637\u0644\u0627\u0639\u0627\u062a \u0627\u0635\u0644\u06cc\u060c \u0644\u0627\u06af\u200c\u0647\u0627 \u0648 \u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u0645\u0648\u062f\u0645/\u0631\u0645\u0632 \u062f\u0627\u062e\u0644 \u0647\u0645\u0627\u0646 \u067e\u0646\u062c\u0631\u0647 \u062f\u0631 \u062f\u0633\u062a\u0631\u0633 \u0628\u0627\u0634\u0646\u062f. +1220|x +1223|stats +1224|system_info +1230| +1232|both +1236|\u06a9\u0644 \u0645\u062e\u0627\u0637\u0628\u200c\u0647\u0627 +1236|contacts +1237|\u0645\u062e\u0627\u0637\u0628 \u0627\u0645\u0646 +1237|secure_contacts +1238|\xd8\xaf\xd8\xb1 \xd8\xa7\xd9\u2020\xd8\xaa\xd8\xb8\xd8\xa7\xd8\xb1 +1238|pending_contacts +1239|\u067e\u06cc\u0627\u0645 \u0627\u0645\u0646 +1239|secure_messages +1243|ew +1249|e +1254|bold +1255|e +1258|ew +1261|\u0627\u0637\u0644\u0627\u0639\u0627\u062a \u0633\u06cc\u0633\u062a\u0645\u06cc +1263|bold +1264|e +1267|platform +1267|modem_port +1267|baudrate +1267|fingerprint +1269|right +1272|e +1275|ew +1278|\u0622\u062e\u0631\u06cc\u0646 \u0644\u0627\u06af\u200c\u0647\u0627 +1280|bold +1281|e +1282|word +1283|x +1284|events +1285|events +1286|end +1288|end +1288|\u0647\u0646\u0648\u0632 \u0644\u0627\u06af\u06cc \u062b\u0628\u062a \u0646\u0634\u062f\u0647 \u0627\u0633\u062a. +1289|disabled +1292|ew +1295|\u0628\u0633\u062a\u0647\u200c\u0647\u0627\u06cc \u0646\u0627\u0642\u0635 +1297|bold +1298|e +1299|word +1300|x +1301|pending_packets +1302|pending_packets +1303|end +1305|end +1305|\u067e\u06cc\u0627\u0645 \u0646\u0627\u0642\u0635\u06cc \u062f\u0631 \u0635\u0641 \u0648\u062c\u0648\u062f \u0646\u062f\u0627\u0631\u062f. +1306|disabled +1309|ew +1312|\u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u0645\u0648\u062f\u0645 \u0648 \u0631\u0645\u0632 +1314|bold +1315|e +1316|\u067e\u0648\u0631\u062a \u0645\u0648\u062f\u0645 \u0645\u062b\u0644 COM3 +1317|x +1318|modem_port +1319|Baudrate +1320|x +1321|baudrate +1322|\u0631\u0645\u0632 \u0641\u0639\u0644\u06cc +1322|* +1323|x +1324|\u0631\u0645\u0632 \u062c\u062f\u06cc\u062f +1324|* +1325|x +1326| +1327|e +1328|\u067e\u0648\u0631\u062a \u0645\u0648\u062f\u0645 +1328|en +1329|Baudrate +1329|numeric +1330|\u0631\u0645\u0632 \u0641\u0639\u0644\u06cc +1330|en +1331|\u0631\u0645\u0632 \u062c\u062f\u06cc\u062f +1331|en +1339|\u062a\u063a\u06cc\u06cc\u0631\u0627\u062a \u0630\u062e\u06cc\u0631\u0647 \u0634\u062f \u0648 \u0645\u0648\u062f\u0645 \u0645\u062a\u0635\u0644 \u0627\u0633\u062a. +1341|\u062a\u0646\u0638\u06cc\u0645\u0627\u062a \u0630\u062e\u06cc\u0631\u0647 \u0634\u062f\u060c \u0627\u0645\u0627 \u0645\u0648\u062f\u0645 \u0647\u0646\u0648\u0632 \u0622\u0641\u0644\u0627\u06cc\u0646 \u0627\u0633\u062a. +1341|#9A6C3C +1347|\u0630\u062e\u06cc\u0631\u0647 \u062a\u063a\u06cc\u06cc\u0631\u0627\u062a +1350|bold +1353|x \ No newline at end of file diff --git a/a.ps1 b/a.ps1 new file mode 100644 index 0000000..1372882 --- /dev/null +++ b/a.ps1 @@ -0,0 +1,44 @@ +# ----------------------------- +# تنظیمات +# ----------------------------- +$LocalPath = "C:\Users\Pars\Desktop\saba-python" # مسیر پروژه روی ویندوز +$PiUser = "pars" # کاربر روی Raspberry Pi +$PiHost = "10.65.40.150" # آی‌پی Raspberry Pi +$RemotePath = "/home/pars/Desktop/" # مسیر پروژه روی Pi +$MainPy = "saba-python/main.py" # فایل اصلی پایتون + +$KeyPath = "$env:USERPROFILE\.ssh\id_ed25519" + +# ----------------------------- +# ساخت کلید SSH اگر موجود نباشد +# ----------------------------- +if (-not (Test-Path $KeyPath)) { + Write-Host "Generating SSH key..." + ssh-keygen -t ed25519 -f $KeyPath -N "" -C "$PiUser@windows" +} else { + Write-Host "SSH key already exists." +} + +# ----------------------------- +# اضافه کردن کلید به Pi (رمز یک بار لازم است) +# ----------------------------- +Write-Host "Copying public key to Raspberry Pi..." +$pubKey = Get-Content "$KeyPath.pub" +# دستور برای اضافه کردن کلید به authorized_keys روی Pi +$command = "mkdir -p ~/.ssh; chmod 700 ~/.ssh; echo '$pubKey' >> ~/.ssh/authorized_keys; chmod 600 ~/.ssh/authorized_keys" +ssh "$PiUser@$PiHost" $command + +# ----------------------------- +# انتقال پروژه با scp +# ----------------------------- +Write-Host "Transferring project to Raspberry Pi..." +$scpTarget = "$PiUser@$PiHost`:$RemotePath" +scp -r $LocalPath $scpTarget + +# ----------------------------- +# اجرای برنامه روی Raspberry Pi +# ----------------------------- +Write-Host "Running program on Raspberry Pi..." +ssh "$PiUser@$PiHost" "cd $RemotePath && python3 $MainPy" + +Write-Host "Deployment complete! Latest version is running on Raspberry Pi." -ForegroundColor Green \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..8ca2075 --- /dev/null +++ b/main.py @@ -0,0 +1,276 @@ +import os +import subprocess +import sys +from importlib.util import find_spec +from pathlib import Path +from tkinter import TclError + +BASE_DIR = Path(__file__).resolve().parent +REQUIREMENTS_FILE = BASE_DIR / "requirements.txt" +PROJECT_VENV_DIR = BASE_DIR / ".runtime-venv" + +REQUIRED_IMPORTS = { + "customtkinter": "customtkinter", + "cryptography": "cryptography", + "pyserial": "serial", + "arabic-reshaper": "arabic_reshaper", + "python-bidi": "bidi", +} + + +def _project_venv_python() -> Path: + if os.name == "nt": + return PROJECT_VENV_DIR / "Scripts" / "python.exe" + return PROJECT_VENV_DIR / "bin" / "python" + + +def _running_inside_virtualenv() -> bool: + return sys.prefix != getattr(sys, "base_prefix", sys.prefix) or hasattr(sys, "real_prefix") + + +def _missing_requirements() -> list[str]: + missing = [] + for package_name, import_name in REQUIRED_IMPORTS.items(): + if find_spec(import_name) is None: + missing.append(package_name) + return missing + + +def _missing_requirements_for_python(python_executable: str) -> tuple[list[str] | None, str | None]: + check_script = "\n".join( + [ + "from importlib.util import find_spec", + f"required = {REQUIRED_IMPORTS!r}", + "missing = [name for name, import_name in required.items() if find_spec(import_name) is None]", + "print('\\n'.join(missing))", + ] + ) + ok, output = _run_subprocess( + [python_executable, "-c", check_script], + "Checking project virtual environment", + ) + if not ok: + return None, output + return [line.strip() for line in output.splitlines() if line.strip()], None + + +def _run_subprocess(command: list[str], description: str) -> tuple[bool, str]: + try: + completed = subprocess.run( + command, + check=False, + capture_output=True, + text=True, + ) + except Exception as exc: + return False, f"{description} failed to start: {exc}" + + if completed.returncode == 0: + return True, completed.stdout.strip() + + details = completed.stderr.strip() or completed.stdout.strip() or "Unknown error" + return False, f"{description} failed: {details}" + + +def _ensure_pip_available(python_executable: str) -> tuple[bool, str | None]: + ok, message = _run_subprocess( + [python_executable, "-m", "pip", "--version"], + "Checking pip", + ) + if ok: + return True, None + + ok, ensure_message = _run_subprocess( + [python_executable, "-m", "ensurepip", "--upgrade"], + "Bootstrapping pip", + ) + if ok: + return True, None + return False, ensure_message or message + + +def _install_requirements_with_python(python_executable: str) -> tuple[bool, str | None]: + if not REQUIREMENTS_FILE.exists(): + return False, f"Requirements file not found: {REQUIREMENTS_FILE}" + + pip_ready, pip_message = _ensure_pip_available(python_executable) + if not pip_ready: + return False, pip_message + + print("Installing missing Python packages...", flush=True) + ok, message = _run_subprocess( + [ + python_executable, + "-m", + "pip", + "install", + "--disable-pip-version-check", + "-r", + str(REQUIREMENTS_FILE), + ], + "Installing requirements", + ) + if not ok: + return False, message + return True, None + + +def _create_project_venv() -> tuple[bool, str | None]: + venv_python = _project_venv_python() + if venv_python.exists(): + return True, None + + print(f"Creating local virtual environment in {PROJECT_VENV_DIR}...", flush=True) + ok, message = _run_subprocess( + [sys.executable, "-m", "venv", str(PROJECT_VENV_DIR)], + "Creating virtual environment", + ) + if ok and venv_python.exists(): + return True, None + + help_message = "\n".join( + [ + message or "Creating virtual environment failed.", + "", + "On Raspberry Pi / Debian, install venv support first:", + " sudo apt install python3-venv python3-full", + ] + ) + return False, help_message + + +def _restart_inside_project_venv() -> tuple[bool, str | None]: + venv_python = _project_venv_python() + if not venv_python.exists(): + return False, f"Virtual environment Python was not found: {venv_python}" + + print("Restarting app inside the local virtual environment...", flush=True) + argv = [str(venv_python), str(BASE_DIR / "main.py"), *sys.argv[1:]] + env = os.environ.copy() + env["SECURE_SMS_RUNTIME_VENV"] = str(PROJECT_VENV_DIR) + try: + os.execve(str(venv_python), argv, env) + except Exception as exc: + return False, f"Restarting inside virtual environment failed: {exc}" + + return True, None + + +def _ensure_runtime_dependencies() -> tuple[bool, str | None]: + missing = _missing_requirements() + if not missing: + return True, None + + print(f"Missing packages detected: {', '.join(missing)}", flush=True) + + if _running_inside_virtualenv(): + ok, message = _install_requirements_with_python(sys.executable) + if not ok: + return False, message + else: + venv_python = _project_venv_python() + if venv_python.exists(): + venv_missing, venv_message = _missing_requirements_for_python(str(venv_python)) + if venv_message: + return False, venv_message + if not venv_missing: + ok, message = _restart_inside_project_venv() + if not ok: + return False, message + return False, "Unexpected bootstrap state while switching to the local virtual environment." + + ok, message = _create_project_venv() + if not ok: + return False, message + + ok, message = _install_requirements_with_python(str(venv_python)) + if not ok: + return False, message + + ok, message = _restart_inside_project_venv() + if not ok: + return False, message + return False, "Unexpected bootstrap state while switching to the local virtual environment." + + remaining = _missing_requirements() + if remaining: + return False, f"Packages are still missing after installation: {', '.join(remaining)}" + + return True, None + + +def _prepare_linux_display() -> tuple[bool, str | None]: + if not sys.platform.startswith("linux"): + return True, None + + display = os.environ.get("DISPLAY") + if display: + return True, None + + x11_socket = Path("/tmp/.X11-unix/X0") + if x11_socket.exists(): + os.environ["DISPLAY"] = ":0" + return True, None + + message = "\n".join( + [ + "No GUI display was found for Tkinter.", + "This app needs a graphical desktop session on Raspberry Pi.", + "", + "If you are on the Pi screen, start the desktop first and then run the app.", + "If you run it from SSH/systemd, usually you need:", + " DISPLAY=:0", + " XAUTHORITY=/home/pars/.Xauthority", + "", + "Example:", + " export DISPLAY=:0", + " export XAUTHORITY=/home/pars/.Xauthority", + " python3 main.py", + ] + ) + return False, message + + +def _display_error_message(exc: Exception) -> str: + return "\n".join( + [ + f"GUI startup failed: {exc}", + "", + "Tkinter could not connect to the Raspberry Pi graphical session.", + "Run the app from the Pi desktop session or set DISPLAY/XAUTHORITY correctly.", + "", + "Typical Raspberry Pi values:", + " DISPLAY=:0", + " XAUTHORITY=/home/pars/.Xauthority", + ] + ) + + +def main(): + deps_ready, deps_message = _ensure_runtime_dependencies() + if not deps_ready: + print(deps_message, file=sys.stderr) + return 1 + + ready, message = _prepare_linux_display() + if not ready: + print(message, file=sys.stderr) + return 1 + + try: + from secure_sms.controller import AppController + from secure_sms.ui import SecureSmsApp + + controller = AppController() + app = SecureSmsApp(controller) + controller.bind_ui(app) + app.protocol("WM_DELETE_WINDOW", controller.shutdown) + app.mainloop() + return 0 + except TclError as exc: + print(_display_error_message(exc), file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..fc4a664 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +customtkinter==5.2.2 +cryptography==42.0.5 +pyserial==3.5 +arabic-reshaper==3.0.0 +python-bidi==0.4.2 diff --git a/secure_sms/__init__.py b/secure_sms/__init__.py new file mode 100644 index 0000000..e5b80de --- /dev/null +++ b/secure_sms/__init__.py @@ -0,0 +1 @@ +"""Secure SMS application package.""" diff --git a/secure_sms/__pycache__/__init__.cpython-313.pyc b/secure_sms/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eee4753014ab7691686a2f52ea4f4d0a58dfeb42 GIT binary patch literal 165 zcmey&%ge<81e5RX%~S)@k3k$5V1zP0a{w7r8G;##7}6OvnX2T2Q)2$EV~c$H%W^_zW`TmRK=ZPkeE1aZG%CW?p7V oe7s&khPZFamp; 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 literal 0 HcmV?d00001 diff --git a/secure_sms/__pycache__/gsm.cpython-313.pyc b/secure_sms/__pycache__/gsm.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..624aad65251c489bd641f0b1101995c825f5aeff GIT binary patch literal 9640 zcmbVSeNbChcE3-0PhSWmkN|}q96U?oB#SE7!+ z-HE5uNy*l!X;L>h>5R3VPJ_GKi978~+1c$hZ8sff|7fLvSMm#!-Ogs(@l5|v*4`zP z{?l{b(|Z!K@Fu+$_xtmlbIODxEN&1D`v3z?gQCp>eS zP$^|7BbHTE1*z(^dYq-~I7d0B)UKf#8)+uIx}Nat0hKfp)y^?4?jaT?Tq;3#(iaZ< zE{21GZgL?Wibi~4ztmBRjk&_3Pa$)Y%n)1?)?=u87vXKZdWPlM8II>>G&9;6c82X% z@v1%*2fgvHPs3~K$qVcY4Asr(dF_m$PsQu7=0I*@T9&KTV=XoEhI&HF`8vJ~W;ViK z`Ah}$FwGcd%rmSwr#O3`j<3kgT*;fDpIL4f=hN_&;(S$NTNSk9+8W*>)>?QgtYYP> zXE@kTjaXOB*V39fm8)JboZ`=%^2LLfeODgpVVe^IyAY*uL3h!&7@*J~nCFABm~SrV z^#k=T`us2YWj^7lkkV@Sd-N(qH_1737*Z*mbA}lu-xJR9gaE%hB#dLkz%x)%W)$1- z;9{7TIYQhOa$P=cCNf=N2Ay0#M_j6jQJ0=#4aFl-lsKU*7Nj9x*z1o*B7z|l^U}pg zBovtw%4pCR@WwA;BIu)Gd`{uc!{46%z9*jO+;TQp}3x9x-=RK|k-MH=+R=3_H+ zvH5)qR|K8c8;XSDUaw%ulX#z8iCc;thUgl}8cB8Cb$!O>yk4Gh_H5`HGj_)kyP>mX z>LI;ul8c+x#QmLlE)EpAUAU>xx-Aj)B<}k8VytAnVGX(zsL)?zi^P#hW8MfPM$e6IOB5HkI1{Z=QGrPnEA6RTz zEmVIILwWiA6YVED$-i{kPIBshWf_RKT`Xj;c(EQs25dBHZ^%gjM5%;=QPc~_Pw5_| zuAVZge#cQ-dEI=P7g1OVEkIEynzcj^cUjR8DDo;^eV75Uhkq`1VsuXG^7){Pp-qr? zX+-go1kDZ=0_P7$K^{d(Vqb`cB2P$6x!m?(A@qH9F91ZAdlnH_!! zHtp<5IeXI1e)yH(w%oCxsVQY_T5U-hn?5q`%c_a7LDZT8Nb&r?DS$jpTsz3&AA~&H&aZrIOD9vY?`T?K|D$#DHPZdKv2mfTu<|M5+T2UkU z+1|Rj89`zBp zbiTh9WxUk>6(yfvQKq5~Y}yW#0(D#*ACc->d0{x*7o7CM78Rq=W^ z-p8WgQ4B5a$^*E#loU3RJw!fn0CqmBauNr~HtK<#Au~*j3fk!GY&aANUR9MF1kM-p zheCoTG#81|;2lQL1SyTun81y=-IMPBe)|@NJA(paa8*!b69rll2{cu~#S6v(HZCm2 zy%!f}XMt}TvK)V_8;t0Co;l6gxTyHmz)uwn%| z3XN6b)aq=iwJ+WBOseIXwX^FjBOAuiO{?v#u{XxvJiRoMvDPe&Y*y90<$A;QW>h4MO#vqmb7Ib`~Wql?OiE*SK8i}viGgq52oz{Df__M+`UUl`#{ouHf{e# z%KnXYyLWl`lUjSm)_B`^%b1wjuytnad)DoJYufkweq_otG=2UltUmt6_)2`k+WNSX zSZcSeeZQ(ZTScsGtL%IFJNmnA>mB_mOaEG1(lWT!1gE}n{>J$)Vu+_+uNmo4|IKhS z#2-{vW87mHIi?l3g-cOn;4JmQW6O{BRSpO$MSJ^akl9J&7E0+H<55}2v-=5&04Q}A z&Xt?5s86} zhQ)^w9fI}gDTts8A`N;Pqvs$JIO*iLydrZsuqL%UDSJ}=7>tNL0};rc$&xlX;g_-4 zKCZTdpr90Xq%0k))oIJYl;z-q+J;S|`NsHn$CqP&Jo%ZH)HGyu#B7xeA-rA!SGLg} zZ-GerBfy27Xph!z*ADj1tvm@}rd*KqsdJ|>xN1=Vl*Lp4^?TLs684N&%UqFDCz%G@ zA_cdSlWPe0N#tEb)I1wuzie-h8f_NOi8kvRXwH&&&t1aq$&%&+x!hqC!ttUY@6uhu zp20&#gHM?V=izwWk#k^OfLFXe!cO)ToCgK1D1aOrzB;A0ogMAVH8=??pDQr1X^!22q9s=LsGhz~!Oo0dZfA(3BP=lOx=tLo-*13d%_j6Q^I z5ZaFsy21qJ*c3%w{tzMCL)^tfB!r-vJWUa(<T@cBDNVG(5tnGwIFfP#O{6buG30x6a|0N%#qNTwbg$4?O zgo`GyyS)Gx6yYQ*~=R#}jPrv9za8=*H}OpK&zT&qLt zHG4C)_H=D~sz>3r5nDy!uaAsM33?&`YX@@W6@U1)i(pmf7c_!1+ zzO`4{+&9RvBF>eZMx8pi9j+uSnB!`uu;a3`oD8>>@{4{pFt zprxP}h&w1wL}f*x6g-s2`Tj>7r7Ic&#gY$h^rhxK(Epy>SE)5icuZxb>n;Hv03g7# z79O=Fz=J$40>G-|gIIaSDyNzwWLoi)y3yfW6aZFeJ&X*kBPx$d84J2&S}_@5f=fI} z^G{w`wMW#`=tUHRdzYNcr=o|~nJ>#ZkGhZH^}OM*+T9}8;x|5V>k3&!6&$r)YY_hrOWn`CnQL(V%2=;G~k_5e%YdId(Y|za*$AJo{on zAN5}n)S*aR;O1y_aY6J=Q>2wp?FD1xhfi?e7XVisY#+ZeNp?z0gW;eL9Cc{Ro_t@S z$r8AEUmPaqLXkl5W#x;NWW8MC0)^Hq?*xY?`B7g{bt8Hsx)cRZc6qh(woh7G^WS2I z)2`ur*8iycur57zAvJa(83-lEF8pllg{;b0y{EteeL4Mnd+^raYQOYy-)!Ce?$}?9 zCHIZqW7k{Htc-l(Xx;4SS{-@s^qtdd%=ahOJB}u}O{WW7q8~YXz=Wl{Mp9iPna=K2 z8(S4%yHNQn zUGLkv{&&;Tfa<;af!Uf>L;6Jw>HeofWh4FMpPeUqN9xE=EcH&dbFinta;6(++Bcd&{wZ4s$_V*yuXNUx=gP2 zDDFJoaOB7EPy;>c(eY)t{gPkGl&41oK8Z&(XTA>o73X6BPILzDo=3}99EH<}<%>|} zN?8GqR^E}5oLQ7y7NGe`d@)l_*93EsQdYym@}^SOBJWO=wZ9W*D8(JK#GU+};DbWt z#N4bH{%i26PWtZU9-!xeI1fRU!RPF}t8Nhy?}Jw}?`Hm9jQ?&_R9>gtM+9aMoq|SD zTM10_)G-t@aTXCfQ+S{fdHUf2mm#k%MCTq2Ks)#f7s8>qpbiHk5=b7!Nf3lf{zJXw zxIr-%RM8jdvzP(>1-gX_26Ui!B{!lG6$$DptP`;_IJw|^S}cZAgZNntk{Sx(EY3nT zR7&OjA@qv6J4D60Vc6~we>yM9oOCQ_0GW!NqOmwKEE0ugHTaDw&O%v?Q1&Py9zh4}SP0Vi z6^d^%$zi<*bNjKCn+->OV7TIo3-CW{z<&(Wv}5b@7~unnx-b&CTW}>zyJs7SrXs6ixRcCE0Ms|6kJL(4R#VB1CTAB@-}>EOi+!`ZwVX9! z$r4Cb19yAxgv9LH@MEmWR_)^I68r=MDB=uYQqj9(f$B)h)hL73c6<`&L5+%LLF#I5_3WXri)=d eCi;(w`D0S{bJF%pQog16x{BHRD}u2&!~X$oZMiW3 literal 0 HcmV?d00001 diff --git a/secure_sms/__pycache__/models.cpython-313.pyc b/secure_sms/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b5b84dfe1089a961e0decec2980af62b8a452074 GIT binary patch literal 2225 zcmbtV&x_ke6dqZ!BwOpBEAKi<8=PIz#)k%a2=q{LX-ny*$p!>Dxd^R2@rt!&Wk%lO zvmxzuDS`bLdhNacL%|Ttz(S#?y}9_%Yu_7?tZ^{RbGYh(|o_5joJ9)32!b=oP)f=fFryzp{)gt0*lK=9y@D7PI7BTY4pC z56fLgRKJ--5J}=q$K!y_E>U4ojE^S~6-E?{0mqX86#*1MbI^GXI3m z1Q9NrB=N_T^I%B*n9W4Bq3ERTq zs=P3zQIJUC;I>#D7d8-@2-^rQLJPq_*g@Dukef0;+Q)3fR2sh$MW9>VT=8!K*2+}-9|vjH&j0`b literal 0 HcmV?d00001 diff --git a/secure_sms/__pycache__/protocol.cpython-313.pyc b/secure_sms/__pycache__/protocol.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..efcd409b09046dd4bac86e43437001048f3c5516 GIT binary patch literal 4767 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 G`~Dv)wL@P3 literal 0 HcmV?d00001 diff --git a/secure_sms/__pycache__/ui.cpython-313.pyc b/secure_sms/__pycache__/ui.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce9c9b9a60ded6d31d45dda933a907dd707812d0 GIT binary patch literal 84520 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 bool: + return self.service.is_bootstrapped() + + def bootstrap(self, password: str): + self.service.bootstrap(password) + self._start_gsm() + + def unlock(self, password: str) -> bool: + unlocked = self.service.unlock(password) + if unlocked: + self._start_gsm() + return unlocked + + def verify_password(self, password: str) -> bool: + return self.service.verify_password(password) + + def change_master_password(self, current_password: str, new_password: str): + self.service.change_master_password(current_password, new_password) + + def _start_gsm(self) -> bool: + self._stop_gsm() + port, baudrate = self.service.get_connection_settings() + self.gsm = GSMGateway(port=port, baudrate=baudrate, message_callback=self._on_sms_received) + connected = self.gsm.connect() + self._notify_ui() + return connected + + def reconnect_modem(self, port: str, baudrate: int) -> bool: + self.service.update_connection_settings(port, baudrate) + return self._start_gsm() + + def modem_status(self) -> dict: + return { + "connected": bool(self.gsm and self.gsm.is_connected), + "port": self.gsm.port if self.gsm else self.service.get_connection_settings()[0], + "baudrate": self.gsm.baudrate if self.gsm else self.service.get_connection_settings()[1], + } + + def list_contacts(self): + return self.service.list_contacts() + + def get_contact(self, phone: str): + return self.service.get_contact(phone) + + def save_contact(self, name: str, phone: str): + self.service.add_or_update_contact(name, phone) + self._notify_ui() + + def get_messages(self, phone: str): + return self.service.get_messages(phone) + + def send_message(self, phone: str, text: str) -> tuple[bool, str]: + try: + frames, mode = self.service.prepare_outgoing_message(phone, text) + except Exception as exc: + return False, str(exc) + + if self.gsm and self.gsm.is_connected: + sent = self.gsm.send_frames(phone, frames) + state = "sent" if sent else "failed" + else: + sent = True + state = "simulated" + + if sent: + self.service.store_outgoing_message(phone, text, mode, state) + self._notify_ui(phone) + return True, state + return False, "failed" + + def request_secure(self, phone: str) -> tuple[bool, str]: + frames = self.service.request_secure_channel(phone) + if self.gsm and self.gsm.is_connected: + ok = self.gsm.send_frames(phone, frames) + status = "sent" if ok else "failed" + else: + ok = True + status = "simulated" + self._notify_ui(phone) + return ok, status + + def switch_to_normal(self, phone: str) -> tuple[bool, str]: + frames = self.service.request_normal_mode(phone) + if self.gsm and self.gsm.is_connected: + ok = self.gsm.send_frames(phone, frames) + status = "sent" if ok else "failed" + else: + ok = True + status = "simulated" + self._notify_ui(phone) + return ok, status + + def get_admin_snapshot(self) -> dict: + snapshot = self.service.get_admin_snapshot() + snapshot["modem"] = self.modem_status() + return snapshot + + def _on_sms_received(self, sender: str, raw_text: str): + try: + _, reply_frames = self.service.process_incoming_sms(sender, raw_text) + if reply_frames and self.gsm and self.gsm.is_connected: + self.gsm.send_frames(sender, reply_frames) + finally: + self._notify_ui(sender) + + def _notify_ui(self, phone: Optional[str] = None): + if self.ui: + self.ui.after(0, lambda: self.ui.handle_background_refresh(phone)) + + def _stop_gsm(self): + if self.gsm: + self.gsm.disconnect() + self.gsm = None + + def shutdown(self): + self._stop_gsm() + if self.ui: + self.ui.destroy() diff --git a/secure_sms/database.py b/secure_sms/database.py new file mode 100644 index 0000000..468807a --- /dev/null +++ b/secure_sms/database.py @@ -0,0 +1,410 @@ +import sqlite3 +from datetime import datetime +from pathlib import Path +from typing import Optional + +from secure_sms.security import SecurityMetadata, StorageCipher + + +DB_FILE = "secure_sms_v2.db" + + +def utc_now() -> str: + return datetime.utcnow().replace(microsecond=0).isoformat() + "Z" + + +class Database: + def __init__(self, db_path: str = DB_FILE): + self.db_path = Path(db_path) + self._initialize() + + def _connect(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.db_path) + conn.row_factory = sqlite3.Row + return conn + + def _initialize(self): + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS app_config ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ) + """ + ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS identity ( + id INTEGER PRIMARY KEY CHECK(id = 1), + private_key_enc TEXT NOT NULL, + public_key_enc TEXT NOT NULL, + fingerprint TEXT NOT NULL, + created_at TEXT NOT NULL + ) + """ + ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS contacts ( + phone TEXT PRIMARY KEY, + name_enc TEXT NOT NULL, + mode TEXT NOT NULL DEFAULT 'normal', + secure_state TEXT NOT NULL DEFAULT 'none', + peer_public_key_enc TEXT, + peer_fingerprint TEXT, + last_secure_at TEXT, + created_at TEXT NOT NULL, + updated_at TEXT NOT NULL + ) + """ + ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone TEXT NOT NULL, + direction TEXT NOT NULL, + body_enc TEXT NOT NULL, + mode TEXT NOT NULL, + transport_state TEXT NOT NULL, + metadata_enc TEXT, + created_at TEXT NOT NULL + ) + """ + ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS packet_fragments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone TEXT NOT NULL, + packet_id TEXT NOT NULL, + packet_kind TEXT NOT NULL, + packet_mode TEXT, + part_no INTEGER NOT NULL, + total_parts INTEGER NOT NULL, + chunk TEXT NOT NULL, + created_at TEXT NOT NULL, + UNIQUE(phone, packet_id, part_no) + ) + """ + ) + cursor.execute( + """ + CREATE TABLE IF NOT EXISTS secure_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + phone TEXT, + event_type TEXT NOT NULL, + details_enc TEXT, + created_at TEXT NOT NULL + ) + """ + ) + conn.commit() + + def is_bootstrapped(self) -> bool: + return self.get_security_metadata() is not None + + def get_security_metadata(self) -> Optional[SecurityMetadata]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM app_config WHERE key = 'password_salt'") + salt_row = cursor.fetchone() + cursor.execute("SELECT value FROM app_config WHERE key = 'password_verifier'") + verifier_row = cursor.fetchone() + if not salt_row or not verifier_row: + return None + return SecurityMetadata(salt=salt_row["value"], verifier=verifier_row["value"]) + + def set_security_metadata(self, meta: SecurityMetadata): + self.set_config("password_salt", meta.salt) + self.set_config("password_verifier", meta.verifier) + + def set_config(self, key: str, value: str): + with self._connect() as conn: + conn.execute( + "INSERT INTO app_config(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value", + (key, value), + ) + conn.commit() + + def get_config(self, key: str, default: Optional[str] = None) -> Optional[str]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute("SELECT value FROM app_config WHERE key = ?", (key,)) + row = cursor.fetchone() + return row["value"] if row else default + + def get_connection_settings(self) -> tuple[str, int]: + port = self.get_config("gsm_port", "COM1") or "COM1" + baudrate = int(self.get_config("gsm_baudrate", "115200") or "115200") + return port, baudrate + + def set_connection_settings(self, port: str, baudrate: int): + self.set_config("gsm_port", port) + self.set_config("gsm_baudrate", str(baudrate)) + + def save_identity(self, private_key_enc: str, public_key_enc: str, fingerprint: str): + with self._connect() as conn: + conn.execute( + """ + INSERT INTO identity(id, private_key_enc, public_key_enc, fingerprint, created_at) + VALUES(1, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + private_key_enc = excluded.private_key_enc, + public_key_enc = excluded.public_key_enc, + fingerprint = excluded.fingerprint + """, + (private_key_enc, public_key_enc, fingerprint, utc_now()), + ) + conn.commit() + + def get_identity_row(self) -> Optional[sqlite3.Row]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM identity WHERE id = 1") + return cursor.fetchone() + + def upsert_contact(self, phone: str, name_enc: str): + now = utc_now() + with self._connect() as conn: + conn.execute( + """ + INSERT INTO contacts(phone, name_enc, mode, secure_state, created_at, updated_at) + VALUES(?, ?, 'normal', 'none', ?, ?) + ON CONFLICT(phone) DO UPDATE SET + name_enc = excluded.name_enc, + updated_at = excluded.updated_at + """, + (phone, name_enc, now, now), + ) + conn.commit() + + def ensure_contact_exists(self, phone: str, name_enc: str): + now = utc_now() + with self._connect() as conn: + conn.execute( + """ + INSERT INTO contacts(phone, name_enc, mode, secure_state, created_at, updated_at) + VALUES(?, ?, 'normal', 'none', ?, ?) + ON CONFLICT(phone) DO NOTHING + """, + (phone, name_enc, now, now), + ) + conn.commit() + + def get_contact_row(self, phone: str) -> Optional[sqlite3.Row]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM contacts WHERE phone = ?", (phone,)) + return cursor.fetchone() + + def list_contact_rows(self) -> list[sqlite3.Row]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT + c.*, + m.body_enc AS last_body_enc + FROM contacts c + LEFT JOIN messages m + ON m.id = ( + SELECT id FROM messages + WHERE phone = c.phone + ORDER BY id DESC + LIMIT 1 + ) + ORDER BY COALESCE(m.id, 0) DESC, c.updated_at DESC + """ + ) + return cursor.fetchall() + + def update_contact_security( + self, + phone: str, + *, + mode: Optional[str] = None, + secure_state: Optional[str] = None, + peer_public_key_enc: Optional[str] = None, + peer_fingerprint: Optional[str] = None, + last_secure_at: Optional[str] = None, + ): + updates = [] + values = [] + if mode is not None: + updates.append("mode = ?") + values.append(mode) + if secure_state is not None: + updates.append("secure_state = ?") + values.append(secure_state) + if peer_public_key_enc is not None: + updates.append("peer_public_key_enc = ?") + values.append(peer_public_key_enc) + if peer_fingerprint is not None: + updates.append("peer_fingerprint = ?") + values.append(peer_fingerprint) + if last_secure_at is not None: + updates.append("last_secure_at = ?") + values.append(last_secure_at) + updates.append("updated_at = ?") + values.append(utc_now()) + values.append(phone) + with self._connect() as conn: + conn.execute( + f"UPDATE contacts SET {', '.join(updates)} WHERE phone = ?", + values, + ) + conn.commit() + + def add_message( + self, + phone: str, + direction: str, + body_enc: str, + mode: str, + transport_state: str, + metadata_enc: Optional[str] = None, + ) -> int: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute( + """ + INSERT INTO messages(phone, direction, body_enc, mode, transport_state, metadata_enc, created_at) + VALUES(?, ?, ?, ?, ?, ?, ?) + """, + (phone, direction, body_enc, mode, transport_state, metadata_enc, utc_now()), + ) + conn.commit() + return int(cursor.lastrowid) + + def list_message_rows(self, phone: str) -> list[sqlite3.Row]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM messages WHERE phone = ? ORDER BY id ASC", (phone,)) + return cursor.fetchall() + + def log_secure_event(self, phone: Optional[str], event_type: str, details_enc: Optional[str]): + with self._connect() as conn: + conn.execute( + "INSERT INTO secure_events(phone, event_type, details_enc, created_at) VALUES(?, ?, ?, ?)", + (phone, event_type, details_enc, utc_now()), + ) + conn.commit() + + def list_secure_event_rows(self, limit: int = 50) -> list[sqlite3.Row]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM secure_events ORDER BY id DESC LIMIT ?", (limit,)) + return cursor.fetchall() + + def save_fragment( + self, + phone: str, + packet_id: str, + packet_kind: str, + packet_mode: Optional[str], + part_no: int, + total_parts: int, + chunk: str, + ): + with self._connect() as conn: + conn.execute( + """ + INSERT INTO packet_fragments(phone, packet_id, packet_kind, packet_mode, part_no, total_parts, chunk, created_at) + VALUES(?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(phone, packet_id, part_no) DO NOTHING + """, + (phone, packet_id, packet_kind, packet_mode, part_no, total_parts, chunk, utc_now()), + ) + conn.commit() + + def get_packet_fragments(self, phone: str, packet_id: str) -> list[sqlite3.Row]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT * FROM packet_fragments + WHERE phone = ? AND packet_id = ? + ORDER BY part_no ASC + """, + (phone, packet_id), + ) + return cursor.fetchall() + + def delete_packet_fragments(self, phone: str, packet_id: str): + with self._connect() as conn: + conn.execute("DELETE FROM packet_fragments WHERE phone = ? AND packet_id = ?", (phone, packet_id)) + conn.commit() + + def list_pending_packets(self) -> list[sqlite3.Row]: + with self._connect() as conn: + cursor = conn.cursor() + cursor.execute( + """ + SELECT + phone, + packet_id, + packet_kind, + packet_mode, + COUNT(*) AS received_parts, + MAX(total_parts) AS total_parts, + MIN(created_at) AS first_seen + FROM packet_fragments + GROUP BY phone, packet_id, packet_kind, packet_mode + ORDER BY MIN(created_at) DESC + """ + ) + return cursor.fetchall() + + def collect_stats(self) -> dict: + with self._connect() as conn: + cursor = conn.cursor() + stats = {} + cursor.execute("SELECT COUNT(*) AS count FROM contacts") + stats["contacts"] = cursor.fetchone()["count"] + cursor.execute("SELECT COUNT(*) AS count FROM contacts WHERE mode = 'secure'") + stats["secure_contacts"] = cursor.fetchone()["count"] + cursor.execute("SELECT COUNT(*) AS count FROM contacts WHERE secure_state = 'pending'") + stats["pending_contacts"] = cursor.fetchone()["count"] + cursor.execute("SELECT COUNT(*) AS count FROM messages") + stats["messages"] = cursor.fetchone()["count"] + cursor.execute("SELECT COUNT(*) AS count FROM messages WHERE mode = 'secure'") + stats["secure_messages"] = cursor.fetchone()["count"] + cursor.execute("SELECT COUNT(DISTINCT packet_id) AS count FROM packet_fragments") + stats["incomplete_packets"] = cursor.fetchone()["count"] + cursor.execute("SELECT COUNT(*) AS count FROM secure_events WHERE event_type = 'secure_established'") + stats["secure_connections"] = cursor.fetchone()["count"] + return stats + + def rotate_encrypted_payloads(self, old_cipher: StorageCipher, new_cipher: StorageCipher): + table_map = { + "contacts": ("phone", ["name_enc", "peer_public_key_enc"]), + "messages": ("id", ["body_enc", "metadata_enc"]), + "secure_events": ("id", ["details_enc"]), + "identity": ("id", ["private_key_enc", "public_key_enc"]), + } + with self._connect() as conn: + cursor = conn.cursor() + for table_name, (pk_column, encrypted_columns) in table_map.items(): + cursor.execute(f"SELECT * FROM {table_name}") + rows = cursor.fetchall() + for row in rows: + assignments = [] + values = [] + for column in encrypted_columns: + current_value = row[column] + if current_value is None: + continue + decrypted = old_cipher.decrypt_text(current_value) + assignments.append(f"{column} = ?") + values.append(new_cipher.encrypt_text(decrypted)) + if not assignments: + continue + values.append(row[pk_column]) + cursor.execute( + f"UPDATE {table_name} SET {', '.join(assignments)} WHERE {pk_column} = ?", + values, + ) + conn.commit() diff --git a/secure_sms/gsm.py b/secure_sms/gsm.py new file mode 100644 index 0000000..00045f9 --- /dev/null +++ b/secure_sms/gsm.py @@ -0,0 +1,154 @@ +import re +import threading +import time +from typing import Callable, Optional + +import serial + + +class GSMGateway: + def __init__( + self, + port: str, + baudrate: int, + message_callback: Optional[Callable[[str, str], None]] = None, + ): + self.port = port + self.baudrate = baudrate + self.message_callback = message_callback + self.serial_conn = None + self.is_running = False + self.read_thread = None + self.lock = threading.Lock() + + @property + def is_connected(self) -> bool: + return bool(self.serial_conn and self.serial_conn.is_open) + + def connect(self) -> bool: + try: + self.serial_conn = serial.Serial( + port=self.port, + baudrate=self.baudrate, + timeout=1, + ) + self.is_running = True + self.send_at_cmd("AT") + self.send_at_cmd("AT+CMGF=1") + self.send_at_cmd('AT+CNMI=2,1,0,0,0') + self.read_thread = threading.Thread(target=self._read_loop, daemon=True) + self.read_thread.start() + return True + except Exception: + self.serial_conn = None + self.is_running = False + return False + + def disconnect(self): + self.is_running = False + if self.serial_conn and self.serial_conn.is_open: + self.serial_conn.close() + if self.read_thread: + self.read_thread.join(timeout=1.5) + + def send_at_cmd(self, command: str, expected_response: str = "OK", timeout: float = 2.0) -> tuple[bool, list[str]]: + with self.lock: + if not self.is_connected: + return False, ["offline"] + self.serial_conn.reset_input_buffer() + self.serial_conn.write((command + "\r\n").encode("ascii")) + start = time.time() + lines = [] + while time.time() - start < timeout: + if self.serial_conn.in_waiting: + line = self.serial_conn.readline().decode("ascii", errors="ignore").strip() + if line: + lines.append(line) + if expected_response in line or "ERROR" in line: + break + else: + time.sleep(0.05) + return expected_response in "\n".join(lines), lines + + def send_frames(self, phone: str, frames: list[str]) -> bool: + if not self.is_connected: + return False + for frame in frames: + if not self._send_single_sms(phone, frame): + return False + time.sleep(0.8) + return True + + def _send_single_sms(self, phone: str, body: str) -> bool: + with self.lock: + try: + self.serial_conn.reset_input_buffer() + self.serial_conn.write(f'AT+CMGS="{phone}"\r\n'.encode("ascii")) + start = time.time() + prompt_ready = False + while time.time() - start < 2: + if self.serial_conn.in_waiting: + char = self.serial_conn.read().decode("ascii", errors="ignore") + if char == ">": + prompt_ready = True + break + time.sleep(0.05) + if not prompt_ready: + self.serial_conn.write(chr(27).encode("ascii")) + return False + self.serial_conn.write(body.encode("ascii") + chr(26).encode("ascii")) + start = time.time() + while time.time() - start < 10: + if self.serial_conn.in_waiting: + line = self.serial_conn.readline().decode("ascii", errors="ignore").strip() + if "OK" in line: + return True + if "ERROR" in line: + return False + else: + time.sleep(0.1) + except Exception: + return False + return False + + def _read_loop(self): + while self.is_running: + try: + if self.is_connected and self.serial_conn.in_waiting and self.lock.acquire(blocking=False): + try: + line = self.serial_conn.readline().decode("ascii", errors="ignore").strip() + if line.startswith("+CMTI:"): + match = re.search(r'\+CMTI:\s*".*?",(\d+)', line) + if match: + index = int(match.group(1)) + threading.Thread( + target=self._process_incoming_sms, + args=(index,), + daemon=True, + ).start() + finally: + self.lock.release() + except Exception: + pass + time.sleep(0.1) + + def _process_incoming_sms(self, index: int): + time.sleep(0.7) + ok, lines = self.send_at_cmd(f"AT+CMGR={index}", expected_response="OK", timeout=3) + if not ok: + return + sender = "ناشناس" + body_lines = [] + reading_body = False + for line in lines: + if line.startswith("+CMGR:"): + parts = line.split(",") + if len(parts) >= 2: + sender = parts[1].strip('"') + reading_body = True + continue + if reading_body and line not in {"OK", "ERROR"}: + body_lines.append(line) + if self.message_callback: + self.message_callback(sender, "\n".join(body_lines)) + self.send_at_cmd(f"AT+CMGD={index}") diff --git a/secure_sms/models.py b/secure_sms/models.py new file mode 100644 index 0000000..dc07b0e --- /dev/null +++ b/secure_sms/models.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class ContactSummary: + phone: str + name: str + mode: str + secure_state: str + has_peer_key: bool + last_message_preview: str = "" + + +@dataclass +class ContactDetails: + phone: str + name: str + mode: str + secure_state: str + peer_fingerprint: Optional[str] + has_peer_key: bool + last_secure_at: Optional[str] + + +@dataclass +class MessageView: + id: int + phone: str + direction: str + body: str + mode: str + transport_state: str + created_at: str + + +@dataclass +class SecureEventView: + created_at: str + event_type: str + phone: str + details: str + + +@dataclass +class PendingPacketView: + phone: str + packet_id: str + packet_kind: str + packet_mode: Optional[str] + received_parts: int + total_parts: int + first_seen: str diff --git a/secure_sms/protocol.py b/secure_sms/protocol.py new file mode 100644 index 0000000..1d37180 --- /dev/null +++ b/secure_sms/protocol.py @@ -0,0 +1,94 @@ +import json +import uuid +from dataclasses import dataclass +from typing import Optional + +from secure_sms.security import b64u_decode, b64u_encode + + +FRAME_PREFIX = "@SSM1" +FRAME_CHUNK_SIZE = 92 + + +@dataclass +class ParsedFrame: + category: str + packet_id: str + part_no: int + total_parts: int + chunk: str + mode: Optional[str] = None + + +def _split_payload(encoded_payload: str) -> list[str]: + return [ + encoded_payload[index:index + FRAME_CHUNK_SIZE] + for index in range(0, len(encoded_payload), FRAME_CHUNK_SIZE) + ] or [""] + + +def encode_plain_body(text: str) -> str: + return b64u_encode(text.encode("utf-8")) + + +def decode_plain_body(encoded_text: str) -> str: + return b64u_decode(encoded_text).decode("utf-8") + + +def encode_control_payload(payload: dict) -> str: + packed = json.dumps(payload, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + return b64u_encode(packed) + + +def decode_control_payload(encoded_payload: str) -> dict: + return json.loads(b64u_decode(encoded_payload).decode("utf-8")) + + +def build_control_frames(payload: dict, packet_id: Optional[str] = None) -> list[str]: + packet_id = packet_id or uuid.uuid4().hex[:10] + encoded_payload = encode_control_payload(payload) + parts = _split_payload(encoded_payload) + return [ + f"{FRAME_PREFIX}|CTL|{packet_id}|{index + 1}|{len(parts)}|{chunk}" + for index, chunk in enumerate(parts) + ] + + +def build_message_frames(mode: str, encoded_payload: str, packet_id: Optional[str] = None) -> list[str]: + packet_id = packet_id or uuid.uuid4().hex[:10] + parts = _split_payload(encoded_payload) + return [ + f"{FRAME_PREFIX}|MSG|{mode}|{packet_id}|{index + 1}|{len(parts)}|{chunk}" + for index, chunk in enumerate(parts) + ] + + +def parse_frame(raw_text: str) -> Optional[ParsedFrame]: + if not raw_text.startswith(FRAME_PREFIX): + return None + if raw_text.startswith(f"{FRAME_PREFIX}|CTL|"): + parts = raw_text.split("|", 5) + if len(parts) != 6: + return None + _, _, packet_id, part_no, total_parts, chunk = parts + return ParsedFrame( + category="control", + packet_id=packet_id, + part_no=int(part_no), + total_parts=int(total_parts), + chunk=chunk, + ) + if raw_text.startswith(f"{FRAME_PREFIX}|MSG|"): + parts = raw_text.split("|", 6) + if len(parts) != 7: + return None + _, _, mode, packet_id, part_no, total_parts, chunk = parts + return ParsedFrame( + category="message", + mode=mode, + packet_id=packet_id, + part_no=int(part_no), + total_parts=int(total_parts), + chunk=chunk, + ) + return None diff --git a/secure_sms/security.py b/secure_sms/security.py new file mode 100644 index 0000000..e90ac55 --- /dev/null +++ b/secure_sms/security.py @@ -0,0 +1,133 @@ +import base64 +import hashlib +import os +from dataclasses import dataclass +from typing import Optional + +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import x25519 +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives.kdf.hkdf import HKDF +from cryptography.hazmat.primitives.kdf.scrypt import Scrypt + + +def b64u_encode(data: bytes) -> str: + return base64.urlsafe_b64encode(data).decode("ascii").rstrip("=") + + +def b64u_decode(value: str) -> bytes: + padding = "=" * (-len(value) % 4) + return base64.urlsafe_b64decode((value + padding).encode("ascii")) + + +@dataclass +class SecurityMetadata: + salt: str + verifier: str + + +class PasswordManager: + def create_metadata(self, password: str) -> SecurityMetadata: + salt = os.urandom(16) + key = self.derive_key(password, b64u_encode(salt)) + return SecurityMetadata( + salt=b64u_encode(salt), + verifier=hashlib.sha256(key).hexdigest(), + ) + + def derive_key(self, password: str, salt_b64: str) -> bytes: + kdf = Scrypt( + salt=b64u_decode(salt_b64), + length=32, + n=2**14, + r=8, + p=1, + ) + return kdf.derive(password.encode("utf-8")) + + def verify_password(self, password: str, meta: SecurityMetadata) -> bool: + key = self.derive_key(password, meta.salt) + return hashlib.sha256(key).hexdigest() == meta.verifier + + +class StorageCipher: + def __init__(self, key: bytes): + self._aes = AESGCM(key) + + def encrypt_text(self, value: Optional[str]) -> Optional[str]: + if value is None: + return None + nonce = os.urandom(12) + payload = self._aes.encrypt(nonce, value.encode("utf-8"), None) + return "enc1:" + b64u_encode(nonce + payload) + + def decrypt_text(self, value: Optional[str]) -> Optional[str]: + if value in (None, ""): + return value + if not value.startswith("enc1:"): + return value + raw = b64u_decode(value[5:]) + nonce = raw[:12] + ciphertext = raw[12:] + plaintext = self._aes.decrypt(nonce, ciphertext, None) + return plaintext.decode("utf-8") + + +class ECCCryptoService: + INFO = b"sms-secure-channel-v2" + + def generate_identity(self) -> tuple[str, str, str]: + private_key = x25519.X25519PrivateKey.generate() + public_key = private_key.public_key() + private_raw = private_key.private_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PrivateFormat.Raw, + encryption_algorithm=serialization.NoEncryption(), + ) + public_raw = public_key.public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + public_b64 = b64u_encode(public_raw) + return b64u_encode(private_raw), public_b64, self.fingerprint_public_key(public_b64) + + def fingerprint_public_key(self, public_key_b64: str) -> str: + digest = hashlib.sha256(b64u_decode(public_key_b64)).hexdigest() + return digest[:16].upper() + + def encrypt_for_peer(self, message: str, peer_public_key_b64: str) -> str: + peer_public = x25519.X25519PublicKey.from_public_bytes(b64u_decode(peer_public_key_b64)) + ephemeral_private = x25519.X25519PrivateKey.generate() + ephemeral_public = ephemeral_private.public_key().public_bytes( + encoding=serialization.Encoding.Raw, + format=serialization.PublicFormat.Raw, + ) + shared_key = ephemeral_private.exchange(peer_public) + derived_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=self.INFO, + ).derive(shared_key) + nonce = os.urandom(12) + ciphertext = AESGCM(derived_key).encrypt(nonce, message.encode("utf-8"), None) + return b64u_encode(ephemeral_public + nonce + ciphertext) + + def decrypt_from_peer(self, payload_b64: str, private_key_b64: str) -> str: + payload = b64u_decode(payload_b64) + if len(payload) < 60: + raise ValueError("Secure payload is too short.") + ephemeral_public_raw = payload[:32] + nonce = payload[32:44] + ciphertext = payload[44:] + private_key = x25519.X25519PrivateKey.from_private_bytes(b64u_decode(private_key_b64)) + ephemeral_public = x25519.X25519PublicKey.from_public_bytes(ephemeral_public_raw) + shared_key = private_key.exchange(ephemeral_public) + derived_key = HKDF( + algorithm=hashes.SHA256(), + length=32, + salt=None, + info=self.INFO, + ).derive(shared_key) + plaintext = AESGCM(derived_key).decrypt(nonce, ciphertext, None) + return plaintext.decode("utf-8") diff --git a/secure_sms/services.py b/secure_sms/services.py new file mode 100644 index 0000000..7abc8a0 --- /dev/null +++ b/secure_sms/services.py @@ -0,0 +1,407 @@ +import platform +from pathlib import Path +from typing import Optional + +from secure_sms.database import Database, utc_now +from secure_sms.models import ContactDetails, ContactSummary, MessageView, PendingPacketView, SecureEventView +from secure_sms.protocol import ( + build_control_frames, + build_message_frames, + decode_control_payload, + decode_plain_body, + encode_plain_body, + parse_frame, +) +from secure_sms.security import ECCCryptoService, PasswordManager, StorageCipher + + +SYSTEM_CONTACT_LABEL = "مخاطب ناشناس" + + +class SecureMessagingService: + def __init__(self, db: Database): + self.db = db + self.password_manager = PasswordManager() + self.crypto = ECCCryptoService() + self.cipher: Optional[StorageCipher] = None + self.identity = None + + @property + def unlocked(self) -> bool: + return self.cipher is not None and self.identity is not None + + def is_bootstrapped(self) -> bool: + return self.db.is_bootstrapped() + + def bootstrap(self, password: str): + if self.db.is_bootstrapped(): + raise ValueError("Application is already configured.") + meta = self.password_manager.create_metadata(password) + key = self.password_manager.derive_key(password, meta.salt) + self.cipher = StorageCipher(key) + private_key, public_key, fingerprint = self.crypto.generate_identity() + self.db.set_security_metadata(meta) + self.db.save_identity( + private_key_enc=self.cipher.encrypt_text(private_key), + public_key_enc=self.cipher.encrypt_text(public_key), + fingerprint=fingerprint, + ) + self.db.set_connection_settings("COM1", 115200) + self.identity = { + "private_key": private_key, + "public_key": public_key, + "fingerprint": fingerprint, + } + self.db.log_secure_event(None, "app_bootstrap", self._enc("راه‌اندازی اولیه برنامه انجام شد.")) + + def unlock(self, password: str) -> bool: + meta = self.db.get_security_metadata() + if not meta: + raise ValueError("Application is not configured.") + if not self.password_manager.verify_password(password, meta): + return False + key = self.password_manager.derive_key(password, meta.salt) + self.cipher = StorageCipher(key) + identity_row = self.db.get_identity_row() + if identity_row is None: + raise ValueError("Secure identity was not found.") + self.identity = { + "private_key": self.cipher.decrypt_text(identity_row["private_key_enc"]), + "public_key": self.cipher.decrypt_text(identity_row["public_key_enc"]), + "fingerprint": identity_row["fingerprint"], + } + return True + + def verify_password(self, password: str) -> bool: + meta = self.db.get_security_metadata() + if not meta: + return False + return self.password_manager.verify_password(password, meta) + + def change_master_password(self, current_password: str, new_password: str): + meta = self.db.get_security_metadata() + if not meta or not self.password_manager.verify_password(current_password, meta): + raise ValueError("رمز فعلی درست نیست.") + old_key = self.password_manager.derive_key(current_password, meta.salt) + new_meta = self.password_manager.create_metadata(new_password) + new_key = self.password_manager.derive_key(new_password, new_meta.salt) + old_cipher = StorageCipher(old_key) + new_cipher = StorageCipher(new_key) + self.db.rotate_encrypted_payloads(old_cipher, new_cipher) + self.db.set_security_metadata(new_meta) + self.cipher = new_cipher + identity_row = self.db.get_identity_row() + self.identity = { + "private_key": self.cipher.decrypt_text(identity_row["private_key_enc"]), + "public_key": self.cipher.decrypt_text(identity_row["public_key_enc"]), + "fingerprint": identity_row["fingerprint"], + } + self.db.log_secure_event(None, "password_changed", self._enc("رمز اصلی برنامه تغییر کرد.")) + + def _enc(self, value: Optional[str]) -> Optional[str]: + if self.cipher is None: + raise RuntimeError("Application is locked.") + return self.cipher.encrypt_text(value) + + def _dec(self, value: Optional[str]) -> Optional[str]: + if self.cipher is None: + raise RuntimeError("Application is locked.") + return self.cipher.decrypt_text(value) + + def add_or_update_contact(self, name: str, phone: str): + self.db.upsert_contact(phone, self._enc(name)) + + def ensure_contact(self, phone: str, fallback_name: Optional[str] = None): + fallback = fallback_name or SYSTEM_CONTACT_LABEL + self.db.ensure_contact_exists(phone, self._enc(fallback)) + + def list_contacts(self) -> list[ContactSummary]: + contacts = [] + for row in self.db.list_contact_rows(): + preview = self._dec(row["last_body_enc"]) if row["last_body_enc"] else "" + if preview: + preview = preview.replace("\n", " ").strip() + contacts.append( + ContactSummary( + phone=row["phone"], + name=self._dec(row["name_enc"]) or SYSTEM_CONTACT_LABEL, + mode=row["mode"], + secure_state=row["secure_state"], + has_peer_key=bool(row["peer_public_key_enc"]), + last_message_preview=(preview[:38] + "...") if preview and len(preview) > 38 else (preview or ""), + ) + ) + return contacts + + def get_contact(self, phone: str) -> Optional[ContactDetails]: + row = self.db.get_contact_row(phone) + if not row: + return None + return ContactDetails( + phone=row["phone"], + name=self._dec(row["name_enc"]) or SYSTEM_CONTACT_LABEL, + mode=row["mode"], + secure_state=row["secure_state"], + peer_fingerprint=row["peer_fingerprint"], + has_peer_key=bool(row["peer_public_key_enc"]), + last_secure_at=row["last_secure_at"], + ) + + def get_messages(self, phone: str) -> list[MessageView]: + return [ + MessageView( + id=row["id"], + phone=row["phone"], + direction=row["direction"], + body=self._dec(row["body_enc"]) or "", + mode=row["mode"], + transport_state=row["transport_state"], + created_at=row["created_at"], + ) + for row in self.db.list_message_rows(phone) + ] + + def get_public_identity(self) -> dict: + if not self.identity: + raise RuntimeError("Application is locked.") + return { + "public_key": self.identity["public_key"], + "fingerprint": self.identity["fingerprint"], + } + + def prepare_outgoing_message(self, phone: str, text: str) -> tuple[list[str], str]: + contact = self.db.get_contact_row(phone) + if not contact: + raise ValueError("مخاطب پیدا نشد.") + mode = contact["mode"] + if mode == "secure": + peer_key = self._dec(contact["peer_public_key_enc"]) + if not peer_key: + raise ValueError("برای این مخاطب کلید امن وجود ندارد.") + encoded_payload = self.crypto.encrypt_for_peer(text, peer_key) + return build_message_frames("S", encoded_payload), "secure" + encoded_payload = encode_plain_body(text) + return build_message_frames("N", encoded_payload), "normal" + + def store_outgoing_message(self, phone: str, text: str, mode: str, transport_state: str): + self.db.add_message( + phone=phone, + direction="out", + body_enc=self._enc(text), + mode=mode, + transport_state=transport_state, + ) + + def request_secure_channel(self, phone: str) -> list[str]: + self.ensure_contact(phone) + payload = { + "type": "hello", + "public_key": self.identity["public_key"], + "fingerprint": self.identity["fingerprint"], + "ts": utc_now(), + } + self.db.update_contact_security(phone, mode="normal", secure_state="pending") + self.db.log_secure_event(phone, "hello_sent", self._enc("درخواست ارتباط امن ارسال شد.")) + self.db.add_message( + phone=phone, + direction="system", + body_enc=self._enc("درخواست ارتباط امن برای مخاطب ارسال شد."), + mode="system", + transport_state="local", + ) + return build_control_frames(payload) + + def request_normal_mode(self, phone: str) -> list[str]: + self.ensure_contact(phone) + payload = { + "type": "normal_mode", + "ts": utc_now(), + } + self.db.update_contact_security(phone, mode="normal", secure_state="ready") + self.db.log_secure_event(phone, "normal_mode_sent", self._enc("بازگشت به حالت عادی برای مخاطب ارسال شد.")) + self.db.add_message( + phone=phone, + direction="system", + body_enc=self._enc("گفتگو به حالت عادی برگشت."), + mode="system", + transport_state="local", + ) + return build_control_frames(payload) + + def process_incoming_sms(self, sender: str, raw_text: str) -> tuple[str, Optional[list[str]]]: + self.ensure_contact(sender) + frame = parse_frame(raw_text) + if not frame: + self.db.add_message( + phone=sender, + direction="in", + body_enc=self._enc(raw_text), + mode="normal", + transport_state="received_raw", + ) + return sender, None + + payload = self._store_or_assemble_frame(sender, frame) + if payload is None: + self.db.log_secure_event( + sender, + "packet_fragment_received", + self._enc(f"بسته {frame.packet_id} در حال تکمیل است ({frame.part_no}/{frame.total_parts})."), + ) + return sender, None + + if frame.category == "control": + return sender, self._handle_control_payload(sender, payload) + else: + self._handle_message_payload(sender, frame.mode or "N", payload) + return sender, None + + def _store_or_assemble_frame(self, sender: str, frame) -> Optional[str]: + if frame.total_parts == 1: + return frame.chunk + self.db.save_fragment( + sender, + frame.packet_id, + frame.category, + frame.mode, + frame.part_no, + frame.total_parts, + frame.chunk, + ) + fragments = self.db.get_packet_fragments(sender, frame.packet_id) + if len(fragments) < frame.total_parts: + return None + payload = "".join(fragment["chunk"] for fragment in fragments) + self.db.delete_packet_fragments(sender, frame.packet_id) + return payload + + def _handle_control_payload(self, sender: str, payload: str) -> Optional[list[str]]: + data = decode_control_payload(payload) + action = data.get("type") + if action == "hello": + public_key = data.get("public_key") + fingerprint = data.get("fingerprint") or self.crypto.fingerprint_public_key(public_key) + self.db.update_contact_security( + sender, + mode="secure", + secure_state="ready", + peer_public_key_enc=self._enc(public_key), + peer_fingerprint=fingerprint, + last_secure_at=utc_now(), + ) + self.db.log_secure_event(sender, "hello_received", self._enc("درخواست ارتباط امن دریافت شد.")) + self.db.log_secure_event(sender, "secure_established", self._enc("ارتباط امن برقرار شد.")) + self.db.add_message( + phone=sender, + direction="system", + body_enc=self._enc("ارتباط امن با این مخاطب فعال شد."), + mode="system", + transport_state="local", + ) + reply = { + "type": "hello_ack", + "public_key": self.identity["public_key"], + "fingerprint": self.identity["fingerprint"], + "ts": utc_now(), + } + return build_control_frames(reply) + elif action == "hello_ack": + public_key = data.get("public_key") + fingerprint = data.get("fingerprint") or self.crypto.fingerprint_public_key(public_key) + self.db.update_contact_security( + sender, + mode="secure", + secure_state="ready", + peer_public_key_enc=self._enc(public_key), + peer_fingerprint=fingerprint, + last_secure_at=utc_now(), + ) + self.db.log_secure_event(sender, "hello_ack_received", self._enc("پاسخ ارتباط امن دریافت شد.")) + self.db.log_secure_event(sender, "secure_established", self._enc("ارتباط امن برقرار شد.")) + self.db.add_message( + phone=sender, + direction="system", + body_enc=self._enc("ارتباط امن آماده استفاده است."), + mode="system", + transport_state="local", + ) + return None + elif action == "normal_mode": + self.db.update_contact_security(sender, mode="normal", secure_state="ready") + self.db.log_secure_event(sender, "normal_mode_received", self._enc("مخاطب گفتگو را به حالت عادی برگرداند.")) + self.db.add_message( + phone=sender, + direction="system", + body_enc=self._enc("مخاطب گفتگو را به حالت عادی برگرداند."), + mode="system", + transport_state="local", + ) + return None + return None + + def _handle_message_payload(self, sender: str, mode_marker: str, payload: str): + if mode_marker == "S": + try: + body = self.crypto.decrypt_from_peer(payload, self.identity["private_key"]) + mode = "secure" + transport_state = "received_secure" + except Exception: + body = "پیام امن دریافت شد اما بازگشایی نشد." + mode = "secure" + transport_state = "decrypt_failed" + self.db.log_secure_event(sender, "decrypt_failed", self._enc("بازگشایی پیام امن ناموفق بود.")) + else: + body = decode_plain_body(payload) + mode = "normal" + transport_state = "received" + + self.db.add_message( + phone=sender, + direction="in", + body_enc=self._enc(body), + mode=mode, + transport_state=transport_state, + ) + + def get_admin_snapshot(self) -> dict: + stats = self.db.collect_stats() + identity = self.get_public_identity() + return { + "stats": stats, + "events": [ + SecureEventView( + created_at=row["created_at"], + event_type=row["event_type"], + phone=row["phone"] or "-", + details=self._dec(row["details_enc"]) or "", + ) + for row in self.db.list_secure_event_rows() + ], + "pending_packets": [ + PendingPacketView( + phone=row["phone"], + packet_id=row["packet_id"], + packet_kind=row["packet_kind"], + packet_mode=row["packet_mode"], + received_parts=row["received_parts"], + total_parts=row["total_parts"], + first_seen=row["first_seen"], + ) + for row in self.db.list_pending_packets() + ], + "system_info": { + "platform": platform.platform(), + "python": platform.python_version(), + "db_path": str(Path(self.db.db_path).resolve()), + "modem_port": self.db.get_connection_settings()[0], + "baudrate": self.db.get_connection_settings()[1], + "fingerprint": identity["fingerprint"], + }, + } + + def get_connection_settings(self) -> tuple[str, int]: + return self.db.get_connection_settings() + + def update_connection_settings(self, port: str, baudrate: int): + self.db.set_connection_settings(port, baudrate) + self.db.log_secure_event(None, "connection_settings_changed", self._enc(f"تنظیمات مودم به {port}/{baudrate} تغییر کرد.")) diff --git a/secure_sms/ui.py b/secure_sms/ui.py new file mode 100644 index 0000000..52a016d --- /dev/null +++ b/secure_sms/ui.py @@ -0,0 +1,1509 @@ +import os +import re +from tkinter import TclError + +import customtkinter as ctk + +try: + import arabic_reshaper + from bidi.algorithm import get_display +except ImportError: + arabic_reshaper = None + get_display = None + + +ctk.set_appearance_mode("light") +ctk.set_default_color_theme("blue") + + +PRIMARY = "#175B4B" +PRIMARY_DARK = "#0E4236" +PRIMARY_SOFT = "#DFF1E8" +ACCENT = "#E8A04D" +ACCENT_DARK = "#C97E2D" +BACKGROUND = "#F5EFE7" +CARD = "#FFFDFC" +SURFACE = "#FBF7F2" +INPUT_BG = "#FFFCF8" +TEXT = "#16312A" +MUTED = "#6B7A77" +DANGER = "#B6465F" +WARNING = "#9A6C3C" +BORDER = "#E5DCCE" +KEYBOARD_BG = "#D4DCE2" +KEY_FACE = "#FFFFFF" +KEY_MUTED = "#BCC1C9" +KEY_TEXT = "#000000" +SIDEBAR = "#1B5A4A" +SIDEBAR_SOFT = "#245E4E" +FONT_BODY = "Tahoma" if os.name == "nt" else "DejaVu Sans" +FONT_TITLE = "Tahoma" if os.name == "nt" else "DejaVu Sans" +RTL_PATTERN = re.compile(r"[\u0600-\u06FF]") +KEYBOARD_LAYOUTS = { + "fa": [ + ['۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹', '۰'], + ['ض', 'ص', 'ث', 'ق', 'ف', 'غ', 'ع', 'ه', 'خ', 'ح', 'ج', 'چ'], + ['ش', 'س', 'ی', 'ب', 'ل', 'ا', 'ت', 'ن', 'م', 'ک', 'گ'], + ['ظ', 'ط', 'ز', 'ر', 'ذ', 'د', 'پ', 'و', '⌫'], + ['123', 'انگلیسی', '،', 'فاصله', '.', 'تایید'], + ], + "en": [ + ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], + ['q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p'], + ['a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l'], + ['z', 'x', 'c', 'v', 'b', 'n', 'm', '⌫'], + ['123', 'فارسی', ',', 'فاصله', '.', 'تایید'], + ], + "numeric": [ + ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], + ['+', '-', '/', '@', '_', '.', ':', '(', ')', '?'], + ['فارسی', 'انگلیسی', '⌫'], + ['بستن', 'فاصله', 'تایید'], + ], +} +KEYBOARD_ACTIONS = [ + ('فارسی', "fa"), + ('انگلیسی', "en"), + ("123", "numeric"), + ('پاک', "backspace"), + ('بستن', "close"), +] + + +def ui_text(value): + if not isinstance(value, str) or not value: + return value + if arabic_reshaper is None or get_display is None or not RTL_PATTERN.search(value): + return value + return get_display(arabic_reshaper.reshape(value)) + + +class _RTLTextMixin: + @staticmethod + def _normalize_kwargs(kwargs): + normalized = dict(kwargs) + if "text" in normalized: + normalized["text"] = ui_text(normalized["text"]) + if "placeholder_text" in normalized: + normalized["placeholder_text"] = ui_text(normalized["placeholder_text"]) + if "label_text" in normalized: + normalized["label_text"] = ui_text(normalized["label_text"]) + return normalized + + +class RTLLabel(_RTLTextMixin, ctk.CTkLabel): + def __init__(self, *args, **kwargs): + super().__init__(*args, **self._normalize_kwargs(kwargs)) + + def configure(self, require_redraw=False, **kwargs): + return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs)) + + +class RTLButton(_RTLTextMixin, ctk.CTkButton): + def __init__(self, *args, **kwargs): + kwargs.setdefault("corner_radius", 16) + super().__init__(*args, **self._normalize_kwargs(kwargs)) + + def configure(self, require_redraw=False, **kwargs): + return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs)) + + +class RTLEntry(_RTLTextMixin, ctk.CTkEntry): + def __init__(self, *args, **kwargs): + kwargs.setdefault("justify", "right") + kwargs.setdefault("fg_color", INPUT_BG) + kwargs.setdefault("border_color", BORDER) + kwargs.setdefault("corner_radius", 16) + super().__init__(*args, **self._normalize_kwargs(kwargs)) + + def configure(self, require_redraw=False, **kwargs): + return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs)) + + +class RTLTextbox(ctk.CTkTextbox): + def insert(self, index, text, *tags): + return super().insert(index, ui_text(text), *tags) + + +class RTLScrollableFrame(_RTLTextMixin, ctk.CTkScrollableFrame): + def __init__(self, *args, **kwargs): + super().__init__(*args, **self._normalize_kwargs(kwargs)) + + def configure(self, require_redraw=False, **kwargs): + return super().configure(require_redraw=require_redraw, **self._normalize_kwargs(kwargs)) + + +class SecureSmsApp(ctk.CTk): + def __init__(self, controller): + super().__init__() + self.controller = controller + self.current_contact_phone = None + self.active_input = None + self.current_keyboard_layout = "fa" + self._keyboard_interaction_guard = False + self._text_inputs = {} + self._input_aliases = {} + self.kiosk_mode = os.environ.get("SECURE_SMS_WINDOWED", "0") != "1" + self.screen_width = max(480, self.winfo_screenwidth()) + self.screen_height = max(320, self.winfo_screenheight()) + self.is_portrait = self.screen_height > self.screen_width + self.compact_mode = self.is_portrait or self.screen_width <= 560 + if self.kiosk_mode: + self.window_width = self.screen_width + self.window_height = self.screen_height + else: + self.window_width, self.window_height = (480, 800) if self.is_portrait else (800, 480) + self.title(ui_text("پیام‌رسان امن صبا")) + self.geometry(f"{self.window_width}x{self.window_height}") + self.minsize(self.window_width, self.window_height) + self.resizable(False, False) + self.configure(fg_color=BACKGROUND) + self.option_add("*Cursor", "none") + self._build_shell() + self.bind_all("", self._handle_global_tap, add="+") + self._show_lock_screen() + self.after(50, self._enable_touch_kiosk_mode) + + def _enable_touch_kiosk_mode(self): + self._hide_mouse_cursor() + if not self.kiosk_mode: + return + try: + self.attributes("-topmost", True) + self.attributes("-fullscreen", True) + except TclError: + self.geometry(f"{self.window_width}x{self.window_height}+0+0") + + def _hide_mouse_cursor(self, widget=None): + current = widget or self + try: + current.configure(cursor="none") + except Exception: + pass + for child in current.winfo_children(): + self._hide_mouse_cursor(child) + + def _input_target(self, widget): + return getattr(widget, "_entry", getattr(widget, "_textbox", widget)) + + def _resolve_registered_input(self, widget): + if widget in self._text_inputs and widget.winfo_exists(): + return widget + resolved = self._input_aliases.get(str(widget)) + if resolved is not None and resolved.winfo_exists(): + return resolved + current = getattr(widget, "master", None) + while current is not None: + if current in self._text_inputs and current.winfo_exists(): + return current + resolved = self._input_aliases.get(str(current)) + if resolved is not None and resolved.winfo_exists(): + return resolved + current = getattr(current, "master", None) + return None + + def _widget_is_descendant(self, widget, ancestor): + current = widget + while current is not None: + if current == ancestor: + return True + current = getattr(current, "master", None) + return False + + def _point_inside_widget(self, widget, x_root, y_root): + if widget is None or not widget.winfo_exists() or not widget.winfo_ismapped(): + return False + left = widget.winfo_rootx() + top = widget.winfo_rooty() + right = left + widget.winfo_width() + bottom = top + widget.winfo_height() + return left <= x_root <= right and top <= y_root <= bottom + + def _register_text_input(self, widget, *, title, layout="fa", multiline=False, submit=None): + self._text_inputs[widget] = { + "title": title, + "layout": layout, + "multiline": multiline, + "submit": submit, + } + self._input_aliases[str(widget)] = widget + bind_target = self._input_target(widget) + self._input_aliases[str(bind_target)] = widget + bind_target.bind("", lambda _event, target=widget: self._activate_text_input(target), add="+") + bind_target.bind("", lambda _event, target=widget: self.after(10, lambda: self._activate_text_input(target)), add="+") + + def _focus_registered_input(self, widget): + if widget not in self._text_inputs or not widget.winfo_exists(): + return + self._input_target(widget).focus_set() + self._activate_text_input(widget) + + def _activate_text_input(self, widget): + if widget not in self._text_inputs or not widget.winfo_exists(): + return + self.active_input = widget + self._show_virtual_keyboard(self._text_inputs[widget]["layout"]) + + def _handle_global_tap(self, event): + widget = event.widget + if self._keyboard_interaction_guard: + return + if self._resolve_registered_input(widget) is not None: + return + if self._widget_is_descendant(widget, self.keyboard_host): + return + if self._point_inside_widget(self.keyboard_frame, event.x_root, event.y_root): + return + self.after(20, self._hide_virtual_keyboard) + + def _register_keyboard_interaction(self): + self._keyboard_interaction_guard = True + self.after(160, self._clear_keyboard_interaction_guard) + + def _clear_keyboard_interaction_guard(self): + self._keyboard_interaction_guard = False + + def _shift_layout_for_keyboard(self, show: bool): + if not hasattr(self, 'main_panel') or not self.active_input: + return + in_main_panel = self._widget_is_descendant(self.active_input, self.main_panel) + if show and in_main_panel: + if self.is_portrait and hasattr(self, 'sidebar') and self.sidebar.winfo_ismapped(): + self.sidebar.grid_remove() + elif not self.is_portrait and hasattr(self, 'header_card') and self.header_card.winfo_ismapped(): + self.header_card.grid_remove() + elif not show: + if self.is_portrait and hasattr(self, 'sidebar') and not self.sidebar.winfo_ismapped(): + self.sidebar.grid() + elif not self.is_portrait and hasattr(self, 'header_card') and not self.header_card.winfo_ismapped(): + self.header_card.grid() + + def _update_keyboard_preview(self): + widget = self.active_input + if widget is None or not widget.winfo_exists(): + return + target = self._input_target(widget) + try: + if isinstance(widget, ctk.CTkTextbox): + text = target.get("1.0", "end-1c") + else: + text = target.get() + text = text.replace('\n', ' ') + if len(text) > 40: + text = "..." + text[-37:] + self.keyboard_preview.configure(text=text + " |") + except Exception: + pass + + def _show_virtual_keyboard(self, layout=None): + if layout: + self.current_keyboard_layout = layout + self.keyboard_hint.configure(text=self._keyboard_title()) + self._refresh_keyboard_action_bar() + self._update_keyboard_preview() + self._shift_layout_for_keyboard(True) + if not self.keyboard_host.winfo_ismapped(): + self.keyboard_host.grid() + self._render_keyboard(self.current_keyboard_layout) + self.after(0, self._hide_mouse_cursor) + + def _hide_virtual_keyboard(self): + self.active_input = None + self._shift_layout_for_keyboard(False) + self.keyboard_host.grid_remove() + + def _keyboard_title(self): + if self.active_input in self._text_inputs: + return self._text_inputs[self.active_input]["title"] + return "ورودی" + + def _keyboard_weight(self, key): + return { + 'فاصله': 50, + "Space": 50, + 'تایید': 20, + "Enter": 20, + 'بستن': 15, + 'حذف': 15, + "Back": 15, + '⌫': 15, + 'فارسی': 15, + "English": 15, + 'انگلیسی': 15, + "123": 15, + }.get(key, 10) + + def _keyboard_style(self, key): + if key in {'تایید', "Enter"}: + return PRIMARY, PRIMARY_DARK, "white" + if key in {"ABC", 'فا', '۱۲۳', "123", "English", 'انگلیسی', 'فارسی', 'حذف', 'بستن', "Back", '⌫'}: + return KEY_MUTED, "#A8AFB9", KEY_TEXT + return KEY_FACE, "#F0F0F0", KEY_TEXT + + def _keyboard_action_style(self, action): + if action in {"fa", "en", "numeric"}: + active = self.current_keyboard_layout == action + return (PRIMARY, PRIMARY_DARK, "white") if active else (ACCENT, ACCENT_DARK, "#3A2514") + if action == "backspace": + return KEY_MUTED, "#E4D4C2", TEXT + return "#F4EFE9", "#E8D8C7", TEXT + + def _keyboard_action_command(self, action): + if action in {"fa", "en", "numeric"}: + return lambda: (self._register_keyboard_interaction(), self._show_virtual_keyboard(action)) + if action == "backspace": + return lambda: (self._register_keyboard_interaction(), self._backspace_active_input()) + if action == "close": + return lambda: (self._register_keyboard_interaction(), self._hide_virtual_keyboard()) + return lambda: None + + def _refresh_keyboard_action_bar(self): + pass + + def _render_keyboard(self, layout_name): + for child in self.keyboard_keys.winfo_children(): + child.destroy() + + key_height = 44 if self.compact_mode else 40 + key_font_size = 17 if self.compact_mode else 15 + for row_index, row in enumerate(KEYBOARD_LAYOUTS.get(layout_name, KEYBOARD_LAYOUTS["fa"])): + row_frame = ctk.CTkFrame(self.keyboard_keys, fg_color="transparent") + row_frame.grid(row=row_index, column=0, sticky="ew", pady=2 if self.compact_mode else 3) + for column_index, key in enumerate(row): + row_frame.grid_columnconfigure(column_index, weight=self._keyboard_weight(key)) + fg_color, hover_color, text_color = self._keyboard_style(key) + RTLButton( + row_frame, + text=key, + height=key_height, + corner_radius=6, + fg_color=fg_color, + hover_color=hover_color, + text_color=text_color, + font=ctk.CTkFont(family=FONT_BODY, size=key_font_size, weight="bold"), + command=lambda value=key: (self._register_keyboard_interaction(), self._apply_virtual_key(value)), + ).grid(row=0, column=column_index, padx=2, sticky="ew") + + def _apply_virtual_key(self, key): + if key in {"ABC", "English", 'انگلیسی'}: + self._show_virtual_keyboard("en") + return + if key in {'فا', 'فارسی'}: + self._show_virtual_keyboard("fa") + return + if key in {'۱۲۳', "123"}: + self._show_virtual_keyboard("numeric") + return + if key in {'فاصله', "Space"}: + self._insert_into_active_input(" ") + return + if key in {'حذف', "Back", '⌫'}: + self._backspace_active_input() + return + if key == 'بستن': + self._hide_virtual_keyboard() + return + if key in {'تایید', "Enter"}: + self._submit_active_input() + return + self._insert_into_active_input(key) + + def _insert_into_active_input(self, text): + widget = self.active_input + if widget is None or not widget.winfo_exists(): + return + target = self._input_target(widget) + try: + if isinstance(widget, ctk.CTkTextbox): + selection = target.tag_ranges("sel") + if len(selection) == 2: + target.delete(selection[0], selection[1]) + target.insert("insert", text) + target.see("insert") + else: + try: + if target.selection_present(): + target.delete("sel.first", "sel.last") + except Exception: + pass + target.insert("insert", text) + target.focus_set() + self._update_keyboard_preview() + except TclError: + self._hide_virtual_keyboard() + + def _backspace_active_input(self): + widget = self.active_input + if widget is None or not widget.winfo_exists(): + return + target = self._input_target(widget) + try: + if isinstance(widget, ctk.CTkTextbox): + selection = target.tag_ranges("sel") + if len(selection) == 2: + target.delete(selection[0], selection[1]) + elif target.compare("insert", ">", "1.0"): + target.delete("insert-1c") + target.see("insert") + else: + try: + if target.selection_present(): + target.delete("sel.first", "sel.last") + target.focus_set() + self._update_keyboard_preview() + return + except Exception: + pass + index = target.index("insert") + if index > 0: + target.delete(index - 1) + target.focus_set() + self._update_keyboard_preview() + except TclError: + self._hide_virtual_keyboard() + + def _submit_active_input(self): + widget = self.active_input + if widget is None or not widget.winfo_exists(): + return + meta = self._text_inputs.get(widget, {}) + submit = meta.get("submit") + if submit is not None: + submit() + return + if meta.get("multiline"): + self._insert_into_active_input("\n") + return + self._hide_virtual_keyboard() + + def _build_shell(self): + self.grid_columnconfigure(0, weight=1) + self.grid_rowconfigure(0, weight=1) + self.grid_rowconfigure(1, weight=0) + + self.root_frame = ctk.CTkFrame(self, fg_color=BACKGROUND, corner_radius=0) + self.root_frame.grid(row=0, column=0, sticky="nsew") + self.root_frame.grid_columnconfigure(1, weight=1) + self.root_frame.grid_rowconfigure(0, weight=1) + + self.keyboard_host = ctk.CTkFrame(self, fg_color=BACKGROUND, corner_radius=0) + self.keyboard_host.grid(row=1, column=0, sticky="ew") + self.keyboard_host.grid_columnconfigure(0, weight=1) + + self.keyboard_frame = ctk.CTkFrame( + self.keyboard_host, + fg_color=KEYBOARD_BG, + corner_radius=0, + border_width=1, + border_color="#C0C5CB", + ) + self.keyboard_frame.grid(row=0, column=0, sticky="ew", padx=0, pady=0) + self.keyboard_frame.grid_columnconfigure(0, weight=1) + + keyboard_header = ctk.CTkFrame(self.keyboard_frame, fg_color="transparent") + keyboard_header.grid(row=0, column=0, sticky="ew", padx=10, pady=(8, 4)) + keyboard_header.grid_columnconfigure(0, weight=1) + + self.keyboard_preview = RTLLabel( + keyboard_header, + text="", + fg_color="#FFFFFF", + text_color="#000000", + corner_radius=8, + padx=12, + justify="right", + font=ctk.CTkFont(family=FONT_BODY, size=16), + ) + self.keyboard_preview.grid(row=0, column=0, sticky="ew", padx=(6, 12), ipady=8) + + RTLButton( + keyboard_header, + text="بستن کیبورد", + width=76, + height=36, + corner_radius=8, + fg_color=KEY_MUTED, + hover_color="#A8AFB9", + text_color=KEY_TEXT, + font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), + command=self._hide_virtual_keyboard, + ).grid(row=0, column=1, padx=(4, 6), sticky="e") + + self.keyboard_hint = RTLLabel( + self.keyboard_frame, + text="", + text_color="#636A73", + font=ctk.CTkFont(family=FONT_BODY, size=13), + ) + self.keyboard_hint.grid(row=1, column=0, padx=14, pady=(0, 2), sticky="e") + + self.keyboard_action_bar = ctk.CTkFrame(self.keyboard_frame, fg_color="transparent", height=0) + + self.keyboard_keys = ctk.CTkFrame(self.keyboard_frame, fg_color="transparent") + self.keyboard_keys.grid(row=2, column=0, sticky="ew", padx=6, pady=(2, 12)) + self.keyboard_keys.grid_columnconfigure(0, weight=1) + + self._refresh_keyboard_action_bar() + self._render_keyboard(self.current_keyboard_layout) + self.keyboard_host.grid_remove() + + def _reset_text_input_registry(self): + self.active_input = None + self._text_inputs.clear() + self._input_aliases.clear() + + def _clear_root(self): + self._hide_virtual_keyboard() + self._reset_text_input_registry() + for widget in self.root_frame.winfo_children(): + widget.destroy() + + def _show_lock_screen(self): + self._clear_root() + frame = ctk.CTkFrame(self.root_frame, fg_color=SURFACE, corner_radius=28, border_width=1, border_color=BORDER) + frame.place( + relx=0.5, + rely=0.5, + anchor="center", + relwidth=0.86 if self.is_portrait else 0.54, + relheight=0.52 if self.is_portrait else 0.66, + ) + title = 'راه\u200cاندازی امن برنامه' if not self.controller.is_bootstrapped() else 'ورود به برنامه' + subtitle = ( + 'یک رمز اصلی تعیین کن تا کلیدها و داده\u200cهای حساس داخل دیتابیس به صورت رمز\u200cشده نگه\u200cداری شوند.' + if not self.controller.is_bootstrapped() + else 'برای ورود، رمز اصلی برنامه را وارد کن.' + ) + RTLLabel( + frame, + text=title, + font=ctk.CTkFont(family=FONT_BODY, size=28, weight="bold"), + text_color=TEXT, + ).pack(pady=(30, 10)) + RTLLabel( + frame, + text=subtitle, + wraplength=min(self.window_width - 110, 420), + justify="right", + font=ctk.CTkFont(family=FONT_BODY, size=16), + text_color=MUTED, + ).pack(padx=28) + self.password_entry = RTLEntry( + frame, + placeholder_text='رمز اصلی', + show="*", + height=48, + font=ctk.CTkFont(family=FONT_BODY, size=17), + ) + self.password_entry.pack(fill="x", padx=42, pady=(24, 12)) + self.confirm_entry = None + if not self.controller.is_bootstrapped(): + self.confirm_entry = RTLEntry( + frame, + placeholder_text='تکرار رمز', + show="*", + height=48, + font=ctk.CTkFont(family=FONT_BODY, size=17), + ) + self.confirm_entry.pack(fill="x", padx=42, pady=8) + self.lock_message = RTLLabel( + frame, + text="", + text_color=DANGER, + font=ctk.CTkFont(family=FONT_BODY, size=14 if self.is_portrait else 15), + ) + self.lock_message.pack(pady=(6, 10)) + action_text = 'شروع برنامه' if not self.controller.is_bootstrapped() else 'ورود' + RTLButton( + frame, + text=action_text, + height=48, + fg_color=PRIMARY, + hover_color=PRIMARY_DARK, + command=self._submit_lock_screen, + font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"), + ).pack(fill="x", padx=42, pady=(8, 12)) + self.password_entry.bind("", lambda _event: self._submit_lock_screen()) + self._register_text_input(self.password_entry, title='رمز اصلی', layout="en", submit=self._submit_lock_screen) + if self.confirm_entry: + self.confirm_entry.bind("", lambda _event: self._submit_lock_screen()) + self._register_text_input(self.confirm_entry, title='تکرار رمز', layout="en", submit=self._submit_lock_screen) + self.after(80, lambda: self._focus_registered_input(self.password_entry)) + + def _configure_root_layout(self): + for column in range(2): + self.root_frame.grid_columnconfigure(column, weight=0, minsize=0) + for row in range(2): + self.root_frame.grid_rowconfigure(row, weight=0, minsize=0) + if self.is_portrait: + top_height = min(max(270, int(self.window_height * 0.36)), 320) + self.root_frame.grid_columnconfigure(0, weight=1) + self.root_frame.grid_rowconfigure(0, weight=0, minsize=top_height) + self.root_frame.grid_rowconfigure(1, weight=1) + else: + self.root_frame.grid_columnconfigure(0, weight=0, minsize=248) + self.root_frame.grid_columnconfigure(1, weight=1) + self.root_frame.grid_rowconfigure(0, weight=1) + + def _configure_profile_card_layout(self): + for widget in ( + self.profile_title_label, + self.profile_name, + self.profile_phone, + self.profile_hint, + self.secure_button, + self.normal_button, + ): + widget.grid_forget() + + if self.is_portrait: + self.profile_card.grid_columnconfigure(0, weight=1) + self.profile_card.grid_columnconfigure(1, weight=1) + self.profile_title_label.grid(row=0, column=0, columnspan=2, padx=14, pady=(14, 6), sticky="e") + self.profile_name.configure(font=ctk.CTkFont(family=FONT_BODY, size=17, weight="bold")) + self.profile_name.grid(row=1, column=0, padx=14, sticky="e") + self.profile_phone.configure(font=ctk.CTkFont(family=FONT_BODY, size=13 if self.is_portrait else 14)) + self.profile_phone.grid(row=2, column=0, padx=14, pady=(0, 8), sticky="e") + self.profile_hint.configure(wraplength=min(self.window_width - 90, 340), font=ctk.CTkFont(family=FONT_BODY, size=14)) + self.profile_hint.grid(row=3, column=0, columnspan=2, padx=14, pady=(0, 10), sticky="e") + self.secure_button.configure(height=42, font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold")) + self.secure_button.grid(row=1, column=1, padx=14, pady=(2, 6), sticky="ew") + self.normal_button.configure(height=40, font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold")) + self.normal_button.grid(row=2, column=1, padx=14, pady=(0, 8), sticky="ew") + else: + self.profile_card.grid_columnconfigure(0, weight=1) + self.profile_title_label.grid(row=0, column=0, padx=18, pady=(18, 6), sticky="e") + self.profile_name.configure(font=ctk.CTkFont(family=FONT_BODY, size=19, weight="bold")) + self.profile_name.grid(row=1, column=0, padx=18, sticky="e") + self.profile_phone.configure(font=ctk.CTkFont(family=FONT_BODY, size=16)) + self.profile_phone.grid(row=2, column=0, padx=18, pady=(0, 18), sticky="e") + self.profile_hint.configure(wraplength=220, font=ctk.CTkFont(family=FONT_BODY, size=15)) + self.profile_hint.grid(row=3, column=0, padx=18, pady=(0, 18), sticky="e") + self.secure_button.configure(height=48, font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold")) + self.secure_button.grid(row=4, column=0, padx=18, pady=(0, 10), sticky="ew") + self.normal_button.configure(height=44, font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold")) + self.normal_button.grid(row=5, column=0, padx=18, pady=(0, 16), sticky="ew") + + def _submit_lock_screen(self): + password = self.password_entry.get().strip() + if len(password) < 6: + self.lock_message.configure(text="رمز باید حداقل ۶ کاراکتر باشد.") + return + if not self.controller.is_bootstrapped(): + confirm = self.confirm_entry.get().strip() + if password != confirm: + self.lock_message.configure(text="تکرار رمز با رمز اصلی یکسان نیست.") + return + self.controller.bootstrap(password) + self._build_main_app() + return + if self.controller.unlock(password): + self._build_main_app() + return + self.lock_message.configure(text="رمز وارد شده درست نیست.") + + def _build_main_app(self): + self._clear_root() + self._configure_root_layout() + outer_pad = 12 if self.is_portrait else 18 + inner_pad = 8 if self.is_portrait else 12 + title_size = 24 if self.is_portrait else 28 + subtitle_size = 12 if self.is_portrait else 13 + action_height = 46 if self.is_portrait else 42 + main_row = 1 if self.is_portrait else 0 + main_column = 0 if self.is_portrait else 1 + + self.sidebar = ctk.CTkFrame(self.root_frame, fg_color=SIDEBAR, corner_radius=0) + self.sidebar.grid(row=0, column=0, sticky="nsew") + self.sidebar.grid_columnconfigure(0, weight=1) + self.sidebar.grid_rowconfigure(5, weight=1) + + RTLLabel( + self.sidebar, + text='صبا', + text_color="white", + font=ctk.CTkFont(family=FONT_TITLE, size=title_size, weight="bold"), + ).grid(row=0, column=0, padx=20, pady=(16, 2), sticky="e") + RTLLabel( + self.sidebar, + text='پیام\u200cرسان امن و ساده برای کاربر غیر فنی', + text_color="#D5E8E1", + font=ctk.CTkFont(family=FONT_BODY, size=subtitle_size), + ).grid(row=1, column=0, padx=20, sticky="e") + + self.connection_badge = RTLLabel( + self.sidebar, + text="", + corner_radius=999, + fg_color="#2E7D62", + text_color="white", + font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"), + padx=14, + pady=8, + ) + self.connection_badge.grid(row=2, column=0, padx=20, pady=(12, 10), sticky="e") + + top_actions = ctk.CTkFrame(self.sidebar, fg_color="transparent") + top_actions.grid(row=3, column=0, padx=14, pady=(0, 8), sticky="ew") + top_actions.grid_columnconfigure((0, 1), weight=1) + RTLButton( + top_actions, + text='مخاطب جدید', + command=self._open_contact_dialog, + fg_color=ACCENT, + text_color="#3A2514", + hover_color=ACCENT_DARK, + height=action_height, + font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"), + ).grid(row=0, column=0, padx=6, sticky="ew") + RTLButton( + top_actions, + text='تنظیمات', + command=self._open_settings_panel, + fg_color="#F4EFE9", + text_color="#15302B", + hover_color="#ECE1D5", + height=action_height, + font=ctk.CTkFont(family=FONT_BODY, size=15 if self.is_portrait else 14, weight="bold"), + ).grid(row=0, column=1, padx=6, sticky="ew") + + self.contact_form_card = ctk.CTkFrame( + self.sidebar, + fg_color=SIDEBAR_SOFT, + corner_radius=18, + border_width=1, + border_color="#3B7B66", + ) + self.contact_form_card.grid(row=4, column=0, padx=14, pady=(0, 10), sticky="ew") + self.contact_form_card.grid_columnconfigure(0, weight=1) + RTLLabel( + self.contact_form_card, + text='مخاطب جدید', + text_color="white", + font=ctk.CTkFont(family=FONT_BODY, size=17, weight="bold"), + ).grid(row=0, column=0, padx=14, pady=(14, 8), sticky="e") + self.contact_name_entry = RTLEntry( + self.contact_form_card, + placeholder_text='نام مخاطب', + height=44, + font=ctk.CTkFont(family=FONT_BODY, size=14 if self.is_portrait else 15), + ) + self.contact_name_entry.grid(row=1, column=0, padx=14, pady=6, sticky="ew") + self.contact_phone_entry = RTLEntry( + self.contact_form_card, + placeholder_text='شماره موبایل', + height=44, + font=ctk.CTkFont(family=FONT_BODY, size=14 if self.is_portrait else 15), + ) + self.contact_phone_entry.grid(row=2, column=0, padx=14, pady=6, sticky="ew") + self.contact_form_message = RTLLabel( + self.contact_form_card, + text="", + text_color="#FDE68A", + font=ctk.CTkFont(family=FONT_BODY, size=13), + ) + self.contact_form_message.grid(row=3, column=0, padx=14, pady=(4, 2), sticky="e") + contact_actions = ctk.CTkFrame(self.contact_form_card, fg_color="transparent") + contact_actions.grid(row=4, column=0, padx=10, pady=(4, 14), sticky="ew") + contact_actions.grid_columnconfigure((0, 1), weight=1) + RTLButton( + contact_actions, + text='ذخیره', + fg_color=ACCENT, + text_color="#3A2514", + hover_color=ACCENT_DARK, + command=self._save_contact_inline, + font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), + height=40, + ).grid(row=0, column=0, padx=4, sticky="ew") + RTLButton( + contact_actions, + text='بستن', + fg_color="#F4EFE9", + text_color=TEXT, + hover_color="#ECE1D5", + command=self._hide_contact_form, + font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), + height=40, + ).grid(row=0, column=1, padx=4, sticky="ew") + self.contact_phone_entry.bind("", lambda _event: self._save_contact_inline()) + self._register_text_input(self.contact_name_entry, title="نام مخاطب", layout="fa") + self._register_text_input( + self.contact_phone_entry, + title="شماره موبایل", + layout="numeric", + submit=self._save_contact_inline, + ) + self.contact_form_card.grid_remove() + + self.contacts_frame = RTLScrollableFrame( + self.sidebar, + height=160 if self.is_portrait else 320, + label_text='مخاطب\u200cها', + label_font=ctk.CTkFont(family=FONT_BODY, size=17 if self.is_portrait else 18, weight="bold"), + fg_color="transparent", + ) + self.contacts_frame.grid(row=5, column=0, padx=12, pady=8, sticky="nsew") + + self.main_panel = ctk.CTkFrame(self.root_frame, fg_color=BACKGROUND, corner_radius=0) + self.main_panel.grid(row=main_row, column=main_column, sticky="nsew") + self.main_panel.grid_rowconfigure(1, weight=1) + self.main_panel.grid_columnconfigure(0, weight=1) + + self.header_card = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=24, border_width=1, border_color=BORDER) + self.header_card.grid(row=0, column=0, padx=outer_pad, pady=(outer_pad, inner_pad), sticky="ew") + self.header_card.grid_columnconfigure(0, weight=1) + self.header_card.grid_columnconfigure(1, weight=0) + self.chat_title = RTLLabel( + self.header_card, + text='یک مخاطب را انتخاب کن', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=18 if self.is_portrait else 20, weight="bold"), + ) + self.chat_title.grid(row=0, column=0, padx=22, pady=(18, 4), sticky="e") + self.chat_subtitle = RTLLabel( + self.header_card, + text='در اینجا فقط دو حالت داری: عادی یا امن', + text_color=MUTED, + font=ctk.CTkFont(family=FONT_BODY, size=14), + ) + self.chat_subtitle.grid(row=1, column=0, padx=22, pady=(0, 18), sticky="e") + self.mode_badge = RTLLabel( + self.header_card, + text='عادی', + fg_color=PRIMARY_SOFT, + text_color=PRIMARY, + corner_radius=999, + padx=20, + pady=10, + font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"), + ) + self.mode_badge.grid(row=0, column=1, rowspan=2, padx=20, sticky="e") + + content = ctk.CTkFrame(self.main_panel, fg_color="transparent") + content.grid(row=1, column=0, padx=outer_pad, pady=(0, inner_pad), sticky="nsew") + if self.is_portrait: + content.grid_rowconfigure(0, weight=1) + content.grid_rowconfigure(1, weight=0) + content.grid_columnconfigure(0, weight=1) + else: + content.grid_rowconfigure(0, weight=1) + content.grid_columnconfigure(0, weight=1) + content.grid_columnconfigure(1, weight=0, minsize=220) + + self.chat_container = RTLScrollableFrame( + content, + fg_color="transparent" + ) + if self.is_portrait: + self.chat_container.grid(row=0, column=0, sticky="nsew", pady=(0, inner_pad)) + else: + self.chat_container.grid(row=0, column=0, sticky="nsew", padx=(0, inner_pad)) + + self.profile_card = ctk.CTkFrame(content, fg_color=CARD, corner_radius=24, border_width=1, border_color=BORDER) + self.profile_card.grid(row=1, column=0, sticky="ew") if self.is_portrait else self.profile_card.grid(row=0, column=1, sticky="nsew") + self.profile_card.grid_columnconfigure(0, weight=1) + self.profile_title_label = RTLLabel( + self.profile_card, + text='پروفایل مخاطب', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=17 if self.is_portrait else 18, weight="bold"), + ) + self.profile_name = RTLLabel( + self.profile_card, + text='نام مخاطب', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_BODY, size=19, weight="bold"), + ) + self.profile_name.grid(row=1, column=0, padx=18, sticky="e") + self.profile_phone = RTLLabel( + self.profile_card, + text='شماره', + text_color=MUTED, + font=ctk.CTkFont(family=FONT_BODY, size=16), + ) + self.profile_phone.grid(row=2, column=0, padx=18, pady=(0, 18), sticky="e") + self.profile_hint = RTLLabel( + self.profile_card, + text='برای این مخاطب هنوز حالت امن فعال نشده است.', + wraplength=220, + justify="right", + text_color=MUTED, + font=ctk.CTkFont(family=FONT_BODY, size=15), + ) + self.profile_hint.grid(row=3, column=0, padx=18, pady=(0, 18), sticky="e") + self.secure_button = RTLButton( + self.profile_card, + text='فعال\u200cسازی ارتباط امن', + fg_color=PRIMARY, + hover_color=PRIMARY_DARK, + font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), + command=self._toggle_secure_mode, + height=48, + ) + self.secure_button.grid(row=4, column=0, padx=18, pady=(0, 10), sticky="ew") + self.normal_button = RTLButton( + self.profile_card, + text='بازگشت به حالت عادی', + fg_color="#F4EFE9", + text_color=TEXT, + hover_color="#ECE1D5", + font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), + command=self._switch_to_normal, + height=44, + ) + self.normal_button.grid(row=5, column=0, padx=18, pady=(0, 16), sticky="ew") + self._configure_profile_card_layout() + + composer = ctk.CTkFrame(self.main_panel, fg_color=CARD, corner_radius=24, border_width=1, border_color=BORDER) + composer.grid(row=2, column=0, padx=outer_pad, pady=(0, outer_pad), sticky="ew") + composer.grid_columnconfigure(0, weight=1) + self.message_entry = RTLTextbox( + composer, + height=72 if self.is_portrait else 82, + fg_color=INPUT_BG, + border_width=0, + font=ctk.CTkFont(family=FONT_BODY, size=16), + wrap="word", + ) + self.message_entry.grid(row=0, column=0, padx=(16, inner_pad), pady=14 if self.is_portrait else 16, sticky="ew") + actions = ctk.CTkFrame(composer, fg_color="transparent") + actions.grid(row=0, column=1, padx=(0, 16), pady=14 if self.is_portrait else 16, sticky="ns") + self.send_state_label = RTLLabel( + actions, + text="", + text_color=MUTED, + font=ctk.CTkFont(family=FONT_BODY, size=14), + ) + self.send_state_label.pack(pady=(4, 8)) + RTLButton( + actions, + text='ارسال', + fg_color=ACCENT, + text_color="#3A2514", + hover_color=ACCENT_DARK, + font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"), + width=110 if self.is_portrait else 126, + height=48, + command=self._send_message, + ).pack() + self.message_entry.bind("", lambda _event: self._send_message()) + self._register_text_input(self.message_entry, title="متن پیام", layout="fa", multiline=True) + + self.overlay_frame = ctk.CTkFrame( + self.main_panel, + fg_color=CARD, + corner_radius=28, + border_width=1, + border_color=BORDER, + ) + self.overlay_frame.grid(row=0, column=0, rowspan=3, padx=outer_pad, pady=outer_pad, sticky="nsew") + self.overlay_frame.grid_columnconfigure(0, weight=1) + self.overlay_frame.grid_remove() + + self.refresh_all() + + def refresh_all(self): + self._refresh_connection_badge() + self._refresh_contacts() + self._refresh_current_chat() + self.after(0, self._hide_mouse_cursor) + + def handle_background_refresh(self, phone=None): + self.refresh_all() + + def _refresh_connection_badge(self): + modem = self.controller.modem_status() + if modem["connected"]: + self.connection_badge.configure(text=f"مودم متصل | {modem['port']}", fg_color="#2E7D62") + else: + self.connection_badge.configure(text=f"مودم آفلاین | {modem['port']}", fg_color="#9A6C3C") + + def _refresh_contacts(self): + for widget in self.contacts_frame.winfo_children(): + widget.destroy() + contacts = self.controller.list_contacts() + if not contacts: + RTLLabel( + self.contacts_frame, + text='هنوز مخاطبی اضافه نشده است.', + text_color="#D5E8E1", + font=ctk.CTkFont(family=FONT_BODY, size=16), + ).grid(row=0, column=0, padx=12, pady=12, sticky="e") + return + for index, contact in enumerate(contacts): + selected = self.current_contact_phone == contact.phone + card = RTLButton( + self.contacts_frame, + text=f"{contact.name}\n{contact.phone}\n{contact.last_message_preview or 'آماده گفتگو'}", + anchor="e", + height=76 if self.is_portrait else 88, + corner_radius=20, + command=lambda phone=contact.phone: self._select_contact(phone), + fg_color="#F7EFE5" if selected else "#2A6956", + hover_color="#F0E3D5" if selected else "#32745F", + text_color=TEXT if selected else "white", + font=ctk.CTkFont(family=FONT_BODY, size=15), + ) + card.grid(row=index, column=0, padx=8, pady=6, sticky="ew") + + def _select_contact(self, phone: str): + self.current_contact_phone = phone + self._refresh_contacts() + self._refresh_current_chat() + + def _refresh_current_chat(self): + if not self.current_contact_phone: + self.chat_title.configure(text='یک مخاطب را انتخاب کن') + self.chat_subtitle.configure(text='در اینجا فقط دو حالت داری: عادی یا امن') + self.mode_badge.configure(text='عادی', fg_color=PRIMARY_SOFT, text_color=PRIMARY) + self.profile_name.configure(text='نام مخاطب') + self.profile_phone.configure(text='شماره') + self.profile_hint.configure(text='برای شروع، یک مخاطب از ستون سمت راست انتخاب کن.') + self._render_messages([]) + return + contact = self.controller.get_contact(self.current_contact_phone) + messages = self.controller.get_messages(self.current_contact_phone) + self.chat_title.configure(text=contact.name) + self.chat_subtitle.configure(text=contact.phone) + self.profile_name.configure(text=contact.name) + self.profile_phone.configure(text=contact.phone) + if contact.secure_state == "pending": + self.mode_badge.configure(text='در انتظار', fg_color="#FCEBD7", text_color="#9A6C3C") + self.profile_hint.configure(text='درخواست ارتباط امن ارسال شده و برنامه منتظر پاسخ طرف مقابل است.') + elif contact.mode == "secure": + self.mode_badge.configure(text='امن', fg_color="#D9F5E8", text_color="#0F8A5F") + self.profile_hint.configure(text='ارتباط امن فعال است. هر زمان بخواهی می\u200cتوانی به حالت عادی برگردی.') + else: + self.mode_badge.configure(text='عادی', fg_color=PRIMARY_SOFT, text_color=PRIMARY) + if contact.has_peer_key: + self.profile_hint.configure(text='کلید این مخاطب آماده است. اگر بخواهی می\u200cتوانی دوباره ارتباط امن را فعال کنی.') + else: + self.profile_hint.configure(text='برای امن شدن گفتگو، فقط روی دکمه فعال\u200cسازی ارتباط امن بزن.') + self.secure_button.configure(state="normal") + self.normal_button.configure(state="normal" if contact.mode == "secure" or contact.secure_state == "pending" else "disabled") + self._render_messages(messages) + + def _render_messages(self, messages): + for widget in self.chat_container.winfo_children(): + widget.destroy() + + if not messages: + RTLLabel( + self.chat_container, + text='هنوز پیامی ثبت نشده است.\nاز نوار پایین برای نوشتن پیام استفاده کن.', + text_color=MUTED, + font=ctk.CTkFont(family=FONT_BODY, size=15) + ).pack(pady=40) + return + + for message in messages: + if message.direction == "system": + RTLLabel( + self.chat_container, + text=message.body, + text_color="#8A5C2E", + font=ctk.CTkFont(family=FONT_BODY, size=13), + wraplength=int(self.window_width * 0.7) + ).pack(pady=12, anchor="center") + continue + + is_out = message.direction == "out" + bubble_color = "#E1F2E9" if is_out else "#FFFFFF" + anchor = "e" if is_out else "w" + + bubble = ctk.CTkFrame( + self.chat_container, + fg_color=bubble_color, + corner_radius=16, + border_width=1 if not is_out else 0, + border_color=BORDER + ) + bubble.pack(anchor=anchor, padx=12, pady=6, fill="none") + + RTLLabel( + bubble, + text=message.body, + text_color="#16312A", + font=ctk.CTkFont(family=FONT_BODY, size=16), + wraplength=max(200, int(self.window_width * 0.55)), + justify="right" + ).pack(padx=16, pady=(12, 4), anchor="e") + + badge_text = f"🛡️ {message.created_at}" if message.mode == "secure" else message.created_at + RTLLabel( + bubble, + text=badge_text, + text_color=MUTED, + font=ctk.CTkFont(family=FONT_BODY, size=11), + justify="right" + ).pack(padx=16, pady=(0, 8), anchor="w" if is_out else "e") + + try: + self.after(50, lambda: self.chat_container._parent_canvas.yview_moveto(1.0)) + except Exception: + pass + + def _send_message(self): + if not self.current_contact_phone: + self.send_state_label.configure(text='اول یک مخاطب را انتخاب کن.', text_color=DANGER) + return + text = self.message_entry.get("1.0", "end-1c").strip() + if not text: + self.send_state_label.configure(text='متن پیام خالی است.', text_color=DANGER) + return + ok, state = self.controller.send_message(self.current_contact_phone, text) + if ok: + self.message_entry.delete("1.0", "end") + label = 'ارسال شد.' if state == "sent" else 'در حالت آفلاین، پیام به صورت شبیه\u200cسازی ثبت شد.' + self.send_state_label.configure(text=label, text_color=PRIMARY) + else: + self.send_state_label.configure(text=state, text_color=DANGER) + self.refresh_all() + + def _toggle_secure_mode(self): + if not self.current_contact_phone: + return + ok, state = self.controller.request_secure(self.current_contact_phone) + if ok: + self.send_state_label.configure( + text='درخواست ارتباط امن ارسال شد.' if state == "sent" else 'در حالت آفلاین، درخواست امن به صورت محلی ثبت شد.', + text_color=PRIMARY, + ) + else: + self.send_state_label.configure(text='ارسال درخواست امن ناموفق بود.', text_color=DANGER) + self.refresh_all() + + def _switch_to_normal(self): + if not self.current_contact_phone: + return + ok, state = self.controller.switch_to_normal(self.current_contact_phone) + if ok: + self.send_state_label.configure( + text='گفتگو به حالت عادی برگشت.' if state == "sent" else 'در حالت آفلاین، بازگشت به عادی محلی ثبت شد.', + text_color=PRIMARY, + ) + else: + self.send_state_label.configure(text='بازگشت به حالت عادی انجام نشد.', text_color=DANGER) + self.refresh_all() + + def _open_contact_dialog(self): + if self.contact_form_card.winfo_ismapped(): + self._hide_contact_form() + return + self.contact_form_message.configure(text="") + self.contact_name_entry.delete(0, "end") + self.contact_phone_entry.delete(0, "end") + self.contact_form_card.grid() + self.after(80, lambda: self._focus_registered_input(self.contact_name_entry)) + + def _hide_contact_form(self): + self._hide_virtual_keyboard() + self.contact_form_card.grid_remove() + self.contact_form_message.configure(text="") + + def _save_contact_inline(self): + name = self.contact_name_entry.get().strip() + phone = self.contact_phone_entry.get().strip() + if not name or not phone: + self.contact_form_message.configure(text='نام و شماره هر دو لازم هستند.') + return + self.controller.save_contact(name, phone) + self.current_contact_phone = phone + self.contact_form_message.configure(text='مخاطب ذخیره شد.') + self.contact_name_entry.delete(0, "end") + self.contact_phone_entry.delete(0, "end") + self._hide_virtual_keyboard() + self.refresh_all() + self.after(900, self._hide_contact_form) + + def _open_settings_panel(self): + self._show_overlay() + header = self._build_overlay_header( + 'تنظیمات', + 'همه بخش\u200cها داخل همین پنجره باز می\u200cشوند تا برای نمایشگر ۷ اینچی ساده و قابل لمس بمانند.', + ) + header.pack(fill="x", padx=16, pady=(16, 10)) + + body = ctk.CTkFrame(self.overlay_frame, fg_color=SURFACE, corner_radius=22) + body.pack(fill="both", expand=True, padx=16, pady=(0, 16)) + + modem = self.controller.modem_status() + RTLLabel( + body, + text='وضعیت فعلی دستگاه', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"), + ).pack(anchor="e", padx=18, pady=(18, 6)) + RTLLabel( + body, + text=f"مودم: {'متصل' if modem['connected'] else 'آفلاین'} | پورت: {modem['port']} | Baudrate: {modem['baudrate']}", + text_color=MUTED, + wraplength=min(self.window_width - 96, 480), + justify="right", + font=ctk.CTkFont(family=FONT_BODY, size=14), + ).pack(anchor="e", padx=18, pady=(0, 14)) + RTLButton( + body, + text='ورود به پنل ادمین', + fg_color=PRIMARY, + hover_color=PRIMARY_DARK, + font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), + height=42, + command=self._open_admin_login, + ).pack(fill="x", padx=18, pady=(8, 8)) + RTLButton( + body, + text='بازگشت به گفتگو', + fg_color="#F1E7DB", + text_color=TEXT, + hover_color="#E8D8C7", + font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), + height=40, + command=self._hide_overlay, + ).pack(fill="x", padx=18, pady=(0, 18)) + + def _show_overlay(self): + self._hide_virtual_keyboard() + for widget in self.overlay_frame.winfo_children(): + widget.destroy() + self.overlay_frame.grid() + self.overlay_frame.lift() + self.after(0, self._hide_mouse_cursor) + + def _hide_overlay(self): + self._hide_virtual_keyboard() + for widget in self.overlay_frame.winfo_children(): + widget.destroy() + self.overlay_frame.grid_remove() + self.after(0, self._hide_mouse_cursor) + + def _build_overlay_header(self, title: str, subtitle: str): + header = ctk.CTkFrame(self.overlay_frame, fg_color="transparent") + header.grid_columnconfigure(0, weight=1) + RTLButton( + header, + text='بستن', + width=86, + height=36, + fg_color="#F1E7DB", + text_color=TEXT, + hover_color="#E8D8C7", + font=ctk.CTkFont(family=FONT_BODY, size=14, weight="bold"), + command=self._hide_overlay, + ).grid(row=0, column=1, padx=(8, 0), sticky="e") + RTLLabel( + header, + text=title, + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=22, weight="bold"), + ).grid(row=0, column=0, sticky="e") + RTLLabel( + header, + text=subtitle, + text_color=MUTED, + wraplength=min(self.window_width - 96, 500), + justify="right", + font=ctk.CTkFont(family=FONT_BODY, size=13), + ).grid(row=1, column=0, columnspan=2, pady=(4, 0), sticky="e") + return header + + def _open_admin_login(self): + self._show_overlay() + header = self._build_overlay_header( + 'ورود ادمین', + 'جزئیات فنی، لاگ\u200cها و تنظیمات امنیتی فقط بعد از ورود ادمین نمایش داده می\u200cشوند.', + ) + header.pack(fill="x", padx=16, pady=(16, 10)) + + body = ctk.CTkFrame(self.overlay_frame, fg_color=SURFACE, corner_radius=22) + body.pack(fill="both", expand=True, padx=16, pady=(0, 16)) + RTLLabel( + body, + text='رمز اصلی را وارد کن', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"), + ).pack(anchor="e", padx=18, pady=(22, 10)) + password = RTLEntry( + body, + placeholder_text='رمز اصلی', + show="*", + height=46, + font=ctk.CTkFont(family=FONT_BODY, size=15), + ) + password.pack(fill="x", padx=18, pady=6) + message = RTLLabel(body, text="", text_color=DANGER, font=ctk.CTkFont(family=FONT_BODY, size=14)) + message.pack(anchor="e", padx=18, pady=(4, 8)) + + def submit(): + if not self.controller.verify_password(password.get().strip()): + message.configure(text='رمز ادمین صحیح نیست.') + return + self._open_admin_panel() + + RTLButton( + body, + text='ورود به پنل ادمین', + fg_color=PRIMARY, + hover_color=PRIMARY_DARK, + font=ctk.CTkFont(family=FONT_BODY, size=16, weight="bold"), + height=42, + command=submit, + ).pack(fill="x", padx=18, pady=(8, 8)) + RTLButton( + body, + text='بازگشت به تنظیمات', + fg_color="#F1E7DB", + text_color=TEXT, + hover_color="#E8D8C7", + font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), + height=40, + command=self._open_settings_panel, + ).pack(fill="x", padx=18, pady=(0, 18)) + self._register_text_input(password, title="رمز اصلی ادمین", layout="en", submit=submit) + self.after(80, lambda: self._focus_registered_input(password)) + def _open_admin_panel(self): + self._show_overlay() + header = self._build_overlay_header( + 'پنل ادمین', + 'این بخش برای نمایشگر کوچک خلاصه شده تا اطلاعات اصلی، لاگ\u200cها و تنظیمات مودم/رمز داخل همان پنجره در دسترس باشند.', + ) + header.pack(fill="x", padx=16, pady=(16, 10)) + + snapshot = self.controller.get_admin_snapshot() + stats = snapshot["stats"] + system_info = snapshot["system_info"] + + body = RTLScrollableFrame( + self.overlay_frame, + fg_color=SURFACE, + corner_radius=22, + label_text="", + ) + body.pack(fill="both", expand=True, padx=16, pady=(0, 16)) + body.grid_columnconfigure((0, 1), weight=1) + + stat_labels = [ + ('کل مخاطب\u200cها', stats["contacts"]), + ('مخاطب امن', stats["secure_contacts"]), + ('در انتظار', stats["pending_contacts"]), + ('پیام امن', stats["secure_messages"]), + ] + for index, (title, value) in enumerate(stat_labels): + card = ctk.CTkFrame(body, fg_color=CARD, corner_radius=18) + card.grid(row=0, column=index % 2, padx=8, pady=8, sticky="ew") + RTLLabel( + card, + text=title, + text_color=MUTED, + font=ctk.CTkFont(family=FONT_BODY, size=13), + ).pack(anchor="e", padx=12, pady=(10, 2)) + RTLLabel( + card, + text=str(value), + text_color=TEXT, + font=ctk.CTkFont(family=FONT_BODY, size=18, weight="bold"), + ).pack(anchor="e", padx=12, pady=(0, 10)) + + system_card = ctk.CTkFrame(body, fg_color=CARD, corner_radius=18) + system_card.grid(row=1, column=0, columnspan=2, padx=8, pady=8, sticky="ew") + RTLLabel( + system_card, + text='اطلاعات سیستمی', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"), + ).pack(anchor="e", padx=12, pady=(12, 4)) + RTLLabel( + system_card, + text=f"پلتفرم: {system_info['platform']}\nپورت: {system_info['modem_port']} | Baudrate: {system_info['baudrate']}\nاثر انگشت کلید: {system_info['fingerprint']}", + text_color=MUTED, + justify="right", + wraplength=min(self.window_width - 96, 500), + font=ctk.CTkFont(family=FONT_BODY, size=13), + ).pack(anchor="e", padx=12, pady=(0, 12)) + + logs_card = ctk.CTkFrame(body, fg_color=CARD, corner_radius=18) + logs_card.grid(row=2, column=0, columnspan=2, padx=8, pady=8, sticky="ew") + RTLLabel( + logs_card, + text='آخرین لاگ\u200cها', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"), + ).pack(anchor="e", padx=12, pady=(12, 4)) + log_text = RTLTextbox(logs_card, height=140, fg_color=SURFACE, font=ctk.CTkFont(family=FONT_BODY, size=13), wrap="word") + log_text.pack(fill="x", padx=12, pady=(0, 12)) + if snapshot["events"]: + for event in snapshot["events"][:8]: + log_text.insert("end", f"{event.created_at} | {event.phone}\n{event.details}\n\n") + else: + log_text.insert("end", 'هنوز لاگی ثبت نشده است.') + log_text.configure(state="disabled") + + pending_card = ctk.CTkFrame(body, fg_color=CARD, corner_radius=18) + pending_card.grid(row=3, column=0, columnspan=2, padx=8, pady=8, sticky="ew") + RTLLabel( + pending_card, + text='بسته\u200cهای ناقص', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"), + ).pack(anchor="e", padx=12, pady=(12, 4)) + pending_text = RTLTextbox(pending_card, height=90, fg_color=SURFACE, font=ctk.CTkFont(family=FONT_BODY, size=13), wrap="word") + pending_text.pack(fill="x", padx=12, pady=(0, 12)) + if snapshot["pending_packets"]: + for packet in snapshot["pending_packets"]: + pending_text.insert("end", f"{packet.phone} | {packet.received_parts}/{packet.total_parts}\n") + else: + pending_text.insert("end", 'پیام ناقصی در صف وجود ندارد.') + pending_text.configure(state="disabled") + + settings_card = ctk.CTkFrame(body, fg_color=CARD, corner_radius=18) + settings_card.grid(row=4, column=0, columnspan=2, padx=8, pady=8, sticky="ew") + RTLLabel( + settings_card, + text='تنظیمات مودم و رمز', + text_color=TEXT, + font=ctk.CTkFont(family=FONT_TITLE, size=18, weight="bold"), + ).pack(anchor="e", padx=12, pady=(12, 4)) + port_entry = RTLEntry(settings_card, placeholder_text='پورت مودم مثل COM3', height=38, font=ctk.CTkFont(family=FONT_BODY, size=14)) + port_entry.pack(fill="x", padx=12, pady=6) + port_entry.insert(0, system_info["modem_port"]) + baud_entry = RTLEntry(settings_card, placeholder_text="Baudrate", height=38, font=ctk.CTkFont(family=FONT_BODY, size=14)) + baud_entry.pack(fill="x", padx=12, pady=6) + baud_entry.insert(0, str(system_info["baudrate"])) + current_password = RTLEntry(settings_card, placeholder_text='رمز فعلی', show="*", height=38, font=ctk.CTkFont(family=FONT_BODY, size=14)) + current_password.pack(fill="x", padx=12, pady=6) + new_password = RTLEntry(settings_card, placeholder_text='رمز جدید', show="*", height=38, font=ctk.CTkFont(family=FONT_BODY, size=14)) + new_password.pack(fill="x", padx=12, pady=6) + admin_message = RTLLabel(settings_card, text="", text_color=DANGER, font=ctk.CTkFont(family=FONT_BODY, size=13)) + admin_message.pack(anchor="e", padx=12, pady=(2, 6)) + self._register_text_input(port_entry, title="پورت مودم", layout="en") + self._register_text_input(baud_entry, title="Baudrate", layout="numeric") + self._register_text_input(current_password, title="رمز فعلی", layout="en") + self._register_text_input(new_password, title="رمز جدید", layout="en") + + def save_admin_changes(): + try: + connected = self.controller.reconnect_modem(port_entry.get().strip(), int(baud_entry.get().strip())) + if current_password.get().strip() and new_password.get().strip(): + self.controller.change_master_password(current_password.get().strip(), new_password.get().strip()) + if connected: + admin_message.configure(text='تغییرات ذخیره شد و مودم متصل است.', text_color=PRIMARY) + else: + admin_message.configure(text='تنظیمات ذخیره شد، اما مودم هنوز آفلاین است.', text_color="#9A6C3C") + except Exception as exc: + admin_message.configure(text=str(exc), text_color=DANGER) + + RTLButton( + settings_card, + text='ذخیره تغییرات', + fg_color=PRIMARY, + hover_color=PRIMARY_DARK, + font=ctk.CTkFont(family=FONT_BODY, size=15, weight="bold"), + height=40, + command=save_admin_changes, + ).pack(fill="x", padx=12, pady=(2, 12)) + + + + + diff --git a/secure_sms_v2.db b/secure_sms_v2.db new file mode 100644 index 0000000000000000000000000000000000000000..4b938135884b56ce424c709abc13543a1a358330 GIT binary patch literal 45056 zcmeI*PgCM%90%|qE~vW>&UAK|9&EFn+3L=2x+MHjc4ntV`6CJ_EK%{23CROyfg~nD zS8uvIZ7+Q(J@nMW9((NzXfJ&U+iQ<~07229cBbvl=re8aK~!#tq3`}EeId~{~Kzd-Lj=jKPvK48B3;jihyU(A$miV1uX z_+|Q!`#(?rIOU!E$^UZFq=lFu009U<;3Es1gr@w#xjE0f&mFniBsNjn7Lmw4(H(ne z;r?A)>PuiTGN(@u3XWkViDOPszi^6od+JY zd8va>-i0RoL5}l$|4En3l$8c?q?#qyTOGBF%acQJ7uWuWc7q|fvz+#XMrmlgS5|Il z`ah}OGSsWWXv&r&>Bb;St`4JPIC4|!Y-8VW3uU*hH*PYzQL%To5DVF&n40e<=`(h~ z*qQB)=gH-i`Fhe9e8qVf%17SVdrkV{OOo4;(Osrv8qOezzgRN0;r9ifKJ`?3Ptslk zHrZ>_oF=F3^%p0G^6kRPd0lRiXSC{*x4h3EoSpT28|}8)BDO8piQT{V54F-S{dn1` zY8Fu(&CqWW&#Ivw4xaAUJPigpdQCmYl6BiOXoPKt?kH~3jTYVUsInsuy5gQsqq6UE z8ojm0yD|GN-i?`J=#H#7cK`m~P|W(JBVu;8k3GM4cJE;)X$FJ1#_6&rn@pt>Vy?{2 z>7Dl*%{li6IX$Gf=$>ytCnGjD39%%zU2STL)F5Z!cYm;M?V6_7iDg=v-rMK9S8d8R zjd}05czSSfuQ)1>?zP({O^q8JZ=T=t1(#<>?L7KnmiYdh?vrpY_8npmUfP%y5^9hdN3v&F>_UQ>Nkh%P_X zjz9DGgO470PW)X%*)%2UThr=ib7Mn}XXPU#X;Suf8s}aNxy$X#&Gv;I%;gS-$9%!e zqfuFL)*FdGoA=Ten-iTEoYV9+9;h&Z-vY-0lg?m*00bZa0SG_<0uX=z1Rwwb2teTG z0uRT%k3X&3EvYKERg0ef;Q6Q!3I)90*@vCt&I!cj^*k?*jXyu0Hf7sBFf3KtCze*z zh!y7L8XvBPLuy1&_*i^F7NV*w5S0iaJ|;x?2nj{hYHWdzD~n-1M$04BT1_UAcwC7H zkuN>ox%i+}TW&g)@7`Wsi>#tm zcAM$dP<`V~(nuyN8(bl8W#ifUD&aUOs&A#7wwYcl>-orbrL>WN9J>JnL6j+JVa?qk%-3(A&MU1`)qi{k)DzlP`=BT~IlX0PHfax9OZ(ztE0L}r9ck$@=Y(5Hq4sK}v9g!u zct>nk5=yRBT!_ZQ5k8$-Oe~~h=}=k-2{A4d<^;YR3cV2c7hz$i^ZuV+^p6Pw5P$## zAOHafKmY;|fB*y_0D)U9(ES2Fy#K$&5ysFU009U<00Izz00bZa0SG_<0=@75!{`6V z1PDL?0uX=z1Rwwb2tWV=5P-n#7U*67|A7g7xZUH#=pXFE-=)|GN`?j1&S8fB*y_009U<00Izz00i!?0IvVPyRpZ}ApijgKmY;| zfB*y_009U<;O+?Y{{MfQ32fh;F=M0&FmosV zsBD^2)i4~}v1GIRvw!*5S}Rhwgltw@TNIRJA!FsU5$>?2n?yX^B}GNpk_xM_qv-Zl z!pLkI+;UbF)kJ&INUAaMXs^`>ZA7xo+)kuik)r!c341-V@LJb02i!(scT-X}^UdAF XVbUy+e7v@uT`dcZotEJC`+t7}0h4P9 literal 0 HcmV?d00001