SBN

Common Security Mistakes when Developing Swift Applications – Part I

Overview: Data Storage and Communication Security

Swift was first introduced in 2014 at Apple’s Worldwide Developers Conference (WWDC) as the iOS, macOS, watchOS and tvOS de facto programming language. Designed by Chris Lattner and many others at Apple Inc., Swift is a general-purpose, multi-paradigm, compiled programming language. Although first released as a proprietary programming language, version 2.2 was open sourced under the Apache License 2.0 in late 2015, enabling it to run on Linux systems as well. This may have helped Swift to become a more popular programming language, as it has triggered people to pursue full stack Swift, letting developers build using only Swift to program both the client-facing and server-side of their projects. In the past few years we’ve noticed common security problems when developing Swift applications, all of them part of the ten most critical security risks on the OWASP Mobile TOP 10 2016. This playground covers two major flaws, Insecure Data Storage (M2) and Insecure Communication (M3).

Insecure Data Storage

At some point every application needs to store some data locally, whether it is execution metadata or a user’s data. Quite often decisions about the criticality of what needs to be stored is undervalued based on a few incorrect premises, such as: “the user will never see this” or “this has no value.” If you change the actor, and think about them as the “attacker” instead of the “user,” decisions around data storage would be surely different.

Given physical access to the mobile device, it is trivial for anyone, using available software, to hook up the device to a computer and see all application directories, files and databases. This is the major cause of identity theft, and privacy violations with severe business impacts due to reputation damage.

According to Apple’s guidelines, Keychain is the right place to store an application’s sensitive data, but because it only allows small pieces of data, the file system is generally used to store local data either using plain files or databases. That’s perfectly fine as soon as you follow the following rules:

  1. Persist only data that you really need: what isn’t stored can’t be stolen
  2. Encrypt your local data as needed
  3. DO NOT roll your own crypto

As you may expect, we’re going to talk about Encryption/Cryptography, a not so simple, yet quite interesting topic. We won’t go into cryptography details, instead we’ll simply cover what you really need to keep your local data safe. For detailed guidelines read NIST Encryption Guidelines.

Cryptography covers three different types of algorithms: cryptographic hash functions, symmetric, and asymmetric key algorithms. To protect your local data we will focus on Symmetric Key Algorithms.

Symmetric Key Algorithms

Symmetric Key Algorithms use a single key to both encrypt and decrypt data. Among other things, these algorithms were designed to provide data confidentiality.

There are multiple Symmetric Key Algorithms, with different requirements (e.g. key length). In this article we will cover AES-256 in ECB/CBC mode, which is the de facto standard of data confidentiality.

To start using AES-256 we need a (secret) key, which will be used to encrypt and decrypt data. As soon as our sensitive data is encrypted, the (secret) key becomes our major concern. As we’re talking about a small piece of data (256 bits), Keychain is the right place to store it (for reference, see Apple’s article: Storing Keys in the Keychain).

Let’s start generating a key which SHOULD NOT be the same as the user’s password, nor any meaningful data. Some random data works just fine.

import Foundation
import Security

func computeSymmetricKey() -> String? {
    var keyData = Data(count: 32) // 32 bytes === 256 bits
    let result = keyData.withUnsafeMutableBytes {
        (mutableBytes: UnsafeMutablePointer) -> Int32 in
        SecRandomCopyBytes(kSecRandomDefault, keyData.count, mutableBytes)
    }
    if result == errSecSuccess {
        return keyData.base64EncodedString()
    } else {
        return nil
    }
}

let secretKey = computeSymmetricKey()

And then we will need to store it in Keychain.

enum KeychainErrors:Error {
    case COULDNOTINSERT
    case COULDNOTREAD
}

func store (key: String, withTag: String) throws {
    let fromKey = key.data(using: .utf8)!
    
    let query: [String: Any] = [
        kSecClass as String: kSecClassKey,
        kSecAttrApplicationTag as String: withTag,
        kSecValueRef as String: fromKey
    ]
    
    let status = SecItemAdd(query as CFDictionary, nil)
    guard status == errSecSuccess else { throw KeychainErrors.COULDNOTINSERT }
}
do {
    try store(key: secretKey!, withTag: "com.myapp.keys.localStore")
} catch {
    print(error)
}

Of course then we’ll need to read the (secret) key from the Keychain so that we can encrypt new data.

func read (tag: String) throws -> CFTypeRef {
    let readQuery: [String: Any] = [
        kSecClass as String: kSecClassKey,
        kSecAttrApplicationTag as String: tag,
        kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
        kSecReturnRef as String: true
    ]
    
    var item: CFTypeRef?
    let readStatus = SecItemCopyMatching(readQuery as CFDictionary, &item)
    guard readStatus == errSecSuccess else { throw KeychainErrors.COULDNOTREAD }
    
    return item!
}

do {
    let keyReadFromKeychain = try read(tag: "com.myapp.keys.localStore")
    print(keyReadFromKeychain)
} catch {
    print(error)
}

Now that we have our key securely saved, we need to know how to encrypt and decrypt a message. It should be as simple as this:

let algorithm: SecKeyAlgorithm = .rsaEncryptionOAEPSHA256
let plainText = "this is our golden secret. Encrypt it!"
var error: Unmanaged?
guard let cipherText = SecKeyCreateEncryptedData(secretKey as! SecKey, algorithm,
    plainText as! CFData, &error) as Data? else {
        throw error!.takeRetainedValue() as Error
    }

