iOS – How To Make Our iOS Applications More Secure ?

Hello Readers, CoolMonkTechie heartily welcomes you in this article.

In this article, We will learn about iOS App Security. Mobile applications are the center of most peoples’ technology usage. They deal with a lot of private and sensitive user data like your personal health information or banking information. Protecting this data as well as possible is heavily important and the topic of this article.

We’ll discuss concrete techniques for making our iOS apps more secure. Our best practices cover means for securely storing data as well as sending & receiving data over the network. We’ll see why it is so hard to get security right and how we can improve our app security by using services from Apple and other providers.

We will focus on three main topics related to iOS App Security:

  • Storing user data safely.
  • Secure data transportation.
  • How to use Apple’s new cryptographic APIs.

A famous quote about Security and Learning is :

” Learning how to live with insecurity is the only security. “


So Let’s begin.

Chances are that our app handles private data that we don’t want to end up in the wrong hands. Therefore, we need to make sure to store this data safely and make data transportation as secure as possible.


Best Practices for Storing User Data

If we are developing iOS apps lots of security features are already provided by the OS. All iOS devices with an A7 processor or later also have a coprocessor called the Secure Enclave. It powers iOS security features in a hardware-accelerated way.


Apple’s App Sandbox

All apps running on iOS run in a sandbox to make sure the app can only access data which is stored in the app’s unique home directory. If an app wants to access data outside of its home directory it needs to use services provided by iOS, like the ones available for accessing iCloud data or the photo album. Therefore, no other app can read or modify data from our app.

Apple’s App Sandbox is powered by UNIX’s user permissions and makes sure that apps get executed with a less privileged “mobile” user. Everything outside the app’s home directory is mounted read-only. All system files and resources are protected. The available APIs don’t allow apps to escalate privileges in order to modify other apps or iOS itself.

For performing specific privileged operations an app needs to declare special entitlements. These entitlements get signed together with the app and are not changeable. Examples of services that need special entitlements are HealthKit or audio input. Some entitlements are even restricted to be only used if Apple gives you access to them. This includes services like CarPlay. They are stronger protected because misusing them could have fatal consequences.

Next to entitlements giving us special rights, apps can make use of the iOS extensions system. The OS has many points to be used by app extensions. App extensions are single-purpose executables bundled with the app. They run in their own address space and get controlled by the OS.

Additionally, iOS has methods to prevent memory-related security bugs. Address space layout randomization (ASLR) randomizes the assigned memory regions for each app on every startup. This makes the exploitation of memory-corruption-bugs much less likely. Also, memory pages are marked as non-executable with ARM’s Execute Never (XN) feature to stop malicious code from being executed.


Data Protection API

All iOS versions since iOS 4 have a built-in security feature called Data Protection. It allows an app to encrypt and decrypt the files stored in their app directory. The encryption and decryption processes are automatic and hardware-accelerated. Data Protection is available for file and database APIs, including NSFileManager, CoreData, NSData, and SQLite.

The feature is enabled by default but can be configured on a per-file basis to increase security. Every file can be configured to use one of 4 available protection levels. By default, all files are encrypted until the first user authentication but it might make sense to increase the protection level for certain data.

The four available protection levels include:

  • No protection: The file is always accessible and not encrypted at all
  • Complete until first user authentication: This is enabled by default and decrypts the file after the user unlocks their device for the first time. Afterward, the file stays decrypted until the device gets rebooted. Locking the device doesn’t encrypt the data again.
  • Complete unless open: The file is encrypted until the app opens the file for the first time. The decryption stays alive even when the device gets locked by the user.
  • Complete: The file is only accessible when the device is unlocked.

We can specify the protection level when we create files like this:

try data.write(to: fileURL, options: .completeFileProtection)

But we are also able to change the protection level of existing files by setting the resource values:

try (fileURL as NSURL).setResourceValue( 
                  URLFileProtection.complete,
                  forKey: .fileProtectionKey)

