1# SophaxChat — Cryptographic Protocol Summary23> Version 1.0 · 2026-03-284> Prepared for independent security audit56All cryptographic operations use Apple's **CryptoKit** framework (backed by the Secure Enclave where available). No third-party cryptographic libraries are used.78---910## 1. Protocol Overview1112```13First contact (X3DH)14 Alice fetches Bob's PreKeyBundle15 Alice performs X3DH → 32-byte master secret16 Alice sends Hello message (encrypted)17 Bob receives Hello → derives same master secret18 │19 ▼20Session established (Double Ratchet)21 Both sides hold DR state (root key, chain keys, ratchet key pair)22 Every message advances the ratchet → new message key23 Used message keys deleted immediately → forward secrecy24 │25 ▼26Message at rest27 StoredMessage → JSON → AES-256-GCM → .enc file in Application Support28 │29 ▼30Group messages (Sender Keys v2)31 Each member has own KDF chain (HMAC-SHA256)32 Sender key exchanged via DR channel on join / member change33```3435---3637## 2. Identity Keys3839### Generation40On first launch, `IdentityManager.loadOrCreateKeys()` generates:41-**Ed25519 signing key pair** — `Curve25519.Signing.PrivateKey()` (system CSPRNG via SecRandomCopyBytes)42-**X25519 DH key pair** — `Curve25519.KeyAgreement.PrivateKey()` (system CSPRNG)4344Both private keys are stored in Keychain with `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`. They are never written to disk outside the Keychain.4546### peerID Derivation47```48peerID = SHA256(signingKeyPublic ∥ dhKeyPublic)[0..<16].hexString // 32 hex chars49```5051### Safety Number52```53safetyNumber = SHA512(signingKeyPublic ∥ dhKeyPublic)54 → 60 decimal digits in groups of 555 → displayed for out-of-band verification56```5758---5960## 3. Prekeys (X3DH Setup Material)6162**File:**`Sources/SophaxChatCore/Crypto/PreKeyManager.swift`6364### Prekey types65| Type | Count | Rotation |66|------|-------|----------|67| Signed Prekey (SPK) | 1 | Monthly (or on demand) |68| One-Time Prekeys (OPK) | 10 initially, top up as consumed | Per-use (consumed on first DH) |6970### PreKeyBundle (broadcast in Hello message)71```72PreKeyBundle {73 peerID: String // SHA256(signing ∥ dh)[:16]74 username: String75 signingKeyPublic: Data // Ed25519 32B76 dhIdentityKeyPublic: Data // X25519 32B77 signedPreKeyPublic: Data // X25519 32B78 signedPreKeySignature: Data // Ed25519 signature of SPK79 oneTimePreKeyPublic: Data? // X25519 32B (nil if exhausted)80 oneTimePreKeyID: String? // UUID of consumed OPK81 tcpAddress: String? // optional host:port for TCP82 avatarData: Data? // optional JPEG 64×6483}84```8586SPK signature: `Ed25519.sign(signedPreKeyPublic)` — verifies the SPK was generated by the claimed identity.8788---8990## 4. Session Initiation — Extended Triple Diffie-Hellman (X3DH)9192**File:**`Sources/SophaxChatCore/Crypto/X3DH.swift`9394Reference: [Signal X3DH Specification](https://signal.org/docs/specifications/x3dh/)9596### Sender (Alice) computes:97```98DH1 = X25519(Alice.IK_priv, Bob.SPK_pub)99DH2 = X25519(Alice.EK_priv, Bob.IK_pub)100DH3 = X25519(Alice.EK_priv, Bob.SPK_pub)101DH4 = X25519(Alice.EK_priv, Bob.OPK_pub) // omitted if no OPK available102103masterSecret = HKDF-SHA256(104 ikm = 0xFF×32 ∥ DH1 ∥ DH2 ∥ DH3 [∥ DH4],105 salt = 0x00×32,106 info = "SophaxChat X3DH v1"107)108```109110DH material is zeroed from memory immediately after deriving masterSecret.111112The `0xFF×32` prefix follows the Signal spec to domain-separate the X3DH from other HKDF usages and prevent length extension.113114### Receiver (Bob) computes:115Identical calculation with mirrored key roles; same masterSecret is derived.116117### Associated data118```119AD = signingKeyPublic(Alice) ∥ signingKeyPublic(Bob)120```121Used as AEAD associated data in the initial Hello message.122123### Hello message format124The first message (`.hello` WireMessageType) carries:125- Alice's PreKeyBundle (so Bob can verify her identity)126- The `senderEphemeralPublicKey` (Alice.EK_pub)127- The `consumedOPKID` so Bob can delete the used one-time prekey128- An encrypted `MessageContent` sealed with the derived masterSecret129130---131132## 5. Double Ratchet Algorithm133134**File:**`Sources/SophaxChatCore/Crypto/DoubleRatchet.swift`135136Reference: [Signal Double Ratchet Specification](https://signal.org/docs/specifications/doubleratchet/)137138### State139```swift140struct DoubleRatchet {141 var rootKey: Data // 32B142 var sendingChainKey: Data // 32B143 var receivingChainKey: Data? // 32B144 var sendRatchetKeyPair: X25519KeyPair // 32B private + 32B public145 var receiveRatchetPublicKey: Data? // 32B146 var sendMessageNumber: UInt32147 var receiveMessageNumber: UInt32148 var previousSendChainLen: UInt32149 // skipped message key cache (bounded, in-memory + Keychain)150}151```152153### Ratchet steps154155**Symmetric ratchet (chain key advance):**156```157messageKey = HMAC-SHA256(chainKey, 0x01) → 32B → SymmetricKey158nextChainKey = HMAC-SHA256(chainKey, 0x02)159```160161**DH ratchet (on new public key from peer):**162```163(newRootKey, newReceivingCK) = HKDF-SHA256(164 ikm = rootKey ∥ X25519(sendRatchetKey_priv, peerRatchetKey_pub),165 info = "SophaxChat DR v1"166)167```168169### Header encryption170Ratchet headers are encrypted with a separate header key (derived during ratchet init) so relay nodes cannot observe message numbers or ratchet public keys.171172### Sealed sender173```174sealedEnvelope = ECIES(175 recipientDHPublicKey,176 ephemeralKeyPair,177 plaintext = WireMessage,178 info = "SophaxChat sealed sender"179)180```181The sender's peerID is included inside the encrypted envelope rather than in the outer header.182183### Out-of-order messages184Skipped message keys are cached in memory (bounded to `MAX_SKIP = 100` per sender, 200 total). Cache is also persisted to Keychain with 7-day expiry to survive app restarts.185186### Message encryption187```188(nonce, ciphertext, tag) = ChaCha20-Poly1305.seal(189 plaintext = MessageContent JSON,190 key = messageKey (32B),191 associatedData = "groupID/senderPeerID" (group) or empty (DM)192)193194wirePayload = nonce ∥ ciphertext ∥ tag195```196197---198199## 6. Group Encryption — Sender Keys v2200201**Files:**`Sources/SophaxChatCore/Group/GroupTypes.swift`, `Sources/SophaxChatCore/ChatManager.swift`202203Reference: [Signal Sender Keys](https://signal.org/docs/specifications/senderkeys/)204205### Per-member KDF chain206Each group member maintains their own sender key chain:207```208messageKey_n = HMAC-SHA256(chainKey_n, 0x01) → 32B SymmetricKey209chainKey_{n+1} = HMAC-SHA256(chainKey_n, 0x02)210```211`iteration` tracks how many steps have been consumed. The current `chainKey` is stored in Keychain (`"skd.mine.<groupID>"`).212213### Key distribution214When a member joins or a key rotation occurs, a `SenderKeyDistributionMessage` is sent via the DR-encrypted channel:215```swift216SenderKeyDistributionMessage {217 groupID: String218 chainKey: Data // current 32B chain key seed219 iteration: UInt32 // steps already consumed from this chain220}221```222223### Bidirectional exchange224When member X receives an SKD from member Y they haven't seen before, X immediately replies with their own current SKD via the DR channel. This ensures convergence without a separate Welcome message structure.225226### Key rotation on member leave227When a member leaves:2281. They broadcast `GroupMemberLeftMessage`2292. Each remaining member generates a new random 32-byte chain key (iteration reset to 0)2303. New SKD is sent to all remaining members via DR231232### Replay protection233`handleSenderKeyDistribution` rejects distributions where `iteration < existingState.iteration`.234235### Skipped key cache (out-of-order group messages)236Skipped group message keys are cached in memory and persisted to Keychain account `"skd.skipped.all"`, bounded to 200 entries, with 7-day automatic eviction.237238---239240## 7. Message Storage at Rest241242**File:**`Sources/SophaxChatCore/Storage/MessageStore.swift`243244```245Application Support/sophax_conversations/<peerID>.enc246```247248```249nonce (12B) ∥ ciphertext ∥ GCM-tag (16B)250```251252The master storage key is a 256-bit `SymmetricKey` stored in Keychain under `"message.store.key"`. Generated randomly on first use.253254Each conversation is a separate file. File is completely re-encrypted on every write. Files are excluded from iCloud backup via `URLResourceValues.isExcludedFromBackup`.255256---257258## 8. Attachment Storage at Rest259260**File:**`Sources/SophaxChatCore/Storage/AttachmentStore.swift`261262```263Application Support/sophax_attachments/<UUID>264```265266Same master key as MessageStore (`"message.store.key"`). Each attachment is a separate AES-256-GCM encrypted file. Referenced by UUID from `StoredMessage.attachmentID`.267268---269270## 9. Backup Encryption271272**File:**`Sources/SophaxChatCore/Storage/BackupManager.swift`273274Exports: messages, contacts, username, identity fingerprint. **Does not export private keys** (device-bound by design) — except for identity backup (see § 10).275276```277"SXBK" (4B) ∥ 0x01 (version, 1B) ∥ salt (32B) ∥ nonce (12B) ∥ AES-256-GCM(ciphertext ∥ tag)278```279280Key derivation:281```282backupKey = PBKDF2-HMAC-SHA256(283 password = passphrase (UTF-8),284 salt = random 32B,285 iterations = 600 000,286 keyLen = 32287)288```289290600 000 iterations makes brute-force ~600 000× harder than a single hash. Minimum passphrase: 16 characters (enforced in UI).291292---293294## 10. Identity Backup Encryption295296**File:**`Sources/SophaxChatCore/Crypto/IdentityExport.swift`297298Exports private identity keypair for cross-device restore.299300```301"SXID" (4B) ∥ 0x01 (version, 1B) ∥ salt (32B) ∥ nonce (12B) ∥ AES-256-GCM(ciphertext ∥ tag)302```303304Ciphertext contains JSON: `{signingKeyPrivate, dhKeyPrivate, username, exportedAt}`305306Same KDF as backup (PBKDF2-HMAC-SHA256, 600 000 iterations). Minimum passphrase: 12 characters.307308**Security note:** Importing an identity backup replaces the current Keychain keys. All existing DR sessions become invalid (different key material). Peers will see a key change alert and must re-verify Safety Numbers.309310---311312## 11. Key Storage Summary313314| Key | Keychain account | Type | Accessible |315|-----|-----------------|------|-----------|316| Ed25519 signing private | `identity.signing` | 32B raw | WhenUnlockedThisDeviceOnly |317| X25519 DH private | `identity.dh` | 32B raw | WhenUnlockedThisDeviceOnly |318| Username | `user.username` | UTF-8 | WhenUnlockedThisDeviceOnly |319| Avatar JPEG | `identity.avatar` | Data | WhenUnlockedThisDeviceOnly |320| Signed prekey (current) | `prekey.signed.current` | JSON | WhenUnlockedThisDeviceOnly |321| One-time prekeys | `prekey.opk.<id>` | JSON | WhenUnlockedThisDeviceOnly |322| DR session state (per peer) | `dr.session.<peerID>` | JSON | WhenUnlockedThisDeviceOnly |323| Message storage master key | `message.store.key` | 32B SymmetricKey | WhenUnlockedThisDeviceOnly |324| App lock enabled flag | `settings.applock` | 1B | WhenUnlockedThisDeviceOnly |325| My sender key (per group) | `skd.mine.<groupID>` | JSON SenderKeyState | WhenUnlockedThisDeviceOnly |326| Peer sender keys (per group) | `skd.peers.<groupID>` | JSON dict | WhenUnlockedThisDeviceOnly |327| Skipped group msg keys | `skd.skipped.all` | JSON | WhenUnlockedThisDeviceOnly |328| Verified peers | `verified.peers` | JSON dict | WhenUnlockedThisDeviceOnly |329330All items use `kSecAttrAccessibleWhenUnlockedThisDeviceOnly`: not migratable, not iCloud-backed, accessible only when device is unlocked.331332---333334## 12. Transport Security335336### MultipeerConnectivity337Apple's MCSession establishes an authenticated, encrypted session between peers. Certificate-based authentication is used internally by the framework. SophaxChat adds application-layer E2EE on top.338339### Tor hidden services340Messages routed over Tor use `.onion` v3 addresses derived from the Ed25519 identity key:341```342onionHostname = Base32(SHA3-256(Ed25519PublicKey) ∥ version(0x03) ∥ checksum) + ".onion"343```344Tor provides anonymity at the network layer. SophaxChat E2EE provides confidentiality at the application layer.345346### TCP (mDNS / LAN)347Direct TCP connections over local network use application-layer E2EE (same wire protocol as Bluetooth). No TLS layer is added — encryption is entirely at the application level.348349---350351## 13. Random Number Generation352353All key generation and nonce generation uses system CSPRNG:354-`Curve25519.Signing.PrivateKey()` — calls `SecRandomCopyBytes` internally (Apple CryptoKit)355-`Curve25519.KeyAgreement.PrivateKey()` — same356-`SymmetricKey(size:)` — same357-`ChaChaPoly.Nonce()` — same358-`AES.GCM.Nonce()` — same359360No custom PRNG or seeding is performed.361362---363364## 14. Recommended Review Files365366| Priority | File | Contents |367|----------|------|----------|368| P0 | `Sources/SophaxChatCore/Crypto/X3DH.swift` | X3DH sender/receiver |369| P0 | `Sources/SophaxChatCore/Crypto/DoubleRatchet.swift` | DR algorithm |370| P0 | `Sources/SophaxChatCore/ChatManager.swift` | sealMessage, unsealMessage, handleHello, handleChatMessage |371| P1 | `Sources/SophaxChatCore/Group/GroupTypes.swift` | SenderKeyState, KDF |372| P1 | `Sources/SophaxChatCore/Crypto/PreKeyManager.swift` | X3DH prekey management |373| P1 | `Sources/SophaxChatCore/Crypto/KeychainManager.swift` | All key storage |374| P1 | `Sources/SophaxChatCore/Storage/BackupManager.swift` | Backup encryption |375| P1 | `Sources/SophaxChatCore/Crypto/IdentityExport.swift` | Identity key export |376| P2 | `Sources/SophaxChatCore/Network/NetworkProtocol.swift` | Wire message types |377| P2 | `Sources/SophaxChatCore/Storage/MessageStore.swift` | Message storage |