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>
26 KiB
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
STARTTLSpoužívá explicitníssl.create_default_context()aEHLOpř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
.envnelze 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) seidentity_keyz 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)vchat_core.py— hard-fail při změně identity klíče. _get_or_create_session(): TOFU check přescheck_identity_key()před X3DH initiate. Přichanged/changed_verifiedvyhodí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:
IdentityKeyChangedzachycena v notification loopu (emitujekey_change_warningsigná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: raisepřed generickýmexcept Exception— výjimka se propaguje do notification loopu místo tichého spolknutí.key_change_warningsignál rozšířen o 5. parametrnew_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íč).IdentityKeyChangedoš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):IdentityKeyChangedoš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_headeri jako rawstr/bytesbez JSON schema validace (server.py:1105-1112,server.py:1146-1151). - Při
get_messagesse 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)vserver.py— přijímá pouzedict, 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()obalenotry/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 pron/pn(type(...) is int) —boolje explicitně odmítnut.- Realtime push notifikace nyní čtou data z validovaných
db_recipients(ne zrecipients_raw). Per-recipient hlavičky se dekódují z validovaných bytes zpět dodictpro JSON notifikaci. encrypted_contentanoncev push notifikacích se skládají z validovaných raw bytes a serializují se přesencode_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.envanizaloha/. - 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
.envmimo tento snapshot, únik takového souboru by stále znamenal okamžitý přístup do DB.
Doporučení
- Okamžitě rotovat DB heslo.
- Nahradit repozitářové
.envšablonou (.env.example) bez tajemství. - 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_KEYa předává je doMySQLConnectionPool(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
- Přidána podpora TLS parametrů v DB vrstvě.
- 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
- Explicitní
chmod(0o600)po zápisu avatar souborů. - Adresář
uploads/avatarszůstává s0700.
H3. session_reset nemá autorizační vazbu na vztah mezi uživateli ✅ OPRAVENO (2026-03-08)
session_reset nemá autorizační vazbu na vztah mezi uživateliEvidence
- Handler přijme libovolné validní
peer_user_ida 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 1přesconversation_membersJOIN. handle_session_reset: před push notifikací ověřujeshares_conversation(). Pokud uživatelé nesdílí žádnou konverzaci → error response.- Rate limit 5 požadavků/min na
session_resetper 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řeswriter_device_map). Bezpeer_device_idzů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_startvrací explicitněUser not found(server.py:763-766).get_user_infovrací 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íoks platně vypadajícím kódem a session se vytvoří vždy (i pro neexistující email), takžepairing_pollvrací nerozlišitelnéready: false.- Přidán globální cap
PAIRING_MAX_SESSIONS = 100pro omezení počtu současných pairing sessions (DoS hardening). pairing_startrate limit je per-IP (bez email komponenty), aby nešel obcházet rotací emailů.pairing_claimipairing_send: sjednocená chybaInvalid 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 parametrsession(vyžaduje login). Lookupovat lze jen sebe nebo kontakty (ověřeno přesshares_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)
create_conversation / find_conversation / add_member (DoS)Evidence
create_conversationvytváří phantom účty pro neznámé emaily bez dedikovaného rate limitu (server.py:906-920).find_conversationaadd_memberrate 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
_can_create_phantom(addr, user_id)helper kontroluje 3 limity před každýmcreate_phantom_user():- Globální cap:
MAX_PHANTOM_USERS = 500(počet vphantom_user_idssetu) - 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é)
- Globální cap:
create_conversationnově má per-user rate limit 10/min + phantom check před každým členem.find_conversationaadd_member— existující per-addr+email limit zůstává (brání hammering jednoho emailu), přidán_can_create_phantomcheck před vytvořením phantomu.- 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)
pending_registrations nemá hard cap (memory/SMTP abuse)Evidence
pending_registrationsbyl 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_startbyl 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
- Přidán globální cap
MAX_PENDING_REGISTRATIONS = 1000(server.py:180). - Přidány slot limity
MAX_PENDING_PER_IP = 5aMAX_PENDING_PER_SUBNET = 20(server.py:181-182). _cleanup_registrations()běží i v periodickém cleanup tasku (server.py:327,server.py:2920).- 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). - 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)
temp_public_key (exfiltrace account private keys)Evidence
- Nové zařízení pošle do
pairing_startsvůjtemp_public_key; server si ho uloží dopairing_sessions(server.py:1067,server.py:1089-1095). - Staré zařízení v
pairing_claimzískátemp_public_keyzpě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_privateaidentity_privatea zašifruje je právě pod tentotemp_public_key(chat_core.py:1451-1477).
Dopad
- Kompromitovaný nebo aktivně škodlivý server může v odpovědi na
pairing_claimpodvrhnout vlastnítemp_public_key. - Staré zařízení pak zašifruje exportovaný payload pod klíč útočníka/serveru, který tím získá
rsa_privateiidentity_privateoběti. - To znamená plné převzetí účtu, možnost přihlášení jako oběť a přístup k self-encrypted historii (derivace
self/localklíčů z identity private key).
Oprava
- Přidán
compute_pairing_fingerprint()helper nad raw dočasným veřejným klíčem (30 číslic pro ruční porovnání). - 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. - Nové zařízení po
pairing_startzobrazuje 8místný kód i fingerprint dočasného pairing klíče. - Staré zařízení při
authorize_devicevyžaduje fingerprint opsaný z nového zařízení; předpairing_sendvypočítá fingerprint klíče vráceného serverem a při neshodě celý pairing odmítne. - 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_sendserver 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řeslist_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
- Server po prvním loginu nově vytvořeného zařízení posílá na ostatní zařízení účtu push notifikaci
device_added. - Payload notifikace obsahuje
device_id,device_name, zdrojovou IP a čas přidání. - GUI zobrazuje bezpečnostní alert a zvýrazněný status bar.
- 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)
mark_read a confirm_delivery neověřují, že message_ids patří do dané konverzaceEvidence
- Handler validuje členství jen v
conversation_id(server.py:1464-1479,server.py:1516-1531). - DB insert metody pro receipts neváží
message_idna 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()adb.mark_messages_delivered()nahrazeny z per-rowINSERT IGNOREna batchINSERT 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())aEHLOpřed/po TLS upgrade (server.py).
Dopad
- Slabší kontrola TLS parametrů/verifikace dle runtime prostředí.
Oprava
- Přidán explicitní TLS context pro STARTTLS.
- Přidán
EHLOpř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()wrappinggetpass.getpass()— heslo se nezobrazuje na terminálu. _sanitize_text()helper stripuje control znaky (\x00-\x1fkromě\t/\n/\r) a ANSI escape sekvence. Aplikováno nasender,text,filenamepři výpisu zpráv v_print_messages().- Follow-up:
_sanitize_text()nyní bezpečně přijímá i non-string vstupy (None -> "", jinakstr(...)), čímž se eliminujeTypeErrorpř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)
get_key_bundle umožňuje OPK depletion (availability)Evidence
handle_get_key_bundlenemá 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_idlze získat přesfind_conversationlookup (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
- Per-caller rate limit:
get_key_bundle|{user_id}— 10/min (omezuje celkový počet fetchů jednoho uživatele). - 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). - Autorizace:
shares_conversation()— caller musí sdílet konverzaci s cílem (self-fetch povolen vždy). - Chybová zpráva pro neautorizovaný přístup je neutrální (
”Key bundle not available”) — shodná s neexistujícím uživatelem. - 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_prekeys5/min,ensure_prekeys5/min,rotate_keys3/min,reencrypt10/min - DB-heavy:
get_messages30/min,delete_conv5/min,delete_msg20/min,react20/min,remove_member10/min,rename_conv5/min - File I/O:
update_avatar5/min (sdílený bucket pro user i group avatar)
- Crypto+DB:
M5. upload_image_start nemá anti-DoS cap na in-flight uploady ✅ OPRAVENO (2026-03-08)
upload_image_start nemá anti-DoS cap na in-flight uploadyEvidence
upload_image_startnevynucuje request rate limit ani limit počtu aktivních uploadů na user/IP (server.py:1786-1823).- In-memory
pending_uploadsje 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
- Per-user rate limit:
upload_start|{user_id}— 10/min. - Globální cap:
MAX_UPLOADS_GLOBAL = 200(kontrolalen(pending_uploads)pod_uploads_lock). - Per-user cap:
MAX_UPLOADS_PER_USER = 5(počet záznamů suploader_id == user_id). - Stale threshold snížen z 3600s na
UPLOAD_STALE_SECONDS = 600(10 min). - Periodic cleanup interval snížen z 600s na 120s (2 min).
LOW
L1. decode_binary není strict base64 ✅ OPRAVENO (2026-03-08)
decode_binary není strict base64Evidence
base64.b64decode(data)bezvalidate=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řesgenerate_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
- Pairing bootstrap už dočasné RSA vůbec nepoužívá; byl nahrazen
X25519 + HKDF + AES-GCM. - 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
- GUI i CLI nyní při párování zobrazují explicitní warning
Never share this pairing code. - 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í.
- 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)
reencrypt_history() po pairingu prozrazuje serveru timing a rozsah self-historyEvidence
- Po úspěšném
pairing_sendstaré 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
- Post-pairing history resync už nezačíná okamžitě; běží po náhodném odkladu.
- Pořadí konverzací i pořadí zpráv se před fetch/upload fází míchá.
- Mezi fetch cykly i mezi upload batchi je náhodný jitter, takže pairing už negeneruje tak snadno korelovatelný burst.
- 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_INSECUREaTLS_AUTOGENjsou blokovány mimoENVIRONMENT=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é chybyInvalid or expired codea krátkodobé pairing sessions.
Prioritní plán oprav
0-48 hodin
- Rotace DB hesla + odstranění tajemství z
.env. Zavést OOB autentizaci pairingu (fingerprint / QR / SAS) a nepovažovat serverem vrácený✅ DONEtemp_public_keyza důvěryhodný.Oprava TOFU bypassu v obou X3DH cestách.✅ DONEZablokování nevalidních message headers na vstupu.✅ DONE- Přepnutí upload storage perms na
0700/0600. Omezit phantom creation (rate limit bez emailu + cap).✅ DONEZavést cap pro✅ DONEpending_registrationsa čistit je i v periodickém cleanupu.Přidat cap/rate limit na in-flight uploady.✅ DONE
7 dní
- Zapnout TLS mezi app a MySQL (mTLS nebo minimálně server cert verify).
Opravit autorizaci✅ DONEsession_reset.Opravit vazbu✅ DONEmessage_idsnaconversation_idpro receipts.Omezit✅ DONEget_key_bundle(rate limit + policy sdílené konverzace).Přidat✅ DONEdevice_addedaudit notifikaci a zobrazit ji v GUI/CLI.
30 dní
Anti-enumeration sjednotit napříč endpointy.✅ DONECLI hardening (✅ DONEgetpass, output sanitace, filename sanitace).- 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).