Resurrecting CommonCrypto in Swift for hashing, encrypting and obfuscating (iOS 9.2.1, Swift 2.1, Xcode 7.2)


The inability to simply import CommonCrypto into a Swift file has been an awkward moment for encrypting and hashing on Apple devices, but there is a simple way around its exclusion using a bridging header. The bridging header simply needs to include the following line of code:
#import "CommonCrypto/CommonCrypto.h"
Or if you prefer then you can use the constituent parts of CommonCrypto:
#import "CommonCrypto/CommonCryptor.h"
#import "CommonCrypto/CommonDigest.h"
#import "CommonCrypto/CommonHMAC.h"
#import "CommonCrypto/CommonKeyDerivation.h"
#import "CommonCrypto/CommonSymmetricKeywrap.h"
And I believe this isn't the only way to get CommonCrypto functionality back. According to iosdevzone you can run the following line from the Terminal to enable CommonCrypto to be used even within a playground:
sudo xcrun -sdk macosx swift GenerateCommonCryptoModule.swift machos
Whether this enables you to also build an app on a device that imports CommonCrypto I don't know.

Digest

Now we are ready to write a String extension to help us generate hashes using the available digest functions in CommonCrypto.
extension String {
    
    func digest(length:Int32, gen:(data: UnsafePointer<Void>, len: CC_LONG, md: UnsafeMutablePointer<UInt8>) -> UnsafeMutablePointer<UInt8>) -> String {
        var cStr = [UInt8](self.utf8)
        var result = [UInt8](count:Int(length), repeatedValue:0)
        gen(data: &cStr, len: CC_LONG(cStr.count), md: &result)
        
        let output = NSMutableString(capacity:Int(length))
        
        for r in result {
            output.appendFormat("%02x", r)
        }

        return String(output)        
    }
    
}
This code can be called in the following way:
let str = "Hello Swift!"
print(str.digest(CC_SHA1_DIGEST_LENGTH, gen: {(data, len, md) in CC_SHA1(data,len,md)}))
print(str.digest(CC_MD5_DIGEST_LENGTH, gen: {(data, len, md) in CC_MD5(data,len,md)}))
print(str.digest(CC_SHA512_DIGEST_LENGTH, gen: {(data, len, md) in CC_SHA512(data,len,md)}))
We could clean this up and make the code simpler to read with the use of enums and so on, but for now it works and is fairly transparent. So we'll push on.

Encrypt

Generating two-way encryption is a bit more of a fuss, but here (thanks to some help from StackOverflow) are a couple of String extensions for encrypting and decrypting to and from Base64 strings:
extension String {
    
    func aesEncrypt(key:String, iv:String, options:Int = kCCOptionPKCS7Padding) -> String? {
        if let keyData = key.dataUsingEncoding(NSUTF8StringEncoding),
            data = self.dataUsingEncoding(NSUTF8StringEncoding),
            cryptData    = NSMutableData(length: Int((data.length)) + kCCBlockSizeAES128) {
                
                
                let keyLength              = size_t(kCCKeySizeAES128)
                let operation: CCOperation = UInt32(kCCEncrypt)
                let algoritm:  CCAlgorithm = UInt32(kCCAlgorithmAES128)
                let options:   CCOptions   = UInt32(options)
                
                
                
                var numBytesEncrypted :size_t = 0
                
                let cryptStatus = CCCrypt(operation,
                    algoritm,
                    options,
                    keyData.bytes, keyLength,
                    iv,
                    data.bytes, data.length,
                    cryptData.mutableBytes, cryptData.length,
                    &numBytesEncrypted)
                
                if UInt32(cryptStatus) == UInt32(kCCSuccess) {
                    cryptData.length = Int(numBytesEncrypted)
                    let base64cryptString = cryptData.base64EncodedStringWithOptions(.Encoding64CharacterLineLength)
                    return base64cryptString
                    
                    
                }
                else {
                    return nil
                }
        }
        return nil
    }

    func aesDecrypt(key:String, iv:String, options:Int = kCCOptionPKCS7Padding) -> String? {
        if let keyData = key.dataUsingEncoding(NSUTF8StringEncoding),
            data = NSData(base64EncodedString: self, options: .IgnoreUnknownCharacters),
            cryptData    = NSMutableData(length: Int((data.length)) + kCCBlockSizeAES128) {
                
                let keyLength              = size_t(kCCKeySizeAES128)
                let operation: CCOperation = UInt32(kCCDecrypt)
                let algoritm:  CCAlgorithm = UInt32(kCCAlgorithmAES128)
                let options:   CCOptions   = UInt32(options)
                
                var numBytesEncrypted :size_t = 0
                
                let cryptStatus = CCCrypt(operation,
                    algoritm,
                    options,
                    keyData.bytes, keyLength,
                    iv,
                    data.bytes, data.length,
                    cryptData.mutableBytes, cryptData.length,
                    &numBytesEncrypted)
                
                if UInt32(cryptStatus) == UInt32(kCCSuccess) {
                    cryptData.length = Int(numBytesEncrypted)
                    let unencryptedMessage = String(data: cryptData, encoding:NSUTF8StringEncoding)
                    return unencryptedMessage
                }
                else {
                    return nil
                }
        }
        return nil
    }

    
}
The use of Base64 matches the output of openssl_encrypt() and _decrypt() functions in PHP. The default is AES-128-CBC in CommonCrypto but AES-128-ECB can also be used (see code):
let keyString = "12345678901234567890123456789012"
let message = "Hello Swift"
let iv = "iv-salt-string--" // string of 16 characters in length