It is important to understand which protection levels fits our needs. By default, we should use the highest protection level possible. However, if we need access to files in the background while the phone is locked we can’t use complete data encryption for them.


Keychain

The keychain is our secure place to store small chunks of data. It is a hardware-accelerated secure data storage that encrypts all of its contents. It is used by the system to store data like passwords and certificates but we as an app developer have also access to this data storage.

Our app or app group has its own space in the keychain and no other app has access to it. This way, we don’t need to store encryption keys in our app and can rely on the system to provide the highest level of security.

The keychain is the secure key-value storage replacement for NSUserDefaults. NSUserDefaults are not encrypted at all and should be avoided for sensitive data.

For every keychain item, we can define specific access policies for accessibility and authentication. We can require user presence (requesting Face ID or Touch ID unlock) or that the biometric ID enrolment didn’t change since the item was added to the keychain.

As an additional feature of the iOS Keychain, we can decide if we want to store the information in the local keychain which is only available on this specific device, or in the iCloud Keychain which gets synchronized across all Apple devices. This gives us the ability to share the information between our iPhone, iPad and Mac counterparts.

Even though the Keychain Services API should be used whenever possible, its interface is not exactly nice to use in Swift. If we plan to use such functionality more often in our codebase consider wrapping it with a nicer Swift API.

let query: [String: Any] = [
    kSecClass as String: kSecClassInternetPassword,
    kSecAttrAccount as String: account,
    kSecAttrServer as String: server,
    kSecValueData as String: password
]

let status = SecItemAdd(query as CFDictionary, nil)

This code snippet stores some credentials to the keychain. A lot of casting is necessary and in the end, we call SecItemAdd which synchronously stores the credentials and returns either a success or error status code.


Best Practices for Secure Data Transportation

Next to storing the user data safely, we should make sure that the communication between our app and its remote counterparts is secured. This prevents attackers from collecting private data by sniffing the network traffic or by running malicious servers.


HTTPs

Most network communication is done over the HTTP protocol between a client and a server. By default, HTTP connections are not encrypted. It is easily possible for attackers to sniff data from our local network or to perform man-in-the-middle attacks.

Since iOS 9, there is a new feature called App Transport Security (ATS). It improves the security of network communication in our apps. ATS blocks insecure connections by default. It requires all HTTP connections to be performed using HTTPS secured with TLS.

ATS can be configured in many ways to loosen up these restrictions. We can, therefore, allow insecure HTTP connections for specific domains or change the minimum TLS version used for HTTPS.

If our app contains an in-app browser we should use the NSAllowsArbitraryLoadsInWebContent configuration which allows our users to browse the web normally and still makes sure that all other network connections in our app use the highest security standards.


SSL Pinning

By default, HTTPS connections are verified by the system: it inspects the server certificate and checks if it’s valid for this domain. This should make sure the connected server is not malicious. However, there are still ways for attackers to perform more complex man-in-middle attacks.

The system certificate trust validation checks if the certificate was signed by a root certificate of a trusted certificate authority. To circumvent this security mechanism attackers would need to explicitly trust another malicious certificate in the user’s device settings or compromise a certificate authority. The attacker could then perform a man-in-the-middle attack to read all messages sent between client and server.

To prevent these kinds of attacks an app can perform additional trust validation of server certificates. This is called SSL or Certificate Pinning. We can implement SSL Pinning by including a list of valid certificates (or its public keys or its hashes) in our app bundle. The app can, therefore, check if the certificate used by the server is on this list and only then communicate with the server.

Implementing this validation from scratch should be avoided since implementation mistakes are very likely and lead to even more security vulnerabilities. We can recommend using an Open Source framework like TrustKit.

Introducing SSL Pinning, unfortunately, introduces the risk of bricking our app. Since we hardcode the trusted certificates, the app itself needs to be updated if a server certificate expires. To avoid such a situation, pin the future certificates in the client app before releasing new server certificates.


