Securing Communications on iOS

Mobile security has become a hot topic. For any app that communicates remotely, it is important to consider the security of user information that is sent across a network. In this post, you’ll learn the current best practices for securing the communications of your iOS app in Swift.

Securing Communications on iOS

Use HTTPS

When developing your app, consider limiting network requests to ones that are essential. For those requests, make sure that they are made over HTTPS and not over HTTP—this will help protect your user’s data from “man in the middle attacks”, where another computer on the network acts as a relay for your connection, but listens in or changes the data that it passes along. The trend in the last few years is to have all connections made over HTTPS. Fortunately for us, newer versions of Xcode already enforce this.

To create a simple HTTPS request on iOS, all we need to do is append “s” to the “http” section of the URL. As long as the host supports HTTPS and has valid certificates, we will get a secure connection. This works for APIs such as URLSession, NSURLConnection, and CFNetwork, as well as popular third-party libraries such as AFNetworking.

App
Transport Security

Over the years, HTTPS has had several attacks against it. Since it’s important to have HTTPS configured correctly, Apple has created App Transport Security (ATS for short). ATS ensures that your app’s network connections are using industry-standard protocols, so that you don’t accidentally send user data insecurely. The good news is that ATS is enabled by default for apps built with current versions of Xcode.

ATS is available as of iOS 9 and OS X El Capitan. Current apps in the store will not suddenly require ATS, but apps built against newer versions of Xcode and its SDKs will have it enabled by default. Some of the best practices enforced by ATS include using TLS version 1.2 or higher, forward secrecy through ECDHE key exchange, AES-128 encryption, and the use of at least SHA-2 certificates.

It’s important to note that while ATS is enabled automatically, it doesn’t necessarily mean ATS is being enforced in your app. ATS works on the foundation classes such as URLSession and NSURLConnection and stream-based CFNetwork interfaces. ATS is not enforced on lower-level networking interfaces such as raw sockets, CFNetwork sockets, or any third-party libraries that would use these lower-level calls. So if you are using low-level networking, you’ll have to be careful to implement ATS’s best practices manually.

ATS Exceptions

Since ATS enforces the use of HTTPS and other secure protocols, you might wonder if you will still be able to make network connections that can’t support HTTPS, such as when you download images from a CDN cache. Not to worry, you can control ATS settings for specific domains in your project’s plist file. In Xcode, find your info.plist file, right click it, and choose Open As > Source Code.

You will find a section called NSAppTransportSecurity. If it is not there, you can add the code yourself; the format is as follows.

<key>NSAppTransportSecurity</key>
<dict>
	<key>NSExceptionDomains</key>
	<dict>
		<key>yourdomain.com</key>
		<dict>
			<key>NSIncludesSubdomains</key>
			<true/>
			<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
			<false/>
		</dict>
	</dict>
</dict>

This lets you change ATS settings for all network connections. Some of the common settings are as follows:

  • NSAllowsArbitraryLoads: Disables ATS. Don’t use this! Future versions of Xcode will remove this key.
  • NSAllowsArbitraryLoadsForMedia: Allows loading of media without ATS restrictions for the AV
    Foundation framework. You should only allow insecure loads if your
    media is already encrypted by another means. (Available on iOS 10 and macOS 10.12.)
  • NSAllowsArbitraryLoadsInWebContent: Can
    be used to turn off the ATS restrictions from web view objects in
    your app. Think first before turning this off as it allows users to
    load arbitrary insecure content within your app. (Available on iOS 10 and macOS 10.12.)
  • NSAllowsLocalNetworking: This can be
    used to allow local
    network resources to be loaded without ATS restrictions.
    (Available on iOS 10 and macOS 10.12.)

The NSExceptionDomains dictionary lets you set settings for specific domains. Here is a description of some of the useful keys you can use for your domain:

  • NSExceptionAllowsInsecureHTTPLoads: Allows the specific domain to use non-HTTPS connections.
  • NSIncludesSubdomains: Specifies if the current rules are passed down to subdomains.
  • NSExceptionMinimumTLSVersion: Used to specify older, less secure TLS versions that are permitted.

Perfect Forward Secrecy

While encrypted traffic is unreadable, it may still get stored. If the private key used to encrypt that traffic is compromised in the future, the key can be used to read all the previously stored traffic.

To prevent this kind of compromise, Perfect Forward Secrecy (PFS) generates a session key that is unique for each communication session. If the key for a specific session is compromised, it will not compromise data from any other sessions. ATS implements PFS by default, and you can control this feature using the plist key NSExceptionRequiresForwardSecrecy. Turning this off will allow TLS ciphers that don’t support perfect forward secrecy.

Certificate
Transparency