If you prefer more friendly APIs and you’re OK using third parties (don’t forget to audit them!), check the following projects:

  • CryptoSwifta growing collection of standard and secure cryptographic algorithms implemented in Swift.
  • KeychainSwifta collection of helper functions for saving text and data in the Keychain.

Insecure Communication

A large number of mobile applications have a back-end server or at least integrate with some service (for example, reverse geocoding). In most cases communication are performed using the HTTP protocol, either using JSON messages or XML, but there are many other formats as well.

When these communications are not performed over a secure channel, that is HTTPS (HTML over SSL/TLS), applications face two major problems:

  • Exchanged data can be tampered or stolen in transit;
  • There’s no guarantee with whom the application is talking to;

Man-in-the-Middle (MitM) attacks are a typical example of a third-party (attacker) who sits between the client (your application) and the server, while both think they talking to each other directly. This attacker is capable of tampering with and stealing exchanged data.

Although it is widely knownthat HTTPS mitigates the flaw, many applications are still not using HTTPS, relying on insecure communications instead.

SSL Certificates

There’s no difference, from an implementation stand point, between using HTTP or HTTPS. If you’re also in change of the back-end, everything you need is provided in an SSL certificate.

If a few years ago SSL certificates were expensive, which might have limited HTTPS adoption, right now you can have them for free. Let’s Encryptis a free, automated, and open Certificate Authority” that provides free of charge, perfectly valid SSL certificates available in every country.

There’s no excuse to keep using insecure communications!

When an SSL certificate is issued, you get two parts:

  • a certificate that is public;
  • a key that is private and should not be shared nor accessible;

Both are meant to be stored on your back-end and configured on your HTTP server. Then, when someone wants to talk to your back-end — and prior to any data exchanging, the server offers the certificate’s public part, so that the client can use it to encrypt the next messages. (This is a very simplistic description of the protocol, but enough to give you an overview on how it works.)

Certificate Pinning

Performing communications over HTTPS isn’t always enough: specifically, channels built using well known protocols such as VPN, SSL, and TLS can be vulnerable to a number of attacks. We won’t go into the details of such attacks. If you want to know more, check out ARP spoofing or DNS cache poisoning.

Anytime we want to be relatively certain about whom we’re talking to, we need to check the remote host’s identity, so its certificate or public key should be pinned. In practice, either allowed certificates or public keys have to be packed within the application. Usually certificates are included as asset files and public keys are included as a string within the application source code.

The OWASP Pinning Cheat Sheet is a great technical guide to implementing certificate and public key pinning, with a section dedicated to iOS. Apple also has resources about the subject, check the references listed below.

self.urlSession = NSURLSession(
    configuration: NSURLSessionConfiguration.defaultSessionConfiguration(),
    delegate: self, delegateQueue: nil)

self.urlSession?.dataTaskWithURL(NSURL("https://github.com")!, completionHandler: { (data: NSData, response: NSURLResponse,  error: NSError) -> Void in
    // response management code
    print("here")
}).resume()

func URLSession(session: NSURLSession,  didReceiveChallenge challenge: NSURLAuthenticationChallenge, completionHandler: (NSURLSessionAuthChallengeDisposition, NSURLCredential?) -> Void) {
    let serverTrust = challenge.protectionSpace.serverTrust
    let certificate = SecTrustGetCertificateAtIndex(serverTrust!, 0)
    
    // Set SSL policies for domain name check
    let policies = NSMutableArray();
    policies.addObject(SecPolicyCreateSSL(true, (challenge.protectionSpace.host)))
    SecTrustSetPolicies(serverTrust!, policies);
    
    // Evaluate server certificate
    var result: SecTrustResultType = 0
    SecTrustEvaluate(serverTrust!, &result)
    let isServerTrusted:Bool = (Int(result) == kSecTrustResultUnspecified || Int(result) == kSecTrustResultProceed)
    
    // Get local and remote cert data
    let remoteCertificateData:NSData = SecCertificateCopyData(certificate!)
    let pathToCert = NSBundle.mainBundle().pathForResource(githubCert, ofType: "cer")
    let localCertificate:NSData = NSData(contentsOfFile: pathToCert!)!
    
    if (isServerTrusted && remoteCertificateData.isEqualToData(localCertificate)) {
        let credential:NSURLCredential = NSURLCredential(forTrust: serverTrust!)
        completionHandler(.UseCredential, credential)
    } else {
        completionHandler(.CancelAuthenticationChallenge, nil)
    }
}

Native APIs are quite verbose, with many data types. There are several projects that make certificate pinning easier: for example, take a look at TrustKit.

Stay tuned for part two, covering code tampering and extraneous functionality when developing Swift applications.

References

Apple

External

Projects

The following two tabs change content below.

Paulo Silva is a Security Researcher with a degree in Computer Sciences. In the last +10 years he has been building software but now he’s having fun, also breaking it. He’s a free and open source enthusiast and a regular OWASP contributor. Apart from IT stuff, you’ll find him on his mountain bike mostly doing cross country (XC).

Latest posts by Paulo Silva (see all)

*** This is a Security Bloggers Network syndicated blog from Blog – Checkmarx authored by Paulo Silva. Read the original post at: https://www.checkmarx.com/2018/10/21/security-mistakes-developing-swift-applications-test/