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>
This commit is contained in:
Filip
2026-06-11 18:22:39 -04:00
commit 2e7b72307d
24 changed files with 21821 additions and 0 deletions

491
SECURITY_AUDIT.md Normal file
View File

@@ -0,0 +1,491 @@
# 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).