Certificate Transparency is an upcoming standard designed to be able to check or audit the certificates presented during the setup of an HTTPS connection.

When your host sets up an HTTPS certificate, it is issued by what is called a Certificate Authority (CA). Certificate Transparency aims at having close to real-time monitoring to find out if a certificate was issued maliciously or has been issued by a compromised certificate authority.

When a certificate is issued, the certificate authority must submit the certificate to a number of append-only certificate logs, which can later be cross-checked by the client and scrutinized by the owner of the domain. The certificate must exist in at least two logs in order for the certificate to be valid.

The plist key for this feature is NSRequiresCertificateTransparency. Turning this on will enforce Certificate Transparency. This is available on iOS 10 and macOS 10.12 and later.

Certificate and Public Key Pinning

When you purchase a certificate to use HTTPS on your server, that certificate is said to be legitimate because it is signed with a certificate from an intermediate certificate authority. That certificate used by the intermediate authority might in turn be signed by another intermediate authority, and so on, as long as the last certificate is signed by a root certificate authority that is trusted.

When an HTTPS connection is established, these certificates are presented to the client. This chain of trust is evaluated to make sure the certificates are correctly signed by a certificate authority that is already trusted by iOS. (There are ways to bypass this check and to accept
your own self-signed certificate for testing, but don’t do this in a production
environment.)

If any of the certificates in the chain of trust are not valid, then the entire certificate is said to be invalid and your data will not be sent out over the untrusted connection. While this is a good system, it’s not foolproof. Various weaknesses exist that can make iOS trust an attacker’s certificate instead of a legitimately signed certificate.

For example, interception proxies may possess an intermediate certificate that is trusted. A reverse engineer can manually instruct iOS to accept their own certificate. Additionally, a corporation’s policy may have provisioned the device to accept their own certificate. All of this leads to the ability to perform a “man in the middle” attack on your traffic, allowing it to be read. But certificate pinning will prevent connections from being established for all of these scenarios.

Certificate pinning comes to the rescue by checking the server’s certificate against a copy of the expected certificate.

In order to implement pinning, the following delegate must be implemented. For URLSession, use the following:

optional func urlSession(_ session: URLSession, 
              didReceive challenge: URLAuthenticationChallenge, 
       completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void)

Or for NSURLConnection, you can use:

optional func connection(_ connection: NSURLConnection, 
              didReceive challenge: URLAuthenticationChallenge)

Both methods allow you to obtain a SecTrust object from challenge.protectionSpace.serverTrust. Because we are overriding the authentication delegates, we must now explicitly call the function which performs the standard certificate chain checks that we’ve just discussed. Do this by calling the SecTrustEvaluate function. Then we can compare the server’s certificate with an expected one.

Here is an example implementation.

import Foundation
import Security

class URLSessionPinningDelegate: NSObject, URLSessionDelegate
{
    func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
    {
        var success: Bool = false
        if let serverTrust = challenge.protectionSpace.serverTrust
        {
            if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
            {
                //Set policy to validate domain
                let policy: SecPolicy = SecPolicyCreateSSL(true, "yourdomain.com" as CFString)
                let policies = NSArray.init(object: policy)
                SecTrustSetPolicies(serverTrust, policies)
                
                let certificateCount: CFIndex = SecTrustGetCertificateCount(serverTrust)
                if certificateCount > 0
                {
                    if let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
                    {
                        let serverCertificateData = SecCertificateCopyData(certificate) as NSData
                        
                        //for loop over array which may contain expired + upcoming certificate
                        let certFilenames: [String] = ["CertificateRenewed", "Certificate"]
                        for filenameString: String in certFilenames
                        {
                            let filePath = Bundle.main.path(forResource: filenameString, ofType: "cer")
                            if let file = filePath
                            {
                                if let localCertData = NSData(contentsOfFile: file)
                                {
                                    //Set anchor cert to your own server
                                    if let localCert: SecCertificate = SecCertificateCreateWithData(nil, localCertData)
                                    {
                                        let certArray = [localCert] as CFArray
                                        SecTrustSetAnchorCertificates(serverTrust, certArray)
                                    }
                                    
                                    //validates a certificate by verifying its signature plus the signatures of the certificates in its certificate chain, up to the anchor certificate
                                    var result = SecTrustResultType.invalid
                                    SecTrustEvaluate(serverTrust, &result);
                                    let isValid: Bool = (result == SecTrustResultType.unspecified || result == SecTrustResultType.proceed)
                                    if (isValid)
                                    {
                                        //Validate host certificate against pinned certificate.
                                        if serverCertificateData.isEqual(to: localCertData as Data)
                                        {
                                            success = true
                                            completionHandler(.useCredential, URLCredential(trust:serverTrust))
                                            break //found a successful certificate, don't need to continue looping
                                        } //end if serverCertificateData.isEqual(to: localCertData as Data)
                                    } //end if (isValid)
                                } //end if let localCertData = NSData(contentsOfFile: file)
                            } //end if let file = filePath
                        } //end for filenameString: String in certFilenames
                    } //end if let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0)
                } //end if certificateCount > 0
            } //end if (challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust)
        } //end if let serverTrust = challenge.protectionSpace.serverTrust
        
        if (success == false)
        {
            completionHandler(.cancelAuthenticationChallenge, nil)
        }
    }
}

