14 KiB
Business Rules -- SimpleX Chat (Android & Desktop, Kotlin Multiplatform)
This document specifies invariants enforced by the Android and Desktop (Kotlin/Compose Multiplatform) clients.
Table of Contents
- Security (RULE-01 through RULE-05)
- Message Integrity (RULE-06 through RULE-09)
- Group Integrity (RULE-10 through RULE-13)
- File Transfer (RULE-14 through RULE-15)
- Notification Delivery (RULE-16 through RULE-17)
- Call Integrity (RULE-18)
1. Security
RULE-01: End-to-End Encryption is Mandatory
Invariant: Every message, file chunk, and call signaling payload MUST be encrypted end-to-end before transmission. The app MUST NOT transmit plaintext content to any relay server.
Enforcement: The Haskell core library handles all encryption. The Kotlin layer never constructs raw SMP messages. All communication flows through ChatController.sendCmd() which delegates to the FFI, ensuring the encryption layer cannot be bypassed.
Location: common/src/commonMain/kotlin/chat/simplex/common/model/SimpleXAPI.kt -- ChatController.sendCmd(), chatSendCmd() FFI call
RULE-02: Database Encryption at Rest
Invariant: The local SQLite database MUST be encrypted. A passphrase (either user-chosen or randomly generated) MUST be set before the database is operational.
Enforcement: On first launch, a random passphrase is generated and stored encrypted via the platform keystore (CryptorInterface.encryptText). The initialRandomDBPassphrase preference tracks whether the user has set a custom passphrase. Database encryption state is tracked in ChatModel.chatDbEncrypted. Encryption/re-encryption is performed via CC.ApiStorageEncryption(config: DBEncryptionConfig).
Caveat: The user is not forced to set a custom passphrase -- the random passphrase is stored in app-accessible encrypted preferences. See GAP: "Database passphrase not enforced."
Location:
common/src/commonMain/kotlin/chat/simplex/common/views/helpers/DatabaseUtils.ktcommon/src/commonMain/kotlin/chat/simplex/common/platform/Cryptor.kt--CryptorInterface- Android:
common/src/androidMain/kotlin/chat/simplex/common/platform/Cryptor.android.kt-- Android Keystore - Desktop:
common/src/desktopMain/kotlin/chat/simplex/common/platform/Cryptor.desktop.kt-- placeholder, not implemented
RULE-03: Local Authentication Gating
Invariant: When local authentication is enabled (AppPreferences.performLA == true), the app MUST require biometric/PIN authentication before displaying any chat content. The lock engages after laLockDelay seconds of inactivity.
Enforcement: AppLock.setPerformLA controls the lock state. The lock delay is configurable via AppPreferences.laLockDelay (default 30 seconds). Authentication mode is set via AppPreferences.laMode (system biometric or passcode).
Location:
common/src/commonMain/kotlin/chat/simplex/common/AppLock.ktSimpleXAPI.kt--AppPreferences.performLA,AppPreferences.laMode,AppPreferences.laLockDelay
RULE-04: Self-Destruct Profile
Invariant: When self-destruct is enabled (AppPreferences.selfDestruct == true), entering the self-destruct passphrase instead of the real passphrase MUST wipe the database and present a clean profile with selfDestructDisplayName.
Enforcement: The self-destruct passphrase is stored separately (encryptedSelfDestructPassphrase / initializationVectorSelfDestructPassphrase). On Android, SimplexService checks for self-destruct on initialization. The comparison happens during the local authentication flow.
Location:
SimpleXAPI.kt--AppPreferences.selfDestruct,AppPreferences.selfDestructDisplayNameandroid/src/main/java/chat/simplex/app/SimplexService.kt-- initialization check
RULE-05: Screen Protection
Invariant: When AppPreferences.privacyProtectScreen == true (default), the app MUST prevent screenshots and screen recording. On Android this uses FLAG_SECURE; on Desktop this is advisory only.
Enforcement: The preference defaults to true. The Android activity applies FLAG_SECURE to its window based on this preference. The Desktop app cannot enforce this at the OS level.
Location: SimpleXAPI.kt -- AppPreferences.privacyProtectScreen
2. Message Integrity
RULE-06: Message Ordering Verification
Invariant: The app MUST detect and surface message integrity violations (gaps, duplicates, out-of-order delivery) to the user.
Enforcement: The Haskell core tracks message sequence numbers per connection. When a gap or integrity error is detected, a CIContent.RcvIntegrityError(msgError: MsgErrorType) chat item is inserted into the conversation. The UI renders these as system messages indicating the integrity issue.
Location: ChatModel.kt:3565 -- CIContent.RcvIntegrityError
RULE-07: Decryption Error Surfacing
Invariant: When a message cannot be decrypted, the app MUST display a RcvDecryptionError item showing the error type and count of affected messages. The app MUST NOT silently drop undecryptable messages.
Enforcement: The Haskell core emits CIContent.RcvDecryptionError(msgDecryptError, msgCount) which the UI renders with an explanation and count. Ratchet re-synchronization can be triggered via APISyncContactRatchet / APISyncGroupMemberRatchet.
Location: ChatModel.kt:3566 -- CIContent.RcvDecryptionError
RULE-08: Delivery Receipt Consistency
Invariant: Delivery receipt settings MUST be consistent: when a user enables/disables receipts globally, the change MUST propagate to all contacts/groups (optionally clearing per-chat overrides via clearOverrides).
Enforcement: Global receipt toggle triggers CC.SetAllContactReceipts(enable). Per-type settings use CC.ApiSetUserContactReceipts / CC.ApiSetUserGroupReceipts with UserMsgReceiptSettings(enable, clearOverrides). The privacyDeliveryReceiptsSet preference gates the initial setup prompt shown during onboarding.
Location:
SimpleXAPI.kt--CC.SetAllContactReceipts,CC.ApiSetUserContactReceipts,CC.ApiSetUserGroupReceiptsSimpleXAPI.kt--ChatController.startChat()-- triggerssetDeliveryReceiptsprompt
RULE-09: Chat Item TTL Enforcement
Invariant: When a chat item TTL (time-to-live) is set globally or per-chat, expired messages MUST be deleted by the core. The app MUST NOT display expired items.
Enforcement: Global TTL set via CC.APISetChatItemTTL(userId, seconds). Per-chat TTL set via CC.APISetChatTTL(userId, chatType, id, seconds). The Haskell core performs periodic cleanup. The current global TTL is stored in ChatModel.chatItemTTL.
Location: SimpleXAPI.kt -- CC.APISetChatItemTTL, CC.APISetChatTTL
3. Group Integrity
RULE-10: Role-Based Access Control
Invariant: Group operations MUST respect the member's role. Only members with sufficient role level can perform privileged operations:
- Owner: can delete group, change any member's role, transfer ownership
- Admin: can add/remove members, change roles (up to Admin), create/delete group links
- Moderator: can delete other members' messages, block members
- Member / Author / Observer: cannot perform administrative actions
Enforcement: The Haskell core validates role permissions server-side. The Kotlin UI layer uses GroupMemberRole comparisons (the enum is ordered: Observer < Author < Member < Moderator < Admin < Owner) to show/hide action buttons.
Location: ChatModel.kt:2369 -- enum class GroupMemberRole; various group management views
RULE-11: Group Member Removal Atomicity
Invariant: When removing members from a group, the removal command MUST specify all member IDs atomically. Partial removal MUST NOT leave the group in an inconsistent state.
Enforcement: CC.ApiRemoveMembers(groupId, memberIds: List<Long>, withMessages: Boolean) sends all member IDs in a single command. The withMessages flag controls whether the removed members' messages are also deleted.
Location: SimpleXAPI.kt -- CC.ApiRemoveMembers
RULE-12: Group Link Role Default
Invariant: When creating a group link, the default member role for joiners MUST be explicitly specified. The role can be updated after creation without regenerating the link.
Enforcement: CC.APICreateGroupLink(groupId, memberRole) requires a role. CC.APIGroupLinkMemberRole(groupId, memberRole) updates it. The link itself remains stable.
Location: SimpleXAPI.kt -- CC.APICreateGroupLink, CC.APIGroupLinkMemberRole
RULE-13: Member Blocking Scope
Invariant: Blocking a member (ApiBlockMembersForAll) MUST apply the block for all group members (not just the requester). The blocked flag is visible to all members. Only roles >= Moderator can block.
Enforcement: CC.ApiBlockMembersForAll(groupId, memberIds, blocked) sends the block/unblock to the core, which propagates it to all group members.
Location: SimpleXAPI.kt -- CC.ApiBlockMembersForAll; ChatModel.kt -- GroupMember.blockedByAdmin
4. File Transfer
RULE-14: File Encryption in Transit and at Rest
Invariant: Files sent via XFTP MUST be encrypted before upload. Files received MUST be decrypted only after download. When privacyEncryptLocalFiles is enabled (default true), files stored locally MUST be encrypted with per-file keys (CryptoFile.cryptoArgs).
Enforcement: The Haskell core handles XFTP encryption. Local file encryption is toggled via CC.ApiSetEncryptLocalFiles(enable). The CryptoFile type carries optional CryptoFileArgs (key + nonce) for local decryption. Files are decrypted on-demand for display via decryptCryptoFile().
Location:
SimpleXAPI.kt--CC.ApiSetEncryptLocalFiles,AppPreferences.privacyEncryptLocalFilesChatModel.kt--CryptoFile,CryptoFileArgsRecAndPlay.desktop.kt--decryptCryptoFile()usage in audio playback
RULE-15: Relay Approval for File Transfer
Invariant: When privacyAskToApproveRelays is enabled (default true), the app MUST prompt the user before using XFTP relay servers suggested by contacts (as opposed to the user's own configured servers). The userApprovedRelays flag on CC.ReceiveFile records the user's consent.
Enforcement: CC.ReceiveFile(fileId, userApprovedRelays, encrypt, inline) passes the approval flag. The UI prompts the user when the file is from an unapproved relay.
Location: SimpleXAPI.kt -- CC.ReceiveFile, AppPreferences.privacyAskToApproveRelays
5. Notification Delivery
RULE-16: Background Message Delivery (Android)
Invariant: On Android, when NotificationsMode.SERVICE is selected (default), the app MUST maintain a foreground service (SimplexService) to ensure continuous message delivery. The service MUST survive app backgrounding and device sleep. When NotificationsMode.PERIODIC is selected, MessagesFetcherWorker MUST periodically wake and fetch messages. When NotificationsMode.OFF, no background delivery occurs.
Enforcement:
SimplexServiceruns as a foreground service withSTART_STICKYand aWakeLock. It displays a persistent notification on theSIMPLEX_SERVICE_NOTIFICATIONchannel.MessagesFetcherWorkeris aPeriodicWorkRequestscheduled viaWorkManager.- The mode is stored in
AppPreferences.notificationsModeand checked at app startup.
Location:
android/src/main/java/chat/simplex/app/SimplexService.ktandroid/src/main/java/chat/simplex/app/MessagesFetcherWorker.ktSimpleXAPI.kt:7739--enum class NotificationsMode
RULE-17: Notification Preview Privacy
Invariant: Notification content MUST respect notificationPreviewMode:
HIDDEN-- notification shows no sender or message contentCONTACT-- notification shows sender name onlyMESSAGE-- notification shows sender name and message preview
Enforcement: NtfManager (Android) reads the preview mode from AppPreferences.notificationPreviewMode and constructs notifications accordingly. The CallService also respects this mode for call notifications (showing or hiding caller identity).
Location:
android/src/main/java/chat/simplex/app/model/NtfManager.android.kt--displayNotification(),notifyCallInvitation()android/src/main/java/chat/simplex/app/CallService.kt--updateNotification()SimpleXAPI.kt--AppPreferences.notificationPreviewMode
6. Call Integrity
RULE-18: Call Lifecycle Management
Invariant: An active call MUST be properly managed across the full lifecycle:
- Incoming calls MUST be reported via
CallManager.reportNewIncomingCall()which triggers a notification (and on Android, a full-screen intent for lock-screen display). - Only one call can be active at a time. Accepting a new call MUST end any existing call first (
CallManager.acceptIncomingCallchecksactiveCalland callsendCallif needed, guarded byswitchingCallflag). - Call state MUST progress through defined states:
WaitCapabilities->InvitationSent/InvitationAccepted->OfferSent/OfferReceived->Negotiated->Connected->Ended. - Call end MUST clean up all resources: send
WCallCommand.End, callapiEndCall, clearactiveCall, cancel call notifications, and release platform resources.
Android enforcement:
CallService(foreground service) keeps the call alive in background with aWakeLockand ongoing notification onCALL_SERVICE_NOTIFICATIONchannel.CallActivityhosts the WebRTC WebView.- Lock-screen behavior controlled by
AppPreferences.callOnLockScreen(DISABLE / SHOW / ACCEPT).
Desktop enforcement:
- Calls run in the system browser via the NanoWSD WebSocket server on
localhost:50395. - The
WebRTCControllercomposable manages the WebSocket lifecycle. - On dispose,
WCallCommand.Endis sent and the server is stopped.
Location:
common/src/commonMain/kotlin/chat/simplex/common/views/call/CallManager.ktcommon/src/commonMain/kotlin/chat/simplex/common/views/call/WebRTC.kt- Android:
android/src/main/java/chat/simplex/app/CallService.kt,android/src/main/java/chat/simplex/app/views/call/CallActivity.kt - Desktop:
common/src/desktopMain/kotlin/chat/simplex/common/views/call/CallView.desktop.kt