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