To use this code, set the delegate of the URLSession when creating your connection.

if let url = NSURL(string: "https://yourdomain.com")
{
    let session = URLSession(
        configuration: URLSessionConfiguration.ephemeral,
        delegate: URLSessionPinningDelegate(),
        delegateQueue: nil)
    
    let dataTask = session.dataTask(with: url as URL, completionHandler: { (data, response, error) -> Void in
        //...
    })
    
    dataTask.resume()
}

Make sure to include the certificate in your app bundle. If your certificate is a .pem file, you will need to convert it to a .cer file in the macOS terminal:

openssl x509 -inform PEM -in mycert.pem -outform DER -out certificate.cer

Now, if the certificate is changed by an attacker, your app will detect it and refuse to make the connection.

Note that some third-party libraries such as AFNetworking support pinning already.

Sanitization and Validation

With all of the protections so far, your connections should be pretty secure against man in the middle attacks. Even so, one important rule regarding network communications is never to blindly trust the data you are receiving. In fact, it’s good programming practice to design by contract. The inputs and outputs of your methods have a contract that defines specific interface expectations; if the interface says it will return an NSNumber, then it should do so. If your server is expecting a string of 24 characters or fewer, make sure that the interface will only return up to 24 characters.

This helps prevent innocent errors, but more importantly, it can also reduce the likelihood of various injection and memory corruption attacks. Common parsers such as the JSONSerialization class will convert text into Swift data types where these kinds of test can be done.

