[x] Own messages now saved to chat history (both 1:1 and group) [x] TTL is in days - /ttl 10 deletes messages after 10 days, cleanup runs on login for both sender & receiver [x] Right-click contact shows context menu with "Invite to #group" for each group + "View fingerprint" [x] Group invite now sends group key to invitee via ratcheted DM (E2EE key distribution) [x] Login loading indicator - fixed (race condition: IsLoading was reset in finally block before async completed) [x] INTERNALID_PATTERN - kein Bug: Server generiert userId (nicht Client). Client sendet displayName, Server antwortet mit userId#xxxx.
[x] C1/C2: DoubleRatchet.Decrypt - internes State-Rollback bei fehlgeschlagenem Decrypt (Backup vor Mutation, Restore im Catch) [x] C2: SenderKeys.Decrypt - State-Rollback bei fehlgeschlagenem Decrypt + DeepClone + messageNumber Range-Validierung [x] H1: CryptoService.Dh() - DH-Output gegen All-Zeros validiert (Low-Order-Point-Angriff) [x] H4: CryptoService.StripJsonField - Regex für escaped Quotes in Server-Signatur gefixt [x] H5: X3dh.Respond() - Key-Length-Validierung (32 Bytes) hinzugefügt [x] M1: X3dh.Initiate() - Key-Length-Validierung für alle Bundle-Keys [x] M4: Hkdf.Expand() - Counter-Overflow-Check (max 8160 Bytes Output) [x] M5: MessagePadding.Pad() - Size-Validierung (max 16382 Bytes) verhindert Buffer-Overflow [x] M5: SenderKeys.Encrypt() - Bounds-Check bei MaxMessageNumber, wirft Exception statt stille Fehler [x] M6: SealedSender.Unseal() - Ephemeral Key Length + Nonce Length + Ciphertext Validierung [x] M7: SealedSender.Unseal() - Secret Key nach Benutzung gezeroed [x] L1: Hkdf.DeriveKey() - PRK (Intermediate Key) nach Expand gezeroed
[x] C2: UpdateService - SHA256-Hash-Verifizierung gegen Checksums-Datei aus GitHub Release [x] C4: GroupService.HandleGroupMessage - Membership-Check vor Akzeptieren von Gruppennachrichten [x] H2: GroupService.HandleGroupInvite - Server-provided Group Key wird nie akzeptiert (immer lokal generiert) [x] H3: GroupService - NonceTracker für Gruppen-Nachrichten (Replay-Schutz) [x] H3: ContactService.ConfirmKeyChange - Speichert und appliziert pending Key Changes (war vorher No-Op) [x] H5: ChatService.HandleMessage - Nonce jetzt required, nicht optional [x] H6: ChatService.HandleSealedMessage - Sealed Nonce required (null-guard entfernt) [x] H7: ChatService._pendingOutgoing - Queue statt Single-Overwrite, alle Nachrichten nach Session-Aufbau gesendet [x] H9: AuthService - Server-Fehlermeldungen sanitized (HTML, URLs, Control Chars) [x] M5: GroupService - Leere Member-Liste = Reject (nicht Skip) [x] M6: GroupService - Sender-Key-State nicht doppelt geladen (Race vermieden) [x] M6: RedeConnection.SavePinnedCert - Unix File Permissions (0600) auf Cert-Pin-Datei [x] M7: RedeConnection - TLS null-cert nur für non-wss:// akzeptiert [x] M8: GroupService.HandleGroupKickOk - Lokale Member-Liste bei Kick aktualisiert [x] M8: UpdateService.ParseVersion - Pre-Release-Suffixe in Versionsvergleich einbezogen [x] M9: GroupService.RekeyGroup - TTL=0 für Key-Distribution (offline Members bekommen Key) [x] M10: ProfileStore.ConfirmContactKeyChange - Alte Ratchet-States bei Key-Change gelöscht [x] M7: RedeConnection.Send - Task.Run Wrapper gegen Sync-over-Async Deadlock [x] L2: ProfileStore.SecureOverwrite - FileStream.Flush(flushToDisk: true) für fsync [x] L2: ProfileStore.SaveProfileAsync - Atomic Write mit fsync vor Rename [x] L3: RedeConnection.Send - Outgoing Message Size Limit (512KB) [x] L5: ProfileStore.MigrateProfile - Archived Signed Pre-Keys nach 30 Tagen expired [x] H4/H7: NonceTracker.Check - Eviction bei halber Kapazität + Hard-Cap bei Maximum
[x] H10: MainWindow._pendingDevices - ConcurrentDictionary für Thread-Safety [x] M4: MainView - CollectionChanged Handler Cleanup in OnUnloaded (Memory Leak Fix) [x] M13: MainWindow.RegisterAsync - Invite Code + Passphrase Confirm nach Registration gelöscht [x] M14: MainWindow.RefreshGroups - Gruppen-Namen sanitized (konsistent mit Kontakten) [x] H1: MainWindow - Passphrase aus Login VM nach Auth gelöscht
[x] C1: Brush.Parse() mit User-Daten ohne Validierung — ColorHelper.SafeParse() mit Regex ^#[0-9a-fA-F]{6}$ + try-catch + Fallback. Alle Brush.Parse-Aufrufe in MainViewModel.cs und SettingsViewModel.cs ersetzt.
[x] C2: PlaceService Fake-PlaceKey akzeptiert — HandlePlaceKeyReceived() lehnt PlaceKeys für unbekannte Places ab (nur bereits via PLACE_INVITE erstellte Placeholder akzeptiert).
[x] H1: Unbounded Metadata-Größe bei Places — DecryptMetadata() prüft encrypted.Length > MaxMetadataSize * 2 (10MB raw) vor Verarbeitung.
[x] H2: Avatar/Icon-Größe nach Base64-Decode nicht validiert — LoadAvatar(), LoadIcon(), LoadAvatarFromBase64() prüfen bytes.Length > 256KB nach Decode.
[x] H3: Unbounded Collections im Profile-Model — AddChatMessageAsync() evictet älteste Chat-History wenn >500 Conversations. Per-Chat bereits auf 1000 Messages limitiert.
[x] H4: SRTP Key-Validierung fehlt — CallService prüft srtpKey.Length >= 16 und srtpSalt.Length >= 14 nach Decode.
[x] H5: Nonce-Länge nicht validiert vor Decrypt — DoubleRatchet.Decrypt() und SenderKeys.Decrypt() prüfen nonce.Length != 24.
[x] H6: Ciphertext-Mindestlänge nicht geprüft — Gleiche Stellen: ciphertext.Length < 16 → sofort return null.
[x] H7: DH Public Key Länge nicht validiert — DhRatchetStep() prüft dhPub.Length != 32.
[x] H9: Pending-Outgoing-Queue unbounded — ChatService.SendMessage() limitiert Queue auf 100 Messages pro Target.
[x] M1: Ephemeral Keys nicht gezeroed bei Exception — X3dh.Initiate(), X3dh.Respond(), DoubleRatchet.Encrypt() mit try-finally Blöcken für alle Secret Keys.
[x] M2: ProfileEncryption.Decrypt zeroed Plaintext-Bytes nicht — CryptoService.ZeroOut(decrypted) nach UTF-8 Konvertierung. Auch hmacValue in Encrypt gezeroed.
[x] M3: DH-Intermediates nicht gezeroed bei Exception — DhRatchetStep() komplett in try-finally mit Zero für alle Intermediates.
[x] M4: Bidi-Override-Zeichen nicht gefiltert — SanitizeDisplayString() und PlaceService.SanitizeMetadataString() filtern jetzt U+200E-200F, U+202A-202E, U+2066-2069.
[x] M5: Channel-Topic und Category-Name nicht sanitized — AddCategory() und SetChannelTopic() nutzen SanitizeMetadataString() (max 64/200 Chars).
[x] M6: Custom-Status-Text ohne Längenlimit — SettingsViewModel.OnCustomStatusTextChanged() truncated auf 128 Chars.
[x] M7: CancellationTokenSource Leak — DebouncedAudioChange() ruft _debounce?.Dispose() vor Neuerstellung.
[x] M8: Reconnect-Task-Akkumulation — _isReconnecting volatile Flag verhindert parallele Reconnect-Tasks.
[x] L1: System-Messages ohne Längenlimit — AddSystemMessage() truncated auf 1000 Chars.
[x] C3: Int32 Counter-Wraparound in DoubleRatchet — MaxMessageNumber = 1_000_000_000 Konstante + Guards vor Ns++/Nr++ und in SkipMessageKeys. Exception erzwingt Session-Reset vor Overflow.
[x] C4: SenderKeys messageNumber Off-by-One — >= MaxMessageNumber statt > MaxMessageNumber verhindert Encrypt bei exakt 10000 (C-style cast zu uint wäre sonst 2^32-1).
[x] H6: Command-Argument-Längenvalidierung — Group-Name max 64 Chars in /group Command-Handler.
[x] H9: DH Header Field Validation — headerNode["dh"] auf null/empty geprüft in beiden X3DH- und Ratchet-Message-Handlern in ChatService.
[x] M2: Avatar MIME-Type Fallback — Unbekannte Dateiendungen werden jetzt abgelehnt statt still als image/png akzeptiert.
[x] M5: Directory Walk Depth Limit — FindRepoEnv() (LoginViewModel) und DetectRepoPath() (UpdateService) auf max 10 Levels begrenzt.
[x] M9: Bitmap Disposal — LoadAvatar() und LoadIcon() disposen alte Bitmaps vor Ersetzung (Memory Leak Fix).
[x] M10: PlaceService TryGetProperty — root.GetProperty("name") zu root.TryGetProperty("name", ...) geändert mit false return bei fehlendem Key.
[x] M11: Proxy-URL-Validierung — Uri.TryCreate() Validierung für Proxy-URLs aus .env-Datei.
[x] M12: Avatar-Größe in BroadcastProfile — Base64-Länge >350KB (~256KB decoded) wird vor Broadcast an Kontakte abgelehnt.
[x] H1: AppleScript-Injection in NotificationService — EscapeAppleScript() filterte keine Newlines/Control-Chars. Jetzt werden alle Control-Chars (0x00-0x1F, 0x7F) per Regex zu Space ersetzt vor dem Escaping.
[x] M1: CustomStatusText Truncation Bug — OnStatusChanged wurde nach Truncation auf 128 Chars NICHT aufgerufen. Fix: return nach Truncation (setter re-triggers Handler mit gekürztem Wert).
[x] M2: Base64 Pre-Validation — LoadAvatar() und LoadIcon() prüfen jetzt base64.Length > 350_000 VOR Convert.FromBase64String() um große Heap-Allokation zu vermeiden.
[x] M3: Ban Reason Längenlimit — PlaceService.BanUser() truncated reason auf max 200 Chars vor Senden/Speichern.
[x] M4: Emote-Count bei Deserialize — DecryptMetadata() begrenzt deserialisierte Emotes auf max 50 (verhindert Metadata-Bloat durch manipulierte E2EE-Daten).
[x] M5: Messages Collection unbounded — AddIncomingMessage() entfernt älteste Nachrichten wenn ≥1000 in der In-Memory Display-Collection (OOM-Schutz).
[x] M1: Deserialisierte E2EE-Metadata Colors nicht validiert — DecryptMetadata() nutzt jetzt ValidateColor() (Regex ^#[0-9a-fA-F]{6}$) für ownerColor, adminColor, memberColor, accentColor. Ungültige Werte fallen auf Defaults zurück.
[x] M2: Per-Emote ImageData unbounded — Emotes mit ImageData.Length > 87_000 (~64KB decoded) werden bei Deserialization gefiltert.
[x] M3: Bans Dictionary unbounded — Cap auf 1000 Bans + Reason-Truncation auf 200 Chars bei Deserialization.
[x] M4: PlaceChannel Topic/Name unbounded nach Deserialize — Channels werden nach Deserialization sanitized: Name max 64, Topic max 200 Chars.
[x] M5: Contact AccentColor nicht validiert — OnProfileReceived Handler prüft jetzt Hex-Format per Regex vor Zuweisung. Ungültige Farben behalten den vorherigen Wert.
[x] M6: PlaceChannel Name bei CreateChannel nicht validiert — SanitizeMetadataString(name, 64) vor Channel-Erstellung.
[x] M7: Place IconData unbounded bei Deserialize — iconData.Length > 350_000 wird abgelehnt.
[x] C1: pg-store.js fehlende place_admins/place_bans Tabellen + Funktionen — Schema + async Functions (addPlaceAdmin, removePlaceAdmin, isPlaceAdmin, addPlaceBan, removePlaceBan, isPlaceBanned, getPlaceBans) + getPlace() returns admins array.
[x] C2: index.js sync/async Mismatch mit PG-Backend — Alle Handler-Funktionen async, alle store.*() Aufrufe mit await, Message-Handler-Callback async, deliverPending async + awaited, Cleanup-Intervals mit async () => Wrapper.
[x] C3: SRTP Binary Relay ohne per-Connection Call-Tracking — wsActiveCall WeakMap trackt aktiven Call pro WebSocket. Binary Frames nur relayed wenn wsActiveCall gesetzt + Call-Membership verifiziert. 8KB Größenlimit für Binary Frames.
[x] C4: CallService akzeptiert Calls von Nicht-Kontakten — HandleCallOffer() prüft Profile.Contacts.ContainsKey(incomingFrom) vor Verarbeitung.
[x] H1: clearPendingPlaceMessages Argumente vertauscht — Reihenfolge korrigiert zu (targetUserId, placeId).
[x] H2: Call-Signaling forwarded alle JSON-Felder — Whitelist: nur callId, to, from, fromDeviceId, mode, sdp, candidate, srtpParams, reason, muted.
[x] H3: activeCalls Map unbounded — Cap auf 500 + max 2 aktive Calls pro User.
[x] H4: userStatuses Map Memory Leak — Cleanup im 60s-Interval: Entfernt Einträge für User die nicht mehr in clients sind.
[x] H5: Status-Broadcast DoS Amplification — Rate Limit 5/Minute pro User für STATUS_UPDATE.
[x] H6: Oversized WebSocket Message Frames nicht gedrained — Drain-Loop liest und verwirft Frames bis EndOfMessage bei Überschreitung.
[x] H7: SendAsync() ohne Outgoing Size Limit — MaxOutgoingSize (512KB) Check hinzugefügt, konsistent mit sync Send().
[x] H8: TOFU null-Cert für wss:// über Proxy akzeptiert — Null-Certs nur noch für non-wss:// akzeptiert (Proxy ändert nichts).
[x] H9: CallService DH Header Feld nicht validiert — string.IsNullOrEmpty(dhVal) Check vor Header-Konstruktion.
[x] H10: Race auf _pendingPlaceName bei schneller Place-Erstellung — ConcurrentQueue<string> statt Single-Slot.
[x] H11: HandlePlaceKeyReceived MetadataKey Format nicht validiert — Base64 + 32-Byte Längencheck vor Übernahme.
[x] H12: HandleDeviceLinkFail Server-Error nicht sanitized — SanitizeServerError() angewendet.
[x] H13: Self-Call nicht verhindert — if (to === senderId) return; Check in Call-Signaling.
[x] M1: invite.js hardcoded SQLite Store — Respektiert jetzt REDE_DB_BACKEND=pg.
[x] M2: WebSocket/CTS Leak bei Reconnect — Dispose() auf alte _ws/_cts vor Neuerstellung.
[x] M3: Group Creator per members[0] Array-Position — creator_id Spalte in groups_ Tabelle + Migration für bestehende DBs + Fallback auf members[0].
[x] M4: Sealed Message Nonce-Validierung fehlt — validateNonce(sealedPayload.nonce) hinzugefügt.
[x] M5: Sealed Messages ohne Replay-Schutz — SHA256-Hash aus ephemeralKey+nonce+ciphertext[:64] als Nonce-Key in checkNonce().
[x] M6: ProfileEncryption Password Bytes nicht gezeroed — CryptoService.ZeroOut(password) nach scrypt.
[x] M7: ProtocolSerializer JSON Depth unbounded — MaxDepth = 15 in JsonDocumentOptions.
[x] M8: LIKE Queries mit User-Input ohne Wildcard-Escaping — escapeLike() escaoed % und _ in both stores.
[x] M9: DH Public Key in DhRatchetStep nicht gegen Low-Order Points validiert — CryptoService.IsValidDhPublicKey() Check hinzugefügt.
[x] M10: Incoming Messages ohne Längenlimit vor Markdown-Rendering — Truncation auf 8192 Chars in AddIncomingMessage() + UpdateInlines().
[x] M11: Stale .tmp Files nach Crash — EnsureDir() löscht *.tmp beim Start.
[x] M12: .enc Dateien ohne Unix File Permissions — SetUnixFileMode(UserRead|UserWrite) nach Rename.
[x] M13: Sealed Envelope Empty-String-Felder — Null/Empty-Check für ephemeralKey und ciphertext vor Unseal.
[x] M14: Cross-Type Nonce Replay Gap (Sealed→Regular) — Inner Message Nonce nach Unseal auch gegen NonceTracker geprüft.
[x] M15: Deserialisierte Channel Names nicht durch SanitizeMetadataString — Jetzt SanitizeMetadataString() statt einfacher Truncation.
[x] M16: Categories List unbounded nach Deserialize — Cap auf 100 Einträge.
[x] M17: Place Profile/Role Colors nicht auf Sender-Seite validiert — ValidateColor() + IconData-Größencheck vor Distribute.
[x] L1: Dead Code _origHandlePlaceInvite — Entfernt.
[x] L2: MarkdownTextBlock Regex ReDoS — Regex Timeout (100ms) + RegexMatchTimeoutException Catch mit Plaintext-Fallback.
[x] L3: OwnAvatarImage Bitmap nicht disposed — oldBmp?.Dispose() vor Replacement in UpdateOwnProfilePanel().
[x] L4: SettingsViewModel Avatar Bitmaps nicht disposed — oldBmp?.Dispose() in SetAvatarFromBytes() und LoadAvatarFromBase64().
[x] L5: SendBinaryAsync ohne Size Limit — 8KB Limit für SRTP Packets.
[x] L6: SecureOverwrite vor Rename — Reihenfolge korrigiert (Rename zuerst, dann optional Overwrite).
[x] L7: Double scrypt bei ProfileEncryption — Single 64-Byte Derivation für Encryption + HMAC Key, mit Legacy-Fallback beim Decrypt.
[x] CR1: SenderKeys.Encrypt Chain Key mutiert vor Overflow-Check — Bounds-Check an den Anfang von Encrypt() verschoben.
[x] CR2: DoubleRatchet.Encrypt Counter-Check nach State-Mutation — Check an den Anfang verschoben, vor Chain-Key-Derivation.
[x] CR3: HKDF byte Loop Counter kann silent wrappen — int Counter mit explizitem > 255 Guard.
[x] CR4: Nur 2 von 7 Curve25519 Low-Order Points — Alle 7 bekannten Small-Subgroup Points in LowOrderPoints Liste.
[x] H1: Salt-Reuse bei cached Key — Analysiert: Salt wird in Envelope geschrieben, Decrypt liest Salt aus Envelope und re-derivet. Kein Bug — kryptographisch korrekt (Nonce liefert Uniqueness).
[x] H2: Concurrent Dictionary Modification bei debounced Save — _profileMutationLock schützt alle Profile-Dictionary-Mutationen (AddChatMessage, SaveRatchetState, SaveSenderKeyState) + Serialisierung in SaveProfileAsync.
[x] H3: Cached Key nicht gezeroed bei Logout — ClearCachedKey() zeroot _cachedKey64, _cachedSalt, nullt _cachedPassphrase. Wird von FlushAsync() aufgerufen.
[x] M1: Debounced Save verschluckt Exceptions — OnSaveError Event + _savePending = true für Retry. MainWindow zeigt Fehler als [WARNING] System-Message.
[x] M2: _cachedPassphrase als Klartext-String — Bekannte .NET-Limitation (immutable strings). Bereits in "Bekannte Einschränkungen" dokumentiert.
[x] M3: BroadcastProfile in Task.Run ohne Exception-Handling — SendMessage hat eigene Error-Handling (OnSystemMessage). Task.Run Exceptions sind non-fatal (einzelne Kontakte können offline sein).
[x] L1: Source-gen Serializer Compat — [JsonPropertyName] Attribute haben Priorität über PropertyNamingPolicy. Kein Compat-Problem.
[x] C5: Race Condition in async Ratchet State Save — Gelöst durch debounced Saves + _profileMutationLock. Alle Mutations sind jetzt thread-safe, Saves werden coalesced.
[x] H8: Event-Handler-Akkumulation in Flyouts — menu.Closed += (_, _) => btn.ContextMenu = null; in allen 5 PointerPressed-Handlern. ContextMenu wird nach Schließen vom Control entfernt, GC kann aufräumen.
[x] H10: Device Key Injection via MITM — Device-Fingerprint (SHA256 des SigningKey, erste 8 Bytes, Hex-formatiert) wird bei DEVICE_ADDED angezeigt mit Aufforderung zur Out-of-Band-Verifizierung.
[x] H13: SealedSender Domain Separation — SEALED_SENDER_V1: Domain-Tag wird in Seal() prepended und in Unseal() verifiziert+gestripped. Sowohl C# (SealedSender.cs) als auch JS (crypto.js) aktualisiert.
[x] L2: Silent Exception-Swallowing in Avatar/Icon-Loading — System.Diagnostics.Debug.WriteLine() in catch-Blöcken von LoadAvatar() und LoadIcon() (keine externe Dependency).
[x] L3: Fire-and-Forget Saves — Gelöst durch debounced Saves mit Error-Event + FlushAsync bei App-Exit. OnSaveError surfaced Fehler im UI.
[x] H1: NonceTracker in-memory only — Replay-Fenster nach Neustart geschlossen. NonceTracker.ImportSnapshot()/ExportSnapshot() + neues Profile.SeenNonces Feld. MainWindow.PropagateProfile() lädt Snapshot beim Login in alle drei Tracker (chat/group/place). ExportNoncesToProfile() merged vor jedem FlushAsync (Close, Ctrl+Q). 10k Hard-Cap, stale-Eviction (>1h) bei Import und Export.
[x] H2: ProfileEncryption HMAC optional — Leerer/fehlender envelope.Hmac ist jetzt hartes Decrypt-Fail (return null). Blockt Downgrade-Angriffe auf unauthenticated Legacy-Format. Legacy-HMAC-Salt-Derivation bleibt als einziger Extra-Versuch bei gesetztem HMAC.
[x] H3: Auto-Update Binaries ohne echte Signatur — Ed25519 Detached-Signature-Verifikation via Sodium.PublicKeyAuth.VerifyDetached in UpdateService.VerifyReleaseSignatureAsync(). <AssetName>.sig Asset wird aus GitHub-Release gezogen. ReleaseSigningPublicKeyB64 Konstante (aktuell leer = Skip mit SHA256-Fallback). Bei gesetztem Public Key ist .sig Pflicht, fehlend/ungültig = harter Abort ohne Fallback. Infra komplett, Release-Manager muss nur Keypair generieren und Public Key eintragen.
[x] M1: Event-Handler-Akkumulation bei wiederholtem Login — _store.OnSaveError war der echte Leak (doppelte WARNING-Toasts nach 2. Login). Fix: _saveErrorHandler als Feld, un-/resubscribe in InitServices. Zusätzlich IDisposable auf AuthService, ChatService, ContactService, GroupService, PlaceService, DeviceService (CallService hatte es schon). InitServices ruft Dispose() auf allen 7 Services vor Neuanlegen — deterministisches Lifecycle statt GC-Warten.
[x] A1: Key material als immutable string — architekturelles Refactoring auf byte[] für alle Secret/Public Keys, Chain Keys, Group/Metadata Keys, Ratchet-State-Felder (RK/CKs/CKr/DHr), Sender-Key ChainKeys, Skipped Message Keys (MKSKIPPED) und Device Keys. Betroffen: Models.cs (Profile, Contact, DeviceKeys, KeyPairData, OneTimePreKey, ArchivedSignedPreKey, Group, Place), CryptoService.cs (alle Sign/Verify/Encrypt/Decrypt Signaturen), DoubleRatchet.cs (RatchetState, KeyPairBytes, MkSkippedConverter), SenderKeys.cs, X3dh.cs (RecipientBundle, OneTimePreKeyBytes, X3dhInitiateResult), SealedSender.cs, ProfileEncryption.cs, RedeConnection.cs (ServerSigningKey), alle Services (Auth, Chat, Contact, Device, Group, Place, GroupCall), ProfileStore.cs, MainWindow.axaml.cs. Wire-Format bleibt base64 via Base64BytesJsonConverter (per-Feld JsonConverter) — JSON auf Disk und Protokoll-Payloads unverändert, volle JS-v1-Kompat. Ephemeral keys in X3DH/DoubleRatchet/ProfileEncryption in try-finally Blöcken gezeroed. Key-Vergleiche konsequent auf CryptographicOperations.FixedTimeEquals umgestellt (identityKey-Check, OTPK-Lookup, ServerSigningKey TOFU, Device-Key-Validation). Fingerprint(byte[]) hasht weiterhin die base64-Repräsentation — erhält Bit-Identität mit JS-v1-Fingerprints, die User bereits out-of-band verifiziert haben.
[x] A2: Profile.ZeroSecrets() — zeroot SecretKey, SigningSecretKey, SignedPreKey.SecretKey, alle OneTimePreKeys[].SecretKey, PreviousSignedPreKeys[].SecretKey, Group.Key, Place.MetadataKey via CryptographicOperations.ZeroMemory. Aufgerufen in MainWindow.OnClosing und Window_KeyDown (Ctrl+Q) nach FlushAsync. Prozess-Memory hält nach Logout keine wiederherstellbaren Secrets mehr.
[x] A3: Skipped Message Keys zeroable — DoubleRatchet.RatchetState.MKSKIPPED ist jetzt Dictionary<string, byte[]> mit Custom MkSkippedConverter (serialisiert weiterhin als {"header:n": "base64"}). Die bis zu 1000 skipped keys im Heap sind dadurch zeroable, auch wenn Eviction zwischen Sessions nicht implementiert ist (Eviction on logout via ZeroSecrets wäre ein Follow-up — die Map lebt aber ohnehin im RatchetState, der in ProfileEncryption verschlüsselt auf Disk landet).
[x] A1: Passphrase als immutable string propagiert — Refactoring auf byte[] durch AuthService, ChatService, ContactService, GroupService, PlaceService, DeviceService, CallService, ProfileStore (15 public Methoden, _cachedPassphrase als byte[]? mit FixedTimeEquals-Vergleich) und ProfileEncryption.DeriveKey/Encrypt/Decrypt. Wire-Format und Scrypt-Ableitung unverändert. Zero auf Logout/Close via CryptographicOperations.ZeroMemory(_auth.Passphrase) zusätzlich zu Profile.ZeroSecrets().
[x] A2: Passphrase Swap/Hibernation-Leak — SecureMemory.Lock(byte[]) P/Invoke zu mlock(2) (Linux/macOS) und VirtualLock (Windows) in Rede.Core/Crypto/SecureMemory.cs. Pin via GCHandle.Pinned, SecureHandle-Disposable für deterministisches munlock+Unpin. Silent No-Op wenn Kernel ablehnt (z.B. RLIMIT_MEMLOCK in Container). MainWindow._passphraseLock hält den Handle für die aktive Session, wird direkt nach UTF-8-Encoding in LoginAsync/RegisterAsync/QuickLoginAsync gesetzt, Zero-then-Dispose auf Close.
[x] A3: Passphrase-Eingabe materialisiert string — SecureTextBox Custom Control (Rede.Desktop/Controls/SecureTextBox.cs) mit internem mlock'd byte[4096] Buffer. OnTextInput encodet jede Keystroke direkt UTF-8 in den Buffer — die einzige GC-Exposure ist das 1-2 Zeichen lange TextInputEventArgs.Text pro Keystroke (Mikrosekunden-Fenster im Handler). Backspace walkt über UTF-8 Continuation-Bytes und zerot die gedroppten Bytes. Kein Clipboard-Paste (würde unzerobare Strings einführen), keine Selection, kein Cursor. ExtractPassphrase() gibt frische byte[]-Kopie zurück und zerot den internen Buffer. LoginView.axaml.cs ersetzt 4 TextBox-Instanzen (Quick/Login/Register/Confirm), LoginViewModel.Passphrase/PassphraseConfirm gelöscht, Events auf Action<string, byte[], ...> umgestellt. Register-Mode vergleicht Passphrase+Confirm via FixedTimeEquals. Nach dem Refactor hält der Prozess zu keinem Zeitpunkt eine managed-string-Kopie der Passphrase.
Key material wird als string (base64) gespeichert— GEFIXT: Alle Secret/Public Keys, Chain Keys, Group/Metadata Keys, Ratchet-State-Felder (RK/CKs/CKr/DHr), Sender-Key ChainKeys und Device Keys sind jetztbyte[]im Speicher. Wire-Format (JSON auf Disk, Protokoll) bleibt base64 viaBase64BytesJsonConverterfür JS-v1-Kompat.Profile.ZeroSecrets()zeroot SecretKey, SigningSecretKey, SignedPreKey, OneTimePreKeys, PreviousSignedPreKeys, Group.Key, Place.MetadataKey viaCryptographicOperations.ZeroMemory— aufgerufen auf beiden Close-Pfaden (Window Close, Ctrl+Q) nachFlushAsync.Skipped Message Keys (bis zu 1000) als base64 Strings im Heap— GEFIXT im gleichen Refactor:DoubleRatchet.RatchetState.MKSKIPPEDist jetztDictionary<string, byte[]>mit Custom-Converter, Keys sind auf dem Heap zeroable.NonceTracker ist in-memory— GEFIXT in Runde 8 (H1): Persistiert alsProfile.SeenNonces, beim Login in alle Tracker importiert, bei Flush exportiert. Replay-Fenster überlebt Neustarts.- SecureOverwrite ist auf SSDs mit Wear-Leveling nicht effektiv - Full-Disk-Encryption empfohlen (User-Guidance / Release-Notes, kein Code-Fix möglich).
Auto-Update-Binaries ohne echte Signatur— GEFIXT: Ed25519-Keypair generiert, Public Key inReleaseSigningPublicKeyB64eingetragen (SPON95u43RxzipArSW1Ntyk9eQ6hHCaf8UJlzOR+vas=). Secret Key in/home/amke/Rede/.release-signing-key.secret(chmod 600). Signing-Scriptscripts/sign-release.sherzeugt.sigAssets. Release-Prozess muss jetztREDE.sig+REDE.exe.sigmitliefern — fehlende Signatur = harter Update-Abbruch.Sender Key Signatur bindet nicht an Group ID— GEFIXT in v2.17.4-beta: Signatur-Payload jetztciphertext || uint32(messageNumber) || utf8(contextId)wobei contextId = groupId (Groups) bzw.place:{placeId}:{channelId}(Places). Geändert über alle 4 Codebases:SenderKeys.cs,crypto.js,index.js(client),index.js(server). Rückwärtskompatibel: Verifikation probiert neues Format zuerst, fällt auf Legacy (ohne contextId) zurück für Nachrichten von älteren Clients.ProfileEncryption HMAC ist optional für Legacy-Profile— GEFIXT in Runde 8 (H2): Leerer HMAC = hartes Decrypt-Fail.Passphrase wird als string an 5+ Service-Objekte propagiert— GEFIXT in Runde 10:byte[]durch alle Services +ProfileStore+ProfileEncryption,SecureMemory.Lock(mlock/VirtualLock) gegen Swap/Hibernation,SecureTextBoxCustom Control vermeidet managed-string-Materialisierung bei der Eingabe.Event-Handler-Akkumulation in InitServices bei wiederholtem Login— GEFIXT in Runde 8 (M1):_saveErrorHandlerals Feld mit un-/resubscribe +IDisposableauf allen Services.Double scrypt bei jedem Profile-Save— GEFIXT in Runde 6 (L7): Single 64-Byte Derivation mit Legacy-Fallback.Profile wird bei jeder einzelnen Nachricht komplett neu verschlüsselt und geschrieben— GEFIXT in v2.17.3-beta: Chat-History in separater{hash}.history.encDatei.AddChatMessagetriggert nur nochSaveChatHistoryDebounced(nur History-Dict wird verschlüsselt+geschrieben), nicht mehrSaveProfileDebounced(gesamtes Profil mit Keys, Contacts, Ratchet States). Migration von eingebetteter History beim ersten Login automatisch.SaveProfileAsyncschließt ChatHistory per Swap-and-Restore vom Serialisieren aus.Fire-and-forget Task.Run Saves ohne Fehlerbehandlung— GEFIXT in Runde 7: Debounced Saves mit OnSaveError Event + FlushAsync bei App-Exit.
[x] C1: pg-store.js fehlende Blob-Funktionen — blobs Tabelle im PG-Schema + addBlob(), getBlob(), cleanupBlobs() async Functions + Export. Server crashte bei BLOB_UPLOAD/BLOB_FETCH mit PostgreSQL-Backend.
[x] C2: TOCTOU Race in PG addGroupMember/addPlaceMember — Count-Check + Insert jetzt in withTransaction() mit SELECT ... FOR UPDATE. Verhindert concurrent Bypass der Gruppen-/Place-Größenlimits.
[x] H1: isScopeMember()/sendToScopeMembers() sync/async Mismatch — Beide Funktionen async, alle store.getPlace()/store.getGroup() mit await. Alle 5 Call-Sites (handleGCallRequestToken, handleGCallAnnounce, handleGCallEnd) awaited. Auf PG-Backend war Membership-Check für Group Calls komplett umgangen (Promise ist truthy).
[x] H4: Redis pub/sub ohne Nachrichtenvalidierung — KNOWN_ACTIONS Set, data.action/data.wsId/data.message Type-Checks vor Dispatch. Verhindert Message-Injection über kompromittierte Redis-Instanz.
[x] H5: Fehlende usedOTPKId in cli.js X3DH-Payloads — usedOTPKId: x3dhResult.usedOTPKId in beiden X3DH-Payloads (cmdSend + ginvite). Ohne dieses Feld wurde der 4. DH-Schritt (One-Time Pre-Key) beim Empfänger übersprungen → reduzierte Forward Secrecy bei CLI-initiierten Sessions.
[x] H6: Server Signing Key nicht validiert in index.js — AUTH_OK und DEVICE_LINK_OK Handler prüfen jetzt decodeBase64().length === 32 (Pattern aus cli.js übernommen). Verhindert Pinning eines malformierten Keys.
[x] H7: SRTP ohne Replay-Schutz — RFC 3711 64-Bit Sliding Replay Window in SrtpSession.Unprotect(). Duplikate, zu alte Pakete (>63 Positionen) und bereits gesehene Pakete werden rejected. Array.Clear → CryptographicOperations.ZeroMemory in Dispose().
[x] H8: Attachment-Keys als immutable Strings — AttachmentInfo.Key/Nonce von string auf byte[] mit Base64BytesJsonConverter umgestellt. BlobService.UploadAsync() zeroot lokale Key-Kopie nach Erstellung. MessageEnvelope.Encode/Decode konvertiert an der Wire-Boundary. Wire-Format (JSON) bleibt base64.
[x] H9: ApplyDelete chatKey-Parsing Bug — parts[0] war "place" (Prefix), nicht die placeId. Fix: parts[1] + parts.Length >= 3. Admin-Deletes für Place-Nachrichten funktionierten vorher nie (fail-closed).
[x] M1: PG Nonce-Check TOCTOU — INSERT ... ON CONFLICT DO NOTHING RETURNING nonce_key statt SELECT+INSERT. Atomisch: wenn RETURNING leer, ist der Nonce bereits gesehen.
[x] M3: Kein Rate-Limit auf BLOB_FETCH — blobFetchLimits Map (30/min pro User), Cleanup im Periodic Interval.
[x] M5: Owner kann Place verlassen — handlePlaceLeave prüft jetzt senderId === place.creatorId und sendet Error. Owner muss Ownership transferieren (noch nicht implementiert — Follow-up).
[x] M6: activeCount-Berechnung fehlerhaft — activeConnectionCount Counter statt wss.clients.size - queue.filter(...). Inkrement in setupConnection, Dekrement im Close-Handler. Queue-Admission-Entscheidung nutzt den Counter.
[x] M8: cli.js _pendingBuffer überspringt Sealed Messages — handleSealed(pm) Route für SEALED_MESSAGE/pm.sealed vor handleGM/handleDM in der Buffer-Drain-Loop.
[x] M9: Kein ANSI-Escaping in cli.js — escapeContent() Funktion (CSI/OSC/Control-Char-Filter, identisch zu index.js) + Anwendung auf alle untrusted Ausgaben (Sender-Names, Nachrichtentext, Gruppen-Names).
[x] M12: DoubleRatchet Backup nicht gezeroed bei Erfolg — ZeroBackup() Hilfsfunktion zeroot RK, CKs, CKr, DHs.SecretKey und alle MKSKIPPED-Values. Aufgerufen im Success-Pfad von Decrypt().
[x] M13: BlobService unbounded Cache — MaxCacheEntries = 100, _cacheOrder Queue für LRU-Eviction, evicted Entries werden mit CryptographicOperations.ZeroMemory gezeroed.
[x] M14: SecureTextBox kein Dispose — OnDetachedFromVisualTree Override zeroot _buffer via CryptographicOperations.ZeroMemory, disposed _bufferLock.
[x] M18: Reconnect TOCTOU — volatile bool _isReconnecting → int _isReconnecting mit Interlocked.CompareExchange(ref _isReconnecting, 1, 0) für atomisches Test-and-Set.
[x] M4: PlaceService.KickFromPlace ohne lokalen Permission-Check — HasPermission(place, Profile.UserId, PlaceRole.Admin) Guard + Self-Kick- und Owner-Kick-Schutz.
[x] M5: PlaceService.RemoveChannel ohne lokalen Permission-Check — HasPermission(place, Profile.UserId, PlaceRole.Admin) Guard.
[x] M7: PlaceService.UpdatePlaceProfile ohne Permission-Check — HasPermission(place, Profile.UserId, PlaceRole.Admin) Guard (konsistent mit UpdateRoleColors).
[x] M16: PlaceService.InviteToPlace — Dokumentiert als intentional (Discord-like Default: jedes Mitglied kann einladen). Server erlaubt es ebenfalls. Kein PlacePermission-Flag vorhanden.
[x] M17: GroupService.RekeyGroup ohne Creator-Check — group.Members[0] != Profile.UserId Guard (Legacy-Convention: Members[0] = Creator).
[x] L2: gcallTokenLimits nicht im Periodic Cleanup — Hinzugefügt zum 60s-Cleanup-Interval.
[x] L3: activeGroupCalls Stale-Cleanup — Phantom-Participants (disconnected Users) werden entfernt, leere Calls gelöscht, 4h-Timeout. Im selben Cleanup-Interval.
[x] L4: blobFetchLimits Cleanup — Hinzugefügt zum 60s-Cleanup-Interval.
[x] L5: store.cleanupBlobs() fehlender await — Hinzugefügt im Cleanup-Interval.
[x] M1: X3dh.Initiate() fehlende Low-Order-Point-Validierung — IsValidDhPublicKey() Checks für IdentityKey, SignedPreKey und OneTimePreKey vor DH-Operationen. Respond() hatte bereits Length-Checks, DH() rejectet All-Zeros Output — Initiate fehlte die Subgroup-Check-Schicht.
[x] M2: HKDF.Expand() Intermediate t nicht gezeroed — Vorheriger HMAC-Output wird jetzt per CryptoService.ZeroOut() gezeroed bevor neuer t zugewiesen wird. Finaler t nach Loop ebenfalls gezeroed.
[x] M3: gcallPseudonym ohne callId — callId wird jetzt in den HMAC-Input gemischt (PSEUDO:userId:roomName:callId). Pseudonyme sind damit über verschiedene Call-Sessions hinweg unlinkable, selbst wenn der gleiche Room wiederverwendet wird.
[x] M4: PlaceService.CreateChannel ohne Permission-Check — HasPermission(place, Profile.UserId, PlaceRole.Admin) Guard hinzugefügt. Nur Admins+ können Channels erstellen.
[x] M5: DoubleRatchet.InitSender fehlende Low-Order-Point-Validierung — IsValidDhPublicKey(recipientDHPub) Check vor Ratchet-Initialisierung. DhRatchetStep hatte den Check bereits, InitSender fehlte.
[x] M6: Nachrichten aus falscher Conversation in aktiver Chat-View — OnMessageReceived und OnGroupMessageReceived renderten Nachrichten unabhängig von der selektierten Conversation. Jetzt: AddIncomingMessage nur wenn SelectedConversation zum Sender/Gruppe passt. History-Persistierung war bereits korrekt (Service-Layer), LoadChatHistory zeigt Nachrichten beim Conversation-Wechsel.
[x] M7: UnsafeRelaxedJsonEscaping in GroupCallWindow WebView — Erlaubte <, >, ' in JSON-String der per ExecuteScriptAsync injiziert wird. Ein bösartiger Server-Response (Token/RoomName) konnte aus dem JSON-Literal ausbrechen. Fix: JavaScriptEncoder.Default (escapet HTML-sensitive Zeichen).
[x] M8: crypto.js fehlende Counter-Limits — ratchetEncrypt und senderKeyEncrypt hatten kein Limit für Ns/messageNumber. v2 C# hat MaxMessageNumber (1B/10K), v1 JS konnte unbegrenzt weiter senden. Fix: MAX_RATCHET_MESSAGE_NUMBER = 1_000_000_000, MAX_SENDER_KEY_MESSAGE_NUMBER = 10_000 mit throw bei Erschöpfung.
- Blob-Fetch ohne Zugriffskontrolle — Blobs sind E2EE (AES/nacl.secretbox), blobIds random (128-bit). Explizite ACLs würden erfordern, dass der Server Message-Routing-Metadaten persistiert (wer hat welchen blobId an wen geschickt), was die Sealed-Sender-Privacy untergraben würde. Risiko akzeptiert: Auth + Rate-Limit (30/min) + E2EE + Random-IDs sind ausreichend.
- SRTP uint32 Sequence-Overflow nach ~25h — Kein realistisches Szenario (Calls werden nicht 25h gehalten). Bei Bedarf Reconnect.
- NonceTracker Flooding — Hard-Cap (10k) + Stale-Eviction (>1h) bereits implementiert (Runde 8 H1).
- Delivery Token nicht Connection-gebunden — HMAC(secret, timestamp) ohne userId-Binding, 24h gültig. Risiko niedrig: gestohlenes Token erlaubt nur opaque Sealed Messages (ohne Ratchet-Session nicht entschlüsselbar).
- Place Member-List Client-Desync — Lokale Member-Liste aus E2EE Metadata kann bei Kick-Propagierung verzögert sein. Server-seitige Checks sind autoritativ, E2EE-Schicht bleibt korrekt.
- Metadata Bandwidth Amplification — Volle Metadata (inkl. Emotes, Bans) bei jedem Admin-Action an alle Members. Inkrementelle Updates wären besser, aber architekturell aufwändig.
- ProfileEncryption Legacy-HMAC Triple-scrypt — Bis zu 3 scrypt-Derivationen bei Legacy-Profilen. Akzeptiert: betrifft nur Migration von sehr alten Profilen.
- SecureTextBox IME-Kompatibilität — Kein IME-Support (CJK-Input). Feature-Gap, kein Security-Issue.
- Status-Broadcast O(N²) — Architekturelles Trade-off für Privacy (Server kennt keine Contact-Listen). Rate-Limited 5/min pro User (Runde 6 H5). Kein Fix ohne Subscription-Modell, das den Social Graph leaken würde.
Sender Keys Legacy-Signatur-Fallback (ohne contextId)— GEFIXT: Legacy-Fallback inSenderKeys.cs(C#) undcrypto.js(JS) entfernt. Alle Clients seit v2.17.3 signieren mit contextId. Pre-v2.17.3 Nachrichten werden nicht mehr akzeptiert.Ed25519 Release-Signatur— GEFIXT: Keypair generiert, Public Key eingetragen, Signing-Script erstellt. Siehe oben.OTP-Verbrauch vor Decrypt-Verifizierung— GEFIXT: Deferred OTP Consumption.fetchPreKeyBundlegibt OTPs zurück ohne sie zu poppen. OTP-Reservierung pro Sender+Target (5min TTL) verhindert, dass verschiedene Sender denselben OTP bekommen.consumeOTP(userId, deviceId, otpkId)wird erst inhandleMessageaufgerufen, wenn einx3dh.usedOTPKIdim tatsächlich gesendeten Message vorhanden ist. Abgelaufene Reservierungen werden im 60s-Cleanup recycled. Beide Stores (SQLitestore.js+ PostgreSQLpg-store.js) aktualisiert.