May 10, 2017
Is it possible to write Quick Look plugins in Swift?
Swift has become the first go-to-language whenever I am going to build something on macOS or iOS. Recently, I was trying to write a quick look plugin in Swift but met some problems.
First, the default quick look plugin files generated by Xcode were .c. Swift is compatible with Objective-C and they can interact with each other well. Swift can also handle C API well but it’s not easy to call Swift from .c files. I changed the file extension from .c to .m and got it fixed.
However, the next problem would not be that easy to fix. After I finished the code and tried to run it in qlmanage, qlmanage failed to load my plugin because it was unable to find libswiftCore.dylib. I checked the Build Settings and set Always Embed Swift Standard Libraries to YES so that all the Swift libraries can be copied to the frameworks directory in the plugin. To make the qlmanage find the frameworks, I also set Runpath Search Paths to @load_path/../Frameworks but the libraries were still not loading. I turned on the debug log by running qlmanage -d4 and found @load_path/../Frameworks “being ignored in restricted program because it is a relative path”.
I posted a question in Apple Developer Forums and quickly got an answer with detail. The short answer is “No”.
No. You could probably make this work but it’s definitely not safe.
The fundamental problem here is the Swift runtime. Quick Look plug-ins are hosted in a system process and it’s possible that that system process might load multiple plug-ins. If there are two plug-ins written in Swift, they might have different versions of the Swift runtime and that will not end well.
The reply also suggested two approaches:
- Eventually Swift will reach ABI stability and be bundled in the system instead of bundled in apps or plug-ins.
- Moving Quick Look over to use the new plug-ins mechanism based on app extension, which allows plug-ins to run in their own process.
As the replier suggested, I opened an request in Apple Bug Reporter and the problem ID is 32095249 . Have a check if you also interested in it.
Mar 23, 2017
Tooth Fairy 2.0 Released!
I just released Tooth Fairy 2.0. The core change of this release is that now Tooth Fairy supports multiple devices.

