Skip to content

Commit c60d292

Browse files
authored
Merge pull request #73 from IdeasOnCanvas/enhancement/relaxDateParsing
Relax date parsing
2 parents 0ea2eb8 + 4452349 commit c60d292

File tree

6 files changed

+92
-15
lines changed

6 files changed

+92
-15
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
//
2+
// ReceiptDateFormatterTests.swift
3+
// AppReceiptValidator
4+
//
5+
// Created by Hannes Oud on 09.10.20.
6+
// Copyright © 2020 IdeasOnCanvas GmbH. All rights reserved.
7+
//
8+
9+
import AppReceiptValidator
10+
import Foundation
11+
import XCTest
12+
13+
14+
final class ReceiptDateFormatterTests: XCTestCase {
15+
16+
func testDateFormatting() throws {
17+
let dateStrings = [
18+
"2020-01-01T12:00:00Z",
19+
"2020-01-01T12:00:00.123Z",
20+
"2020-01-01T12:00:00.999Z",
21+
"2020-01-01T12:00:01Z"
22+
]
23+
for dateString in dateStrings {
24+
let parsed = try XCTUnwrap(AppReceiptValidator.ReceiptDateFormatter.date(from: dateString))
25+
XCTAssertEqual(AppReceiptValidator.ReceiptDateFormatter.string(from: parsed), dateString)
26+
}
27+
}
28+
}

AppReceiptValidator/AppReceiptValidator.xcodeproj/project.pbxproj

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
/* Begin PBXBuildFile section */
1010
D114544621A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */; };
1111
D114544721A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */; };
12+
D11B81CF2530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */; };
13+
D11B81D02530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */; };
1214
D1239FFF1F6A7B5000D0421E /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */; };
1315
D123A0001F6A7CCF00D0421E /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */; };
1416
D13E5B7D20331B9B001880F0 /* DropAcceptingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13E5B7C20331B9B001880F0 /* DropAcceptingTextView.swift */; };
@@ -278,6 +280,7 @@
278280

279281
/* Begin PBXFileReference section */
280282
D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceIdentifierTests.swift; sourceTree = "<group>"; };
283+
D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptDateFormatterTests.swift; sourceTree = "<group>"; };
281284
D13E5B7C20331B9B001880F0 /* DropAcceptingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropAcceptingTextView.swift; sourceTree = "<group>"; };
282285
D14FA72E1F6143C400545540 /* Date+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Convenience.swift"; sourceTree = "<group>"; };
283286
D14FA7311F61472400545540 /* mac_mindnode_rebought_receipt */ = {isa = PBXFileReference; lastKnownFileType = file; path = mac_mindnode_rebought_receipt; sourceTree = "<group>"; };
@@ -606,6 +609,7 @@
606609
D1D6F5411F5D8A3800E86FE1 /* AppReceiptValidationTests.swift */,
607610
D1AA845A1F6ABB31007F2558 /* AppReceiptPropertyValidationTests.swift */,
608611
D150A0ED1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift */,
612+
D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */,
609613
D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */,
610614
D1D6F5481F5D9B1100E86FE1 /* Tools */,
611615
D1D6F5431F5D8DBC00E86FE1 /* Test Assets */,
@@ -1403,6 +1407,7 @@
14031407
D19095CD1F601E960095729B /* AppReceiptValidationTests.swift in Sources */,
14041408
D1AA845D1F6ABB59007F2558 /* AppReceiptPropertyValidationTests.swift in Sources */,
14051409
D150A0EF1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift in Sources */,
1410+
D11B81D02530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */,
14061411
D114544721A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */,
14071412
D150A0F01F67E0990026ED04 /* Date+Convenience.swift in Sources */,
14081413
);
@@ -1416,6 +1421,7 @@
14161421
D19095CE1F601E980095729B /* AppReceiptValidationTests.swift in Sources */,
14171422
D1AA845C1F6ABB59007F2558 /* AppReceiptPropertyValidationTests.swift in Sources */,
14181423
D150A0EE1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift in Sources */,
1424+
D11B81CF2530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */,
14191425
D114544621A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */,
14201426
D150A0F11F67E0990026ED04 /* Date+Convenience.swift in Sources */,
14211427
);

AppReceiptValidator/AppReceiptValidator/AppReceiptValidator.swift

Lines changed: 53 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -88,16 +88,6 @@ public struct AppReceiptValidator {
8888
let receiptContainer = try self.extractPKCS7Container(data: receiptData)
8989
return try parseReceipt(pkcs7: receiptContainer, parseUnofficialParts: true)
9090
}
91-
92-
/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z"
93-
public static let asn1DateFormatter: DateFormatter = {
94-
// Date formatter code from https://www.objc.io/issues/17-security/receipt-validation/#parsing-the-receipt
95-
let dateFormatter = DateFormatter()
96-
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
97-
dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
98-
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
99-
return dateFormatter
100-
}()
10191
}
10292

