1# Security Architecture & Review23*Last reviewed: 2026-04-04. Reviewed against source code; not an independent external audit.*45---67## Threat Model89SophaxChat is designed to protect against:1011| Threat | Mitigation |12|--------|-----------|13| Passive eavesdropping (network sniffing) | End-to-end encryption (Double Ratchet + ChaCha20-Poly1305) |14| Active MITM attack | Ed25519 signatures on every message; Safety Number QR verification |15| Server compromise | No servers — P2P only |16| Compromised device (past messages) | Forward secrecy (per-message keys via symmetric ratchet) |17| Compromised device (future messages) | Break-in recovery (Double Ratchet DH ratchet with fresh Curve25519 keys) |18| Metadata analysis | Anonymous identity (no phone/email); peerID = hash of identity keys |19| Replay attacks | Message authentication + unique nonces; message-ID deduplication at storage layer |20| Key exhaustion / DoS | Bounded skipped-key cache (max 1000); relay rate limiting (20/10s per peer) |21| Message forgery | Ed25519 signatures on all wire messages |22| Sender identity leakage to relay nodes | Sealed sender: ECDH(ephemeral, recipient DH) → ChaCha20-Poly1305 wraps inner message |23| Header metadata leakage to relay nodes | Header Encryption (HE) variant of Double Ratchet — headers sealed with rotating header keys |24| Physical data-at-rest access | AES-256-GCM encrypted message store + private keys in Keychain |25| iCloud backup exfiltration | `kSecAttrAccessibleWhenUnlockedThisDeviceOnly` — keys never backed up or transferred |26| App switcher screenshot | Blur overlay on `willResignActive` |27| App access without biometric | App lock (Face ID / Touch ID / passcode, auto-lock on background) |28| DoS via oversized messages | Attachment ≤ 512 KB; username ≤ 64 chars |29| Prekey exhaustion | OTPK pool auto-replenished when below 50%; SPK rotated every 7 days |3031---3233## Cryptographic Primitives3435All primitives are from Apple's **[CryptoKit](https://developer.apple.com/documentation/cryptokit)** (audited, hardware-accelerated):3637| Layer | Operation | Algorithm | Key Size |38|-------|-----------|-----------|----------|39| Identity | Signing | Ed25519 | 256-bit |40| Identity | Key agreement | X25519 | 256-bit |41| Session init | Key exchange | X3DH (Signal spec) | — |42| Session init | Key derivation | HKDF-SHA256 | 256-bit |43| Messaging | Symmetric ratchet | HMAC-SHA256 | 256-bit |44| Messaging | DH ratchet | X25519 | 256-bit |45| Messaging | AEAD encryption | ChaCha20-Poly1305 | 256-bit |46| Header encryption | AEAD | ChaCha20-Poly1305 | 256-bit |47| Sealed sender | Key derivation | HKDF-SHA256 | 256-bit |48| Sealed sender | AEAD | ChaCha20-Poly1305 | 256-bit |49| Group Sender Keys | KDF chain | HMAC-SHA256 | 256-bit |50| Group encryption | AEAD | ChaCha20-Poly1305 | 256-bit |51| Storage | At-rest encryption | AES-256-GCM | 256-bit |52| Identity verification | Safety numbers | SHA-512 | — |53| Random number generation | OS CSPRNG | (via CryptoKit) | — |5455---5657## Protocol Details5859### Session Establishment (X3DH)6061Based on the [Signal X3DH specification](https://signal.org/docs/specifications/x3dh/).6263```64Alice Bob65──────────────────────────────────────────────────────66 ←─── Hello (Bob's PreKeyBundle) ────6768Alice computes:69 EK_A = ephemeral key pair70 DH1 = DH(IK_A, SPK_B) — identity auth71 DH2 = DH(EK_A, IK_B) — ephemeral x identity72 DH3 = DH(EK_A, SPK_B) — ephemeral x signed prekey73 DH4 = DH(EK_A, OPK_B) [if OPK available]74 SK = HKDF(0xFF×32 ∥ DH1 ∥ DH2 ∥ DH3 [∥ DH4])7576 ──── InitiateSession (EK_A, first DR message) ────→7778Bob computes: same DH operations symmetrically → same SK79```8081### Message Encryption (Double Ratchet with Header Encryption)8283Based on the [Signal Double Ratchet specification §4.3](https://signal.org/docs/specifications/doubleratchet/).8485-**DH Ratchet**: New Curve25519 key pair generated on each direction change. Provides break-in recovery.86-**Symmetric Ratchet**: `KDF_CK(chainKey)` via HMAC-SHA256. Provides per-message forward secrecy.87-**Header Encryption**: `RatchetHeader` (ratchet public key, counters) is sealed with rotating header keys derived from the same HKDF chain. Relay nodes see only opaque ciphertext.88-**Body AEAD**: `ChaCha20-Poly1305(messageKey, plaintext, AAD = associatedData ∥ encryptedHeader)` — header and body are cryptographically bound.8990### Sealed Sender9192```93Sender generates ephemeral key pair (EK_s).94sharedSecret = ECDH(EK_s_private, recipient_DH_public)95sealingKey = HKDF-SHA256(sharedSecret, info="SophaxChat_SealedSender_v1", len=32)96encryptedPayload = ChaChaPoly(sealingKey, JSON(innerWireMessage))97```9899Relay nodes see origin + target + `EK_s_public` + ciphertext. The inner sender identity is revealed only to the recipient.100101### Group Messaging (Sender Keys v2)102103Each group member independently maintains a KDF chain:104105```106messageKey_n = HMAC-SHA256(chainKey_n, 0x01) — encrypt/decrypt one message107chainKey_{n+1} = HMAC-SHA256(chainKey_n, 0x02) — advance chain108```109110The sender's chain key is distributed to all group members via the per-peer Double Ratchet channel (so only authenticated members can read it). Group messages are independently encrypted by each sender using their current message key, then broadcast to all members.111112Properties:113-**Per-message forward secrecy**: Compromising `chainKey_n` exposes only messages ≥ n.114-**Break-in recovery**: Each sender independently ratchets; no shared state to compromise.115-**No transcript server**: Messages flood the local mesh; only members with valid chain keys can decrypt.116117### Wire Message Authentication118119Every network message is signed with the sender's Ed25519 identity key:120121```122signingBytes = type ∥ payload ∥ senderID ∥ ISO8601(timestamp)123signature = Ed25519.sign(signingBytes, with: signingPrivateKey)124```125126Recipients verify this signature before processing. Session-initiation messages are self-authenticating (the verification key is carried inline in the bundle).127128---129130## Key Storage131132| Key Material | Keychain Account | Access Control |133|---|---|---|134| Ed25519 identity signing key | `identity.signing` | WhenUnlockedThisDeviceOnly |135| X25519 identity DH key | `identity.dh` | WhenUnlockedThisDeviceOnly |136| Signed prekey | `spk.<id>` | WhenUnlockedThisDeviceOnly |137| One-time prekeys | `otpk.<id>` | WhenUnlockedThisDeviceOnly |138| Double Ratchet session state | `session.<peerID>` | WhenUnlockedThisDeviceOnly |139| Message storage master key | `storage.master` | WhenUnlockedThisDeviceOnly |140| Group sender key (my chain) | `skd.mine.<groupID>` | WhenUnlockedThisDeviceOnly |141| Group peer sender keys | `skd.peers.<groupID>` | WhenUnlockedThisDeviceOnly |142| Encrypted messages | App filesystem | AES-256-GCM encrypted |143144`kSecAttrAccessibleWhenUnlockedThisDeviceOnly`:145- Keys are **not** backed up to iCloud146- Keys are **not** transferred when setting up a new device147- Keys are **not** accessible when the device is locked148- Keys are wiped on factory reset149150---151152## Security Review Findings153154### HIGH155156#### H-1 — No group re-keying on member change157When a member leaves a group, remaining members continue using their existing sender key chains. The departed member retains all chain states from the time of departure and can decrypt any group traffic they subsequently capture. **There is no automatic re-keying on member leave or removal.**158159*Mitigation required:* After a member change, all remaining members should generate fresh sender keys and redistribute them via the DR channel.160161---162163### MEDIUM164165#### M-1 — Group sender key: skipped messages are irrecoverable166The receiver fast-forwards the sender chain up to `MAX_SKIP = 100` iterations for out-of-order delivery, but discards intermediate message keys (not cached). Any message that arrives after being skipped over cannot be decrypted.167168*Recommendation:* Cache skipped message keys in a bounded table (as the Double Ratchet layer already does).169170#### M-2 — Sender key distribution may arrive after messages in high-load scenarios171When a group member joins, their sender key distribution (SKD) is sent via the DR channel to all members. If the joiner's first group messages arrive before the SKD (possible in a high-traffic mesh), those messages cannot be decrypted.172173*Recommendation:* Include the current chain state (not freshly generated) in subsequent SKD resends; or hold outgoing group messages briefly until SKD delivery is confirmed.174175#### M-3 — One-time prekey exhaustion window176If Bob's OTPK pool is empty, Alice initiates X3DH without DH4, removing the one-time component from the session secret. Auto-replenishment occurs when the pool falls below 50%, but there is a brief window of reduced security.177178#### M-4 — Notification previews may expose content on lock screen179`UNNotificationContent` previews are shown on the lock screen by default, potentially revealing message content before the user unlocks the app.180181*Recommendation:* Set `hiddenPreviewsBodyPlaceholder` and use `UNNotificationContent.threadIdentifier` to group without revealing content.182183---184185### LOW186187#### L-1 — No transcript consistency for groups188Decentralized group messaging cannot guarantee all members receive the same messages in the same order without a trusted coordinator or threshold scheme. A malicious member can selectively withhold or reorder messages.189190#### L-2 — Group message timestamps are sender-controlled191`GroupWireMessage.timestamp` is set by the sender. A malicious or clock-skewed sender can cause messages to appear in the wrong order in the UI.192193#### L-3 — `ISO8601DateFormatter` allocated per `signingBytes()` call194Performance micro-issue; no security impact.195196---197198## What SophaxChat Does NOT Protect Against199200-**Traffic analysis**: Packet timing and size correlations on the mesh can reveal communication patterns.201-**Endpoint compromise**: A compromised device has access to all Keychain material and plaintext in memory.202-**Selective delivery attacks**: Group members can selectively withhold messages from other members.203-**Identity bootstrapping (TOFU)**: First contact is trust-on-first-use. Safety Number QR scan is the only out-of-band verification mechanism.204-**Physical device seizure**: Bypasses all software-layer protections.205-**Bluetooth range limitations**: Mesh is limited to ~100m (WiFi Direct) or ~30m (Bluetooth LE).206-**No key recovery**: If a device is wiped or lost, all message history and identity keys are permanently gone. There is intentionally no backup or recovery mechanism.207208---209210## 2026-04 Internal Audit — Fixed Findings211212The following findings were identified and addressed during an internal review conducted ahead of a planned third-party audit. All HIGH findings are fixed.213214### HIGH — Fixed215216#### H1 · Integer underflow in Double Ratchet skip-key calculation217**File:**`Sources/SophaxChatCore/Crypto/DoubleRatchet.swift`218**Status:** ✅ Fixed219220A malicious peer could send a message with `messageNumber < receiveMessageCount`. The original signed-`Int` subtraction produced a negative `totalSkipped` value that bypassed the `maxSkippedMessages` guard, potentially triggering unbounded key derivation (memory exhaustion / DoS).221222**Fix:** Changed to UInt32 subtraction after an explicit `guard target > state.receiveMessageCount` check, making the arithmetic inherently non-negative.223224#### H2 · Timing side-channel in PIN verification225**File:**`Sources/SophaxChatCore/Crypto/KeychainManager.swift`226**Status:** ✅ Fixed227228Both `verifyRealLockPIN` and `verifyDuressPIN` used Swift's `Data ==` operator, which short-circuits on the first differing byte. An attacker with physical device access could brute-force a 4-digit PIN faster via timing analysis.229230**Fix:** Replaced with `timingsafe_bcmp` — the same constant-time comparison already used in `SerializableSymmetricKey` (CryptoTypes.swift).231232#### H3 · TCP frame size: UInt32 → Int conversion before bounds check233**File:**`Sources/SophaxChatCore/Network/TCPTransport.swift`234**Status:** ✅ Fixed235236The raw `UInt32` frame length was implicitly converted to `Int` before comparison against `maxFrameSize`, obscuring overflow risk and making the guard's safety property non-obvious.237238**Fix:** Added an explicit `guard Int(lengthU32) <= TCPTransport.maxFrameSize` check before using the value, then assigned to a named `length: Int` constant.239240---241242### MEDIUM — Fixed243244#### M1 · Prekey bundle replay window too wide (24 hours)245**File:**`Sources/SophaxChatCore/Crypto/CryptoTypes.swift`246**Status:** ✅ Fixed247248`maxPreKeyBundleAge` was 86 400 s (24 h), giving a passive adversary a full day to replay a captured bundle. Reduced to 3 600 s (1 h).249250#### M3 · No rate limiting for incoming typing / reaction messages251**File:**`Sources/SophaxChatCore/ChatManager.swift`252**Status:** ✅ Fixed253254`.typing` and `.reaction` wire messages were processed unconditionally, allowing a malicious peer to flood the UI thread.255256**Fix:** Added a per-peer token bucket (`TokenBucket` struct): `.typing` — 10 events / 10 s; `.reaction` — 20 events / 10 s. Excess messages are silently dropped before decoding.257258---259260### MEDIUM — Deferred261262#### M2 · DHConcat intermediates not explicitly zeroed263**File:**`Sources/SophaxChatCore/Crypto/X3DH.swift`264**Status:** ⚠️ Deferred265266CryptoKit's `SharedSecret` does not expose a public zeroing API. Deferred until CryptoKit provides a zeroing interface or a third-party auditor identifies a concrete exploitation path on iOS 17+.267268---269270### LOW — Fixed271272#### L2 · No length guard on Keychain account keys273**File:**`Sources/SophaxChatCore/Crypto/KeychainManager.swift`274**Status:** ✅ Fixed — `guard account.count <= 256` added before Keychain API calls.275276#### L3 · DoubleRatchet importState: no pre-check on JSON data size277**File:**`Sources/SophaxChatCore/Crypto/DoubleRatchet.swift`278**Status:** ✅ Fixed — `guard data.count < 100_000` added before `JSONDecoder` call.279280---281282### LOW — Deferred283284#### L1 · MessageStore filenames derived from raw peerID285**File:**`Sources/SophaxChatCore/Storage/MessageStore.swift`286**Status:** ⚠️ Deferred — changing the filename scheme (e.g. to SHA256(peerID)) would break existing installations without a migration path. Risk is low: the directory is under `FileProtectionType.complete` (encrypted at rest, inaccessible when device is locked). Will be addressed alongside a planned storage-layer migration.287288---289290## Responsible Disclosure291292Please **do not open public GitHub issues** for security vulnerabilities.293294Use **[GitHub Security Advisories](https://github.com/richardmetzner/SophaxChat/security/advisories/new)** — private, end-to-end encrypted between reporter and maintainers, no central server involved.295296We aim to respond within 72 hours. Coordinated disclosure window: 90 days.