# Security Audit (Encrypted Chat) Aktualizace: 2026-03-27 Scope: `server.py`, `db.py`, `chat_core.py`, `gui_client.py`, `client.py`, `protocol.py`, `schema.sql`, `.env.example`, markdown dokumentace. Metodika: statický audit kódu + konfigurace. Nebyl proveden aktivní penetrační test ani fuzzing. ## Refresh 2026-03-27 Při re-review aktuálního stavu kódu byly uzavřeny tyto nálezy (KEC-26): - MySQL TLS konfigurace je podporovaná přes `MYSQL_SSL_CA`, `MYSQL_SSL_CERT`, `MYSQL_SSL_KEY` (`db.py`, `.env.example`). - SMTP `STARTTLS` používá explicitní `ssl.create_default_context()` a `EHLO` před/po TLS upgrade (`server.py`). - Avatar upload flow nastavuje explicitně `chmod(0o600)` i pro user/group avatary (`server.py`). Scope limitation aktuálního workspace: - V repozitáři chybí `ios_client/` i jakýkoli Android klient, přestože jsou zmiňované v zadání i README. Tento refresh proto pokrývá pouze server a Python klienty. - Historický nález o plaintext secrets v `.env` nelze v tomto snapshotu reprodukovat; workspace obsahuje pouze `.env.example`. Reziduální architektonické riziko: - Self-encryption klíč je z definice statický a deterministický; kompromitace identity private key proto zpřístupní všechny self-copies napříč historií (`crypto_utils.py:329-341`). To je tradeoff současného cross-device designu, ne implementační bug. ## Executive Summary Nejzávažnější aktuálně otevřené nálezy: - Reziduální architektonický tradeoff: statický/deterministický self-encryption klíč pro self-copies. - Mobilní klienti deklarovaní v dokumentaci nejsou součástí tohoto workspace, takže jejich security stav zůstává neověřený. ## CRITICAL ### ~~C1. TOFU / verifikace identity klíče se obchází při běžném X3DH flow~~ ✅ OPRAVENO (2026-03-08) **Evidence** - TOFU kontrola existuje jen v `_get_user_info()` (`chat_core.py:799-803`). - Při navazování session (`_get_or_create_session`) se `identity_key` z bundle bere přímo bez TOFU kontroly (`chat_core.py:1497-1501`, `chat_core.py:1534-1538`). - U příchozího X3DH (`_process_x3dh_header`) se remote IK také uloží bez TOFU kontroly (`chat_core.py:1551-1553`, `chat_core.py:1580-1584`). **Dopad** - Pokud server nebo MITM podstrčí jiný identity key, klient může navázat session bez varování. - Prakticky to obchází uživatelskou verifikaci kontaktu ve výchozím messaging flow. **Oprava** - Nová výjimka `IdentityKeyChanged(user_id, new_key_bytes, status)` v `chat_core.py` — hard-fail při změně identity klíče. - `_get_or_create_session()`: TOFU check přes `check_identity_key()` před X3DH initiate. Při `changed`/`changed_verified` vyhodí `IdentityKeyChanged` — session se nenaváže. - `_process_x3dh_header()`: TOFU check před X3DH respond. Stejný hard-fail — příchozí zpráva s podvrženým klíčem je odmítnuta. - GUI: `IdentityKeyChanged` zachycena v notification loopu (emituje `key_change_warning` signál místo pádu loopu) a v `_do_send_message` (zobrazí error + warning dialog). - Session je blokována dokud uživatel explicitně neakceptuje key change přes `accept_key_change()`. - `decrypt_notification()`: explicitní `except IdentityKeyChanged: raise` před generickým `except Exception` — výjimka se propaguje do notification loopu místo tichého spolknutí. - `key_change_warning` signál rozšířen o 5. parametr `new_key_bytes: bytes` — "Accept New Key" dialog předává nový klíč přímo z výjimky, ne z cache (která mohla obsahovat starý klíč). - `IdentityKeyChanged` ošetřena ve všech GUI send cestách: `_do_send_image`, `_do_send_file`, `_do_forward_message`, `_do_find_or_create_and_send` — zobrazí warning dialog + error message. - CLI (`client.py`): `IdentityKeyChanged` ošetřena ve všech 6 send cestách (send_message ×3, send_image, send_file, forward_message). --- ### ~~C2. Perzistentní DoS konverzace přes nevalidní message headers~~ ✅ OPRAVENO (2026-03-08) **Evidence** - Server přijímá `ratchet_header` / `x3dh_header` i jako raw `str/bytes` bez JSON schema validace (`server.py:1105-1112`, `server.py:1146-1151`). - Při `get_messages` se hodnoty bez ochrany parsují `json.loads(...)` (`server.py:1266`, `server.py:1274`). **Dopad** - Útočník v konverzaci může uložit “poisoned” hlavičku a rozbít načtení historie ostatním členům (`Internal server error`). - Chyba je perzistentní, dokud je vadná zpráva v historii. **Oprava** - Nový helper `_validate_header(raw, name)` v `server.py` — přijímá pouze `dict`, odmítá `str`/`bytes`, limit 4096 bajtů. - `handle_send_message`: message-level i per-recipient headers procházejí `_validate_header()`. Nevalidní hlavička → error response, zpráva se neuloží. - `handle_get_messages`: `json.loads()` obaleno `try/except` (JSONDecodeError, TypeError, UnicodeDecodeError). Corrupted header → prázdný dict `{}` + warning log, ostatní zprávy se načtou normálně. - `_validate_header()` rozšířena o validaci očekávaných klíčů a typů pro ratchet headers (`dh_pub`: str, `n`: int, `pn`: int) a používá striktní kontrolu typu pro `n/pn` (`type(...) is int`) — `bool` je explicitně odmítnut. - Realtime push notifikace nyní čtou data z validovaných `db_recipients` (ne z `recipients_raw`). Per-recipient hlavičky se dekódují z validovaných bytes zpět do `dict` pro JSON notifikaci. - `encrypted_content` a `nonce` v push notifikacích se skládají z validovaných raw bytes a serializují se přes `encode_binary()` — untrusted hodnoty z raw requestu se do push větve nepropíší. --- ### C3. Historický nález: plaintext tajemství v `.env` a `zaloha/.env` Status 2026-03-27: v aktuálním workspace nereprodukovatelné. **Evidence** - Tento snapshot obsahuje `.env.example`, ale neobsahuje `.env` ani `zaloha/`. - Původní nález tedy nelze znovu ověřit bez jiného artefaktu nebo deploy prostředí. **Dopad** - Pokud jsou runtime secrets stále ukládány v reálném `.env` mimo tento snapshot, únik takového souboru by stále znamenal okamžitý přístup do DB. **Doporučení** 1. Okamžitě rotovat DB heslo. 2. Nahradit repozitářové `.env` šablonou (`.env.example`) bez tajemství. 3. Použít secrets manager / deployment-level secret injection. ## HIGH ### ~~H1. Chybí TLS mezi aplikací a MySQL~~ ✅ OPRAVENO (2026-03-27) **Evidence** - DB pool načítá volitelné TLS parametry `MYSQL_SSL_CA`, `MYSQL_SSL_CERT`, `MYSQL_SSL_KEY` a předává je do `MySQLConnectionPool` (`db.py`). - Konfigurační šablona je doplněná o stejné proměnné (`.env.example`). **Dopad** - Odposlech nebo MITM na trase app<->DB může odhalit credentials i data. **Oprava** 1. Přidána podpora TLS parametrů v DB vrstvě. 2. Přidány dokumentované env proměnné pro CA/client cert/client key. --- ### ~~H2. Upload/avatary na disku mají slabá oprávnění~~ ✅ OPRAVENO (2026-03-27) **Evidence** - Avatar upload flow nyní po zápisu explicitně nastavuje `chmod(0o600)` pro user i group avatary (`server.py`). **Dopad** - Lokální uživatelé na stejném hostu mohou číst citlivá data (včetně avatarů v plaintextu). **Oprava** 1. Explicitní `chmod(0o600)` po zápisu avatar souborů. 2. Adresář `uploads/avatars` zůstává s `0700`. --- ### ~~H3. `session_reset` nemá autorizační vazbu na vztah mezi uživateli~~ ✅ OPRAVENO (2026-03-08) **Evidence** - Handler přijme libovolné validní `peer_user_id` a pošle notifikaci (`server.py:2040-2052`). - Neověřuje, že uživatelé sdílí konverzaci nebo existuje session. **Dopad** - Možnost spam/DoS reset notifikací na cílové uživatele. **Oprava** - Nová DB funkce `db.shares_conversation(user_id_a, user_id_b)` — `SELECT 1 ... LIMIT 1` přes `conversation_members` JOIN. - `handle_session_reset`: před push notifikací ověřuje `shares_conversation()`. Pokud uživatelé nesdílí žádnou konverzaci → error response. - Rate limit 5 požadavků/min na `session_reset` per user (`session_reset|{user_id}`) — IP adresa není součást klíče, takže změnou IP nejde limit obejít. - Pokud je předán `peer_device_id`, reset notifikace se doručí pouze cílovému zařízení (filtr přes `writer_device_map`). Bez `peer_device_id` zůstává broadcast na všechna zařízení peera. --- ### ~~H4. User enumeration přes pairing a user-info endpointy~~ ✅ OPRAVENO (2026-03-08) **Evidence** - `pairing_start` vrací explicitně `User not found` (`server.py:763-766`). - `get_user_info` vrací metadata uživatele při lookupu přes email/user_id (`server.py:551-564`). **Dopad** - Snadné mapování existence účtů. **Oprava** - `handle_pairing_start`: vždy vrací `ok` s platně vypadajícím kódem a session se vytvoří vždy (i pro neexistující email), takže `pairing_poll` vrací nerozlišitelné `ready: false`. - Přidán globální cap `PAIRING_MAX_SESSIONS = 100` pro omezení počtu současných pairing sessions (DoS hardening). - `pairing_start` rate limit je per-IP (bez email komponenty), aby nešel obcházet rotací emailů. - `pairing_claim` i `pairing_send`: sjednocená chyba `Invalid or expired code` (žádné rozlišení "neexistuje" vs "patří jinému účtu"). - V pairing flow se síťové I/O (`send_resp`) volá až po uvolnění `_pairing_lock`. - `handle_get_user_info`: přidán parametr `session` (vyžaduje login). Lookupovat lze jen sebe nebo kontakty (ověřeno přes `shares_conversation()`). Pro neexistující i nepovolené cíle vrací neutrální "User not found". - Doplňuje dřívější anti-enumeration opravy: `register_start` (generická odpověď), `login_start` (fake challenge), `login_finish` (generická chyba). --- ### ~~H5. Phantom user inflation přes `create_conversation` / `find_conversation` / `add_member` (DoS)~~ ✅ OPRAVENO (2026-03-08) **Evidence** - `create_conversation` vytváří phantom účty pro neznámé emaily bez dedikovaného rate limitu (`server.py:906-920`). - `find_conversation` a `add_member` rate limitují přes `_rate_limit_key(..., addr, email)`, takže rotace emailů obchází limit (`server.py:972`, `server.py:1001`, `server.py:209-212`). - `create_phantom_user()` pro každý nový email generuje IK+SPK+OTP a zapisuje více řádků do DB (`db.py:1470-1507`). **Dopad** - Útočník může nafukovat DB a CPU náklady (kryptografická generace + zápisy), případně degradovat výkon serveru. **Oprava** 1. `_can_create_phantom(addr, user_id)` helper kontroluje 3 limity před každým `create_phantom_user()`: - Globální cap: `MAX_PHANTOM_USERS = 500` (počet v `phantom_user_ids` setu) - Per-user rate: `phantom_create|{user_id}` — 10/min (email-nezávislé, neobejitelné rotací) - Per-IP rate: `phantom_create_ip|{addr}` — 10/min (email-nezávislé) 2. `create_conversation` nově má per-user rate limit 10/min + phantom check před každým členem. 3. `find_conversation` a `add_member` — existující per-addr+email limit zůstává (brání hammering jednoho emailu), přidán `_can_create_phantom` check před vytvořením phantomu. 4. Stávající `cleanup_stale_phantoms(30)` v periodic cleanup (10 min) zajišťuje garbage collection. --- ### ~~H6. `pending_registrations` nemá hard cap (memory/SMTP abuse)~~ ✅ OPRAVENO (2026-03-16) **Evidence** - `pending_registrations` byl původně globální in-memory dict bez horního limitu. - Cleanup expirovaných registrací se dříve spouštěl jen v `register_*` flow. - Rate limit `register_start` byl vázaný i na email, takže šel obcházet rotací emailových adres. **Dopad** - Riziko růstu paměti, zaplnění slotů a SMTP abuse při masivním `register_start`. **Oprava** 1. Přidán globální cap `MAX_PENDING_REGISTRATIONS = 1000` (`server.py:180`). 2. Přidány slot limity `MAX_PENDING_PER_IP = 5` a `MAX_PENDING_PER_SUBNET = 20` (`server.py:181-182`). 3. `_cleanup_registrations()` běží i v periodickém cleanup tasku (`server.py:327`, `server.py:2920`). 4. Přidán per-IP rate limit `register_start_ip|{addr}` (`server.py:476`) a pressure mode s PoW při vysokém zaplnění (`server.py:183`, `server.py:521`). 5. SMTP throttling je vícevrstvý: global, per-IP a per-target (`server.py:185-187`, `server.py:580`). **Poznámka** - Residual risk zůstává při multi-process nasazení nad stejnou DB: caps a rate-limity jsou in-memory per process, ne globálně distribuované. --- ### ~~H7. Pairing flow důvěřuje serverem vrácenému `temp_public_key` (exfiltrace account private keys)~~ ✅ OPRAVENO (2026-03-16) **Evidence** - Nové zařízení pošle do `pairing_start` svůj `temp_public_key`; server si ho uloží do `pairing_sessions` (`server.py:1067`, `server.py:1089-1095`). - Staré zařízení v `pairing_claim` získá `temp_public_key` zpět ze serveru a bez další autentizace ho načte (`server.py:1116-1140`, `chat_core.py:1441-1446`). - Staré zařízení následně do payloadu zabalí `rsa_private` a `identity_private` a zašifruje je právě pod tento `temp_public_key` (`chat_core.py:1451-1477`). **Dopad** - Kompromitovaný nebo aktivně škodlivý server může v odpovědi na `pairing_claim` podvrhnout vlastní `temp_public_key`. - Staré zařízení pak zašifruje exportovaný payload pod klíč útočníka/serveru, který tím získá `rsa_private` i `identity_private` oběti. - To znamená plné převzetí účtu, možnost přihlášení jako oběť a přístup k self-encrypted historii (derivace `self/local` klíčů z identity private key). **Oprava** 1. Přidán `compute_pairing_fingerprint()` helper nad raw dočasným veřejným klíčem (30 číslic pro ruční porovnání). 2. Pairing bootstrap už nepoužívá dočasný RSA transport, ale `X25519 + HKDF + AES-GCM`: nové zařízení pošle dočasný X25519 public key, staré zařízení vygeneruje jednorázový X25519 sender key a obě strany odvodí stejný symmetric bootstrap key z DH shared secret. 3. Nové zařízení po `pairing_start` zobrazuje 8místný kód i fingerprint dočasného pairing klíče. 4. Staré zařízení při `authorize_device` vyžaduje fingerprint opsaný z nového zařízení; před `pairing_send` vypočítá fingerprint klíče vráceného serverem a při neshodě celý pairing odmítne. 5. Tím se zavádí povinná out-of-band vazba mezi oběma zařízeními a server už nemůže nepozorovaně podvrhnout vlastní pairing key ani získat bootstrap secret. --- ## MEDIUM ### ~~M6. Chybí auditní notifikace po přidání nového zařízení~~ ✅ OPRAVENO (2026-03-16) **Evidence** - Po úspěšném `pairing_send` server pouze uloží payload a vrátí `OK` (`server.py:1143-1175`). - V serveru ani klientech není samostatný notif type typu `device_added` / `device_linked`; zařízení lze zjistit až dodatečně přes `list_devices`. **Dopad** - Pokud uživatel omylem nebo po sociálním inženýrství schválí cizí pairing kód, ostatní aktivní zařízení nedostanou okamžitý auditní signál. - Zhoršuje to detekci zneužití a forenzní dohledatelnost. **Oprava** 1. Server po prvním loginu nově vytvořeného zařízení posílá na ostatní zařízení účtu push notifikaci `device_added`. 2. Payload notifikace obsahuje `device_id`, `device_name`, zdrojovou IP a čas přidání. 3. GUI zobrazuje bezpečnostní alert a zvýrazněný status bar. 4. CLI vypisuje explicitní auditní hlášku s doporučením okamžité rotace klíčů, pokud zařízení uživatel nepoznává. --- ### ~~M1. `mark_read` a `confirm_delivery` neověřují, že `message_ids` patří do dané konverzace~~ ✅ OPRAVENO (2026-03-08) **Evidence** - Handler validuje členství jen v `conversation_id` (`server.py:1464-1479`, `server.py:1516-1531`). - DB insert metody pro receipts neváží `message_id` na konverzaci (`db.py:1102-1113`, `db.py:1188-1200`). **Dopad** - Možná manipulace read/delivery stavu cizích zpráv (integrita metadat). **Oprava** - `db.mark_messages_read()` a `db.mark_messages_delivered()` nahrazeny z per-row `INSERT IGNORE` na batch `INSERT IGNORE ... SELECT m.id, %s FROM messages m WHERE m.id IN (...) AND m.conversation_id = %s`. - Message IDs, které nepatří do dané konverzace, jsou tiše přeskočeny (SELECT je nevrátí). --- ### ~~M2. SMTP STARTTLS bez explicitního TLS contextu~~ ✅ OPRAVENO (2026-03-27) **Evidence** - SMTP flow používá `server.starttls(context=ssl.create_default_context())` a `EHLO` před/po TLS upgrade (`server.py`). **Dopad** - Slabší kontrola TLS parametrů/verifikace dle runtime prostředí. **Oprava** 1. Přidán explicitní TLS context pro STARTTLS. 2. Přidán `EHLO` před i po TLS upgrade. --- ### ~~M3. CLI klient: několik lokálních hardening mezer~~ ✅ OPRAVENO (2026-03-08) **Evidence** - Heslo se zadává přes `input()` (echo on) (`client.py:730`, `client.py:749`, `client.py:754`). - Zprávy se tisknou bez sanitace escape sekvencí (`client.py:491`). - Default save path při downloadu je převzat z remote `filename` (`client.py:523-530`). **Dopad** - Shoulder-surfing hesla, terminal escape spoofing, riskantní defaultní save path. **Oprava** - Všechny password prompty (register, login, pairing, authorize device, rotate keys) nahrazeny `prompt_password()` wrapping `getpass.getpass()` — heslo se nezobrazuje na terminálu. - `_sanitize_text()` helper stripuje control znaky (`\x00-\x1f` kromě `\t`/`\n`/`\r`) a ANSI escape sekvence. Aplikováno na `sender`, `text`, `filename` při výpisu zpráv v `_print_messages()`. - Follow-up: `_sanitize_text()` nyní bezpečně přijímá i non-string vstupy (`None -> ""`, jinak `str(...)`), čímž se eliminuje `TypeError` při neočekávaném typu z payloadu (`client.py:32-36`). - Follow-up: sanitace rozšířena na zbývající user-controlled CLI výpisy — seznam konverzací (`client.py:63-67`), search výsledky (`client.py:293-300`), seznam pozvánek (`client.py:612-614`), profil (`client.py:637-644`), seznam zařízení (`client.py:709-713`), verify view (`client.py:435`, `client.py:446`) a notifikace včetně reaction hodnoty (`client.py:752-774`). - `_safe_filename()` helper: `os.path.basename()` + odstranění NUL + fallback na `"download"` pro prázdné/tečkové názvy. Aplikováno na default save path při downloadu. --- ### ~~M4. `get_key_bundle` umožňuje OPK depletion (availability)~~ ✅ OPRAVENO (2026-03-08) **Evidence** - `handle_get_key_bundle` nemá rate limit ani authorizační vazbu na vztah mezi uživateli (`server.py:648-660`). - DB vrstva při každém volání spotřebovává one-time prekeys (`get_key_bundles_for_user` — „Consumes one OPK per device atomically”, `db.py:394-450`). - `target_user_id` lze získat přes `find_conversation` lookup (`server.py:966-987`). **Dopad** - Útočník může opakovanými dotazy vyčerpat OPK oběti, zhoršit doručitelnost a vynutit časté doplňování prekeys. **Oprava** 1. Per-caller rate limit: `get_key_bundle|{user_id}` — 10/min (omezuje celkový počet fetchů jednoho uživatele). 2. Per-target rate limit: `get_key_bundle_target|{target_user_id}` — 20/min (omezuje rychlost vyčerpávání OPK konkrétní oběti). Autorizace probíhá před per-target RL (neautorizovaný request nespálí bucket cíle). 3. Autorizace: `shares_conversation()` — caller musí sdílet konverzaci s cílem (self-fetch povolen vždy). 4. Chybová zpráva pro neautorizovaný přístup je neutrální (`”Key bundle not available”`) — shodná s neexistujícím uživatelem. 5. **Doplňující per-user rate limity** na všechny zbývající výpočetně/DB náročné handlery (celkem 29 RL checks): - Crypto+DB: `upload_prekeys` 5/min, `ensure_prekeys` 5/min, `rotate_keys` 3/min, `reencrypt` 10/min - DB-heavy: `get_messages` 30/min, `delete_conv` 5/min, `delete_msg` 20/min, `react` 20/min, `remove_member` 10/min, `rename_conv` 5/min - File I/O: `update_avatar` 5/min (sdílený bucket pro user i group avatar) --- ### ~~M5. `upload_image_start` nemá anti-DoS cap na in-flight uploady~~ ✅ OPRAVENO (2026-03-08) **Evidence** - `upload_image_start` nevynucuje request rate limit ani limit počtu aktivních uploadů na user/IP (`server.py:1786-1823`). - In-memory `pending_uploads` je bez explicitního capu (`server.py:58`, `server.py:1812-1819`). - Cleanup stale uploadů běží periodicky (600s) a DB stale threshold je 3600s (`server.py:2488-2490`, `db.py:1626-1633`). **Dopad** - Útočník může zahájit mnoho uploadů a vytvářet dočasné soubory/záznamy, což zvyšuje memory/disk tlak. **Oprava** 1. Per-user rate limit: `upload_start|{user_id}` — 10/min. 2. Globální cap: `MAX_UPLOADS_GLOBAL = 200` (kontrola `len(pending_uploads)` pod `_uploads_lock`). 3. Per-user cap: `MAX_UPLOADS_PER_USER = 5` (počet záznamů s `uploader_id == user_id`). 4. Stale threshold snížen z 3600s na `UPLOAD_STALE_SECONDS = 600` (10 min). 5. Periodic cleanup interval snížen z 600s na 120s (2 min). ## LOW ### ~~L1. `decode_binary` není strict base64~~ ✅ OPRAVENO (2026-03-08) **Evidence** - `base64.b64decode(data)` bez `validate=True` (`protocol.py:18`). **Dopad** - Méně striktní input parsing (robustnost), ne přímý průnik. **Oprava** - `decode_binary()` nyní volá `base64.b64decode(data, validate=True)` — odmítá neplatné base64 znaky (whitespace, non-alphabet). --- ### ~~L2. Pairing používá dočasný RSA-2048 klíč, zatímco zbytek aplikace standardně generuje RSA-4096~~ ✅ OPRAVENO (2026-03-16) **Evidence** - `pairing_start()` generuje dočasný RSA klíč přes `generate_rsa_keypair(2048)` (`chat_core.py:1359`). - Default helper `generate_rsa_keypair()` používá 4096 bitů (`crypto_utils.py:64`). **Dopad** - Není to primární slabina pairing flow; hlavní problém je autenticita `temp_public_key` (H7). - Přesto jde o zbytečně slabší parametr pro přenos payloadu obsahujícího account private keys. **Oprava** 1. Pairing bootstrap už dočasné RSA vůbec nepoužívá; byl nahrazen `X25519 + HKDF + AES-GCM`. 2. Tím odpadá původní důvod pro sjednocení na RSA-4096. --- ### ~~L3. Pairing UX explicitně nevaruje, že párovací kód se nesmí sdílet~~ ✅ OPRAVENO (2026-03-16) **Evidence** - GUI po vygenerování kódu zobrazuje instrukci k jeho schválení, ale bez výrazného warningu proti sdílení kódu. - Aktuální pairing model spoléhá na to, že uživatel 8místný kód neprozradí třetí straně. **Dopad** - Zvyšuje se riziko sociálního inženýrství ("nadiktujte mi kód z nového zařízení"), i když brute-force samotného kódu je při současných limitech nepraktický. **Oprava** 1. GUI i CLI nyní při párování zobrazují explicitní warning `Never share this pairing code.` 2. Fingerprint nového zařízení se zobrazuje spolu s kódem, takže uživatel dostává zároveň instrukci k bezpečnému ručnímu ověření. 3. GUI nově zobrazuje pairing QR a staré zařízení ho může načíst ze souboru místo ručního opisování kódu a fingerprintu. --- ### ~~L4. `reencrypt_history()` po pairingu prozrazuje serveru timing a rozsah self-history~~ ✅ MITIGOVÁNO (2026-03-17) **Evidence** - Po úspěšném `pairing_send` staré zařízení asynchronně spouští `reencrypt_history()` (`chat_core.py:1477-1485`). - Server z batch operací vidí, že právě proběhlo párování, a přibližně kolik self-encrypted zpráv bylo potřeba přegenerovat. **Dopad** - Jde o metadata leak, nikoli o únik obsahu zpráv. - Server může odhadnout velikost historie a intenzitu používání účtu. **Oprava** 1. Post-pairing history resync už nezačíná okamžitě; běží po náhodném odkladu. 2. Pořadí konverzací i pořadí zpráv se před fetch/upload fází míchá. 3. Mezi fetch cykly i mezi upload batchi je náhodný jitter, takže pairing už negeneruje tak snadno korelovatelný burst. 4. Residual leak zůstává nízký: server stále ví, že nějaký history resync proběhl, ale výrazně hůř z něj odvodí přesný okamžik pairingu a strukturu resyncu po konverzacích. ## Positive Findings - Dev-only guardy: `TLS_INSECURE` a `TLS_AUTOGEN` jsou blokovány mimo `ENVIRONMENT=dev`. - Server používá UUID validace v řadě handlerů. - Upload/download ověřuje členství v konverzaci. - Klientské private keys/storage používají PBKDF2 + AES-GCM a restriktivní perms (`0700`/`0600`) v key storage. - Přítomný client-side lockout na opakované chybné login pokusy. - Pairing má anti-enumeration ochrany: generická odpověď, per-IP rate limit, `poll_token`, sjednocené chyby `Invalid or expired code` a krátkodobé pairing sessions. ## Prioritní plán oprav ### 0-48 hodin 1. Rotace DB hesla + odstranění tajemství z `.env`. 2. ~~Zavést OOB autentizaci pairingu (fingerprint / QR / SAS) a nepovažovat serverem vrácený `temp_public_key` za důvěryhodný.~~ ✅ DONE 3. ~~Oprava TOFU bypassu v obou X3DH cestách.~~ ✅ DONE 4. ~~Zablokování nevalidních message headers na vstupu.~~ ✅ DONE 5. Přepnutí upload storage perms na `0700/0600`. 6. ~~Omezit phantom creation (rate limit bez emailu + cap).~~ ✅ DONE 7. ~~Zavést cap pro `pending_registrations` a čistit je i v periodickém cleanupu.~~ ✅ DONE 8. ~~Přidat cap/rate limit na in-flight uploady.~~ ✅ DONE ### 7 dní 1. Zapnout TLS mezi app a MySQL (mTLS nebo minimálně server cert verify). 2. ~~Opravit autorizaci `session_reset`.~~ ✅ DONE 3. ~~Opravit vazbu `message_ids` na `conversation_id` pro receipts.~~ ✅ DONE 4. ~~Omezit `get_key_bundle` (rate limit + policy sdílené konverzace).~~ ✅ DONE 5. ~~Přidat `device_added` audit notifikaci a zobrazit ji v GUI/CLI.~~ ✅ DONE ### 30 dní 1. ~~Anti-enumeration sjednotit napříč endpointy.~~ ✅ DONE 2. ~~CLI hardening (`getpass`, output sanitace, filename sanitace).~~ ✅ DONE 3. Doplnit integrační testy pro bezpečnostní regresi (TOFU, poisoned headers, receipt authz, session_reset device targeting, anti-enumeration, DoS caps, pairing MITM / device-added audit).