Files
Kecalek_python/SECURITY_AUDIT.md
Filip 2e7b72307d Initial commit — encrypted chat server + Python clients (v0.8.5)
E2E encrypted chat (X3DH + Double Ratchet, Signal Protocol).
Server: asyncio TCP + TLS, MySQL. Clients: PyQt6 GUI + CLI.
Secrets (.env, TLS keys, Cloudflare token), runtime data and
mobile clients (separate repos) are gitignored.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 18:22:39 -04:00

26 KiB
Raw Permalink Blame History

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).