Securing your iOS app & protecting it against Vulnerabilities: Easy Tips for a Safe User Experience

Rohit Kumar
8 min readDec 29, 2023

--

In the ever-evolving landscape of mobile app development, where innovation meets user-centric experiences, security is a cornerstone that cannot be overlooked. Users of our app expect that their piece of information are being kept confidential. Thus, as an app developer, it is our responsibility to make our application as secure as possible in terms of data storage, data communication, code security and so on.

So, this article will be mainly focused on topics that need to be taken care of by every iOS developer. Those are:

  • Data Storage
  • Encrypting Data
  • Data communication channels
  • SSL Pinning
  • Jailbroken device detection

Data Storage

One common mispractice observed among the developers is to store sensitive data where they do not belong. More often, developers use UserDefaults to store their data, thus making them vulnerable.

As you know, that UserDefaults saves data into a plist file. User can get access to Library/Preferences folder of their iPhone and read or modify the UserDefaults plist data easily (e.g.: user can change the boolean value of “isPremiumUser” from false to true, or change the value of trails left).
Other examples are user password, API key, access token. You shouldn’t store these in UserDefaults for the same reason. This is where KeyChain comes in to save us. For sensitive information, I strongly recommend using the keychain as the data stored in the keychain is automatically encrypted.

Read more about UserDefaults and KeyChain here for more details and code implementation.

Encrypting Data

Data encryption is a critical component of modern mobile application development. But, implementing hashing by ourselves can be really complicated and overkill, so for this article, we will use the framework provided by Apple itself CryptoKit. The CryptoKit framework, introduced in iOS 13, provides a set of cryptographic APIs to perform hashing, encryption, and signature operations.

Hashing Data:

Hashing is a one-way process of transforming data into a fixed-length hash value. The Crypto framework supports several hashing algorithms like SHA-256, SHA-512, and MD5. Let’s see an example of hashing data using SHA-256:

import CryptoKit

func hashData(data: Data) -> String {
let hashedData = SHA256.hash(data: data)
let hashString = hashedData.compactMap { String(format: "%02x", $0) }.joined()
return hashString
}

// Usage:
let inputData = "Hello, Crypto!".data(using: .utf8)!
let hashedString = hashData(data: inputData)
print(hashedString) // Output: 4c776e56ac9ef79641470704e02b57e41a3e395d1c5eece8a6a8d1be10e2f0f0

Symmetric Key Encryption:

Encrypting and decrypting data using a symmetric key is simple, too. You can use one of two available ciphers: AES-GCM and ChaChaPoly in CryptoKit. Here’s an example of encrypting and decrypting data using AES-GCM:

import CryptoKit

func encryptData(data: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.seal(data, using: key)
return sealedBox.combined!
}

func decryptData(ciphertext: Data, key: SymmetricKey) throws -> Data {
let sealedBox = try AES.GCM.SealedBox(combined: ciphertext)
return try AES.GCM.open(sealedBox, using: key)
}

// Usage:
let inputData = "Symmetric Key".data(using: .utf8)!
let key = SymmetricKey(size: .bits256)
let encryptedData = try encryptData(data: inputData, key: key)
let decryptedData = try decryptData(ciphertext: encryptedData, key: key)
let decryptedString = String(data: decryptedData, encoding: .utf8)
print(decryptedString ?? "") // Output: Symmetric Key

Asymmetric Key Encryption:

Asymmetric key encryption, also known as public-key encryption, involves using different keys for encryption and decryption. You can use one of two available algorithms : RSA and Elliptic Curve Cryptography (ECC) for asymmetric encryption in CryptoKit. Here’s an example of encrypting and decrypting data using RSA:

import CryptoKit

func encryptData(data: Data, publicKey: SecKey) throws -> Data {
return try RSA.encrypt(data, using: .init(publicKey: publicKey, padding: .PKCS1))
}

func decryptData(ciphertext: Data, privateKey: SecKey) throws -> Data {
return try RSA.decrypt(ciphertext, using: .init(privateKey: privateKey, padding: .PKCS1))
}

