19 KiB
SimpleX Chat iOS -- File Transfer Service
Technical specification for file transfer: inline/XFTP protocols, auto-receive thresholds, CryptoFile encryption, and file constants.
Related specs: Compose Module | Chat View | API Reference | Database | README Related product: Product Overview
Source: FileUtils.swift | CryptoFile.swift | ChatTypes.swift | AppAPITypes.swift | SimpleXAPI.swift
Table of Contents
- Overview
- Transfer Methods
- Auto-Receive Thresholds
- File Size Constants
- Image Handling
- Voice Messages
- CryptoFile -- At-Rest Encryption
- File Storage Paths
- File Lifecycle
- API Commands
1. Overview
SimpleX Chat supports two file transfer methods depending on file size:
File ≤ 255KB (inline)
├── Base64 encoded directly in SMP message
├── Single message delivery
└── No extra server infrastructure needed
File > 255KB up to 1GB (XFTP)
├── Encrypted and chunked
├── Uploaded to XFTP relay servers
├── Recipient downloads chunks from relays
└── Files auto-deleted from relays after download or expiry
All files are end-to-end encrypted. The XFTP protocol adds a second encryption layer on top of the SMP channel encryption.
2. Transfer Methods
Inline Transfer
- Files up to
MAX_IMAGE_SIZE(255KB) are base64-encoded and embedded directly in the SMP message body - No additional protocol or server needed
- Delivered with the same reliability guarantees as regular messages
- Used primarily for compressed images
XFTP Transfer
For files exceeding the inline threshold (up to MAX_FILE_SIZE_XFTP = 1GB):
-
Sender side:
- File is AES-encrypted with a random key
- Encrypted file is split into chunks
- Chunks are uploaded to one or more XFTP relay servers
- File metadata (key, chunk locations) sent to recipient via SMP message
-
Recipient side:
- Receives file metadata via SMP
- Downloads chunks from XFTP relays
- Reassembles and decrypts the file
-
Cleanup:
- XFTP relays delete chunks after download or after expiry period
- No persistent storage on relays
SMP Transfer (legacy)
MAX_FILE_SIZE_SMP (8MB) exists as a constant for larger inline transfers through SMP, used in specific scenarios.
3. Auto-Receive Thresholds
Files below certain size thresholds are automatically accepted and downloaded without user confirmation:
| Media Type | Auto-Receive Threshold | Constant | Line |
|---|---|---|---|
| Images | 510 KB | MAX_IMAGE_SIZE_AUTO_RCV |
L21 |
| Voice messages | 510 KB | MAX_VOICE_SIZE_AUTO_RCV |
L24 |
| Video | 1023 KB | MAX_VIDEO_SIZE_AUTO_RCV |
L27 |
| Other files | Not auto-received | Requires manual acceptance | -- |
Behavior
- When a message with a file attachment arrives:
- Check if file size is below the auto-receive threshold for its type
- If below: automatically call
setFileToReceive(fileId:, userApprovedRelays:, encrypted:)followed by download - If above: show download button in chat item, wait for user action
- User manually triggers download via
receiveFile(fileId:, userApprovedRelays:, encrypted:, inline:)
Relay Approval
userApprovedRelays parameter: when the file is hosted on relays not in the user's configured server list, the user is asked for confirmation before connecting to unknown relays.
4. File Size Constants
Defined in SimpleXChat/FileUtils.swift:
| Constant | Value | Line |
|---|---|---|
MAX_IMAGE_SIZE |
261,120 (255 KB) | L18 |
MAX_IMAGE_SIZE_AUTO_RCV |
522,240 (510 KB) | L21 |
MAX_VOICE_SIZE_AUTO_RCV |
522,240 (510 KB) | L24 |
MAX_VIDEO_SIZE_AUTO_RCV |
1,047,552 (1023 KB) | L27 |
MAX_FILE_SIZE_XFTP |
1,073,741,824 (1 GB) | L30 |
MAX_FILE_SIZE_LOCAL |
Int64.max (no limit) | L32 |
MAX_FILE_SIZE_SMP |
8,000,000 (~7.6 MB) | L34 |
MAX_VOICE_MESSAGE_LENGTH |
300 s (5 min) | L36 |
// Image compression target for inline transfer
public let MAX_IMAGE_SIZE: Int64 = 261_120 // 255 KB
// Auto-receive thresholds
public let MAX_IMAGE_SIZE_AUTO_RCV: Int64 = 522_240 // 510 KB (2 * MAX_IMAGE_SIZE)
public let MAX_VOICE_SIZE_AUTO_RCV: Int64 = 522_240 // 510 KB (2 * MAX_IMAGE_SIZE)
public let MAX_VIDEO_SIZE_AUTO_RCV: Int64 = 1_047_552 // 1023 KB
// Transfer method limits
public let MAX_FILE_SIZE_XFTP: Int64 = 1_073_741_824 // 1 GB
public let MAX_FILE_SIZE_SMP: Int64 = 8_000_000 // ~7.6 MB
public let MAX_FILE_SIZE_LOCAL: Int64 = Int64.max // No limit (local notes)
// Voice message constraints
public let MAX_VOICE_MESSAGE_LENGTH = TimeInterval(300) // 5 minutes (300 seconds)
5. Image Handling
Compression Pipeline
- User selects image (camera or photo library)
- Image is compressed to fit within
MAX_IMAGE_SIZE(255KB):- Progressive JPEG compression with decreasing quality
- Resize if dimensions are too large
- Compressed image is base64-encoded into the message content
- For larger images that cannot compress to 255KB: sent via XFTP
Display
CIImageViewrenders images in chat bubbles with aspect-fit sizing- Tapping opens
FullScreenMediaViewwith zoom/pan/share capabilities - Thumbnail is displayed immediately; full-size loaded on demand for XFTP images
Animated Images
- GIFs are handled by
AnimatedImageView - Displayed inline with animation support
6. Voice Messages
Recording
ComposeVoiceViewmanages the recording UIAudioRecPlayhandlesAVAudioRecorderlifecycle- Recorded in compressed audio format
- Maximum duration:
MAX_VOICE_MESSAGE_LENGTH= 300 seconds (5 minutes) - Waveform data extracted for visualization
Transfer
- Voice files up to
MAX_VOICE_SIZE_AUTO_RCV(510KB) are auto-received - Larger voice files follow standard file transfer flow
- Voice messages include waveform metadata for UI rendering
Playback
CIVoiceView/FramedCIVoiceViewrender voice messages- Shows waveform visualization and play/pause control
ChatModel.stopPreviousRecPlayensures only one audio source plays at a time- Playback position and progress tracked
7. CryptoFile -- At-Rest Encryption
When apiSetEncryptLocalFiles(enable: true) is configured, files stored on the device are AES-encrypted.
CryptoFile Type
struct CryptoFile {
var filePath: String
var cryptoArgs: CryptoFileArgs? // nil = unencrypted
}
struct CryptoFileArgs {
var fileKey: String // AES encryption key
var fileNonce: String // AES nonce/IV
}
Defined in
ChatTypes.swiftL4241 (CryptoFile) and L4289 (CryptoFileArgs).
Encryption Operations (C FFI)
Implemented in CryptoFile.swift:
| Function | Purpose | Line |
|---|---|---|
writeCryptoFile |
Write encrypted file, returns CryptoFileArgs |
L18 |
readCryptoFile |
Read and decrypt file, returns Data |
L31 |
encryptCryptoFile |
Encrypt existing file to new path | L54 |
decryptCryptoFile |
Decrypt file to new path | L66 |
Storage
- Encrypted files stored alongside unencrypted files in
Documents/files/ - The
CryptoFileArgs(key + nonce) are stored in the Haskell database, not on the filesystem - Toggle via privacy settings:
apiSetEncryptLocalFiles(enable:)
8. File Storage Paths
Directory Structure
| Function | Path | Line |
|---|---|---|
getAppFilesDirectory() |
Documents/files/ |
L208 |
getTempFilesDirectory() |
Documents/temp_files/ |
L199 |
getWallpaperDirectory() |
Documents/wallpapers/ |
L217 |
getAppFilePath(_:) |
Documents/files/{filename} |
L212 |
getWallpaperFilePath(_:) |
Documents/wallpapers/{filename} |
L221 |
func getAppFilesDirectory() -> URL // Documents/files/
func getTempFilesDirectory() -> URL // Documents/temp_files/
func getWallpaperDirectory() -> URL // Documents/wallpapers/
Path Management
- Downloaded files:
Documents/files/{filename} - Temporary files during transfer:
Documents/temp_files/ - Wallpaper images:
Documents/wallpapers/ - File paths are set via
apiSetAppFilePaths(filesFolder:, tempFolder:, assetsFolder:)at startup
9. File Lifecycle
Sending
1. User selects file/image/video in compose
2. ComposeView creates ComposedMessage with file reference
3. apiSendMessages() → Haskell core processes:
a. File ≤ inline threshold: base64 encode into message
b. File > inline threshold: start XFTP upload
4. Upload events:
- ChatEvent.sndFileStart
- ChatEvent.sndFileProgressXFTP (periodic progress)
- ChatEvent.sndFileCompleteXFTP (upload done)
- ChatEvent.sndFileError (on failure)
Receiving
1. Message with file attachment arrives
2. Auto-receive check:
a. Below threshold: automatic download starts
b. Above threshold: user sees download button
3. User triggers download (or auto-triggered):
- receiveFile(fileId:, userApprovedRelays:, encrypted:, inline:)
4. Download events:
- ChatEvent.rcvFileStart
- ChatEvent.rcvFileProgressXFTP (periodic progress)
- ChatEvent.rcvFileComplete (download done)
- ChatEvent.rcvFileError (on failure)
- ChatEvent.rcvFileSndCancelled (sender cancelled)
Cancellation
ChatCommand.cancelFile(fileId: Int64)
Cancels an in-progress upload or download. For XFTP transfers, also requests chunk deletion from relays.
Cleanup
| Function | Purpose | Line |
|---|---|---|
cleanupFile(_:) |
Remove file associated with a chat item | L267 |
cleanupDirectFile(_:) |
Remove file only for direct chats | L260 |
removeFile(_:) |
Delete file at URL | L243 |
removeFile(_:) |
Delete file by name | L251 |
deleteAppFiles() |
Remove all app files (preserving databases) | L108 |
deleteAppDatabaseAndFiles() |
Remove everything | L86 |
- When a
ChatItemis deleted, its associated file is deleted from disk - When a timed message expires, its file is deleted
ChatModel.filesToDeletequeues files for deferred deletiondeleteAppFiles()removes all files (preserving databases)deleteAppDatabaseAndFiles()removes everything
10. API Commands
| Command | Parameters | Description | Line |
|---|---|---|---|
receiveFile |
fileId, userApprovedRelays, encrypted, inline |
Accept and start downloading a file | L167 |
setFileToReceive |
fileId, userApprovedRelays, encrypted |
Mark file for auto-receive (no immediate download) | L168 |
cancelFile |
fileId |
Cancel in-progress transfer | L169 |
apiUploadStandaloneFile |
userId, file: CryptoFile |
Upload file to XFTP without a chat context | L179 |
apiDownloadStandaloneFile |
userId, url, file: CryptoFile |
Download from XFTP URL | L180 |
apiStandaloneFileInfo |
url |
Get metadata for an XFTP URL | L181 |
File Transfer Events
| Event | Description | Line |
|---|---|---|
rcvFileAccepted |
Download request accepted | L1095 |
rcvFileStart |
Download started | L1097 |
rcvFileProgressXFTP |
Download progress (receivedSize, totalSize) | L1098 |
rcvFileComplete |
Download complete | L1099 |
rcvFileSndCancelled |
Sender cancelled the transfer | L1101 |
rcvFileError |
Download failed | L1102 |
rcvFileWarning |
Download warning (non-fatal) | L1103 |
sndFileStart |
Upload started | L1105 |
sndFileComplete |
Inline upload complete | L1106 |
sndFileProgressXFTP |
XFTP upload progress (sentSize, totalSize) | L1108 |
sndFileCompleteXFTP |
XFTP upload complete | L1110 |
sndFileRcvCancelled |
Receiver cancelled | L1107 |
sndFileError |
Upload failed | L1112 |
sndFileWarning |
Upload warning (non-fatal) | L1113 |
Source Files
| File | Path | Key Definitions |
|---|---|---|
| File utilities & constants | SimpleXChat/FileUtils.swift |
MAX_IMAGE_SIZE, saveFile, removeFile, getMaxFileSize |
| CryptoFile FFI operations | SimpleXChat/CryptoFile.swift |
writeCryptoFile, readCryptoFile, encryptCryptoFile, decryptCryptoFile |
| CryptoFile / CryptoFileArgs types | SimpleXChat/ChatTypes.swift |
CryptoFile (L4241), CryptoFileArgs (L4289) |
| API command definitions | Shared/Model/AppAPITypes.swift |
receiveFile, cancelFile, ChatEvent file events |
| API implementations | Shared/Model/SimpleXAPI.swift |
receiveFile (L1471), cancelFile (L1590) |
| File view (chat item) | Shared/Views/Chat/ChatItem/CIFileView.swift |
|
| Image view (chat item) | Shared/Views/Chat/ChatItem/CIImageView.swift |
|
| Video view (chat item) | Shared/Views/Chat/ChatItem/CIVideoView.swift |
|
| Voice view (chat item) | Shared/Views/Chat/ChatItem/CIVoiceView.swift |
|
| Compose file preview | Shared/Views/Chat/ComposeMessage/ComposeFileView.swift |
|
| Compose image preview | Shared/Views/Chat/ComposeMessage/ComposeImageView.swift |
|
| Compose voice preview | Shared/Views/Chat/ComposeMessage/ComposeVoiceView.swift |
|
| C FFI (file encryption) | SimpleXChat/SimpleX.h |
chat_write_file, chat_read_file, chat_encrypt_file, chat_decrypt_file |
| Haskell file logic | ../../src/Simplex/Chat/Files.hs |
-- |
| Haskell file store | ../../src/Simplex/Chat/Store/Files.hs |
-- |