Tuesday, September 10, 2019

Swift ASN.1 Decoder for iOS Receipt Validation

If you want to have in-app purchases in an iOS or MacOS app, you need a way to check what purchases have been made. Annoyingly, Apple does not provide developers with any code for doing this. Apple's APIs will give your program a receipt, listing what was purchased, but the receipt is encoded in a weird format, and Apple doesn't provide any code for reading this format. Apple's reasoning is that not providing code for this is like a very limited form of DRM/copy protection. If every program has custom code for parsing and interpreting the receipt, software pirates will need to do extra work to crack your software.

It is true that software piracy is rampant on Android, and it probably exists on iOS too. Some of us aren't really too concerned with this software piracy issue though, and we just want to implement some quick and dirty handling of IAP with the assumption that most software pirates wouldn't have purchased the software anyway. 

Apple's preferred solution is for you to create your own receipt validation server that your programs can connect to, which will then contact Apple's servers to parse the receipt and to confirm that it's valid. This is a bit of hassle because you have to make an online service, figure out how to keep it running, protect it from hackers, and make your app more fragile because it will always be connecting to this online service.

The other solution is to do receipt validation on the app itself. This is annoying because Apple doesn't provide code for parsing the receipt, the receipt stored on the app contains less information than what Apple provides to servers, and iOS doesn't really bother to keep the receipt up-to-date all the time meaning you often have to go out of your way to update the receipt yourself. The most common way to do the receipt parsing is to just include a copy of OpenSSL in the app, but that involves some annoying interfacing with C code.

I just wanted something quick & dirty, and I'm not too concerned about doing all the signature checking and whatnot, so I just wanted some simpler Objective-C or Swift code online for doing receipt parsing. I tried looking around online a lot, but I couldn't find one, so eventually, I just rolled my own. It's pretty rough since I just threw it together until it worked just enough that it would work for my own app, so use at your own risk. Here it is:

struct Asn1BerTag : CustomStringConvertible {
    var constructed: Bool
    var tagClass: Int
    var tag: Int
    var description: String {
        return String(tagClass) + (constructed ? "C": "-") + String(tag);
    }
}

struct Asn1Entry {
    let tag : Asn1BerTag
    let data : Data
    let len : Int
}

// TODO: This parser thing is sort of insecure because it doesn't really do bounds-checking on
// anything, but it's only used for reading internal data structures so whatever
class Asn1Parser {
    // Parse a single ASN 1 BER entry
    static func parse(_ data: Data, startIdx: Int = 0) -> Asn1Entry {
        var idx = startIdx
        // Try to parse the tag
        var val = data[idx]
        idx += 1
        let tagClass = Int((val >> 6) & 3)
        let constructed = (val & (1 << 5)) != 0
        var tagVal = Int(val & 0x1F)
        if tagVal == 31 {
            val = data[idx]
            idx += 1
            while (val & 0x80) != 0 {
                tagVal <<= 8
                tagVal |= Int(val & 0x7F)
                val = data[idx]
                idx += 1
            }
            tagVal <<= 8
            tagVal |= Int(val & 0x7F)
        }
        let tag = Asn1BerTag(constructed: constructed, tagClass: tagClass, tag: tagVal)
        
        // Try to parse the size
        var len = 0
        var nextTag = 0
        val = data[idx]
        idx += 1
        if val & 0x80 == 0 {
            len = Int(val)
            nextTag = idx + len
        } else if val != 0x80 {
            let numOctets = Int(val & 0x7f)
            for _ in 0..<numoctets {
                len <<= 8
                val = data[idx]
                idx += 1
                len |= Int(val) & 0xFF
            }
            nextTag = idx + len
        } else {
            // Indefinite length. Scan until we encounter 2 zero bytes
            var scanIdx = idx
            while data[scanIdx] != 0 && data[scanIdx+1] != 0 {
                scanIdx += 1
            }
            len = scanIdx - idx
            nextTag = scanIdx + 2
        }
        return Asn1Entry(tag: tag, data: data.subdata(in: idx..<(idx + len)), len: nextTag - startIdx)
    }
    