// Usage:
let inputData = "Asymmetric Key Encryption".data(using: .utf8)!
let (privateKey, publicKey) = try RSA.KeyPair.generate(keySize: 2048)
let encryptedData = try encryptData(data: inputData, publicKey: publicKey)
let decryptedData = try decryptData(ciphertext: encryptedData, privateKey: privateKey)
let decryptedString = String(data: decryptedData, encoding: .utf8)
print(decryptedString ?? "") // Output: Asymmetric Key Encryption

Here, we explored how to use the CryptoKit for hashing data, symmetric key encryption and asymmetric key encryption. By leveraging these APIs, we can ensure secure data handling and enhance the security of your applications.

Data communications channels

Next to securely storing user data, we also need to ensure that communication with the services outside your application is secured. There’s no point in securing all the data storage-related stuff if it’s easy to capture the information you send to your back end.

App transport security (ATS)

This feature improves the network security between your application and the outside world and has been available since iOS 9. App Transport Security (ATS) blocks insecure connections by default, requiring you to use HTTPS rather than HTTP.

Even though it’s recommended to keep ATS enabled, you can still visit HTTP domains by disabling it for particular domains. You can specify these unsecured domains in the info.plist file.

SSL Pinning

Using Secure Socket Layer (SSL) Pinning allows you to protect your apps against the many types of Man-in-the-middle (MITM) attacks and interception of its network traffic. Thus, SSL Pinning will tell us whether our connection is compromised or monitored or not.

So, SSL Pinning is a process of introducing SSL Certificate between the Client App and Server so that, each connection is encrypted & secure. It’s more like, a private key is kept at the Server and a Public key is distributed to the clients such that each conversation can be encrypted by the respective key(s).

What types of SSL pinning methods are there?
There are two ways of SSL Pinning in your app —

  1. SSL Pinning via certificate : You can extract the server’s certificate and embed into your app bundle. When the app connects to the server and presents its certificate, compare it with the one embedded in your application.
  2. SSL Pinning via Public Key : You can extract the certificate’s public key and define into your code or place into the app bundle. When the app connects to the server and presents its certificate, compare its public key with the one embedded in your application.

The only one advantage of Public Key method over Certificate is expiration of key. Public Key will never expire, where SSL Certificate has some definite time of validity. Since you hardcode the trusted certificates, the app itself need to be updated if a server certificate expires.

How to implement the SSL Pinning on iOS?

  1. SSL Pinning Using Alamofire : If you are using Alamofire in your project, then, it will be pretty easy to add SSL Pinning. There will be a session object through which you are creating any data request (or, any request), that session object needs to have a SeverTrustManager instance. That’s all, you are done with SSL Pinning.
  2. SSL Pinning Using URLSession : In URLSession, you need to enable the URLSessionDelegate and implement the following method —
private lazy var session = URLSession(configuration: .default, delegate: self,
delegateQueue: nil)
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

and, Validate the SSL Certificate data —

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
if let serverPublicKey = SecTrustCopyPublicKey(serverTrust) {
var error: Unmanaged<CFError>?
if let serverKeyData = SecKeyCopyExternalRepresentation(serverPublicKey, &error) as Data? {
if pinnedPublicKeys.contains(serverKeyData) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
return
}
}
}
completionHandler(.cancelAuthenticationChallenge, nil)
}
priavte let pinnedPublicKeys: [Data] = {
let publicKey = "YOUR_PUBLIC_KEY"
let publicKeyData = Data(base64Encoded: publicKey)!
return [publicKeyData]
}()

This is it, now your request will be pinned with the SSL Certificate and it case it got monitored, then you’ll get SSL Certificate Mismatch error.

Jailbroken device detection

For iOS devices, jail breaking gives the unrestricted access (root privileges) of the application, making it accessible to source code, and crucial files of the application. It allows you to perform actions that wouldn’t otherwise be possible.

