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 machosWhether 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 wrapperCryptoSwift - Pure Swift Cryptography
Sources
http://stackoverflow.com/questions/25388747/sha256-in-swifthttp://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 }
Hello, thanks for this post which is really helpful!
ReplyDeleteI 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
Thanks a lot.It really helped me to solve cross platform encryption in OSX and PHP.
ReplyDelete