if let dictionary = json as? [String: Any]
                {
                    if let count = dictionary["count"] as? Int
                    {
                        //...

Other parsers may work with Objective-C equivalent objects. Here is a way to validate that an object is of the expected type in Swift.

if someObject is NSArray

Before you send a delegate a method, make sure the object is of the right type so that it will respond to the method—otherwise the app will crash with an “unrecognized selector” error.

if someObject.responds(to: #selector(getter: NSNumber.intValue)

Additionally, you can see if an object conforms to a protocol before trying to send messages to it:

if someObject.conforms(to: MyProtocol.self)

Or you can check that it matches a Core Foundation object type.

if CFGetTypeID(someObject) != CFNullGetTypeID()

It’s a good idea to carefully choose which information from the server the user can see. For example, it’s a bad idea to display an error alert that directly passes a message from the server. Error messages could disclose debugging and security-related information. One solution is to have the server send specific error codes that cause the client to show predefined messages.

Also, make sure you encode your URLs so that they only contain valid characters. NSString’s stringByAddingPercentEscapesUsingEncoding will work. It does not encode some characters such as ampersands and plus signs, but the CFURLCreateStringByAddingPercentEscapes function allows customization of what will be encoded.

Sanitizing User Data

When sending data to a server, be extremely careful when any user input is passed into commands that will be executed by an SQL server or a server that will run code. While securing a server against such attacks is beyond the scope of this article, as mobile developers we can do our part by removing characters for the language that the server is using so that the input is not susceptible to command injection attacks. Examples might be stripping quotes, semicolons, and slashes when they are not needed for the specific user input.

var mutableString: String = string
mutableString = mutableString.replacingOccurrences(of: "%", with: "")
mutableString = mutableString.replacingOccurrences(of: """, with: "")
mutableString = mutableString.replacingOccurrences(of: "'", with: "")
mutableString = mutableString.replacingOccurrences(of: "t", with: "")
mutableString = mutableString.replacingOccurrences(of: "n", with: "")

It is good practice to limit the length of user input. We can limit the number of characters typed in a text field by setting the UITextField‘s delegate and then implementing its shouldChangeCharactersInRange delegate method.

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool 
{
    let newLength: Int = textField.text!.characters.count + string.characters.count - range.length
    if newLength > maxSearchLength
    {
        return false
    }
    else
    {
        return true
    }
}

For a UITextView, the delegate method to implement this is:

optional func textField(_ textField: UITextField, 
shouldChangeCharactersIn range: NSRange, 
      replacementString string: String) -> Bool

User input can be further validated so that the input is of an expected format. For example, if a user is to enter an email address, we can check for a valid address:

class func validateEmail(from emailString: String, useStrictValidation isStrict: Bool) -> Bool
{
    var filterString: String? = nil
    if isStrict
    {
        filterString = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}"
    }
    else
    {
        filterString = ".+@.+\.[A-Za-z]{2}[A-Za-z]*"
    }
    let emailPredicate = NSPredicate(format: "SELF MATCHES %@", filterString!)
    return emailPredicate.evaluate(with: emailString)
}

If a user is uploading an image to the server, we can check that it is a valid image. For example, for a JPEG file, the first two bytes and last two bytes are always FF D8 and FF D9.

class func validateImageData(_ data: Data) -> Bool
{
    let totalBytes: Int = data.count
    if totalBytes < 12
    {
        return false
    }
        
    let bytes = [UInt8](data)
    let isValid: Bool = (bytes[0] == UInt8(0xff) && bytes[1] == UInt8(0xd8) && bytes[totalBytes - 2] == UInt8(0xff) && bytes[totalBytes - 1] == UInt8(0xd9))
    return isValid
}

The list goes on, but only you as the developer will know what the expected input and output should be, given the design requirements.

URLCache

The data you send over the network has the potential to be cached in memory and on device storage. You can go to great lengths to protect your network communications, as we have been doing, only to find out that the communication is being stored.

Various versions of iOS have had some unexpected behaviour when it comes to the cache settings, and some of the rules for what gets cached in iOS keep changing over the versions. While caching helps network performance by reducing the number of requests, turning it off for any data you think is highly sensitive can be a good idea. You can remove the shared cache at any time (such as on app startup) by calling:

URLCache.shared.removeAllCachedResponses()

To disable caching on a global level, use:

let theURLCache = URLCache(memoryCapacity: 0, diskCapacity: 0, diskPath: nil)
URLCache.shared = theURLCache

And if you are using URLSession, you can disable cache for the session like this:

let configuration = URLSessionConfiguration.default
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
configuration.urlCache = nil
let session = URLSession.init(configuration: configuration)

If you are using an NSURLConnection object with a delegate, you can disable the cache per connection with this delegate method:

func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse?
{
    return nil
}

And to create a URL request that will not check the cache, use:

var request = NSMutableURLRequest(url: theUrl, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: urlTimeoutTime)

Various versions of iOS 8 had some bugs where some of these methods on their own would do nothing. That means it’s a good idea to implementing all of the above code for sensitive connections, when you need to reliably prevent caching of network requests.

The
Future

It is important to understand the limits of HTTPS for protecting network communications.

In most cases, HTTPS stops at the server. For example, my connection to a corporation’s server may be over HTTPS, but once that traffic hits the server, it is unencrypted. This means that the corporation will be able to see the information that was sent (in most cases it needs to), and it also means that company could then proxy or pass that information out again unencrypted.

I can’t finish this article without covering one more concept that is a recent trend—what is called “end-to-end encryption”. A good example is an encrypted chat app where two mobile devices are communicating with each other through a server. The two devices create public and private keys—they exchange public keys, while their private keys never leave the device. The data is still sent over HTTPS through the server, but it is first encrypted by the other party’s public key in such a way that only the devices holding the private keys can decrypt each other’s messages.

As an analogy to help you understand end-to-end encryption, imagine that I want someone to send me a message securely that only I can read. So I provide them a box with an open padlock on it (the public key) while I keep the padlock key (private key). The user writes a message, puts it in the box, locks the padlock, and sends it back to me. Only I can read what the message is because I am the only one with the key to unlock the padlock.

With end-to-end encryption, the server provides a service for communication, but it can not read the content of the communication—they ship the locked box, but they don’t have the key to open it. While the implementation details are beyond the scope of this article, it’s a powerful concept if you want to allow secure communication between users of your app.

If you want to learn more about this approach, a place to start is the GitHub repo for Open Whisper System, an open-source project.

Conclusion

Almost all mobile apps today will communicate across a network, and security is a critically important but often neglected aspect of mobile app development.

In this article, we’ve covered some security best practices, including simple HTTPS, application hardening of network communications, data sanitization, and end-to-end encryption. These best practices should serve as a foundation for security when coding your mobile app.

And while you’re here, check out some of our other popular iOS app tutorials and courses!

  • Securing Communications on iOS
    iOS 10
    Using the Speech Recognition API in iOS 10
    Patrick Balestra
  • Securing Communications on iOS
    iOS SDK
    How to Use Apple’s CloudKit for Push Notifications
    Davis Allie
  • Securing Communications on iOS
    Mobile Development
    Back-End as a Service for Mobile Apps
    Bala Durage Sandamal Siripathi
  • Securing Communications on iOS
    iOS
    Go Further With Swift: Animation, Networking, and Custom Controls
    Markus Mühlberger