How do you handle data encryption at rest and in transit on a mobile app?
Full-disk encryption and HTTPS are the floor, not the ceiling — the interview question is really about key management: where keys live, who can unlock them, and what happens when a certificate rotates.
LEARN BY DOINGPut your intuition to work3 steps
How do you handle data encryption at rest and in transit on a mobile app?
Use the lesson’s examples and explanation to check your idea.
Personal reflection. Nothing is sent to an AI service.#Two different problems, two different toolchains
"Encryption" in a mobile interview is really two separate questions wearing one word. At rest is about protecting data sitting on the device — a database, a cache file, a preferences store — from another app, from a rooted/jailbroken device inspecting the filesystem, or from someone who has the phone unlocked in their hands. In transit is about protecting data moving between the app and your backend from a network-level attacker: public wifi, a malicious proxy, a compromised or rogue CA. They use completely different tools, and conflating them is the fastest way to give a shallow answer — "we use HTTPS" answers half the question and says nothing about what happens to that data the moment it lands on disk.
#Encrypting data at rest: don't build your own vault
Modern OSes give you full-disk encryption for free, but that only protects a device that's powered off or locked — it does nothing once the device is unlocked and a rogue app (or a rooted device's shell) can read another app's sandbox, and it does nothing about that data quietly ending up in an unencrypted cloud backup. Real app-level protection means keeping small secrets — tokens, keys — in the platform's dedicated secure storage, and layering encryption on top of anything larger: iOS's Keychain (backed by the Secure Enclave, with an explicit kSecAttrAccessible class) plus the Data Protection API for files; Android's Jetpack Security library (EncryptedSharedPreferences/EncryptedFile), backed by a master key that never leaves the Android Keystore.
For anything larger than a few key-value pairs, the pattern that scales is envelope encryption: a symmetric data-encryption key (DEK) encrypts the actual payload with AES-GCM, and the DEK itself is encrypted ("wrapped") by a key-encryption key (KEK) that lives exclusively inside the Keystore/Secure Enclave and never appears in plaintext outside it.
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()val encryptedPrefs = EncryptedSharedPreferences.create(context,"secure_prefs",masterKey,EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM)encryptedPrefs.edit().putString("refresh_token", token).apply()
func saveToken(_ token: String) throws {let query: [String: Any] = [kSecClass as String: kSecClassGenericPassword,kSecAttrAccount as String: "refresh_token",kSecValueData as String: Data(token.utf8),kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly,]SecItemDelete(query as CFDictionary)let status = SecItemAdd(query as CFDictionary, nil)guard status == errSecSuccess else { throw KeychainError.saveFailed(status) }}
#Encrypting data in transit: TLS is necessary, not sufficient
Enforce TLS 1.2 as a floor (prefer 1.3) and refuse cleartext entirely — iOS's App Transport Security defaults to exactly that, and Android's Network Security Config lets you set cleartextTrafficPermitted="false" per domain. For anything high-value — banking, health data, admin tooling — add certificate pinning on top of standard chain validation. Pin the SPKI hash of the public key, not the full leaf certificate: a leaf-cert pin breaks the moment the server renews its certificate, even with the same key pair, while an SPKI pin survives routine renewal. Always ship at least one backup pin (a second key you control but haven't rotated into yet), and keep the pin set updatable from your backend — a certificate authority forced rotation with no remote kill-switch on your pins will hard-lock every installed copy of the app out of your API simultaneously.
val certificatePinner = CertificatePinner.Builder().add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=").add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // backup pin.build()val client = OkHttpClient.Builder().certificatePinner(certificatePinner).build()
<!-- res/xml/network_security_config.xml --><network-security-config><domain-config cleartextTrafficPermitted="false"><domain includeSubdomains="true">api.example.com</domain><pin-set expiration="2027-01-01"><pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin><pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin></pin-set></domain-config></network-security-config>
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge,completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {guard let trust = challenge.protectionSpace.serverTrust,let certificate = SecTrustGetCertificateAtIndex(trust, 0) else {completionHandler(.cancelAuthenticationChallenge, nil); return}let publicKeyHash = spkiSha256(of: certificate)if pinnedHashes.contains(publicKeyHash) {completionHandler(.useCredential, URLCredential(trust: trust))} else {completionHandler(.cancelAuthenticationChallenge, nil)}}
#The anti-pattern that "fixes" a TLS error the wrong way
Almost every trust-all TrustManager in production started as a debug-only workaround for a self-signed staging certificate that never got removed:
// ANTI-PATTERN — never ship thisval trustAllCerts = arrayOf<TrustManager>(object : X509TrustManager {override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {}override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {}override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()})
Once this ships, TLS is providing encryption without authentication — any machine on the network path, including a coffee-shop MITM proxy, is accepted as the server. If you need to hit a self-signed staging endpoint, gate the trust override behind a debug build flag that can never compile into a release artifact, never a runtime toggle.
🧠 Encryption at rest and in transit
0/3 · 0/3 answered1. Why is OS-level full-disk encryption not sufficient on its own for a sensitive app?
2. Why pin the SPKI hash of the public key instead of the full leaf certificate?
3. What's the main danger of a trust-all TrustManager left in a release build?