10393
// MARK: - Full Validation
@@ -351,6 +341,59 @@ private extension AppReceiptValidator {
351341
}
352342
}
353343

344+
// MARK: - ReceiptDateFormatter
345+
346+
extension AppReceiptValidator {
347+
348+
/// Static formatting methods to use for string encoded date values in receipts
349+
public enum ReceiptDateFormatter {
350+
351+
/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z",
352+
/// as a fallback, dates like "2017-01-01T12:00:00.123Z" are also parsed.
353+
public static func date(from string: String) -> Date? {
354+
return self.asn1DateFormatter.date(from: string) // expected
355+
?? self.fallbackDateFormatterWithMS.date(from: string) // try again with milliseconds
356+
}
357+
358+
/// Returns receipt-conform string representation of dates like "2017-01-01T12:00:00Z",
359+
/// but if the date has sub-second fractions a millisecond representation like "2017-01-01T12:00:00.123Z" is returned.
360+
public static func string(from date: Date) -> String {
361+
if floor(date.timeIntervalSince1970) == date.timeIntervalSince1970 {
362+
// Integer seconds granularity is what we expect
363+
return self.asn1DateFormatter.string(from: date)
364+
} else {
365+
// millis seconds granularity is what we expect
366+
return self.fallbackDateFormatterWithMS.string(from: date)
367+
}
368+
}
369+
370+
/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z"
371+
static let asn1DateFormatter: DateFormatter = {
372+
// Date formatter code from https://www.objc.io/issues/17-security/receipt-validation/#parsing-the-receipt
373+
let dateFormatter = DateFormatter()
374+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
375+
dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
376+
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
377+
return dateFormatter
378+
}()
379+
380+
/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00.123Z"
381+
///
382+
/// This is not the officially intended format, but added after hearing reports about new format adding ms https://twitter.com/depth42/status/1314179654811607041
383+
private static let fallbackDateFormatterWithMS: DateFormatter = {
384+
let dateFormatter = DateFormatter()
385+
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
386+
dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"
387+
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
388+
return dateFormatter
389+
}()
390+
}
391+
392+
/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z"
393+
@available(*, deprecated, message: "Use AppReceiptValidator.ReceiptDateFormatter.string(from:) or AppReceiptValidator.ReceiptDateFormatter.date(from:) instead, to cover unexpected date formats")
394+
public static let asn1DateFormatter: DateFormatter = ReceiptDateFormatter.asn1DateFormatter
395+
}
396+
354397
// MARK: - Result
355398

356399
extension AppReceiptValidator {

AppReceiptValidator/AppReceiptValidator/OpenSSL/ASN1Helpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ extension ASN1Object {
153153
var dateValue: Date? {
154154
guard let string = self.stringValue else { return nil }
155155

156-
return AppReceiptValidator.asn1DateFormatter.date(from: string)
156+
return AppReceiptValidator.ReceiptDateFormatter.date(from: string)
157157
}
158158
}
159159

AppReceiptValidator/AppReceiptValidator/Receipt.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ private struct StringFormatter {
294294
func format(_ date: Date?) -> String {
295295
guard let date = date else { return fallback }
296296

297-
return quoted(AppReceiptValidator.asn1DateFormatter.string(from: date))
297+
return quoted(AppReceiptValidator.ReceiptDateFormatter.string(from: date))
298298
}
299299

300300
func format(_ string: String?) -> String {
@@ -318,7 +318,7 @@ private func parseBase64(string: String) -> Data? {
318318

319319
// Parses a string of type "2017-01-01T12:00:00Z"
320320
private func parseDate(string: String) -> Date? {
321-
guard let date = AppReceiptValidator.asn1DateFormatter.date(from: string) else {
321+
guard let date = AppReceiptValidator.ReceiptDateFormatter.date(from: string) else {
322322
assertionFailure("Date could not be parsed from string '\(string)', make sure it has a correct format, example `2017-01-01T12:00:00Z`")
323323
return nil
324324
}

AppReceiptValidator/AppReceiptValidator/UnofficialReceipt.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public enum KnownUnofficialReceiptAttribute: Int32 {
6161
var parsingType: ParsingType {
6262
switch self {
6363
case .date1, .date2, .date3:
64-
return .string
64+
return .date
6565
case .provisioningType, .ageRating, .clientName:
6666
return .string
6767
}
@@ -115,7 +115,7 @@ extension UnofficialReceipt.Entry.Value: CustomStringConvertible {
115115
case .string(let value):
116116
return "\"\(value)\""
117117
case .date(let date):
118-
return AppReceiptValidator.asn1DateFormatter.string(from: date)
118+
return AppReceiptValidator.ReceiptDateFormatter.string(from: date)
119119
case .bytes(let bytes):
120120
if bytes.count == 2 && bytes.first == 12 && bytes.dropFirst().first == 0 {
121121
return "2 bytes (12, 0)"

0 commit comments

Comments
 (0)