1 // swiftlint:disable all 2 // 3 // PKCS7_AppleReceipt.swift 4 // 5 // Copyright © 2018 Filippo Maguolo. 6 // 7 // Permission is hereby granted, free of charge, to any person obtaining a copy 8 // of this software and associated documentation files (the "Software"), to deal 9 // in the Software without restriction, including without limitation the rights 10 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 // copies of the Software, and to permit persons to whom the Software is 12 // furnished to do so, subject to the following conditions: 13 // 14 // The above copyright notice and this permission notice shall be included in all 15 // copies or substantial portions of the Software. 16 // 17 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 // SOFTWARE. 24 25 import Foundation 26 27 /* 28 This extension allow to parse the content of an Apple receipt from the AppStore. 29 30 Reference documentation 31 https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html 32 */ 33 extension PKCS7 { 34 35 public struct ReceiptInfo { 36 37 /// CFBundleIdentifier in Info.plist 38 public fileprivate(set) var bundleIdentifier: String? 39 40 /// CFBundleIdentifier in Info.plist as bytes, used, with other data, to compute the SHA-1 hash during validation. 41 public fileprivate(set) var bundleIdentifierData: Data? 42 43 /// CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in Info.plist 44 public fileprivate(set) var bundleVersion: String? 45 46 /// CFBundleVersion (in iOS) or CFBundleShortVersionString (in macOS) in Info.plist 47 public fileprivate(set) var originalApplicationVersion: String? 48 49 /// Opaque value used, with other data, to compute the SHA-1 hash during validation. 50 public fileprivate(set) var opaqueValue: Data? 51 52 /// SHA-1 hash, used to validate the receipt. 53 public fileprivate(set) var sha1: Data? 54 55 public fileprivate(set) var receiptCreationDate: Date? 56 public fileprivate(set) var receiptCreationDateString: String? 57 public fileprivate(set) var receiptExpirationDate: Date? 58 public fileprivate(set) var receiptExpirationDateString: String? 59 public fileprivate(set) var inAppPurchases: [InAppPurchaseInfo]? 60 } 61 62 public struct InAppPurchaseInfo { 63 public fileprivate(set) var quantity: UInt64? 64 public fileprivate(set) var productId: String? 65 public fileprivate(set) var transactionId: String? 66 public fileprivate(set) var originalTransactionId: String? 67 public fileprivate(set) var purchaseDate: Date? 68 public fileprivate(set) var originalPurchaseDate: Date? 69 public fileprivate(set) var expiresDate: Date? 70 public fileprivate(set) var isInIntroOfferPeriod: UInt64? 71 public fileprivate(set) var cancellationDate: Date? 72 public fileprivate(set) var webOrderLineItemId: UInt64? 73 } 74 parseDatenull75 func parseDate(_ dateString: String) -> Date? { 76 return ReceiptDateFormatter.date(from: dateString) 77 } 78 receiptnull79 public func receipt() -> ReceiptInfo? { 80 guard let block = mainBlock.findOid(.pkcs7data) else { return nil } 81 guard var receiptBlock = block.parent?.sub?.last?.sub(0)?.sub(0) else { return nil } 82 var receiptInfo = ReceiptInfo() 83 84 if receiptBlock.asString == "Xcode" { 85 receiptBlock = receiptBlock.sub(0)! 86 } 87 88 for item in receiptBlock.sub ?? [] { 89 let fieldType = (item.sub(0)?.value as? Data)?.uint64Value ?? 0 90 let fieldValueString = item.sub(2)?.asString 91 switch fieldType { 92 case 2: 93 receiptInfo.bundleIdentifier = fieldValueString 94 receiptInfo.bundleIdentifierData = item.sub(2)?.rawValue 95 96 case 3: 97 receiptInfo.bundleVersion = fieldValueString 98 99 case 4: 100 receiptInfo.opaqueValue = item.sub(2)?.rawValue 101 102 case 5: 103 receiptInfo.sha1 = item.sub(2)?.rawValue 104 105 case 19: 106 receiptInfo.originalApplicationVersion = fieldValueString 107 108 case 12: 109 guard let fieldValueString = fieldValueString else { continue } 110 receiptInfo.receiptCreationDateString = fieldValueString 111 receiptInfo.receiptCreationDate = parseDate(fieldValueString) 112 113 case 21: 114 guard let fieldValueString = fieldValueString else { continue } 115 receiptInfo.receiptExpirationDateString = fieldValueString 116 receiptInfo.receiptExpirationDate = parseDate(fieldValueString) 117 118 case 17: 119 let subItems = item.sub(2)?.sub?.first?.sub ?? [] 120 if receiptInfo.inAppPurchases == nil { 121 receiptInfo.inAppPurchases = [] 122 } 123 receiptInfo.inAppPurchases?.append(inAppPurchase(subItems)) 124 125 default: 126 break 127 } 128 } 129 return receiptInfo 130 } 131 inAppPurchasenull132 private func inAppPurchase(_ subItems: [ASN1Object]) -> InAppPurchaseInfo { 133 var inAppPurchaseInfo = InAppPurchaseInfo() 134 subItems.forEach { subItem in 135 let fieldType = (subItem.sub(0)?.value as? Data)?.uint64Value ?? 0 136 let fieldValue = subItem.sub(2)?.sub?.first?.value 137 switch fieldType { 138 case 1701: 139 inAppPurchaseInfo.quantity = (fieldValue as? Data)?.uint64Value 140 case 1702: 141 inAppPurchaseInfo.productId = fieldValue as? String 142 case 1703: 143 inAppPurchaseInfo.transactionId = fieldValue as? String 144 case 1705: 145 inAppPurchaseInfo.originalTransactionId = fieldValue as? String 146 case 1704: 147 if let fieldValueString = fieldValue as? String { 148 inAppPurchaseInfo.purchaseDate = parseDate(fieldValueString) 149 } 150 case 1706: 151 if let fieldValueString = fieldValue as? String { 152 inAppPurchaseInfo.originalPurchaseDate = parseDate(fieldValueString) 153 } 154 case 1708: 155 if let fieldValueString = fieldValue as? String { 156 inAppPurchaseInfo.expiresDate = parseDate(fieldValueString) 157 } 158 case 1719: 159 inAppPurchaseInfo.isInIntroOfferPeriod = (fieldValue as? Data)?.uint64Value 160 case 1712: 161 if let fieldValueString = fieldValue as? String { 162 inAppPurchaseInfo.cancellationDate = parseDate(fieldValueString) 163 } 164 case 1711: 165 inAppPurchaseInfo.webOrderLineItemId = (fieldValue as? Data)?.uint64Value 166 default: 167 break 168 } 169 } 170 return inAppPurchaseInfo 171 } 172 173 } 174 175 // MARK: ReceiptDateFormatter 176 177 /// Static formatting methods to use for string encoded date values in receipts 178 private enum ReceiptDateFormatter { 179 180 /// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z", 181 /// as a fallback, dates like "2017-01-01T12:00:00.123Z" are also parsed. datenull182 static func date(from string: String) -> Date? { 183 return self.defaultDateFormatter.date(from: string) // expected 184 ?? self.fallbackDateFormatterWithMS.date(from: string) // try again with milliseconds 185 } 186 187 /// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z" (rfc3339 without millis) 188 private static let defaultDateFormatter: DateFormatter = { 189 let dateDateFormatter = DateFormatter() 190 dateDateFormatter.locale = Locale(identifier: "en_US_POSIX") 191 dateDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 192 dateDateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 193 return dateDateFormatter 194 }() 195 196 /// Uses representation of dates like "2017-01-01T12:00:00.123Z" 197 /// 198 /// This is not the officially intended format, but added after hearing reports about new format adding ms https://twitter.com/depth42/status/1314179654811607041 199 /// 200 /// The formatting String was taken from https://github.com/IdeasOnCanvas/AppReceiptValidator/pull/73 201 /// where tests were performed to check if it works 202 private static let fallbackDateFormatterWithMS: DateFormatter = { 203 let dateFormatter = DateFormatter() 204 dateFormatter.locale = Locale(identifier: "en_US_POSIX") 205 dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'" 206 dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) 207 return dateFormatter 208 }() 209 } 210 // swiftlint:enable all 211