Multiple device support is one of the most requested features. The first release of Tooth Fairy was a quick implementation to meet a simple requirement. In 2.0, I rewrote most part of the code to fit for the multiple device management.
“making life just a tiny bit easier” is one of my most favorite reviews from all the lovely users leaving reviews on Mac App Store. Hoping the new release is still getting a tiny bit better.
Jan 23, 2017
Local Receipt Validation in Swift 3 Part II - Validation
The receipt validation consists of following steps:
- Verify if the receipt is issued by Apple
- Verify if the receipt is valid for the app
- If you want to verify an in-app-purchase, you need to verify if the IAP receipt is valid.
Let’s go through the process step by step.
Verify the source
First, we need to verify that the receipt is signed by Apple.
guard let receiptURL = Bundle.main.appStoreReceiptURL,
let certificateURL = Bundle.main.url(forResource: "AppleIncRootCertificate", withExtension: "cer"),
let receiptData = NSData(contentsOf: receiptURL),
let certificateData = NSData(contentsOf: certificateURL) else {
throw ReceiptError.invalidReceipt
}
let bio = BIOWrapper(data: receiptData)
let p7 : UnsafeMutablePointer<PKCS7> = d2i_PKCS7_bio(bio.bio, nil)
if p7 == nil {
throw ReceiptError.unexpected
}
OpenSSL_add_all_digests()
let x509Store = X509StoreWrapper()
let certificate = X509Wrapper(data: certificateData)
x509Store.addCert(x509: certificate)
let payload = BIOWrapper()
guard PKCS7_verify(p7!, nil, x509Store.store, nil, payload.bio, 0) == 1 else {
throw ReceiptError.invalidReceipt
}
Verify the receipt
Now we begin to parse the receipt and get following information from the receipt:
- bundle Identifier
- bundle Version
- hash data
- in-app-purchase receipts, if you have
if let contents = p7.pointee.d.sign.pointee.contents,
OBJ_obj2nid(contents.pointee.type) == NID_pkcs7_data ,
let octets = contents.pointee.d.data {
var ptr : UnsafePointer? = UnsafePointer(octets.pointee.data)
let end = ptr!.advanced(by: Int(octets.pointee.length))
var type : Int32 = 0
var xclass: Int32 = 0
var length = 0
ASN1_get_object(&ptr, &length, &type, &xclass,Int(octets.pointee.length))
guard type == V_ASN1_SET else {
return
}
while ptr! < end {
ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_SEQUENCE else {
return
}
guard let attrType = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
return
}
guard let _ = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
return
}
ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_OCTET_STRING else {
return
}
switch attrType {
case 2:
var strPtr = ptr
self.bundleIdData = NSData(bytes: strPtr, length: length)
self.bundleIdString = ASN1ReadString(pointer: &strPtr, length: length)
case 3:
var strPtr = ptr
self.bundleVersionString = ASN1ReadString(pointer: &strPtr, length: length)
case 4:
self.opaqueData = NSData(bytes: ptr!, length: length)
case 5:
self.hashData = NSData(bytes: ptr!, length: length)
case 17:
let p = ptr
let iapReceipt = IAPurchaseReceipt(with: p!, len: length)
self.iapReceipts.append(iapReceipt)
case 21:
var strPtr = ptr
self.expirationDate = ASN1ReadDate(pointer: &strPtr, length: length)
default:
break
}
ptr = ptr?.advanced(by: length)
}
}
After the parsing done, check if the bundle identifier and version are the same as the ones of the app. You also need to check the hash data. Before checking, you need to compute a hash data with the device identifier.
func computedHashData() -> NSData {
let device = UIDevice.current
var uuid = device.identifierForVendor?.uuid
let address = withUnsafePointer(to: &uuid) {UnsafeRawPointer($0)}
let data = NSData(bytes: address, length: 16)
var hash = Array<UInt8>(repeating: 0, count: 20)
var ctx = SHA_CTX()
SHA1_Init(&ctx)
SHA1_Update(&ctx, data.bytes, data.length)
SHA1_Update(&ctx, opaqueData!.bytes, opaqueData!.length)
SHA1_Update(&ctx, bundleIdData!.bytes, bundleIdData!.length)
SHA1_Final(&hash, &ctx)
return NSData(bytes: &hash, length: 20)
}
and then check if it is the same as the one from the receipt.
Verify IAP receipts
IAP receipts are stored in the receipt data and parsed during last step. There could be one or more IAP receipts in the receipt data.
To verify each IAP receipt, we also need to parse the IAP receipt data and get following information from it:
- quantity
- product identifier
- transaction identifier
- original transaction identifier
- purchase date
- original purchase data
- subscription expiration date
- cancellation date
- web order line item ID
var ptr : UnsafePointer<UInt8>? = asn1Data
let end = asn1Data.advanced(by: len)
var type : Int32 = 0
var xclass: Int32 = 0
var length = 0
ASN1_get_object(&ptr, &length, &type, &xclass,Int(len))
guard type == V_ASN1_SET else {
return
}
while ptr! < end {
ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_SEQUENCE else {
return
}
guard let attrType = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
return
}
guard let _ = ASN1ReadInteger(pointer: &ptr, length: ptr!.distance(to: end)) else {
return
}
ASN1_get_object(&ptr, &length, &type, &xclass, ptr!.distance(to: end))
guard type == V_ASN1_OCTET_STRING else {
return
}
switch attrType {
case 1701:
var p = ptr
self.quantity = ASN1ReadInteger(pointer: &p , length: length)
case 1702:
var p = ptr
self.productIdentifier = ASN1ReadString(pointer: &p, length: length)
case 1703:
var p = ptr
self.transactionIdentifier = ASN1ReadString(pointer: &p, length: length)
case 1705:
var p = ptr
self.originalTransactionIdentifier = ASN1ReadString(pointer: &p, length: length)
case 1704:
var p = ptr
self.purchaseDate = ASN1ReadDate(pointer: &p, length: length)
case 1706:
var p = ptr
self.originalPurchaseDate = ASN1ReadDate(pointer: &p, length: length)
case 1708:
var p = ptr
self.subscriptionExpirationDate = ASN1ReadDate(pointer: &p, length: length)
case 1712:
var p = ptr
self.cancellationDate = ASN1ReadDate(pointer: &p, length: length)
case 1711:
var p = ptr
self.webOrderLineItemID = ASN1ReadInteger(pointer: &p, length: length)
default:
break
}
ptr = ptr?.advanced(by: length)
}
After the parsing, you can check if the information is correct according to your own IAP setup.
Table of Content
- Introduction
- Preparation
- Validation