Skip to content

Commit

Permalink
Merge pull request #73 from IdeasOnCanvas/enhancement/relaxDateParsing
Browse files Browse the repository at this point in the history
Relax date parsing
  • Loading branch information
schwmi authored Oct 16, 2020
2 parents 0ea2eb8 + 4452349 commit c60d292
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 15 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// ReceiptDateFormatterTests.swift
// AppReceiptValidator
//
// Created by Hannes Oud on 09.10.20.
// Copyright © 2020 IdeasOnCanvas GmbH. All rights reserved.
//

import AppReceiptValidator
import Foundation
import XCTest


final class ReceiptDateFormatterTests: XCTestCase {

func testDateFormatting() throws {
let dateStrings = [
"2020-01-01T12:00:00Z",
"2020-01-01T12:00:00.123Z",
"2020-01-01T12:00:00.999Z",
"2020-01-01T12:00:01Z"
]
for dateString in dateStrings {
let parsed = try XCTUnwrap(AppReceiptValidator.ReceiptDateFormatter.date(from: dateString))
XCTAssertEqual(AppReceiptValidator.ReceiptDateFormatter.string(from: parsed), dateString)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
/* Begin PBXBuildFile section */
D114544621A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */; };
D114544721A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */; };
D11B81CF2530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */; };
D11B81D02530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */; };
D1239FFF1F6A7B5000D0421E /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */; };
D123A0001F6A7CCF00D0421E /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */; };
D13E5B7D20331B9B001880F0 /* DropAcceptingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13E5B7C20331B9B001880F0 /* DropAcceptingTextView.swift */; };
Expand Down Expand Up @@ -278,6 +280,7 @@

/* Begin PBXFileReference section */
D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceIdentifierTests.swift; sourceTree = "<group>"; };
D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptDateFormatterTests.swift; sourceTree = "<group>"; };
D13E5B7C20331B9B001880F0 /* DropAcceptingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropAcceptingTextView.swift; sourceTree = "<group>"; };
D14FA72E1F6143C400545540 /* Date+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Convenience.swift"; sourceTree = "<group>"; };
D14FA7311F61472400545540 /* mac_mindnode_rebought_receipt */ = {isa = PBXFileReference; lastKnownFileType = file; path = mac_mindnode_rebought_receipt; sourceTree = "<group>"; };
Expand Down Expand Up @@ -606,6 +609,7 @@
D1D6F5411F5D8A3800E86FE1 /* AppReceiptValidationTests.swift */,
D1AA845A1F6ABB31007F2558 /* AppReceiptPropertyValidationTests.swift */,
D150A0ED1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift */,
D11B81CE2530687D00E19863 /* ReceiptDateFormatterTests.swift */,
D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */,
D1D6F5481F5D9B1100E86FE1 /* Tools */,
D1D6F5431F5D8DBC00E86FE1 /* Test Assets */,
Expand Down Expand Up @@ -1403,6 +1407,7 @@
D19095CD1F601E960095729B /* AppReceiptValidationTests.swift in Sources */,
D1AA845D1F6ABB59007F2558 /* AppReceiptPropertyValidationTests.swift in Sources */,
D150A0EF1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift in Sources */,
D11B81D02530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */,
D114544721A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */,
D150A0F01F67E0990026ED04 /* Date+Convenience.swift in Sources */,
);
Expand All @@ -1416,6 +1421,7 @@
D19095CE1F601E980095729B /* AppReceiptValidationTests.swift in Sources */,
D1AA845C1F6ABB59007F2558 /* AppReceiptPropertyValidationTests.swift in Sources */,
D150A0EE1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift in Sources */,
D11B81CF2530687D00E19863 /* ReceiptDateFormatterTests.swift in Sources */,
D114544621A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */,
D150A0F11F67E0990026ED04 /* Date+Convenience.swift in Sources */,
);
Expand Down
63 changes: 53 additions & 10 deletions AppReceiptValidator/AppReceiptValidator/AppReceiptValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,16 +88,6 @@ public struct AppReceiptValidator {
let receiptContainer = try self.extractPKCS7Container(data: receiptData)
return try parseReceipt(pkcs7: receiptContainer, parseUnofficialParts: true)
}

