Implementing traffic encryption in mobile applications
Standard HTTPS closes the channel, but doesn't solve several problems: what exactly is transmitted in the request body, how to protect against compromised CA, and how encryption works for data that doesn't go to REST API, but to WebSocket, MQTT or BLE channel.
TLS as foundation — and why it's not enough
Every production application uses HTTPS, but default TLS configuration on iOS and Android is vulnerable to downgrade attacks and accepts certificates from hundreds of system CAs. Network Security Configuration on Android and App Transport Security on iOS set minimum TLS requirements.
Android Network Security Configuration (res/xml/network_security_config.xml):
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<trust-anchors>
<certificates src="@raw/my_ca"/>
</trust-anchors>
<pin-set expiration="2026-01-01">
<pin digest="SHA-256">primaryPinBase64==</pin>
<pin digest="SHA-256">backupPinBase64==</pin>
</pin-set>
</domain-config>
<base-config cleartextTrafficPermitted="false"/>
</network-security-config>
cleartextTrafficPermitted="false" blocks HTTP at OS level — no component of the app can push unencrypted request. On iOS the equivalent is NSAllowsArbitraryLoads: false in Info.plist (default since iOS 9).
Force TLS 1.2+ on Android via OkHttp:
val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
.tlsVersions(TlsVersion.TLS_1_2, TlsVersion.TLS_1_3)
.cipherSuites(
CipherSuite.TLS_AES_128_GCM_SHA256,
CipherSuite.TLS_AES_256_GCM_SHA384,
CipherSuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
)
.build()
Encrypting request body — when TLS is not enough
If API is accessible through multiple endpoints (CDN, API gateway, third-party services), data can be visible at intermediate nodes. End-to-end request body encryption solves this: server receives encrypted blob, intermediate nodes see only metadata.
Scheme with libsodium (via wrapper swift-sodium or lazysodium-android):
- Client on first run generates X25519 keypair, registers public key on server
- Server publishes its public key
- For each request:
crypto_box_easy(message, nonce, server_public_key, client_private_key)— ECDH + XSalsa20-Poly1305 - Server response encrypted similarly
Nonce must be unique for each message — 24 random bytes from SecRandomCopyBytes / SecureRandom. Never incremental counter without additional protection.
Encrypting WebSocket traffic
WSS (WebSocket Secure) — TLS over WebSocket. Problem is same as with HTTPS: TLS closes channel, but not body. For chats and fintech apps where server shouldn't store messages in plaintext, additional level is needed.
Typical approach — Signal Protocol (libsignal): Double Ratchet + X3DH. Implemented in official SDKs for iOS and Android. Simpler alternative for apps without cross-device sync — NaCl secretbox with pre-shared key, exchanged via TLS during session initialization.
BLE and local connections
Bluetooth Low Energy doesn't use TLS. If app exchanges data with IoT device via BLE, encryption must be implemented at application level.
Minimal scheme: ECDH key exchange during pairing (Curve25519), then AES-256-GCM for each packet with incremental nonce (replay protection via counter, transmitted in associated data). Stack: CryptoKit on iOS (native), Bouncy Castle or Tink on Android.
Anti-detect and traffic obfuscation
For applications in regions with deep packet inspection (DPI) — separate task: obfuscating TLS fingerprint by changing TLS extensions order, using QUIC (HTTP/3) or domain fronting. Beyond scope of standard traffic encryption.
Process
Audit current network calls — HTTP or HTTPS, TLS versions, presence of certificate validation. Next: ATS/NSC configuration, force TLS 1.2+, certificate pinning on critical endpoints, encrypt body where threat model requires. For non-standard channels (BLE, MQTT) — separate scheme.
Timelines — 2–5 days. Basic TLS configuration + NSC/ATS — 1 day. Request body encryption with key exchange — 2–3 more days.