Push Notifications

To send push notifications to our users, we need to use Apple’s APNS services. If we want to use end-to-end encryption or if we just don’t want to give Apple the (theoretical) chance to read your messages you can use UNNotificationServiceExtension extensions to modify the messages on the client-side.

This allows us to send either encrypted messages to our clients or use placeholders for sensitive data. The messages will simply be used as a wakeup call for the app. The app can then decrypt the message or respectively for placeholder messages fetch the necessary information from the local device and replace the placeholders with the sensitive data. The clear text message will then be shown on the user’s lock screen without any sensitive information being sent to Apple’s servers.

In the following example, we can see how easily we can change our message content in our notification service extension.

class NotificationService: UNNotificationServiceExtension {

    override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
        guard let mutableContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
            fatalError("Cannot convert notification content to mutable content")
        }

        mutableContent.title = decryptText(mutableContent.title)
        mutableContent.body = decryptText(mutableContent.body)

        contentHandler(mutableContent)
    }

    // ...

}


End-to-end encryption

End-to-end encryption is the “holy grail” for secure data transportation. It allows us to encrypt messages in a way that only the sender and receiver can decrypt them and neither Apple or our servers can read the cleartext data.

End-to-end encryption is not easy to implement and requires a lot of experience in cryptographic processes. If our team doesn’t have the experience it is very advisable to consult a third party expert helping with the implementation of the encryption mechanism.


CloudKit

If our app doesn’t need a server , we can use Apple’s CloudKit. CloudKit allows us to store data in iCloud containers while using our Apple ID as the login mechanism for our app. This way, we don’t need to implement all of these services on our own.

CloudKit was made with security in mind. As an app developer, we don’t have access to concrete user data and the communication is encrypted by default.

All the communication between our app and the server can be done using Apple’s client-side CloudKit framework. If we have an additional Android or Web app, we can still use CloudKit using its JavaScript and Web services.

To make this even better: CloudKit is completely free of charge to a certain amount. We can reach millions of users without fearing costs for traffic, data storage or requests.


Best Practices of Using Apple’s cryptographic APIs

The iOS SDK includes APIs to handle common cryptographic tasks for us. As we said above it is generally a good idea to rely on proven crypto implementations and not reimplement them ourself.


Apples CryptoKit

Apples CryptoKit is a new API that was introduced in iOS 13 and provides lower-level APIs to perform cryptographic operations or implement security protocols.

CryptoKit is based on top of more lower-level APIs. They were available before but introduced additional risk factors since developers often used them in a wrong way.

CryptoKit also allows us to use the SecureEnclave to get cryptographically safe functions that are performant and optimized for the device’s hardware.

If we want to support older iOS versions, we can use those lower-level APIs or use well known open-source third-party libraries like CryptoSwift.


Hashing data

Hash functions are functions that convert data of arbitrary size to fixed-size values. Good hash functions should minimize duplication of output values (so-called collisions) and be very fast to compute. Swift includes hashing functionality in the Swift Standard Library, but these functions are heavily focused on being fast and have a relative high collision rate. This makes them a good fit for performance-critical operations, but not so much for security related operations.

Securely hashing data is simple using CryptoKit. We just need to call the hash function on one of our Structs and choose a hash algorithm. CryptoKit supports the most common ones from SHA512 to SHA256.

let data = ...
let dataDigest = SHA512.hash(data: data)
let hashString = dataDigest.description


Authenticating Data using Message authentication codes

A message authentication code (MAC) is used to authenticate the sender of a message and confirm the integrity of that message. It allows the receiver to verify the origins of messages and detect changes to the message’s content.

To use this kind of integrity checks we can, for example, use CryptoSwift’s hashed authentication codes, also known as HMACs. The HMAC struct is a generic type that can be used with all the hash functions included in CryptoSwift.

To create a message authentication code , we can use this simple code snippet.

