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
No comments:
Post a Comment