avalin proge
This commit is contained in:
commit
2f128f9a1a
249
App_GUI.py
Normal file
249
App_GUI.py
Normal file
|
|
@ -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("<Return>", 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()
|
||||
162
Crypto_Engine.py
Normal file
162
Crypto_Engine.py
Normal file
|
|
@ -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.")
|
||||
134
DB_Handler.py
Normal file
134
DB_Handler.py
Normal file
|
|
@ -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")
|
||||
218
GSM_Manager.py
Normal file
218
GSM_Manager.py
Normal file
|
|
@ -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()
|
||||
BIN
__pycache__/App_GUI.cpython-313.pyc
Normal file
BIN
__pycache__/App_GUI.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/Crypto_Engine.cpython-313.pyc
Normal file
BIN
__pycache__/Crypto_Engine.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/DB_Handler.cpython-313.pyc
Normal file
BIN
__pycache__/DB_Handler.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/GSM_Manager.cpython-313.pyc
Normal file
BIN
__pycache__/GSM_Manager.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/main.cpython-313.pyc
Normal file
BIN
__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
7
_bad_strings.txt
Normal file
7
_bad_strings.txt
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
57: ?
|
||||
276: ?????
|
||||
278: ?????
|
||||
280: ????
|
||||
281: ???
|
||||
283: ???
|
||||
284: ?????
|
||||
6
_question_strings.txt
Normal file
6
_question_strings.txt
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
276|?????
|
||||
278|?????
|
||||
280|????
|
||||
281|???
|
||||
283|???
|
||||
284|?????
|
||||
643
_string_tokens.txt
Normal file
643
_string_tokens.txt
Normal file
|
|
@ -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|<Button-1>
|
||||
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|<FocusIn>
|
||||
221|+
|
||||
222|<Button-1>
|
||||
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|<Return>
|
||||
579|\u0631\u0645\u0632 \u0627\u0635\u0644\u06cc
|
||||
579|en
|
||||
581|<Return>
|
||||
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|<Return>
|
||||
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|<Control-Return>
|
||||
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
|
||||
44
a.ps1
Normal file
44
a.ps1
Normal file
|
|
@ -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
|
||||
276
main.py
Normal file
276
main.py
Normal file
|
|
@ -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())
|
||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
|
|
@ -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
|
||||
1
secure_sms/__init__.py
Normal file
1
secure_sms/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
"""Secure SMS application package."""
|
||||
BIN
secure_sms/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/controller.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/controller.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/database.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/database.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/gsm.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/gsm.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/models.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/models.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/protocol.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/protocol.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/security.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/security.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/services.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/services.cpython-313.pyc
Normal file
Binary file not shown.
BIN
secure_sms/__pycache__/ui.cpython-313.pyc
Normal file
BIN
secure_sms/__pycache__/ui.cpython-313.pyc
Normal file
Binary file not shown.
137
secure_sms/controller.py
Normal file
137
secure_sms/controller.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from secure_sms.database import Database
|
||||
from secure_sms.gsm import GSMGateway
|
||||
from secure_sms.services import SecureMessagingService
|
||||
|
||||
|
||||
class AppController:
|
||||
def __init__(self):
|
||||
self.db = Database()
|
||||
self.service = SecureMessagingService(self.db)
|
||||
self.gsm: Optional[GSMGateway] = None
|
||||
self.ui = None
|
||||
|
||||
def bind_ui(self, ui):
|
||||
self.ui = ui
|
||||
|
||||
def is_bootstrapped(self) -> 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()
|
||||
410
secure_sms/database.py
Normal file
410
secure_sms/database.py
Normal file
|
|
@ -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()
|
||||
154
secure_sms/gsm.py
Normal file
154
secure_sms/gsm.py
Normal file
|
|
@ -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}")
|
||||
53
secure_sms/models.py
Normal file
53
secure_sms/models.py
Normal file
|
|
@ -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
|
||||
94
secure_sms/protocol.py
Normal file
94
secure_sms/protocol.py
Normal file
|
|
@ -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
|
||||
133
secure_sms/security.py
Normal file
133
secure_sms/security.py
Normal file
|
|
@ -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")
|
||||
407
secure_sms/services.py
Normal file
407
secure_sms/services.py
Normal file
|
|
@ -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} تغییر کرد."))
|
||||
1509
secure_sms/ui.py
Normal file
1509
secure_sms/ui.py
Normal file
File diff suppressed because it is too large
Load Diff
BIN
secure_sms_v2.db
Normal file
BIN
secure_sms_v2.db
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user