/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z"
public static let asn1DateFormatter: DateFormatter = {
// Date formatter code from https://www.objc.io/issues/17-security/receipt-validation/#parsing-the-receipt
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter
}()
}

// MARK: - Full Validation
Expand Down Expand Up @@ -351,6 +341,59 @@ private extension AppReceiptValidator {
}
}

// MARK: - ReceiptDateFormatter

extension AppReceiptValidator {

/// Static formatting methods to use for string encoded date values in receipts
public enum ReceiptDateFormatter {

/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z",
/// as a fallback, dates like "2017-01-01T12:00:00.123Z" are also parsed.
public static func date(from string: String) -> Date? {
return self.asn1DateFormatter.date(from: string) // expected
?? self.fallbackDateFormatterWithMS.date(from: string) // try again with milliseconds
}

/// Returns receipt-conform string representation of dates like "2017-01-01T12:00:00Z",
/// but if the date has sub-second fractions a millisecond representation like "2017-01-01T12:00:00.123Z" is returned.
public static func string(from date: Date) -> String {
if floor(date.timeIntervalSince1970) == date.timeIntervalSince1970 {
// Integer seconds granularity is what we expect
return self.asn1DateFormatter.string(from: date)
} else {
// millis seconds granularity is what we expect
return self.fallbackDateFormatterWithMS.string(from: date)
}
}

/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z"
static let asn1DateFormatter: DateFormatter = {
// Date formatter code from https://www.objc.io/issues/17-security/receipt-validation/#parsing-the-receipt
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'Z'"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter
}()

/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00.123Z"
///
/// This is not the officially intended format, but added after hearing reports about new format adding ms https://twitter.com/depth42/status/1314179654811607041
private static let fallbackDateFormatterWithMS: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
dateFormatter.dateFormat = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'SSS'Z'"
dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
return dateFormatter
}()
}

/// Uses receipt-conform representation of dates like "2017-01-01T12:00:00Z"
@available(*, deprecated, message: "Use AppReceiptValidator.ReceiptDateFormatter.string(from:) or AppReceiptValidator.ReceiptDateFormatter.date(from:) instead, to cover unexpected date formats")
public static let asn1DateFormatter: DateFormatter = ReceiptDateFormatter.asn1DateFormatter
}

// MARK: - Result

extension AppReceiptValidator {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ extension ASN1Object {
var dateValue: Date? {
guard let string = self.stringValue else { return nil }

return AppReceiptValidator.asn1DateFormatter.date(from: string)
return AppReceiptValidator.ReceiptDateFormatter.date(from: string)
}
}

Expand Down
4 changes: 2 additions & 2 deletions AppReceiptValidator/AppReceiptValidator/Receipt.swift
Original file line number Diff line number Diff line change
Expand Up @@ -294,7 +294,7 @@ private struct StringFormatter {
func format(_ date: Date?) -> String {
guard let date = date else { return fallback }

return quoted(AppReceiptValidator.asn1DateFormatter.string(from: date))
return quoted(AppReceiptValidator.ReceiptDateFormatter.string(from: date))
}

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

// Parses a string of type "2017-01-01T12:00:00Z"
private func parseDate(string: String) -> Date? {
guard let date = AppReceiptValidator.asn1DateFormatter.date(from: string) else {
guard let date = AppReceiptValidator.ReceiptDateFormatter.date(from: string) else {
assertionFailure("Date could not be parsed from string '\(string)', make sure it has a correct format, example `2017-01-01T12:00:00Z`")
return nil
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public enum KnownUnofficialReceiptAttribute: Int32 {
var parsingType: ParsingType {
switch self {
case .date1, .date2, .date3:
return .string
return .date
case .provisioningType, .ageRating, .clientName:
return .string
}
Expand Down Expand Up @@ -115,7 +115,7 @@ extension UnofficialReceipt.Entry.Value: CustomStringConvertible {
case .string(let value):
return "\"\(value)\""
case .date(let date):
return AppReceiptValidator.asn1DateFormatter.string(from: date)
return AppReceiptValidator.ReceiptDateFormatter.string(from: date)
case .bytes(let bytes):
if bytes.count == 2 && bytes.first == 12 && bytes.dropFirst().first == 0 {
return "2 bytes (12, 0)"
Expand Down

0 comments on commit c60d292

Please sign in to comment.