    static func parseSequence(_ data: Data) -> [Asn1Entry] {
        var toReturn : [Asn1Entry] = []
        var idx = 0
        while idx < data.count {
            let entry = Asn1Parser.parse(data, startIdx: idx)
            toReturn.append(entry)
            idx += entry.len
        }
        
        return toReturn
    }
    
    static func parseInteger(_ data: Data) -> Int {
        let len = data.count
        var val = 0
        
        for i in 0..<len {
            if i == 0 {
                val = Int(data[i] & 0x7F)
            } else {
                val <<= 8
                val |= Int(data[i])
            }
        }
        if len > 0 && data[0] & 0x80 != 0 {
            let complement = 1 << (len * 8)
            val -= complement
        }
        return val
    }
    
    static func parseObjectIdentifier(_ data:Data, startIdx: Int = 0, len: Int? = nil) -> [Int] {
        let dataLen = len ?? data.count
        var idx = startIdx
        var identifier: [Int] = []
        while idx < startIdx + dataLen {
            var subidentifier = 0
            var val = data[idx]
            idx += 1
            while (val & 0x80) != 0 {
                subidentifier <<= 7
                subidentifier |= Int(val & 0x7F)
                val = data[idx]
                idx += 1
            }
            subidentifier <<= 7
            subidentifier |= Int(val & 0x7F)
            identifier.append(subidentifier)
        }
        
        return identifier
    }
}

class IapReceipt {
    var quantity: Int?
    var product_id: String?
    var transaction_id: String?
    var original_transaction_id: String?
    var purchase_date: Date?
    var original_purchase_date: Date?
    var expires_date: Date?
    var is_in_intro_offer_period: Int?
    var cancellation_date: Date?
    var web_order_line_item_id: Int?
}

class AppReceipt {
    var bundle_id : String?
    var application_version : String?
    var receipt_creation_date: Date?
    var expiration_date: Date?
    var original_application_version : String?
    var iaps: [IapReceipt] = []
}

class ReceiptInsecureChecker {
   
    func parsePkcs7ReceiptForPayload(_ data: Data) -> Data? {
        
        // Root is a sequence (tag 16 is sequence)
        let root = Asn1Parser.parseSequence(data)
        guard root.count == 1 && root[0].tag.tag == 16 else { return nil }
        
        // Inside the sequence is some signed data (tag 6 is object identifier)
        let rootSeq = Asn1Parser.parseSequence(root[0].data)
        guard rootSeq.count == 2 && rootSeq[0].tag.tag == 6 && Asn1Parser.parseObjectIdentifier(rootSeq[0].data) == [42, 840, 113549, 1, 7, 2] else { return nil }
        
        // Signed Data contains a sequence
        let signedData = Asn1Parser.parseSequence(rootSeq[1].data)
        guard signedData.count == 1 && signedData[0].tag.tag == 16 else { return nil }
        
        // The third field of the signed data sequence is the actual data
        let signedDataSeq = Asn1Parser.parseSequence(signedData[0].data)
        guard signedDataSeq.count > 3 && signedDataSeq[2].tag.tag == 16 else { return nil }
        
        // The content data should be tagged correctly
        let contentData = Asn1Parser.parseSequence(signedDataSeq[2].data)
        guard contentData.count == 2 && contentData[0].tag.tag == 6 && Asn1Parser.parseObjectIdentifier(contentData[0].data) == [42, 840, 113549, 1, 7, 1] else { return nil }
        
        // Payload should just be some bytes (tag 4 is octet string)
        let payload = Asn1Parser.parse(contentData[1].data)
        guard payload.tag.tag == 4 else { return nil }
        
        return payload.data
    }
    
