When authorizing a new device, include the TOFU registry
(known_identity_keys) and manual verifications (verified_contacts) in
the encrypted pairing payload, so a contact verified on the existing
device stays verified on the newly paired one. Previously these stores
are device-local and started empty on the new device, dropping verified
status. Fields are optional and ignored by older clients; symmetric with
the iOS client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
- _on_image_download_failed called self.statusBar() which does not
exist on a QWidget -> AttributeError crash; use status_bar label.
- Ctrl+Shift+P no longer disables the privacy overlay while the session
is password-locked (lock bypass).
- Registration code confirmation no longer touches Qt widgets from the
asyncio thread; new AsyncBridge.confirm_result signal carries the
result back to the Qt thread.
- MainWindow.closeEvent now disconnects all bridge signal connections
(tracked in _bridge_connections), removes the theme listener and
stops the periodic refresh timer — every logout/login cycle leaked a
window that kept handling notifications (duplicate mark_read, tray
toasts).
- AsyncBridge logout rewires _key_change_cb onto the fresh ChatClient
(key-change MITM warning was dead after logout) and clears
_pending_send_queue so queued messages cannot be sent under a
different identity.
- CLI: fix await precedence crash in the react-to-message prompt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Adopt a new X3DH session (install into self.sessions + persist) only
after the first message decrypts successfully. Previously
_process_x3dh_header saved the candidate session immediately, so a
replayed/forged X3DH header permanently overwrote a working ratchet.
- Advance the incremental-sync watermark (__last_server_ts) only across
the prefix of messages settled in the cache. An undecryptable message
(e.g. sender key not yet received) is re-fetched and retried up to
_MAX_DECRYPT_RETRIES=3 times instead of being silently lost forever.
Watermark is no longer touched on offset>0 pages and never regresses.
- Fix NameError in the proof-of-work registration path (logger ->
self._logger) and run _solve_pow in an executor so it does not block
the event loop.
- Persist the rotated RSA login key only after the server confirmed
rotate_keys; writing private.pem first bricked the account when the
request failed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ProtocolReader discarded the entire chunk containing the newline
delimiter while draining an oversized message, including bytes after
the newline that belong to the next pipelined message. This corrupted
framing for the rest of the connection (affects server and client).
Salvaged bytes are now kept in a _leftover buffer that read_message()
consumes before touching the stream.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- db: cleanup_old_messages(days) purges messages older than N days in
batches; recipients/reads/deliveries/reactions follow via ON DELETE
CASCADE. Returns attachment file_ids no longer referenced by any
surviving message (forwarded copies keep their files) and removes
their image_uploads rows
- server: MESSAGE_RETENTION_DAYS env var (default 0 = keep forever);
hourly cleanup deletes expired messages and securely removes orphaned
attachment blobs from the upload dir
- schema: email_visible now defaults to 0 — previously any logged-in
user who knew a UUID could read another user's email via get_profile
- migrations: SQL script to apply the new default and reset the flag on
existing databases (run manually, see file header)
- docker-compose: document MESSAGE_RETENTION_DAYS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- chat_core: defer one-time-prekey deletion until the first message
decrypts successfully; deleting it on load made the SPK grace-period
retry derive a wrong shared secret and lose the message permanently
- chat_core: fix get_deleted_since params (since -> since_ts) and
response field (message_ids -> deleted_ids) so incremental deletion
sync actually works
- chat_core: route keys_updated pushes into the notification queue
- server: notify contacts with keys_updated when a user uploads a new
SPK or logs in with a new device, so clients invalidate cached key
bundles instead of waiting for the TTL
- server: rate-limit download_stream like other heavy handlers
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>