Document client hardening round: AS-IS in CLAUDE.md + change requests for server and iOS/Android

CHANGES_2026-06-12_client_hardening.md lists deployment steps for the
server (shared protocol.py fix) and mirror requirements R1-R7 with
acceptance test scenarios for the native iOS/Android clients.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
filip
2026-06-12 16:08:59 +02:00
parent 4d15799b5e
commit 20f006cf5e
2 changed files with 127 additions and 0 deletions

View File

@@ -743,6 +743,17 @@ Four measures to minimize metadata leakage:
- **Cache-First Message Loading** — Při přepnutí konverzace se okamžitě zobrazí zprávy z lokálního cache (disk), server fetch běží na pozadí. `chat_core.get_cached_messages(conv_id)` čte z message_cache bez server callu. `_on_conv_selected()` volá `get_cached_messages()` synchronně → zobrazí → poté `bridge.load_messages()` async doplní nové. Fetch deduplication: `_messages_inflight` set v AsyncBridge zabraňuje duplicitním fetchům stejné konverzace.
- **Notification Push Logging** — Server loguje `[PUSH] msg=... conv=... targets=[uid(Nw)]` s počtem writerů per příjemce. `_notify_users_individual()` loguje warning při selhání doručení s user_id a chybou.
- **Image/File Transfer Performance Overhaul** — Drastické zrychlení downloadu obrázků a souborů: **(A)** Chunk size zvětšen z 32KB na 256KB (8× méně chunků, méně JSON/base64 overhead). **(B)** `MAX_MESSAGE_BYTES` zvětšen z 64KB na 1MB (nutné pro větší chunky). **(C)** Nový `download_stream` handler na serveru — jedna DB autorizace, pak server streamuje všechny chunky bez čekání na per-chunk request (dříve 2 DB queries × N chunků). Klient sbírá stream chunky přes `asyncio.Queue` v `_background_listener`. **(D)** Fallback na legacy `download_image` pro starší servery. **(E)** `image_download_failed` signál v GUI — `_pending_image_download` se vyčistí při selhání (dříve zůstal navždy a blokoval další downloads). **(F)** Sender cache: obrázek se cachuje lokálně po uploadu (`media_cache/{file_id}.bin`), sender vidí obrázek okamžitě bez server round-trip.
- **Client hardening round (2026-06-12)** — 9 oprav z hloubkové revize klienta (detailní požadavky pro server/iOS/Android: `CHANGES_2026-06-12_client_hardening.md`):
- **X3DH session adoption fix (chat_core.py):** `_process_x3dh_header()` už NEinstaluje novou session do `self.sessions` ani ji neukládá na disk — to dělá volající (`_decrypt_dm`) až po prvním úspěšném dešifrování. Dříve replay/forge zprávy s X3DH hlavičkou trvale přepsal funkční Double Ratchet session.
- **Sync watermark fix (chat_core.py):** `__last_server_ts` se posouvá jen přes prefix zpráv, které jsou "settled" v cache (dešifrované / control / deleted / trvale failnuté). Nedešifrovatelná zpráva (např. sender key ještě nedorazil) se znovu stáhne a zkusí při dalším syncu — max `_MAX_DECRYPT_RETRIES=3` pokusů, pak se v cache označí `_decrypt_failed` a watermark pokračuje. Dříve se watermark posunul vždy → tichá trvalá ztráta zprávy. Watermark se navíc neaktualizuje při `offset > 0` (paginace starších zpráv ho regresovala) a nikdy se neposune zpět.
- **PoW registrace fix (chat_core.py):** `register()` PoW větev používala neexistující `logger` (NameError = pád registrace když server vyžadoval proof-of-work). Opraveno na `self._logger` + `_solve_pow` přesunut do `run_in_executor` (neblokuje event loop).
- **rotate_keys fix (chat_core.py):** nový RSA klíč se ukládá na disk až PO `status == "ok"` od serveru. Dřív se `private.pem` přepsal před odesláním — selhání rotace = trvale zamčený účet.
- **GUI statusBar crash fix:** `_on_image_download_failed` volal `self.statusBar()` (neexistuje, MainWindow je QWidget) → AttributeError při selhání downloadu obrázku. Opraveno na `self.status_bar` + `_clear_status_bar`.
- **Privacy lock bypass fix:** Ctrl+Shift+P (`_toggle_privacy`) při `_privacy_locked=True` odemkl zamčenou session bez hesla. Zkratka je teď při zámku ignorována.
- **Registration confirm threading fix:** potvrzení registračního kódu sahalo na Qt widgety z asyncio vlákna. Nový signál `AsyncBridge.confirm_result(bool, str)` — koroutina jen emituje, widgety obsluhuje `on_confirm_result` na Qt vlákně.
- **Logout/login leak fix:** `MainWindow.closeEvent` odpojí všechny bridge signály (evidované v `self._bridge_connections` přes helper `conn()` v `_connect_signals`), odregistruje theme listener (`tm().remove_listener`) a zastaví `_refresh_timer`. Dříve každý logout/login cyklus leakoval celé okno, které dál zpracovávalo notifikace (duplicitní mark_read, tray toasty). Součást: `bridge.logout()` znovu zapojí `client._key_change_cb` (jinak po logoutu mrtvé MITM varování) a vyčistí `_pending_send_queue` (zprávy z fronty se nesmí odeslat pod jinou identitou).
- **CLI react crash fix (client.py):** `await prompt(...).strip()` — precedence (`.strip()` na koroutině) → AttributeError při reakci na zprávu. Opraveno na `(await prompt(...)).lower()`.
- **Protocol framing fix (protocol.py, sdílené server+klient):** `ProtocolReader` drain oversized zprávy zahazoval celý chunk včetně bajtů ZA newline (= začátek další zprávy) → rozbití framingu spojení. Nový `_leftover` buffer: bajty za delimiterem se uchovají a `read_message()` je obslouží před čtením ze streamu. Ověřeno testem (oversized + 2 pipelined zprávy v jednom chunku).
### 🐛 Známé bugy a problémy
- **Sender Key Redistribution (High Priority):** New group member can't decrypt old messages. On `add_member`, existing members should re-create and redistribute sender keys.