    func parseReceiptAttributes(_ data: Data) -> AppReceipt? {
        var appReceipt = AppReceipt()
        
        // Root is a set (tag 17 is a set)
        let root = Asn1Parser.parse(data)
        guard root.tag.tag == 17 else { return nil }
        
        // Read set entries
        let receiptAttributes = Asn1Parser.parseSequence(root.data)
        
        // Parse each attribute
        for attr in receiptAttributes {
            if attr.tag.tag != 16 { continue }
            let attrEntries = Asn1Parser.parseSequence(attr.data)
            guard attrEntries.count == 3 && attrEntries[0].tag.tag == 2 && attrEntries[1].tag.tag == 2 && attrEntries[2].tag.tag == 4 else { return nil }
            
            let type = Asn1Parser.parseInteger(attrEntries[0].data)
            let version = Asn1Parser.parseInteger(attrEntries[1].data)
            let value = attrEntries[2].data
            switch (type) {
            case 2:
                let valEntry = Asn1Parser.parse(value)
                // tag 12 = utf8 string
                guard valEntry.tag.tag == 12 else { break }
                appReceipt.bundle_id = String(bytes: valEntry.data, encoding: .utf8)
            case 3:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 12 else { break }
                appReceipt.application_version = String(bytes: valEntry.data, encoding: .utf8)
            case 12:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 22 else { return nil }
                appReceipt.receipt_creation_date = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
            case 17:
                let iap = parseIapAttributes(value)
                if iap != nil {
                    appReceipt.iaps.append(iap!)
                }
            case 19:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 12 else { break }
                appReceipt.original_application_version = String(bytes: valEntry.data, encoding: .utf8)
            case 21:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 22 else { return nil }
                appReceipt.expiration_date = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
            default:
                break
            }
        }
        
        return appReceipt
    }
    
    func parseIapAttributes(_ data: Data) -> IapReceipt? {
        let iap = IapReceipt()
        
        // Root is a set (tag 17 is a set)
        let root = Asn1Parser.parse(data)
        guard root.tag.tag == 17 else { return nil }
        
        // Read set entries
        let receiptAttributes = Asn1Parser.parseSequence(root.data)
        
        // Parse each attribute
        for attr in receiptAttributes {
            if attr.tag.tag != 16 { continue }
            let attrEntries = Asn1Parser.parseSequence(attr.data)
            guard attrEntries.count == 3 && attrEntries[0].tag.tag == 2 && attrEntries[1].tag.tag == 2 && attrEntries[2].tag.tag == 4 else { return nil }
            
            let type = Asn1Parser.parseInteger(attrEntries[0].data)
            let version = Asn1Parser.parseInteger(attrEntries[1].data)
            let value = attrEntries[2].data
            switch (type) {
            case 1701:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 2 else { return nil }
                iap.quantity = Asn1Parser.parseInteger(valEntry.data)
            case 1702:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 12 else { return nil }
                iap.product_id = String(bytes: valEntry.data, encoding: .utf8)
            case 1703:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 12 else { return nil }
                iap.transaction_id = String(bytes: valEntry.data, encoding: .utf8)
            case 1704:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 22 else { return nil }
                iap.purchase_date = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
            case 1706:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 22 else { return nil }
                iap.original_purchase_date = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
            case 1708:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 22 else { return nil }
                iap.expires_date = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
            case 1719:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 2 else { return nil }
                iap.is_in_intro_offer_period = Asn1Parser.parseInteger(valEntry.data)
            case 1712:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 22 else { return nil }
                iap.cancellation_date = parseRfc3339Date(String(bytes: valEntry.data, encoding: .utf8) ?? "")
            case 1711:
                let valEntry = Asn1Parser.parse(value)
                guard valEntry.tag.tag == 2 else { return nil }
                iap.web_order_line_item_id = Asn1Parser.parseInteger(valEntry.data)
            default:
                break
            }
        }
        return iap
    }
    
    func parseRfc3339Date(_ str: String) -> Date? {
        let posixLocale = Locale(identifier: "en_US_POSIX")
        
        let formatter1 = DateFormatter()
        formatter1.locale = posixLocale
        formatter1.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ssX5"
        formatter1.timeZone = TimeZone(secondsFromGMT: 0)
        
        let result = formatter1.date(from: str)
        if result != nil {
            return result
        }
        
        let formatter2 = DateFormatter()
        formatter2.locale = posixLocale
        formatter2.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss.SSSSSSX5"
        formatter2.timeZone = TimeZone(secondsFromGMT: 0)
        
        return formatter2.date(from: str)
    }    
}

To use the code, you would write something like this:

let data = Data(base64Encoded: "... BASE 64 DATA ...")
let receiptChecker = ReceiptInsecureChecker()
let payload = receiptChecker.parsePkcs7ReceiptForPayload(receipt!)
let appReceipt = receiptChecker.parseReceiptAttributes(payload!)
print(appReceipt!.iaps)