You should consider having your app check the device for jail breaking. You can do so with this code:

extension UIDevice {
var isSimulator: Bool {
return TARGET_OS_SIMULATOR != 0
}

var isJailBroken: Bool {
if UIDevice.current.isSimulator { return false }
if JailBrokenHelper.hasCydiaInstalled() { return true }
if JailBrokenHelper.containsSuspiciousApps() { return true }
if JailBrokenHelper.hasSuspiciousSystemPaths() { return true }
return JailBrokenHelper.canEditSystemFiles()
}
}

private struct JailBrokenHelper {
static func hasCydiaInstalled() -> Bool {
return UIApplication.shared.canOpenURL(URL(string: "cydia://package/com.example.package")!)
}

static func containsSuspiciousApps() -> Bool {
for path in suspiciousAppsPathToCheck {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
return false
}

static func hasSuspiciousSystemPaths() -> Bool {
for path in suspiciousSystemPathsToCheck {
if FileManager.default.fileExists(atPath: path) {
return true
}
}
return false
}

static func canEditSystemFiles() -> Bool {
let jailBreakText = "Developer Insider"
do {
try jailBreakText.write(toFile: "/private/jailbreak.txt", atomically: true, encoding: .utf8)
return true
} catch {
return false
}
}

static var suspiciousAppsPathToCheck: [String] {
return [
"/Applications/Cydia.app",
"/Applications/blackra1n.app",
"/Applications/FakeCarrier.app",
"/Applications/Icy.app",
"/Applications/IntelliScreen.app",
"/Applications/MxTube.app",
"/Applications/RockApp.app",
"/Applications/SBSettings.app",
"/Applications/WinterBoard.app",
"/Applications/LibertyLite.app",
"/Applications/Snoop-itConfig.app",
]
}

static var suspiciousSystemPathsToCheck: [String] {
return [
"/Library/MobileSubstrate/DynamicLibraries/LiveClock.plist",
"/Library/MobileSubstrate/DynamicLibraries/Veency.plist",
"/Library/MobileSubstrate/MobileSubstrate.dylib",
"/System/Library/LaunchDaemons/com.ikey.bbot.plist",
"/System/Library/LaunchDaemons/com.saurik.Cydia.Startup.plist",
"/bin/bash",
"/bin/sh",
"/etc/apt",
"/etc/ssh/sshd_config",
"/private/var/tmp/cydia.log",
"/var/tmp/cydia.log",
"/usr/bin/sshd",
"/usr/libexec/sftp-server",
"/usr/libexec/ssh-keysign",
"/usr/sbin/sshd",
"/var/cache/apt",
"/var/lib/apt",
"/var/lib/cydia",
"/usr/sbin/frida-server",
"/usr/bin/cycript",
"/usr/local/bin/cycript",
"/usr/lib/libcycript.dylib",
"/var/log/syslog",
"/private/var/lib/apt",
"/private/var/lib/cydia",
"/private/var/mobile/Library/SBSettings/Themes",
"/private/var/stash"
]
}
}

Please be aware that this is not a foolproof way to detect jail breaking on a device. Security is an active field, and new malware is released every day. You need to stay up to date and add to the code above as new vulnerabilities appear.

Conclusion

Our apps’ security implementation is a task that should never be neglected.

Initially, we discussed the issues that may arise from saving data to the wrong place, and how we can avoid that. Later on, we talked more about taking the security to the next level by saving sensitive data by encrypting first and also discussed the right way to ensure that communication with the services outside your application is secured. And, in the last, how we can alert our app in case of a jailbroken device.

There is no such thing as a perfect defense. Given enough time and resources, a determined hacker will be able to penetrate any system. Instead, we employ security techniques to make the hacker’s job as difficult as possible. Our goal is to harden your application so that hacking it isn’t worth their time.

For reading & learning about these topics, I have found the following articles very useful :-

Thanks for Reading!
If you enjoy this article, make sure to clap (👏) to show your support.
For any questions or suggestions, feel free to leave a note here.

--

--