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

492 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).