Note: I'm not a Swift coder. I only starting learning Swift about a month ago, so I apologize if the code is not very Swift-y

Wednesday, July 31, 2019

CorelDraw Graphics Suite 2019 Review

Since I'm originally from Ottawa, I've always used CorelDRAW for vector graphics. This actually works out well. Since I'm not an artist or designer, I rarely need to do any vector graphics work, so CorelDRAW has worked for me because it comes with a lot of functionality, I could make a one-time purchase of a perpetual license to the software, and I was occasionally able to get good deals when buying it.

I previously used CorelDraw X5, and it did what I needed it to do, but the menus didn't work quite right on Windows 10, so I was looking to upgrade if the upgrade price ever dropped to around $100-$150 or so, but the price never dropped that low, so I just kept using my old version. Especially since I now make my own vector graphics package, I rarely needed CorelDraw except for some occasional obscure feature. Unfortunately, Corel declared that 2019 would be the last year they would offer upgrade pricing on CorelDraw, so I decided to pick up a copy of CorelDraw 2019 since it would be my last chance to get an upgrade.

I have to say that I feel a little disappointed with CorelDraw 2019. CorelDraw has always been a buggy piece of software. But usually it's the new features that are buggy, but if you stick with the core vector graphics stuff then it works fine. Usually, the new features would be so buggy that they would be unusable, but Corel wouldn't bother fixing it until a later version, so you would just have to pay for an upgrade to fix those bugs and get working versions of the new features. Unfortunately, it seems like they rewrote the core user interface code in this version, so now the core vector graphics functionality is buggy. I suspect it might be related to the fact that they've rewritten stuff so that it works on the Mac (previously, CorelDraw was Windows only). This is annoying to me because CorelDraw 2019 is too buggy for basic vector graphics work, but it likely won't be fixed unless I buy an upgrade to a later version, but Corel isn't going to be selling upgrades any more. I'm sorely tempted to keep using CorelDraw X5. The bugs are just little annoying little things like the screen blanking out if you scroll the window using the scrollbar, requiring you to press ctrl-W to manually refresh the screen. Groups also no longer snaps properly to grids. If you try to move a group, CorelDraw will choose one of the objects of the group (I think it's the top one?), and snap that to the grid instead of aligning the group as a whole. This makes grids sort of useless to me. CorelDraw also doesn't let you snap to grids and snap to objects at the same time. It gets confused and tries snapping to objects, and will completely ignore any possible grid snapping you can do. If basic functionality like scrolling and snap to grid don't work, then how is anyone supposed to get any productive vector graphics work done with CorelDraw?

On top of that, CorelDraw feels slow and sluggish. To be fair, CorelDraw has always felt slow and sluggish, but if you keep using an old version, then after a few years, your computer gets fast enough that it feels snappy and usable. Still, I was hoping that Corel would have left well enough alone, and stopped meddling with the old code so that it would stay fast. That's not the case. It feels sluggish. After all these years, Corel still has not learned that responsiveness is one of those magic unspoken features that make a graphics package feel good to use. Even though Corel Photo-Paint has many more features than my old copy of Photoshop Elements, I still use Photoshop Elements as my primary paint program because it's just so much faster and responsive. CorelDraw 2019 also just stops and hangs for a couple of seconds sometimes. I think it might be that the saving code is now very slow for some reason. Since CorelDraw autosaves fairly often (due to its buggy nature), I think CorelDraw will just occasionally become unresponsive as its incredibly slow autosave happens.

In the end, I feel like I've wasted my money. I bought CorelDraw 2019 because it was the last upgrade version they would offer. But CorelDraw 2019 is really buggy and not very usable. These bugs likely won't be fixed until a later version of Corel, which there won't be any upgrade pricing available for. Every time I use CorelDraw 2019, I keep wanting to go back to using my old version of CorelDraw X5 instead, which I sometimes do. I think the verdict is that if you are in a rush to upgrade CorelDraw because it's a last upgrade version available, DON'T get CorelDraw 2019 because it's too slow and buggy. If you can find an upgrade to an older version of CorelDraw, that might be a better choice to buy actually. Otherwise, just stick to your old version.