let key = SymmetricKey(size: .bits256)

let authenticationCode = HMAC<SHA256>.authenticationCode(for: messageData, using: key)

We can verify the message by using:

let isValid = HMAC<SHA256>.isValidAuthenticationCode(
    Data(authenticationCode),
    authenticating: messageData,
    using: key
)

As we can see, we need to create a symmetric key first and securely share it between the sender and receiver.


Encrypting Data using symmetric keys

Encrypting and decrypting data using a symmetric key is simple, too. We can use one of two available ciphers: ChaChaPoly (ChaCha20-Poly1305) or AES-GCM in CryptoSwift:

let encryptedData = try! ChaChaPoly.seal(data, using: key).combined

let sealedBox = try! ChaChaPoly.SealedBox(combined: encryptedData)

let decryptedData = try! ChaChaPoly.open(sealedBox, using: key)

We should always make sure not to hardcode these symmetric keys in our app though. We can generate a symmetric key at runtime and then store it safely in the keychain. That way, no one has access to our key to decrypt data.


Performing Key Agreement

In a lot of cases, we will need to perform some form of key exchange to exchange cryptographic keys over an insecure channel. With key agreement protocols like Elliptic-curve Diffie–Hellman (ECDH), we can derive a common secret.

1. Create private/public key-pairs for Alice and Bob

let alicePrivateKey = P256.KeyAgreement.PrivateKey()
let alicePublicKey = alicePrivateKey.publicKey
let bobPrivateKey = P256.KeyAgreement.PrivateKey()
let bobPublicKey = bobPrivateKey.publicKey

Both Alice and Bob create their own private/public key-pairs and share their public keys.

2. Deriving shared secret

let aliceSharedSecret = try! alicePrivateKey.sharedSecretFromKeyAgreement(with: bobPublicKey)

let bobSharedSecret = try! bobPrivateKey.sharedSecretFromKeyAgreement(with: alicePublicKey)

We were now able to derive a common secret by using the own private key together with the counterpart’s public key. aliceSharedSecret and bobSharedSecret are now the same.

3. The created secret number should not be used as an encryption key by itself. Instead it can be used to generate a much larger and more secure encryption key using HKDF or X9.63 key derivation.

let usedSalt = "Secure iOS App".data(using: .utf8)!

let symmetricKey = aliceSharedSecret.hkdfDerivedSymmetricKey(
    using: SHA256.self,
    salt: protocolSalt,
    sharedInfo: Data(),
    outputByteCount: 32
)

That’s generally how you could implement it! Also note that Apple has an implementation of Diffie-Hellman in iOS as part of Secure Transport.


Creating and Verifying Signatures

If we want to send messages and make sure that the sender is the person we thought it is we can use signatures. To do so, we need a private/public key pair first.

let signingKey = Curve25519.Signing.PrivateKey()
let signingPublicKey = signingKey.publicKey

Using the private key any form of data can be signed.

let data = ...
let signature = try! signingKey.signature(for: data)

This signature is then sent together with the actual data to the receiver which can use the public key to validate the signature.

let isSignatureValid = signingPublicKey.isValidSignature(signature, for: data)

That’s all about in this article.


Conclusion

In this article, We understood about Apple’s iOS security Best Practices. Creating secure and robust iOS applications is not easy. However, we can improve security in our iOS apps without much effort by sticking to best practices. Protecting user data must be a high priority and should never be ignored.

Thanks for reading ! I hope you enjoyed and learned about Apple’s iOS security Best Practices. Reading is one thing, but the only way to master it is to do it yourself.

Please follow and subscribe us on this blog and and support us in any way possible. Also like and share the article with others for spread valuable knowledge.

If you have any comments, questions, or think I missed something, feel free to leave them below in the comment box.

Thanks again Reading. HAPPY READING !!???

Loading

1 thought on “iOS – How To Make Our iOS Applications More Secure ?”

Leave a Comment