SophaxChat · the code
The journey of one message
From the moment you press send to the moment it appears on someone else’s phone — every step, and the lines of code that take it.
-
01
You press send
The words leave the box on the screen. The app empties it, stops the “typing…” sign and hands the text on.
private func sendMessage() { let text = vm.messageText.trimmingCharacters(in: .whitespacesAndNewlines) guard !text.isEmpty else { return } UIImpactFeedbackGenerator(style: .light).impactOccurred() vm.typingTask?.cancel() vm.typingTask = nil appState.sendTypingIndicator(toPeerID: peer.id, isTyping: false) vm.messageText = "" vm.clearDraft() if let editing = vm.editingMessage { withAnimation { vm.editingMessage = nil } appState.sendEditMessage(messageID: editing.id, newBody: text, toPeerID: peer.id) } else { let reply = vm.replyingTo withAnimation { vm.replyingTo = nil } let expiresAt = vm.disappearingInterval.seconds.map { Date().addingTimeInterval($0) } appState.sendMessage(text, toPeerID: peer.id, expiresAt: expiresAt, replyToID: reply?.id) } }ChatView.swift · lines 675–693 -
02
Kept, then passed on
Before anything else, the message is saved on your own phone, so it is never lost. Then it goes to the core of the app, where all the security lives.
public func sendMessage(_ text: String, toPeerID peerID: String, expiresAt: Date? = nil, replyToID: String? = nil) { guard !text.isEmpty, text.utf8.count <= Self.maxMessageBytes else { delegate?.chatManager(self, didEncounterError: SophaxError.invalidMessageFormat("Message must be 1–65536 bytes")) return } let messageID = UUID().uuidString let stored = StoredMessage( id: messageID, peerID: peerID, direction: .sent, body: text, status: .sending, replyToID: replyToID ) do { try messageStore.append(message: stored) } catch { CrashLogManager.shared.log(error, context: "MessageStore") delegate?.chatManager(self, didEncounterError: error) return } forwardToLinkedDevices(stored) DispatchQueue.main.async { [weak self] in guard let self else { return } self.delegate?.chatManager(self, didSendMessage: stored, toPeer: peerID) } do { let content = MessageContent(body: text, replyToID: replyToID, expiresAt: expiresAt) let wire = try buildOutboundWire(content: content, messageID: messageID, toPeerID: peerID) try sendOrQueue(wire, toPeerID: peerID, messageID: messageID) } catch SophaxError.sessionStateCorrupted { // Corrupt session was cleared in withSession(). Broadcast Hello so the // peer re-initiates X3DH; the user should retry sending after reconnection. CrashLogManager.shared.log("Session state corrupted on send — cleared, broadcasting Hello", context: "DR") try? messageStore.updateStatus(.failed, forMessageID: messageID, peerID: peerID) broadcastHello() delegate?.chatManager(self, didEncounterError: SophaxError.sessionStateCorrupted) } catch { CrashLogManager.shared.log(error, context: "MessageSend") try? messageStore.updateStatus(.failed, forMessageID: messageID, peerID: peerID) delegate?.chatManager(self, didEncounterError: error) } }ChatManager.swift · lines 778–820 -
03
Locked with a key used once
Every message gets its own key, made from the key before it, which is then forgotten — so a key taken today does not open yesterday’s messages. Even the label that says which key was used is locked.
/// Encrypt a plaintext message. /// - Parameters: /// - plaintext: Raw message bytes /// - associatedData: Additional data to authenticate (e.g. sorted peer IDs) /// - Returns: Encrypted `RatchetMessage` ready to send public func encrypt(plaintext: Data, associatedData: Data = Data()) throws -> RatchetMessage { guard let ck = state.sendingChainKey?.key else { throw SophaxError.missingChainKey } guard let hks = state.sendingHeaderKey?.key else { throw SophaxError.missingChainKey } // Advance the sending chain let (newCK, mk) = Self.kdfCK(ck) state.sendingChainKey = SerializableSymmetricKey(newCK) guard state.sendMessageCount < CryptoConstants.maxChainMessages else { throw SophaxError.counterOverflow } let header = RatchetHeader( senderRatchetKey: state.sendingRatchetPublicKey, previousChainLength: state.previousSendingChainLength, messageNumber: state.sendMessageCount ) state.sendMessageCount += 1 let encryptedHeader = try encryptHeader(header, using: hks) let ciphertext = try encryptBody(mk, plaintext: plaintext, encryptedHeader: encryptedHeader, associatedData: associatedData) return RatchetMessage(encryptedHeader: encryptedHeader, ciphertext: ciphertext) }DoubleRatchet.swift · lines 164–194 /// KDF_CK(ck) → (new_chain_key, message_key) /// HMAC-SHA256(ck, 0x02) = next chain key /// HMAC-SHA256(ck, 0x01) = message key static func kdfCK(_ chainKey: ChainKey) -> (ChainKey, MessageKey) { let newCKBytes = Data(HMAC<SHA256>.authenticationCode(for: Data([0x02]), using: chainKey)) let mkBytes = Data(HMAC<SHA256>.authenticationCode(for: Data([0x01]), using: chainKey)) return (SymmetricKey(data: newCKBytes), SymmetricKey(data: mkBytes)) }DoubleRatchet.swift · lines 462–469 -
04
The first time: a handshake
If you have never written to this person, the two phones first agree on a secret no one else can know (X3DH), using keys the other phone shared earlier. The first message travels with the handshake.
// ── Case 2: new session — need peer's PreKeyBundle for X3DH ────────── guard let bundle = peerBundles[peerID] else { throw SophaxError.sessionNotInitialized } let x3dhResult = try X3DH.initiateSender( senderIdentity: identity.dhKeyPair, recipientBundle: bundle ) let ratchet = try DoubleRatchet.initAsInitiator( sharedSecret: x3dhResult.sharedSecret, remoteRatchetPublicKey: bundle.signedPreKeyPublic ) let ratchetMsg = try ratchet.encrypt(plaintext: plaintext, associatedData: ad) try storeNewSession(ratchet, peerID: peerID) let senderBundle = try preKeys.generateBundle() let initPayload = InitiateSessionMessage( senderBundle: senderBundle, ephemeralPublicKey: x3dhResult.ephemeralPublicKey, usedSignedPreKeyId: bundle.signedPreKeyId, usedOneTimePreKeyId: x3dhResult.usedOneTimePreKeyId, initialMessage: ratchetMsg, pqEncapsulatedKey: x3dhResult.pqEncapsulatedKey ) return try wireBuilder.build(.initiateSession, payload: initPayload)ChatManager.swift · lines 1774–1799 -
05
Signed
The locked message goes into an envelope signed with your identity key. Anyone can check the signature; no one can forge it.
/// Build a signed WireMessage. /// Uses a single `Date()` snapshot so signing bytes and the final /// message carry the SAME timestamp — avoiding the previous bug where /// two separate `init` calls produced two different timestamps. public func build<T: Codable>(_ type: WireMessageType, payload: T) throws -> WireMessage { let payloadData = try JSONEncoder().encode(payload) let senderID = identity.publicIdentity.peerID let timestamp = Date() // ← captured ONCE // Construct unsigned message to compute the canonical bytes to sign let unsigned = WireMessage( type: type, payload: payloadData, senderID: senderID, timestamp: timestamp, signature: Data() ) let signature = try identity.sign(unsigned.signingBytes()) // Return the final message with the same timestamp return WireMessage( type: type, payload: payloadData, senderID: senderID, timestamp: timestamp, signature: signature ) }NetworkProtocol.swift · lines 847–868 -
06
Choosing the way
Straight to the other phone over the internet or Tor if it can, straight over Bluetooth or Wi‑Fi if they are near — otherwise through other people’s phones, or kept until they are back.
/// Send a wire message: direct → relay → offline queue (in priority order). func sendOrQueue( _ wire: WireMessage, toPeerID peerID: String, messageID: String ) throws { // ── TCP direct path (internet / Tor) ────────────────────────────────── if let tcp = tcpTransport, tcp.isConnected(peerID: peerID) { try tcp.send(wire, toPeerID: peerID) return } if mesh.isConnected(peerID: peerID) { // ── BLE/WiFi direct path ────────────────────────────────────────── try mesh.send(wire, toPeerID: peerID) } else if mesh.directPeerCount > 0 {ChatManager.swift · lines 1804–1820 // ── No connectivity: queue for later ────────────────────────────── var queue = pendingQueue[peerID, default: []] guard queue.count < Self.maxQueuedMessagesPerPeer else { throw SophaxError.invalidMessageFormat("Offline message queue is full — reconnect before sending more") } queue.append((wire: wire, messageID: messageID)) pendingQueue[peerID] = queue persistQueue()ChatManager.swift · lines 1873–1880 -
07
Sealed for the phones in between
To pass through other people’s phones, it is sealed once more, for the recipient alone. The phones on the way see where it is going, but cannot read it or tell what kind of message it is.
private func sealWireMessage(_ wire: WireMessage, recipientDHPublicKey: Data) throws -> SealedMessage { // Generate ephemeral key pair let ephPair = DHKeyPair() let recipKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: recipientDHPublicKey) let shared = try ephPair.privateKey.sharedSecretFromKeyAgreement(with: recipKey) // Derive sealing key var ikmData = Data() shared.withUnsafeBytes { ikmData.append(contentsOf: $0) } let sealingKey = HKDF<SHA256>.deriveKey( inputKeyMaterial: SymmetricKey(data: ikmData), info: CryptoConstants.sealedSenderInfo, outputByteCount: 32 ) // Bind the ciphertext to the intended recipient's static DH public key. // The sealing key is already derived from ECDH(eph, recipientDHKey), so the // recipient is implicitly authenticated via key derivation. Explicitly including // the recipient's DH public key as AAD additionally prevents key-confusion: even // if two recipients somehow shared the same DH key the AAD tag would differ. let wireJSON = try JSONEncoder().encode(wire) let nonce = ChaChaPoly.Nonce() let sealed = try ChaChaPoly.seal(wireJSON, using: sealingKey, nonce: nonce, authenticating: recipientDHPublicKey) return SealedMessage(ephemeralPublicKey: ephPair.publicKeyData, encryptedPayload: sealed.combined) }ChatManager.swift · lines 24–49 let innerWire: WireMessage var sealedForTarget: SealedMessage? = nil if let peer = knownPeers[peerID] { let sealed = try sealWireMessage(wire, recipientDHPublicKey: peer.dhKeyPublic) sealedForTarget = sealed innerWire = try wireBuilder.build(.sealed, payload: sealed) } else { innerWire = wire // Unknown peer — fallback to plaintext relay } let envelope = RelayEnvelope( id: UUID().uuidString, targetPeerID: peerID, originPeerID: identity.publicIdentity.peerID, ttl: RelayEnvelope.randomInitialTTL(), hopCount: 0, message: innerWire ) let relayWire = try wireBuilder.build(.relay, payload: envelope) try mesh.broadcast(relayWire)ChatManager.swift · lines 1825–1844 -
08
From phone to phone
Each phone on the way checks it has not seen the envelope before, lowers the count of hops left and passes it on. The count starts at a random number from four to six, so no one can tell how far it has come.
public static func randomInitialTTL() -> UInt8 { UInt8.random(in: 4...6) }NetworkProtocol.swift · lines 550–552 public func shouldProcess(_ envelope: RelayEnvelope) -> Bool { // Drop if TTL exhausted OR if a malicious peer crafted an inflated TTL // that would cause unbounded flooding across the mesh. guard envelope.ttl > 0, envelope.ttl <= RelayEnvelope.maxTTL else { return false } lock.lock() defer { lock.unlock() } guard !seenSet.contains(envelope.id) else { return false } seenSet.insert(envelope.id) seenQueue.append(envelope.id) // Evict oldest entry if we've exceeded the cap if seenSet.count > maxSeen { let evicted = seenQueue.removeFirst() seenSet.remove(evicted) } return true }RelayRouter.swift · lines 54–73 // ── Not for me — check dedup cache and forward if still alive ───── guard !relayRouter.isRateLimited(senderID: relayPeerID) else { return } guard relayRouter.shouldProcess(envelope) else { return } let forwarded = envelope.forwarded() let relayWire = try wireBuilder.build(.relay, payload: forwarded) // Exclude the peer who sent us this envelope to avoid looping try mesh.broadcast(relayWire, excluding: relayPeerID)ChatManager.swift · lines 2895–2902 -
09
It arrives
The other phone receives it over Bluetooth or Wi‑Fi. Anything too large or malformed is dropped at once.
public func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { queue.async { [weak self] in guard let self else { return } // Reject oversized payloads before decoding to prevent memory DoS. // Matches TCPTransport.maxFrameSize (3 MiB) — both transports must accept // the same message types, so an attacker cannot bypass the TCP limit by // switching to the BLE/WiFi-Direct transport. guard data.count <= 3 * 1024 * 1024 else { return } guard let message = try? JSONDecoder().decode(WireMessage.self, from: data) else { return // Malformed — drop silently } // Learn/update the mapping from MC display name to application peerID self.displayNameToPeerID[peerID.displayName] = message.senderID self.peerIDToMCPeer[message.senderID] = peerID let senderID = message.senderID DispatchQueue.main.async { self.delegate?.meshManager(self, didReceiveMessage: message, fromPeer: senderID) } } }MeshManager.swift · lines 163–183 -
10
Is it really from you?
It checks the time — envelopes too old, or from the future, are refused — and the signature, against the key it already knows for you. A forged envelope goes no further.
private func handleIncomingMessage(_ message: WireMessage) { // Reject messages with timestamps too far from now. // Relay envelopes get a wider window (10 min) to tolerate multi-hop latency; // all other types use a strict 5-minute window. // Directional validation: reject messages too far in the past OR more than 30s in the future. // Using abs() previously allowed future-dated messages up to maxAge seconds ahead. let offset: TimeInterval = message.timestamp.timeIntervalSinceNow // positive = future let maxAge: TimeInterval = message.type == .relay ? 600 : 300 guard offset > -maxAge, offset < 30 else { return } // Signature verification: // • .hello — self-verifying (signing key inside bundle); handled below. // • .initiateSession — self-verifying (signing key inside sender bundle); handled below. // • everything else — MUST come from a known peer with a verified signature. // Unknown senders cannot send arbitrary message types; we drop silently. if message.type != .hello && message.type != .initiateSession { guard let peer = knownPeers[message.senderID] else { #if DEBUG print("[ChatManager] ⚠️ Unknown sender: type=\(message.type.rawValue) sender=\(message.senderID.prefix(8))") #endif return } do { guard try WireMessageBuilder.verify(message, signingKeyPublic: peer.signingKeyPublic) else { #if DEBUG print("[ChatManager] ⚠️ Invalid signature: type=\(message.type.rawValue) sender=\(message.senderID.prefix(8))") #endif return } } catch { #if DEBUG print("[ChatManager] ⚠️ Sig verify error: type=\(message.type.rawValue) sender=\(message.senderID.prefix(8)) error=\(error)") #endif return } }ChatManager.swift · lines 3373–3408 -
11
The seal is opened
Only this phone’s private key can open the sealed envelope. If anyone has tampered with it, it does not open.
private func unsealMessage(_ sealed: SealedMessage, recipientDHPrivateKey: Curve25519.KeyAgreement.PrivateKey) throws -> WireMessage { let ephKey = try Curve25519.KeyAgreement.PublicKey(rawRepresentation: sealed.ephemeralPublicKey) let shared = try recipientDHPrivateKey.sharedSecretFromKeyAgreement(with: ephKey) var ikmData = Data() shared.withUnsafeBytes { ikmData.append(contentsOf: $0) } let sealingKey = HKDF<SHA256>.deriveKey( inputKeyMaterial: SymmetricKey(data: ikmData), info: CryptoConstants.sealedSenderInfo, outputByteCount: 32 ) // Reconstruct the same AAD used during sealing: our own DH public key. let recipientDHPublicKey = recipientDHPrivateKey.publicKey.rawRepresentation do { let box = try ChaChaPoly.SealedBox(combined: sealed.encryptedPayload) let wireJSON = try ChaChaPoly.open(box, using: sealingKey, authenticating: recipientDHPublicKey) return try JSONDecoder().decode(WireMessage.self, from: wireJSON) } catch { throw SophaxError.decryptionFailed } }ChatManager.swift · lines 51–73 -
12
Unlocked
The phone finds the one key for this message — even if messages arrived out of order — and opens it. If a single letter was changed on the way, it will not open.
/// Decrypt an incoming `RatchetMessage`. /// - Parameters: /// - message: The received encrypted message /// - associatedData: Must match what the sender used in `encrypt` /// - Returns: Decrypted plaintext public func decrypt(message: RatchetMessage, associatedData: Data = Data()) throws -> Data { // 1. Check skipped message keys first (handles out-of-order delivery) if let plaintext = try decryptWithSkippedKey(message: message, associatedData: associatedData) { return plaintext } // 2. Try decrypting the header with the current receiving header key (same DH epoch) if let hkr = state.receivingHeaderKey?.key, let header = try? decryptHeaderBytes(message.encryptedHeader, using: hkr) { try skipMessageKeys(until: header.messageNumber) return try decryptBody(advanceReceivingChain(), message: message, associatedData: associatedData) } // 3. Try the next receiving header key (new DH epoch — performs DHRatchet first) guard let nhkr = state.nextReceivingHeaderKey?.key, let header = try? decryptHeaderBytes(message.encryptedHeader, using: nhkr) else { throw SophaxError.decryptionFailed } try skipMessageKeys(until: header.previousChainLength) try dhRatchetStep(with: header.senderRatchetKey) try skipMessageKeys(until: header.messageNumber) return try decryptBody(advanceReceivingChain(), message: message, associatedData: associatedData) }DoubleRatchet.swift · lines 198–226 -
13
Kept, and a receipt sent back
The message is saved on the phone, encrypted, and a small “delivered” note goes back to you. Then it appears on the screen.
try messageStore.append(message: stored) forwardToLinkedDevices(stored) // Acknowledge delivery — errors here are non-fatal (peer may be gone), // but surfaced to the delegate so they are visible in logs. do { let ack = AckMessage(messageID: payload.messageID, status: .delivered) let wire = try wireBuilder.build(.ack, payload: ack) try sendOrQueue(wire, toPeerID: peerID, messageID: payload.messageID) } catch { delegate?.chatManager(self, didEncounterError: error) } DispatchQueue.main.async { [weak self] in guard let self else { return } self.delegate?.chatManager(self, didReceiveMessage: stored, fromPeer: peerID) }ChatManager.swift · lines 2236–2252 private func saveToDisk(messages: [StoredMessage], peerID: String) throws { let plaintext = try JSONEncoder().encode(messages) let encryptedData = try encrypt(plaintext) let url = fileURL(for: peerID) try encryptedData.write(to: url, options: .atomic) // Ensure each written file also carries .complete protection (the directory // attribute is inherited by new files, but setting explicitly is more reliable). try? FileManager.default.setAttributes( [.protectionKey: FileProtectionType.complete], ofItemAtPath: url.path ) }MessageStore.swift · lines 238–249