let encoded = message.aesEncrypt(keyString, iv: iv)
print(encoded)
let unencode = encoded?.aesDecrypt(keyString, iv: iv)
print(unencode)
        
let encodedECB = message.aesEncrypt(keyString, iv: iv, options: kCCOptionPKCS7Padding + kCCOptionECBMode)
print(encodedECB)
let unencodeECB = encodedECB?.aesDecrypt(keyString, iv: iv, options: kCCOptionPKCS7Padding + kCCOptionECBMode)
print(unencodeECB)
Note: use of ECB ignores any iv value.

PHP equivalent

For reference, to generate the same results in PHP for digest and encryption, here is some code:
<?php

// Digest
echo sha1("Hello Swift!")."
";
echo md5("Hello Swift!")."
";

// Encrypt/Decrypt
$data = 'Hello Swift';
$iv = 'iv-salt-string--';
$password = "123456789012asdsadasd";
$method = 'aes-128-cbc';
$enc = openssl_encrypt($data, $method, $password, false, $iv);
echo $enc."\n";
echo openssl_decrypt($data, $method, $password, false, $iv);
?>

Conclusion

Cryptography is a messy area that evolves and changes, and never seems to quite keep up with itself. Often straight answers are hard to find online. CommonCrypto is far from the easiest way to go about things especially when compared to the PHP equivalents. But this post is about getting things working. If you are interested in looking in more detail at cryptography then I'd recommend looking at CryptoSwift and Rob Napier's discussion of AES. As well as taking a look at some pseudo-code and trying to write your Swift from there. I know that working from pseudo-code certainly helped me when I wrote IDPF font obfuscation code, which I include here in the Appendix.

Repositories

IDZSwiftCommonCrypto - CommonCrypto wrapper
CryptoSwift - Pure Swift Cryptography

Sources

http://stackoverflow.com/questions/25388747/sha256-in-swift
http://stackoverflow.com/questions/26449878/sha256-in-swift-importing-framework-issue
http://stackoverflow.com/questions/25754147/issue-using-cccrypt-commoncrypt-in-swift
http://stackoverflow.com/questions/5999370/converting-between-nsdata-and-base64-strings

Further reading

Rob Napier on AES and CommonCrypto (Obj-C)

Appendix

Since it works with the CommonCrypto methods written here, I thought it worth including IDPF font obfuscation code first written in Swift a good few months back.
func obfuscateFontIDPF(data:NSData, key:String) -> NSData {
    // convert string to data
    // now do obfuscation
    let source = data
    var destination = [UInt8]()
    let keyData =  sha1data(key)
    var arr = [UInt8](count: source.length, repeatedValue: 0)
    source.getBytes(&arr, length:source.length)
    arr.count
    var outer = 0
    while outer < 52 && arr.isEmpty == false {
        var inner = 0
        while inner < 20 && arr.isEmpty == false {
            let byte = arr.removeAtIndex(0)      //Assumes read advances file position
            let sourceByte = byte
            let keyByte = keyData[inner]
          //  println(keyByte)
            let obfuscatedByte = (sourceByte ^ keyByte)
            destination.append(obfuscatedByte)
            inner++
        }
        
        outer++
    }
    
    destination.appendContentsOf(arr)
    let newData = NSData(bytes: &destination, length: destination.count*sizeof(UInt8))
    arr.removeAll(keepCapacity: false)
    return newData
}

// thanks to this SO response for strtod http://stackoverflow.com/questions/24031621/swift-how-to-convert-string-to-double/27144221?stw=2#27144221
func sha1data(str:String) -> [UInt8] {
    var keydata = ""
    var dataArray = [UInt8]()
    let crypto = str.digest(CC_SHA1_DIGEST_LENGTH, gen: {(data, len, md) in CC_SHA1(data,len,md)})
    keydata = crypto
        for _ in 0.stride(to: keydata.characters.count, by: 2) {
            var str = "0x\(keydata.characters.first!)"
            keydata.removeAtIndex(keydata.startIndex)
            str.append(keydata.characters.first!)
            keydata.removeAtIndex(keydata.startIndex)
            dataArray.append(UInt8(strtod(str,nil)))
        
    }
    return dataArray
}
and here's a quick test to show that it works:
if let url = NSBundle.mainBundle().URLForResource("Lobster-Regular", withExtension: "ttf"),
    source = NSData(contentsOfURL: url) {
        // obfuscate font using IDPF approach
        let obFont:NSData = obfuscateFontIDPF(source,key: "urn:uuid:9A6376C9-9E0A-4BA4-87CE-667AA91A70DE")
        obFont == source // false, font has been obfuscated
        obfuscateFontIDPF(obFont,key: "urn:uuid:9A6376C9-9E0A-4BA4-87CE-667AA91A70DE") == source // true, font has been obfuscated back to original stat
}



Comments

  1. Hello, thanks for this post which is really helpful!
    I can now communicate between my iOS app and PHP however I remark that when the encrypted message contains a character '+' (at least), the PHP side can not decrypt it correctly... do you have an idea why ?
    Many thanks in advance

    ReplyDelete
  2. Thanks a lot.It really helped me to solve cross platform encryption in OSX and PHP.

    ReplyDelete

Post a Comment