Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bf518cd21 | |||
| be46b25247 | |||
| 2519fdfb23 | |||
| 2f1ba126e7 | |||
| 8e9c94bb88 | |||
| ecee35a2e9 | |||
| 9ef265073b | |||
| 3e69affe7e | |||
| 3702ec3648 | |||
| eb78f3f493 | |||
| 7477d83d4c | |||
| 5393889f39 | |||
| be99440c4a | |||
| bfa0c279cc | |||
| 15144274a9 | |||
| 3928822cbb | |||
| 92409e4498 | |||
| b2764203d4 | |||
| 9817657584 | |||
| a0f87dbc46 | |||
| 20d7a7a2ba | |||
| 12d1bb2321 | |||
| cfea40ac9c | |||
| d928914869 | |||
| 476c0636c1 | |||
| 7f9b8b198e | |||
| 18968e62a4 | |||
| 7f6517f4cb | |||
| a59fc1bdda | |||
| 182108841a | |||
| 47d1c00d2b | |||
| 4f12e04b1f | |||
| ab702861a9 | |||
| 209d633a43 | |||
| e04f07c75a | |||
| 8e6be0199d | |||
| 3afdc70c86 | |||
| 6cfcea8355 | |||
| 0e41f9ac40 | |||
| c2c3f2cdec |
@@ -0,0 +1,2 @@
|
||||
Hekate/Hekate/OpenSSL/include-ios/* linguist-vendored
|
||||
Hekate/Hekate/OpenSSL/include-macos/* linguist-vendored
|
||||
@@ -1,11 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13189.4" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<device id="retina4_7" orientation="portrait">
|
||||
<adaptation id="fullscreen"/>
|
||||
</device>
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13165.3"/>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13772"/>
|
||||
<capability name="Constraints to layout margins" minToolsVersion="6.0"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="Stack View standard spacing" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
@@ -17,57 +21,76 @@
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="ycc-vI-8g6">
|
||||
<rect key="frame" x="20" y="40" width="335" height="607"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" layoutMarginsFollowReadableWidth="YES" axis="vertical" spacingType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ZPu-bH-r6v">
|
||||
<rect key="frame" x="15" y="40" width="345" height="627"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="750" verticalCompressionResistancePriority="800" text="Hekate" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="QDJ-Q7-MuR">
|
||||
<rect key="frame" x="0.0" y="0.0" width="335" height="20.5"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="I7S-AU-qti">
|
||||
<rect key="frame" x="0.0" y="20.5" width="335" height="456.5"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="hUb-y9-69b">
|
||||
<rect key="frame" x="0.0" y="0.0" width="345" height="30"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Base64" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3Pt-dr-EUg">
|
||||
<rect key="frame" x="0.0" y="0.0" width="309" height="30"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="749" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kT0-fB-cXz">
|
||||
<rect key="frame" x="309" y="0.0" width="36" height="30"/>
|
||||
<state key="normal" title="Clear"/>
|
||||
<connections>
|
||||
<action selector="clear" destination="BYZ-38-t0r" eventType="touchUpInside" id="xOC-lG-aMe"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="g3x-aA-9dO">
|
||||
<rect key="frame" x="0.0" y="38" width="345" height="167"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||
</textView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalHuggingPriority="255" verticalCompressionResistancePriority="748" text="(no data)" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="GRW-kL-w14">
|
||||
<rect key="frame" x="0.0" y="477" width="335" height="100"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="100" id="rkj-Uz-pFJ"/>
|
||||
</constraints>
|
||||
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="czZ-e2-zE0">
|
||||
<rect key="frame" x="0.0" y="213" width="345" height="30"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Receipt" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="vyX-F8-RGT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="309" height="30"/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="17"/>
|
||||
<nil key="textColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" horizontalCompressionResistancePriority="749" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="TDa-xH-OJJ">
|
||||
<rect key="frame" x="309" y="0.0" width="36" height="30"/>
|
||||
<state key="normal" title="Copy"/>
|
||||
<connections>
|
||||
<action selector="copyOutput" destination="BYZ-38-t0r" eventType="touchUpInside" id="cO0-6s-2Ul"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="250" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="Wjc-3l-w1w">
|
||||
<rect key="frame" x="0.0" y="251" width="345" height="376"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
||||
<textInputTraits key="textInputTraits" autocorrectionType="no" spellCheckingType="no" smartDashesType="no" smartInsertDeleteType="no" smartQuotesType="no"/>
|
||||
</textView>
|
||||
<button opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" verticalCompressionResistancePriority="850" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8wd-R7-o0b">
|
||||
<rect key="frame" x="0.0" y="577" width="335" height="30"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="30" id="xPJ-u9-TvY"/>
|
||||
</constraints>
|
||||
<state key="normal" title="Refresh Receipt from Store"/>
|
||||
<connections>
|
||||
<action selector="refreshReceiptFromStoreTapped" destination="BYZ-38-t0r" eventType="touchUpInside" id="i9X-hc-Dww"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
</stackView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<color key="backgroundColor" cocoaTouchSystemColor="groupTableViewBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstItem="MPn-6g-0qv" firstAttribute="bottom" secondItem="ycc-vI-8g6" secondAttribute="bottom" constant="20" id="J1k-CU-xyi"/>
|
||||
<constraint firstItem="MPn-6g-0qv" firstAttribute="trailing" secondItem="ycc-vI-8g6" secondAttribute="trailing" constant="20" id="KiK-CX-iz8"/>
|
||||
<constraint firstItem="ycc-vI-8g6" firstAttribute="leading" secondItem="MPn-6g-0qv" secondAttribute="leading" constant="20" id="ZbF-Cm-dp2"/>
|
||||
<constraint firstItem="ycc-vI-8g6" firstAttribute="top" secondItem="MPn-6g-0qv" secondAttribute="top" constant="20" id="r2h-em-aKZ"/>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="top" secondItem="8bC-Xf-vdC" secondAttribute="topMargin" id="O9X-Jq-xyq"/>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="bottom" secondItem="MPn-6g-0qv" secondAttribute="bottom" id="VFh-St-0bL"/>
|
||||
<constraint firstItem="g3x-aA-9dO" firstAttribute="height" secondItem="8bC-Xf-vdC" secondAttribute="height" multiplier="1:4" id="hbr-Fh-GQs"/>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="leading" secondItem="MPn-6g-0qv" secondAttribute="leading" constant="15" id="iaR-gF-ocn"/>
|
||||
<constraint firstItem="ZPu-bH-r6v" firstAttribute="trailing" secondItem="MPn-6g-0qv" secondAttribute="trailing" constant="-15" id="rHb-Bb-pZ9"/>
|
||||
</constraints>
|
||||
<edgeInsets key="layoutMargins" top="20" left="20" bottom="20" right="20"/>
|
||||
<viewLayoutGuide key="safeArea" id="MPn-6g-0qv"/>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="receiptDataTextView" destination="GRW-kL-w14" id="zMl-tu-gie"/>
|
||||
<outlet property="textView" destination="I7S-AU-qti" id="PPF-3A-8xS"/>
|
||||
<outlet property="inputTextView" destination="g3x-aA-9dO" id="HAQ-V9-HfJ"/>
|
||||
<outlet property="outputTextView" destination="Wjc-3l-w1w" id="ZdS-SB-fPa"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
//
|
||||
// HekateDemoViewModel.swift
|
||||
// Hekate Demo iOS
|
||||
//
|
||||
// Created by Hannes Oud on 08.09.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Hekate
|
||||
|
||||
struct HekateDemoViewModel {
|
||||
|
||||
var hasReceipt: Bool { return self.lastReceiptData != nil }
|
||||
var lastReceiptData: Data?
|
||||
var lastValidationResult: LocalReceiptValidator.Result?
|
||||
var receiptIsValid: Bool {
|
||||
guard let result = self.lastValidationResult else { return false }
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
var descriptionText: String {
|
||||
guard let result = self.lastValidationResult else { return "(No result)" }
|
||||
|
||||
switch result {
|
||||
case .success(let receipt):
|
||||
return "Valid\n" + receipt.description
|
||||
case .error(let error):
|
||||
return "Invalid: \(error)"
|
||||
}
|
||||
}
|
||||
var receiptDataBase64Text: String {
|
||||
guard let data = self.lastReceiptData else { return "(no data)" }
|
||||
|
||||
return data.base64EncodedString(options: [.lineLength64Characters])
|
||||
}
|
||||
|
||||
mutating func update() {
|
||||
self.lastReceiptData = LocalReceiptValidator.Parameters.ReceiptOrigin.installedInMainBundle.loadData()
|
||||
self.lastValidationResult = LocalReceiptValidator().validateReceipt()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
//
|
||||
// StoreKitHelper.swift
|
||||
// Hekate Demo iOS
|
||||
//
|
||||
// Created by Hannes Oud on 08.09.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import StoreKit
|
||||
|
||||
@objc
|
||||
final class StoreKitHelper: NSObject {
|
||||
|
||||
@objc static let shared = StoreKitHelper()
|
||||
@objc var refreshCompletedAction: ((NSError?) -> Void)?
|
||||
|
||||
private lazy var delegateHolder: DelegateHolder = {
|
||||
let delegateHolder = DelegateHolder()
|
||||
delegateHolder.refreshCompletedAction = { [weak self] error in
|
||||
self?.refreshCompletedAction?(error)
|
||||
}
|
||||
return delegateHolder
|
||||
}()
|
||||
|
||||
public func refresh() {
|
||||
let request = SKReceiptRefreshRequest(receiptProperties: nil)
|
||||
request.delegate = self.delegateHolder
|
||||
request.start()
|
||||
}
|
||||
|
||||
public func logReceipt() {
|
||||
print("Local Device ID (GUID):\n" + (UIDevice.current.identifierForVendor?.uuidString ?? "nil"))
|
||||
guard let data = self.receiptData else {
|
||||
print("No receipt")
|
||||
return
|
||||
}
|
||||
let base64 = data.base64EncodedString()
|
||||
print("ReceiptData:\n" + base64)
|
||||
}
|
||||
|
||||
public var receiptData: Data? {
|
||||
guard let url = Bundle.main.appStoreReceiptURL else { return nil }
|
||||
|
||||
return try? Data(contentsOf: url)
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsules SKRequestDelegate so it is not exposed if not necessary
|
||||
private final class DelegateHolder: NSObject, SKRequestDelegate {
|
||||
|
||||
var refreshCompletedAction: ((NSError?) -> Void)?
|
||||
|
||||
func requestDidFinish(_ request: SKRequest) {
|
||||
DispatchQueue.main.async {
|
||||
self.refreshCompletedAction?(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func request(_ request: SKRequest, didFailWithError error: Error) {
|
||||
DispatchQueue.main.async {
|
||||
self.refreshCompletedAction?(error as NSError)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,38 +7,92 @@
|
||||
//
|
||||
|
||||
import Hekate
|
||||
import StoreKit
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
private var storeKitHelper = StoreKitHelper()
|
||||
private var viewModel = HekateDemoViewModel() {
|
||||
didSet {
|
||||
self.updateViewFromViewModel()
|
||||
}
|
||||
}
|
||||
@IBOutlet private weak var textView: UITextView!
|
||||
@IBOutlet private weak var receiptDataTextView: UITextView!
|
||||
// MARK: - ViewController
|
||||
|
||||
class ViewController: UIViewController, UITextViewDelegate {
|
||||
|
||||
@IBOutlet private var inputTextView: UITextView!
|
||||
@IBOutlet private var outputTextView: UITextView!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.storeKitHelper.refreshCompletedAction = { [weak self] _ in
|
||||
self?.updateViewModel()
|
||||
self.inputTextView.delegate = self
|
||||
self.inputTextView.text = ""
|
||||
self.outputTextView.text = "Parsed Receipt will be shown here"
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(triggerAutoPaste), name: .UIApplicationWillEnterForeground, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(triggerAutoPaste), name: .UIPasteboardChanged, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
if self.autoPaste() {
|
||||
return
|
||||
} else {
|
||||
self.inputTextView.becomeFirstResponder()
|
||||
}
|
||||
updateViewModel()
|
||||
}
|
||||
|
||||
private func updateViewFromViewModel() {
|
||||
textView.text = self.viewModel.descriptionText
|
||||
receiptDataTextView.text = self.viewModel.receiptDataBase64Text
|
||||
}
|
||||
// MARK: - UITextViewDelegate
|
||||
|
||||
private func updateViewModel() {
|
||||
viewModel.update()
|
||||
}
|
||||
|
||||
@IBAction func refreshReceiptFromStoreTapped() {
|
||||
storeKitHelper.refresh()
|
||||
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
|
||||
DispatchQueue.main.async {
|
||||
self.update(base64String: self.inputTextView.text)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension ViewController {
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@objc
|
||||
func triggerAutoPaste() {
|
||||
self.autoPaste()
|
||||
}
|
||||
|
||||
@IBAction func clear() {
|
||||
self.inputTextView.text = ""
|
||||
}
|
||||
|
||||
@IBAction func copyOutput() {
|
||||
UIPasteboard.general.string = self.outputTextView.text
|
||||
}
|
||||
|
||||
// MARK: Updating
|
||||
|
||||
/// pastes from clipboard if it is base64 decodable
|
||||
@discardableResult
|
||||
func autoPaste() -> Bool {
|
||||
guard let string = UIPasteboard.general.string,
|
||||
Data(base64Encoded: string, options: .ignoreUnknownCharacters) != nil else { return false }
|
||||
|
||||
self.inputTextView.text = string
|
||||
self.update(base64String: string)
|
||||
return true
|
||||
}
|
||||
|
||||
func update(base64String: String) {
|
||||
guard let data = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
|
||||
self.render(string: "Base64 decoding failed.")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let result = try LocalReceiptValidator().parseUnofficialReceipt(origin: .data(data))
|
||||
render(string: "\(result.receipt)\n\(result.unofficialReceipt)")
|
||||
} catch {
|
||||
self.render(string: "\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func render(string: String) {
|
||||
self.outputTextView.text = string
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="11134" systemVersion="15F34" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="13771" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11134"/>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
|
||||
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Application-->
|
||||
@@ -673,7 +676,7 @@
|
||||
<outlet property="delegate" destination="Voe-Tx-rLC" id="PrD-fu-P6m"/>
|
||||
</connections>
|
||||
</application>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModuleProvider="target"/>
|
||||
<customObject id="Voe-Tx-rLC" customClass="AppDelegate" customModule="Hekate_Demo_macOS" customModuleProvider="target"/>
|
||||
<customObject id="YLy-65-1bz" customClass="NSFontManager"/>
|
||||
<customObject id="Ady-hI-5gd" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -683,12 +686,12 @@
|
||||
<scene sceneID="R2V-B0-nI4">
|
||||
<objects>
|
||||
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
|
||||
<window key="window" title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<window key="window" title="Hekate Receipt Parser" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="480" height="270"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="1680" height="1027"/>
|
||||
<connections>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="B8D-0N-5wS" id="98r-iN-zZc"/>
|
||||
</connections>
|
||||
</window>
|
||||
@@ -703,15 +706,83 @@
|
||||
<!--View Controller-->
|
||||
<scene sceneID="hIz-AP-VOD">
|
||||
<objects>
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="Hekate_Demo_macOS" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" wantsLayer="YES" id="m2S-Jp-Qdl">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="270"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="631" height="270"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<scrollView horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ynK-dw-fFc">
|
||||
<rect key="frame" x="0.0" y="0.0" width="316" height="270"/>
|
||||
<clipView key="contentView" id="yEL-yO-YvB">
|
||||
<rect key="frame" x="1" y="1" width="314" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView toolTip="Paste Base64 here" importsGraphics="NO" richText="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" allowsCharacterPickerTouchBarItem="NO" allowsUndo="YES" usesRuler="YES" allowsNonContiguousLayout="YES" textCompletion="NO" id="0xW-mT-lME" customClass="TextView" customModule="Hekate_Demo_macOS" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="314" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<size key="minSize" width="314" height="268"/>
|
||||
<size key="maxSize" width="629" height="10000000"/>
|
||||
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="TaD-v9-pOg">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="hkR-e4-WtN">
|
||||
<rect key="frame" x="299" y="1" width="16" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
<scrollView toolTip="Parsed Receipt will be shown here" horizontalLineScroll="10" horizontalPageScroll="10" verticalLineScroll="10" verticalPageScroll="10" hasHorizontalScroller="NO" usesPredominantAxisScrolling="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6mt-ay-mAL">
|
||||
<rect key="frame" x="316" y="0.0" width="315" height="270"/>
|
||||
<clipView key="contentView" id="YwZ-F9-Cvh">
|
||||
<rect key="frame" x="1" y="1" width="313" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<textView importsGraphics="NO" richText="NO" verticallyResizable="YES" usesFontPanel="YES" findStyle="panel" allowsCharacterPickerTouchBarItem="NO" allowsUndo="YES" usesRuler="YES" allowsNonContiguousLayout="YES" textCompletion="NO" id="GHT-gS-G1g" customClass="TextView" customModule="Hekate_Demo_macOS" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="313" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<size key="minSize" width="313" height="268"/>
|
||||
<size key="maxSize" width="463" height="10000000"/>
|
||||
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</textView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</clipView>
|
||||
<scroller key="horizontalScroller" hidden="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="YES" id="xmd-vm-w1P">
|
||||
<rect key="frame" x="-100" y="-100" width="87" height="18"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
<scroller key="verticalScroller" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="uk9-Sg-RUp">
|
||||
<rect key="frame" x="298" y="1" width="16" height="268"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</scroller>
|
||||
</scrollView>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="6mt-ay-mAL" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="CdY-lv-wee"/>
|
||||
<constraint firstItem="ynK-dw-fFc" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" id="Cvz-FD-lzg"/>
|
||||
<constraint firstItem="6mt-ay-mAL" firstAttribute="width" secondItem="m2S-Jp-Qdl" secondAttribute="width" multiplier="1:2" id="SWu-Yn-pVc"/>
|
||||
<constraint firstAttribute="trailing" secondItem="6mt-ay-mAL" secondAttribute="trailing" id="Yun-Rd-wsm"/>
|
||||
<constraint firstItem="ynK-dw-fFc" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="gIn-Ps-HEe"/>
|
||||
<constraint firstAttribute="bottom" secondItem="6mt-ay-mAL" secondAttribute="bottom" id="lKc-RV-ybx"/>
|
||||
<constraint firstAttribute="bottom" secondItem="ynK-dw-fFc" secondAttribute="bottom" id="qAW-TV-hHz"/>
|
||||
<constraint firstItem="ynK-dw-fFc" firstAttribute="width" secondItem="m2S-Jp-Qdl" secondAttribute="width" multiplier="1:2" id="wsI-iB-DvJ"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="inputTextView" destination="0xW-mT-lME" id="8Y7-yb-63r"/>
|
||||
<outlet property="outputTextView" destination="GHT-gS-G1g" id="cVN-4m-knJ"/>
|
||||
</connections>
|
||||
</viewController>
|
||||
<customObject id="rPt-NT-nkU" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="75" y="655"/>
|
||||
<point key="canvasLocation" x="76" y="726"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
|
||||
@@ -7,6 +7,69 @@
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
import Hekate
|
||||
|
||||
class ViewController: NSViewController {
|
||||
|
||||
// MARK: - ViewController
|
||||
|
||||
class ViewController: NSViewController, NSTextViewDelegate {
|
||||
|
||||
@IBOutlet private var inputTextView: NSTextView!
|
||||
@IBOutlet private var outputTextView: NSTextView!
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.inputTextView.delegate = self
|
||||
self.inputTextView.string = "Paste Base64 here"
|
||||
self.outputTextView.string = "Parsed Receipt will be shown here"
|
||||
}
|
||||
|
||||
// MARK: - NSTextViewDelegate
|
||||
|
||||
func textDidChange(_ notification: Notification) {
|
||||
let string = inputTextView.string
|
||||
self.update(base64String: string)
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
func paste(_ sender: Any) {
|
||||
self.inputTextView.paste(sender)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension ViewController {
|
||||
|
||||
// MARK: Updating
|
||||
func update(base64String: String) {
|
||||
guard let data = Data(base64Encoded: base64String, options: .ignoreUnknownCharacters) else {
|
||||
self.render(string: "Base64 decoding failed.")
|
||||
return
|
||||
}
|
||||
do {
|
||||
let result = try LocalReceiptValidator().parseUnofficialReceipt(origin: .data(data))
|
||||
self.render(string: "\(result.receipt)\n\(result.unofficialReceipt)")
|
||||
} catch {
|
||||
self.render(string: "\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func render(string: String) {
|
||||
self.outputTextView.string = string
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - TextView
|
||||
|
||||
/// TextView that clears contents before pasting
|
||||
private class TextView: NSTextView {
|
||||
|
||||
override func paste(_ sender: Any?) {
|
||||
self.string = ""
|
||||
super.paste(sender)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// LocalReceiptPropertyValidationTests.swift
|
||||
// Hekate iOS
|
||||
//
|
||||
// Created by Hannes Oud on 14.09.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Hekate
|
||||
import XCTest
|
||||
|
||||
class LocalReceiptPropertyValidationTests: XCTestCase {
|
||||
|
||||
private let receiptValidator = LocalReceiptValidator()
|
||||
|
||||
func testCorrectMainBundlePropertiesiOS() {
|
||||
let receipt = Receipt(bundleIdentifier: Bundle.main.bundleIdentifier,
|
||||
bundleIdData: nil,
|
||||
appVersion: Bundle.main.infoDictionary?[String(kCFBundleVersionKey)] as? String,
|
||||
opaqueValue: nil,
|
||||
sha1Hash: nil,
|
||||
originalAppVersion: nil,
|
||||
receiptCreationDate: nil,
|
||||
expirationDate: nil,
|
||||
inAppPurchaseReceipts: [])
|
||||
do {
|
||||
try receiptValidator.validateProperties(receipt: receipt, validations: [
|
||||
.bundleIdMatchingMainBundle,
|
||||
.appVersionMatchingMainBundleIOS
|
||||
])
|
||||
} catch {
|
||||
XCTFail("validation failed unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
func testCorrectMainBundlePropertiesMacOS() {
|
||||
let receipt = Receipt(bundleIdentifier: Bundle.main.bundleIdentifier,
|
||||
bundleIdData: nil,
|
||||
appVersion: Bundle.main.infoDictionary?[String("CFBundleShortVersionString")] as? String,
|
||||
opaqueValue: nil,
|
||||
sha1Hash: nil,
|
||||
originalAppVersion: nil,
|
||||
receiptCreationDate: nil,
|
||||
expirationDate: nil,
|
||||
inAppPurchaseReceipts: [])
|
||||
do {
|
||||
try receiptValidator.validateProperties(receipt: receipt, validations: [
|
||||
.bundleIdMatchingMainBundle,
|
||||
.appVersionMatchingMainBundleMacOS
|
||||
])
|
||||
} catch {
|
||||
XCTFail("validation failed unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
func testSpecificHardcodedPropertyMatches() {
|
||||
let receipt = Receipt(bundleIdentifier: "bundleIdentifier",
|
||||
bundleIdData: nil,
|
||||
appVersion: "appVersion",
|
||||
opaqueValue: nil,
|
||||
sha1Hash: nil,
|
||||
originalAppVersion: "originalAppVersion",
|
||||
receiptCreationDate: nil,
|
||||
expirationDate: nil,
|
||||
inAppPurchaseReceipts: [])
|
||||
do {
|
||||
try receiptValidator.validateProperties(receipt: receipt, validations: [
|
||||
.string(\Receipt.bundleIdentifier, expected: "bundleIdentifier"),
|
||||
.string(\Receipt.appVersion, expected: "appVersion"),
|
||||
.string(\Receipt.originalAppVersion, expected: "originalAppVersion")
|
||||
])
|
||||
} catch {
|
||||
XCTFail("validation failed unexpectedly")
|
||||
}
|
||||
}
|
||||
|
||||
func testMindNodeProMacReceiptPropertyMismatches() {
|
||||
guard let data = assertTestAsset(filename: "hannes_mac_mindnode_pro_receipt") else { return }
|
||||
|
||||
@discardableResult
|
||||
func assertPropertyMismatch(line: UInt = #line, configuration: (inout LocalReceiptValidator.Parameters) -> Void) -> Bool {
|
||||
let result = receiptValidator.validateReceipt {
|
||||
$0.receiptOrigin = .data(data)
|
||||
$0.shouldValidateHash = false // the original device identifier is unknown
|
||||
$0.propertyValidations = [ .string(\.appVersion, expected: "mismatching property"),
|
||||
.string(\.originalAppVersion, expected: "1.10.6")]
|
||||
}
|
||||
guard let error = result.error else {
|
||||
XCTFail("Unexpectedly succeeded validating, but expected a property mismatch)", file: #file, line: line)
|
||||
return false
|
||||
}
|
||||
guard error == LocalReceiptValidator.Error.propertyValueMismatch else {
|
||||
XCTFail("Expected a property mismatch, but found an \(error)", file: #file, line: line)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.string(\.appVersion, expected: "mismatching property"),
|
||||
.string(\.originalAppVersion, expected: "1.10.6")]
|
||||
}
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.string(\.appVersion, expected: "1.11.5"),
|
||||
.string(\.originalAppVersion, expected: "mismatching property")]
|
||||
}
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.string(\.bundleIdentifier, expected: "mismatching property")]
|
||||
}
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.string(\.bundleIdentifier, expected: "mismatching property"),
|
||||
.string(\.appVersion, expected: "mismatching property") ]
|
||||
}
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.string(\.bundleIdentifier, expected: "mismatching property"),
|
||||
.string(\.appVersion, expected: "mismatching property") ]
|
||||
}
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.bundleIdMatchingMainBundle]
|
||||
}
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.appVersionMatchingMainBundleIOS]
|
||||
}
|
||||
assertPropertyMismatch {
|
||||
$0.propertyValidations = [.appVersionMatchingMainBundleMacOS]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ class LocalReceiptValidationInAppPurchaseTests: XCTestCase {
|
||||
$0.receiptOrigin = .data(data)
|
||||
$0.shouldValidateHash = false
|
||||
$0.shouldValidateSignatureAuthenticity = false
|
||||
$0.propertyValidations = []
|
||||
}
|
||||
guard let receipt = result.receipt else {
|
||||
XCTFail("Unexpectedly failed parsing a receipt \(result.error!)")
|
||||
|
||||
@@ -46,6 +46,34 @@ class LocalReceiptValidationTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
|
||||
func testMindNodeProMacReceiptPropertyValidation() {
|
||||
guard let data = assertTestAsset(filename: "hannes_mac_mindnode_pro_receipt") else { return }
|
||||
|
||||
let expected = Receipt(
|
||||
bundleIdentifier: "com.mindnode.MindNodePro",
|
||||
bundleIdData: Data(base64Encoded: "DBhjb20ubWluZG5vZGUuTWluZE5vZGVQcm8=")!,
|
||||
appVersion: "1.11.5",
|
||||
opaqueValue: Data(base64Encoded: "/cPmDfuyFyluvodJXQRvig=="),
|
||||
sha1Hash: Data(base64Encoded: "MDBF4hAt6Y+7IlAydxroa/SQeY4="),
|
||||
originalAppVersion: "1.10.6",
|
||||
receiptCreationDate: Date.demoDate(string: "2016-02-12T10:57:42Z"),
|
||||
expirationDate: nil,
|
||||
inAppPurchaseReceipts: []
|
||||
)
|
||||
let result = receiptValidator.validateReceipt {
|
||||
$0.receiptOrigin = .data(data)
|
||||
$0.shouldValidateHash = false // the original device identifier is unknown
|
||||
$0.propertyValidations = [ .string(\.appVersion, expected: "1.11.5"),
|
||||
.string(\.originalAppVersion, expected: "1.10.6")]
|
||||
}
|
||||
guard let receipt = result.receipt else {
|
||||
XCTFail("Unexpectedly failed parsing a receipt \(result.error!)")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(receipt, expected)
|
||||
}
|
||||
|
||||
func testMindNodeProMacReceiptParsing() {
|
||||
guard let data = assertTestAsset(filename: "hannes_mac_mindnode_pro_receipt") else { return }
|
||||
|
||||
@@ -63,6 +91,8 @@ class LocalReceiptValidationTests: XCTestCase {
|
||||
let result = receiptValidator.validateReceipt {
|
||||
$0.receiptOrigin = .data(data)
|
||||
$0.shouldValidateHash = false // the original device identifier is unknown
|
||||
$0.propertyValidations = [ .string(\.appVersion, expected: "1.11.5"),
|
||||
.string(\.originalAppVersion, expected: "1.10.6")]
|
||||
}
|
||||
guard let receipt = result.receipt else {
|
||||
XCTFail("Unexpectedly failed parsing a receipt \(result.error!)")
|
||||
@@ -246,7 +276,7 @@ class LocalReceiptValidationTests: XCTestCase {
|
||||
func testiOSParsingPerformance() {
|
||||
guard let data = assertB64TestAsset(filename: "mindnode_ios_michaelsandbox_receipt1.b64") else { return }
|
||||
|
||||
let parameters = LocalReceiptValidator.Parameters.allSteps.with {
|
||||
let parameters = LocalReceiptValidator.Parameters.default.with {
|
||||
$0.receiptOrigin = .data(data)
|
||||
$0.deviceIdentifier = LocalReceiptValidator.Parameters.DeviceIdentifier(uuid: UUID(uuidString: "3B76A7BD-8F5B-46A4-BCB1-CCE8DBD1B3CD")!)
|
||||
}
|
||||
@@ -255,3 +285,13 @@ class LocalReceiptValidationTests: XCTestCase {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - LocalReceiptValidator + Convenience
|
||||
|
||||
extension LocalReceiptValidator {
|
||||
|
||||
/// Validates a local receipt and returns the result using the parameters `LocalReceiptValidator.Parameters.default`, which can be further configured in the passed block.
|
||||
func validateReceipt(configuration: (inout Parameters) -> Void) -> Result {
|
||||
return validateReceipt(parameters: Parameters.default.with(block: configuration))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// KnownOrUnknown.swift
|
||||
// Hekate
|
||||
//
|
||||
// Created by Hannes Oud on 08.01.18.
|
||||
// Copyright © 2018 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
// MARK: - KnownOrUnknown
|
||||
|
||||
/// A known or unknown RawRepresentable
|
||||
///
|
||||
/// - known: the raw value is known and could be assigned to a known strong value type
|
||||
/// - unknown: the raw value is unknown and is stored as is
|
||||
public enum KnownOrUnknown<Known: RawRepresentable> where Known.RawValue: Hashable {
|
||||
public typealias Unknown = Known.RawValue
|
||||
|
||||
case known(value: Known)
|
||||
case unknown(rawValue: Unknown)
|
||||
}
|
||||
|
||||
// MARK: - RawRepresentable
|
||||
|
||||
extension KnownOrUnknown: RawRepresentable {
|
||||
|
||||
public typealias RawValue = Unknown
|
||||
|
||||
public init?(rawValue: Unknown) {
|
||||
if let known = Known(rawValue: rawValue) {
|
||||
self = .known(value: known)
|
||||
} else {
|
||||
self = .unknown(rawValue: rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: Unknown {
|
||||
switch self {
|
||||
case .known(let value):
|
||||
return value.rawValue
|
||||
case .unknown(let rawValue):
|
||||
return rawValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Hashable
|
||||
|
||||
extension KnownOrUnknown: Hashable {
|
||||
|
||||
public var hashValue: Int {
|
||||
return self.rawValue.hashValue
|
||||
}
|
||||
|
||||
public static func ==(lhs: KnownOrUnknown<Known>, rhs: KnownOrUnknown<Known>) -> Bool {
|
||||
return lhs.rawValue == rhs.rawValue
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension KnownOrUnknown: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
case .known(let value):
|
||||
return "\(value)"
|
||||
case .unknown(let rawValue):
|
||||
return "\"\(rawValue)\""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@
|
||||
D15358B41F62C47400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 in Resources */ = {isa = PBXBuildFile; fileRef = D15358B11F62C3C400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 */; };
|
||||
D15358B51F62C47500F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 in Resources */ = {isa = PBXBuildFile; fileRef = D15358B11F62C3C400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 */; };
|
||||
D15358EF1F62D2C100F297D0 /* Hekate.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D1D6F4B51F5D684C00E86FE1 /* Hekate.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
D15358F11F62D43400F297D0 /* HekateDemoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D15358F01F62D43400F297D0 /* HekateDemoViewModel.swift */; };
|
||||
D15C59111F697D4D006F66FE /* pkcs7_union_accessors.h in Headers */ = {isa = PBXBuildFile; fileRef = D19095BA1F6004D10095729B /* pkcs7_union_accessors.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
D15C59151F698061006F66FE /* aes.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D431061F69627600F7F39D /* aes.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
D15C59181F6981C4006F66FE /* asn1.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D431071F69627600F7F39D /* asn1.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
@@ -134,7 +133,10 @@
|
||||
D19095CC1F601E5D0095729B /* not_a_receipt in Resources */ = {isa = PBXBuildFile; fileRef = D1D6F54F1F5D9E8D00E86FE1 /* not_a_receipt */; };
|
||||
D19095CD1F601E960095729B /* LocalReceiptValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F5411F5D8A3800E86FE1 /* LocalReceiptValidationTests.swift */; };
|
||||
D19095CE1F601E980095729B /* LocalReceiptValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F5411F5D8A3800E86FE1 /* LocalReceiptValidationTests.swift */; };
|
||||
D1A46B821F62E26900A390EC /* StoreKitHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A46B811F62E26900A390EC /* StoreKitHelper.swift */; };
|
||||
D1A428A91FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */; };
|
||||
D1A428AA1FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */; };
|
||||
D1AA845C1F6ABB59007F2558 /* LocalReceiptPropertyValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AA845A1F6ABB31007F2558 /* LocalReceiptPropertyValidationTests.swift */; };
|
||||
D1AA845D1F6ABB59007F2558 /* LocalReceiptPropertyValidationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1AA845A1F6ABB31007F2558 /* LocalReceiptPropertyValidationTests.swift */; };
|
||||
D1AB81AA1F696F0200B57E29 /* aes.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D430B61F69627600F7F39D /* aes.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
D1AB81AB1F696F0200B57E29 /* asn1.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D430B71F69627600F7F39D /* asn1.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
D1AB81AC1F696F0200B57E29 /* asn1_mac.h in Headers */ = {isa = PBXBuildFile; fileRef = D1D430B81F69627600F7F39D /* asn1_mac.h */; settings = {ATTRIBUTES = (Private, ); }; };
|
||||
@@ -222,6 +224,8 @@
|
||||
D1D6F4F11F5D691400E86FE1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D1D6F4EF1F5D691400E86FE1 /* LaunchScreen.storyboard */; };
|
||||
D1D6F53F1F5D89D000E86FE1 /* LocalReceiptValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */; };
|
||||
D1D6F5401F5D89D800E86FE1 /* LocalReceiptValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */; };
|
||||
D1DFC5DA20037B8400C7B99B /* KnownOrUnknown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */; };
|
||||
D1DFC5DB20037B8400C7B99B /* KnownOrUnknown.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */; };
|
||||
D1FE343D1F604F020029576B /* Receipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343C1F604F020029576B /* Receipt.swift */; };
|
||||
D1FE343E1F604F020029576B /* Receipt.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343C1F604F020029576B /* Receipt.swift */; };
|
||||
D1FE34401F604F540029576B /* LocalReceiptValidator+Parameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */; };
|
||||
@@ -286,7 +290,6 @@
|
||||
D150A0ED1F669A880026ED04 /* LocalReceiptValidationInAppPurchaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalReceiptValidationInAppPurchaseTests.swift; sourceTree = "<group>"; };
|
||||
D15358A51F62BEC100F297D0 /* grandUnifiedExpiredAppleCert_receipt.b64 */ = {isa = PBXFileReference; lastKnownFileType = text; path = grandUnifiedExpiredAppleCert_receipt.b64; sourceTree = "<group>"; };
|
||||
D15358B11F62C3C400F297D0 /* deprecatedSinglesTypeExpiredAppleCert_receipt.b64 */ = {isa = PBXFileReference; lastKnownFileType = text; path = deprecatedSinglesTypeExpiredAppleCert_receipt.b64; sourceTree = "<group>"; };
|
||||
D15358F01F62D43400F297D0 /* HekateDemoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HekateDemoViewModel.swift; sourceTree = "<group>"; };
|
||||
D15C59141F698005006F66FE /* Hekate.modulemap */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.module-map"; path = Hekate.modulemap; sourceTree = "<group>"; };
|
||||
D19095811F6000A40095729B /* Hekate Demo macOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Hekate Demo macOS.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
D19095831F6000A40095729B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
@@ -304,7 +307,8 @@
|
||||
D19095BF1F60158B0095729B /* DeviceIdentifier+installedDeviceIdentifier_macOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceIdentifier+installedDeviceIdentifier_macOS.swift"; sourceTree = "<group>"; };
|
||||
D19095C11F6019E70095729B /* DeviceIdentifier+installedDeviceIdentifier_iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DeviceIdentifier+installedDeviceIdentifier_iOS.swift"; sourceTree = "<group>"; };
|
||||
D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = AppleIncRootCertificate.cer; sourceTree = "<group>"; };
|
||||
D1A46B811F62E26900A390EC /* StoreKitHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreKitHelper.swift; sourceTree = "<group>"; };
|
||||
D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnofficialReceipt.swift; sourceTree = "<group>"; };
|
||||
D1AA845A1F6ABB31007F2558 /* LocalReceiptPropertyValidationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalReceiptPropertyValidationTests.swift; sourceTree = "<group>"; };
|
||||
D1D430B21F69627600F7F39D /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libcrypto.a; sourceTree = "<group>"; };
|
||||
D1D430B31F69627600F7F39D /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libssl.a; sourceTree = "<group>"; };
|
||||
D1D430B61F69627600F7F39D /* aes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = aes.h; sourceTree = "<group>"; };
|
||||
@@ -478,6 +482,7 @@
|
||||
D1D6F5471F5D8DF700E86FE1 /* hannes_mac_mindnode_pro_Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = hannes_mac_mindnode_pro_Info.plist; sourceTree = "<group>"; };
|
||||
D1D6F5491F5D9B1F00E86FE1 /* TestAssetLoading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAssetLoading.swift; sourceTree = "<group>"; };
|
||||
D1D6F54F1F5D9E8D00E86FE1 /* not_a_receipt */ = {isa = PBXFileReference; lastKnownFileType = text; path = not_a_receipt; sourceTree = "<group>"; };
|
||||
D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KnownOrUnknown.swift; sourceTree = "<group>"; };
|
||||
D1FE343C1F604F020029576B /* Receipt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Receipt.swift; sourceTree = "<group>"; };
|
||||
D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "LocalReceiptValidator+Parameters.swift"; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
@@ -634,6 +639,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
D1D6F5411F5D8A3800E86FE1 /* LocalReceiptValidationTests.swift */,
|
||||
D1AA845A1F6ABB31007F2558 /* LocalReceiptPropertyValidationTests.swift */,
|
||||
D150A0ED1F669A880026ED04 /* LocalReceiptValidationInAppPurchaseTests.swift */,
|
||||
D1D6F5481F5D9B1100E86FE1 /* Tools */,
|
||||
D1D6F5431F5D8DBC00E86FE1 /* Test Assets */,
|
||||
@@ -871,8 +877,6 @@
|
||||
children = (
|
||||
D1D6F4E61F5D691400E86FE1 /* AppDelegate.swift */,
|
||||
D1D6F4E81F5D691400E86FE1 /* ViewController.swift */,
|
||||
D1A46B811F62E26900A390EC /* StoreKitHelper.swift */,
|
||||
D15358F01F62D43400F297D0 /* HekateDemoViewModel.swift */,
|
||||
D1D6F4EA1F5D691400E86FE1 /* Main.storyboard */,
|
||||
D1D6F4ED1F5D691400E86FE1 /* Assets.xcassets */,
|
||||
D1D6F4EF1F5D691400E86FE1 /* LaunchScreen.storyboard */,
|
||||
@@ -889,6 +893,7 @@
|
||||
D1D6F5271F5D863900E86FE1 /* Supporting Files */,
|
||||
D1D6F53E1F5D89D000E86FE1 /* LocalReceiptValidator.swift */,
|
||||
D1FE343C1F604F020029576B /* Receipt.swift */,
|
||||
D1A428A81FE4267A00926BA5 /* UnofficialReceipt.swift */,
|
||||
D1FE343F1F604F540029576B /* LocalReceiptValidator+Parameters.swift */,
|
||||
D19095BF1F60158B0095729B /* DeviceIdentifier+installedDeviceIdentifier_macOS.swift */,
|
||||
D19095C11F6019E70095729B /* DeviceIdentifier+installedDeviceIdentifier_iOS.swift */,
|
||||
@@ -943,6 +948,7 @@
|
||||
children = (
|
||||
D14FA72E1F6143C400545540 /* Date+Convenience.swift */,
|
||||
D1D6F5491F5D9B1F00E86FE1 /* TestAssetLoading.swift */,
|
||||
D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */,
|
||||
);
|
||||
path = Tools;
|
||||
sourceTree = "<group>";
|
||||
@@ -1413,6 +1419,7 @@
|
||||
files = (
|
||||
D190959F1F6000DE0095729B /* TestAssetLoading.swift in Sources */,
|
||||
D19095CD1F601E960095729B /* LocalReceiptValidationTests.swift in Sources */,
|
||||
D1AA845D1F6ABB59007F2558 /* LocalReceiptPropertyValidationTests.swift in Sources */,
|
||||
D150A0EF1F669A880026ED04 /* LocalReceiptValidationInAppPurchaseTests.swift in Sources */,
|
||||
D150A0F01F67E0990026ED04 /* Date+Convenience.swift in Sources */,
|
||||
);
|
||||
@@ -1424,6 +1431,7 @@
|
||||
files = (
|
||||
D19095C71F601E580095729B /* TestAssetLoading.swift in Sources */,
|
||||
D19095CE1F601E980095729B /* LocalReceiptValidationTests.swift in Sources */,
|
||||
D1AA845C1F6ABB59007F2558 /* LocalReceiptPropertyValidationTests.swift in Sources */,
|
||||
D150A0EE1F669A880026ED04 /* LocalReceiptValidationInAppPurchaseTests.swift in Sources */,
|
||||
D150A0F11F67E0990026ED04 /* Date+Convenience.swift in Sources */,
|
||||
);
|
||||
@@ -1433,11 +1441,13 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1A428A91FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */,
|
||||
D1D6F53F1F5D89D000E86FE1 /* LocalReceiptValidator.swift in Sources */,
|
||||
D1FE34401F604F540029576B /* LocalReceiptValidator+Parameters.swift in Sources */,
|
||||
D19095C31F6019FC0095729B /* DeviceIdentifier+installedDeviceIdentifier_iOS.swift in Sources */,
|
||||
D14FA7271F61351000545540 /* AutoEquatable.swift in Sources */,
|
||||
D14FA7291F61351400545540 /* AutoEquatable.generated.swift in Sources */,
|
||||
D1DFC5DA20037B8400C7B99B /* KnownOrUnknown.swift in Sources */,
|
||||
D1FE343D1F604F020029576B /* Receipt.swift in Sources */,
|
||||
D14FA73B1F618B0100545540 /* ASN1Helpers.swift in Sources */,
|
||||
D14FA7381F6181C700545540 /* OpenSSLWrappers.swift in Sources */,
|
||||
@@ -1449,11 +1459,13 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1A428AA1FE4267A00926BA5 /* UnofficialReceipt.swift in Sources */,
|
||||
D1D6F5401F5D89D800E86FE1 /* LocalReceiptValidator.swift in Sources */,
|
||||
D1FE34411F604F540029576B /* LocalReceiptValidator+Parameters.swift in Sources */,
|
||||
D19095C01F60158B0095729B /* DeviceIdentifier+installedDeviceIdentifier_macOS.swift in Sources */,
|
||||
D1FE343E1F604F020029576B /* Receipt.swift in Sources */,
|
||||
D14FA72A1F61351500545540 /* AutoEquatable.generated.swift in Sources */,
|
||||
D1DFC5DB20037B8400C7B99B /* KnownOrUnknown.swift in Sources */,
|
||||
D14FA73C1F618B0100545540 /* ASN1Helpers.swift in Sources */,
|
||||
D14FA7391F6181D000545540 /* OpenSSLWrappers.swift in Sources */,
|
||||
D14FA7261F61350F00545540 /* AutoEquatable.swift in Sources */,
|
||||
@@ -1465,10 +1477,8 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
D1A46B821F62E26900A390EC /* StoreKitHelper.swift in Sources */,
|
||||
D1D6F4E91F5D691400E86FE1 /* ViewController.swift in Sources */,
|
||||
D1D6F4E71F5D691400E86FE1 /* AppDelegate.swift in Sources */,
|
||||
D15358F11F62D43400F297D0 /* HekateDemoViewModel.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
||||
@@ -11,15 +11,37 @@ import Foundation
|
||||
public extension LocalReceiptValidator {
|
||||
|
||||
/// Describes how to validate a receipt, and how/where to obtain the dependencies (receipt, deviceIdentifier, apple root certificate)
|
||||
/// Use .allSteps to initialize the standard parameters.
|
||||
/// Use .default to initialize the standard parameters. By default, no `propertyValidations` are active.
|
||||
public struct Parameters {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
public var receiptOrigin: ReceiptOrigin = .installedInMainBundle
|
||||
public var shouldValidateSignaturePresence: Bool = true
|
||||
public var shouldValidateSignatureAuthenticity: Bool = true
|
||||
public var shouldValidateHash: Bool = true
|
||||
public var deviceIdentifier: DeviceIdentifier = .currentDevice
|
||||
public let rootCertificateOrigin: RootCertificateOrigin = .cerFileBundledWithHekate
|
||||
public var rootCertificateOrigin: RootCertificateOrigin = .cerFileBundledWithHekate
|
||||
public var propertyValidations: [PropertyValidation] = []
|
||||
|
||||
// MARK: - Lifecycle
|
||||
|
||||
/// Or use .default to initialize a sensible defaults
|
||||
public init(receiptOrigin: ReceiptOrigin, shouldValidateSignaturePresence: Bool, shouldValidateSignatureAuthenticity: Bool, shouldValidateHash: Bool, deviceIdentifier: DeviceIdentifier, rootCertificateOrigin: RootCertificateOrigin, propertyValidations: [PropertyValidation]) {
|
||||
self.receiptOrigin = receiptOrigin
|
||||
self.shouldValidateSignaturePresence = shouldValidateSignaturePresence
|
||||
self.shouldValidateSignatureAuthenticity = shouldValidateSignatureAuthenticity
|
||||
self.shouldValidateHash = shouldValidateHash
|
||||
self.deviceIdentifier = deviceIdentifier
|
||||
self.rootCertificateOrigin = rootCertificateOrigin
|
||||
}
|
||||
|
||||
/// Either use `.default` to get a default preset, or specify everything via the complete init(…) with all parameters.
|
||||
private init() {}
|
||||
|
||||
public static var `default`: Parameters {
|
||||
return Parameters()
|
||||
}
|
||||
|
||||
/// Configure an instance with a block
|
||||
public func with(block: (inout Parameters) -> Void) -> Parameters {
|
||||
@@ -27,24 +49,17 @@ public extension LocalReceiptValidator {
|
||||
block(©)
|
||||
return copy
|
||||
}
|
||||
|
||||
/// Use .allSteps to initialize
|
||||
private init() {}
|
||||
|
||||
public static var allSteps: Parameters {
|
||||
return Parameters()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ReceiptOrigin
|
||||
|
||||
/// Used for obtaining the receipt data to parse or validate.
|
||||
///
|
||||
/// - installedInMainBundle: Loads it from Bundle.main.appStoreReceiptURL.
|
||||
/// - data: Loads specific data.
|
||||
extension LocalReceiptValidator.Parameters {
|
||||
|
||||
/// Used for obtaining the receipt data to parse or validate.
|
||||
///
|
||||
/// - installedInMainBundle: Loads it from Bundle.main.appStoreReceiptURL.
|
||||
/// - data: Loads specific data.
|
||||
public enum ReceiptOrigin {
|
||||
|
||||
case installedInMainBundle
|
||||
@@ -66,12 +81,12 @@ extension LocalReceiptValidator.Parameters {
|
||||
|
||||
// MARK: - DeviceIdentifier
|
||||
|
||||
/// Used for calculating/validating the SHA1-Hash part of a receipt.
|
||||
///
|
||||
/// - currentDevice: Obtains it from the system location: MAC Adress on macOS, deviceIdentifierForVendor on iOS
|
||||
/// - data: Specific Data to use
|
||||
public extension LocalReceiptValidator.Parameters {
|
||||
|
||||
/// Used for calculating/validating the SHA1-Hash part of a receipt.
|
||||
///
|
||||
/// - currentDevice: Obtains it from the system location: MAC Adress on macOS, deviceIdentifierForVendor on iOS
|
||||
/// - data: Specific Data to use
|
||||
public enum DeviceIdentifier {
|
||||
|
||||
case currentDevice
|
||||
@@ -103,12 +118,12 @@ public extension LocalReceiptValidator.Parameters {
|
||||
|
||||
// MARK: - RootCertificateOrigin
|
||||
|
||||
/// Instructs how to find the Apple root certificate for receipt validation.
|
||||
///
|
||||
/// - cerFileBundledWithHekate: Uses the "AppleIncRootCertificate.cer" bundled with Hekate
|
||||
/// - data: Specific Data to use
|
||||
extension LocalReceiptValidator.Parameters {
|
||||
|
||||
/// Instructs how to find the Apple root certificate for receipt validation.
|
||||
///
|
||||
/// - cerFileBundledWithHekate: Uses the "AppleIncRootCertificate.cer" bundled with Hekate
|
||||
/// - data: Specific Data to use
|
||||
public enum RootCertificateOrigin {
|
||||
case cerFileBundledWithHekate
|
||||
case data(Data)
|
||||
@@ -127,6 +142,70 @@ extension LocalReceiptValidator.Parameters {
|
||||
private class BundleToken {}
|
||||
}
|
||||
|
||||
// MARK: - PropertyValidation
|
||||
|
||||
extension LocalReceiptValidator.Parameters {
|
||||
|
||||
/// Compares a String property of a receipt with an info dictionary entry or a provided value.
|
||||
///
|
||||
/// Apple recommends comparing against hard coded values. Note the platform dependence of `Receipt.appVersion`.
|
||||
///
|
||||
/// See convieniences `bundleIdMatchingMainBundle`, `appVersionMatchingMainBundleIOS`, and `appVersionMatchingMainBundleMacOS`.
|
||||
///
|
||||
/// - string: Compare a property with a hardcoded string (as recommended by apple)
|
||||
public enum PropertyValidation {
|
||||
|
||||
case string(KeyPath<Receipt, String?>, expected: String?)
|
||||
|
||||
/// Compares the receipts bundle id with the main bundle's info plist CFBundleIdentifier.
|
||||
public static var bundleIdMatchingMainBundle: PropertyValidation {
|
||||
return compareWithMainBundle(receiptProperty: \Receipt.bundleIdentifier, infoDictionaryKey: String(kCFBundleIdentifierKey))
|
||||
}
|
||||
|
||||
/// Compares the receipts appVersion with the main bundle's info plist CFBundleVersionString, as adequate for iOS
|
||||
public static var appVersionMatchingMainBundleIOS: PropertyValidation {
|
||||
return compareWithMainBundle(receiptProperty: \Receipt.appVersion, infoDictionaryKey: String(kCFBundleVersionKey))
|
||||
}
|
||||
|
||||
/// Compares the receipts appVersion with the main bundle's info plist CFBundleShortVersionString, as adequate for macOS
|
||||
public static var appVersionMatchingMainBundleMacOS: PropertyValidation {
|
||||
return compareWithMainBundle(receiptProperty: \Receipt.appVersion, infoDictionaryKey: "CFBundleShortVersionString")
|
||||
}
|
||||
|
||||
private static func compareWithMainBundle(receiptProperty: KeyPath<Receipt, String?>, infoDictionaryKey: String) -> PropertyValidation {
|
||||
let expected = Bundle.main.infoDictionary?[infoDictionaryKey] as? String
|
||||
return .string(receiptProperty, expected: expected)
|
||||
}
|
||||
|
||||
// MARK: Validation Execution
|
||||
|
||||
/// Validates a receipts property. May throw Error.couldNotGetExpectedPropertyValue or Error.propertyValueMismatch.
|
||||
public func validateProperty(of receipt: Receipt) throws {
|
||||
let expected = self.getExpectedValue()
|
||||
|
||||
if self.propertyValue(of: receipt) != expected {
|
||||
throw LocalReceiptValidator.Error.propertyValueMismatch
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Value and Expected Value
|
||||
|
||||
private func propertyValue(of receipt: Receipt) -> String? {
|
||||
switch self {
|
||||
case .string(let keyPath, _):
|
||||
return receipt[keyPath: keyPath]
|
||||
}
|
||||
}
|
||||
|
||||
private func getExpectedValue() -> String? {
|
||||
switch self {
|
||||
case .string(_, let expected):
|
||||
return expected
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UUID + data
|
||||
|
||||
extension UUID {
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
import Hekate.OpenSSL
|
||||
import StoreKit
|
||||
|
||||
/// Apple guide: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html
|
||||
///
|
||||
@@ -24,16 +23,15 @@ public struct LocalReceiptValidator {
|
||||
|
||||
// MARK: - Local Receipt Validation
|
||||
|
||||
/// Validates a local receipt and returns the result using the parameters `LocalReceiptValidator.Parameters.allSteps`, which can be further configured in the passed block.
|
||||
public func validateReceipt(configuration: (inout Parameters) -> Void) -> Result {
|
||||
return validateReceipt(parameters: Parameters.allSteps.with(block: configuration))
|
||||
}
|
||||
|
||||
/// Validates a local receipt and returns the result using the passed parameters.
|
||||
public func validateReceipt(parameters: Parameters = Parameters.allSteps) -> Result {
|
||||
public func validateReceipt(parameters: Parameters = Parameters.default) -> Result {
|
||||
var data: Data?
|
||||
var deviceIdData: Data?
|
||||
do {
|
||||
deviceIdData = parameters.deviceIdentifier.getData()
|
||||
guard let receiptData = parameters.receiptOrigin.loadData() else { throw Error.couldNotFindReceipt }
|
||||
|
||||
data = receiptData
|
||||
let receiptContainer = try self.extractPKCS7Container(data: receiptData)
|
||||
|
||||
if parameters.shouldValidateSignaturePresence {
|
||||
@@ -44,30 +42,51 @@ public struct LocalReceiptValidator {
|
||||
|
||||
try self.checkSignatureAuthenticity(pkcs7: receiptContainer, appleRootCertificateData: appleRootCertificateData)
|
||||
}
|
||||
let parsedReceipt = try parseReceipt(pkcs7: receiptContainer)
|
||||
|
||||
let receipt = try self.parseReceipt(pkcs7: receiptContainer).receipt
|
||||
|
||||
try self.validateProperties(receipt: receipt, validations: parameters.propertyValidations)
|
||||
|
||||
if parameters.shouldValidateHash {
|
||||
guard let deviceIdentifierData = parameters.deviceIdentifier.getData() else { throw Error.deviceIdentifierNotDeterminable }
|
||||
guard let deviceIdentifierData = deviceIdData else { throw Error.deviceIdentifierNotDeterminable }
|
||||
|
||||
try self.validateHash(receipt: parsedReceipt, deviceIdentifierData: deviceIdentifierData)
|
||||
try self.validateHash(receipt: receipt, deviceIdentifierData: deviceIdentifierData)
|
||||
}
|
||||
return .success(parsedReceipt)
|
||||
return .success(receipt, receiptData: receiptData, deviceIdentifier: deviceIdData)
|
||||
} catch {
|
||||
assert(error is LocalReceiptValidator.Error)
|
||||
return .error(error as? LocalReceiptValidator.Error ?? .unknown)
|
||||
return .error(error as? LocalReceiptValidator.Error ?? .unknown, receiptData: data, deviceIdentifier: deviceIdData)
|
||||
}
|
||||
}
|
||||
|
||||
public func validateProperties(receipt: Receipt, validations: [Parameters.PropertyValidation]) throws {
|
||||
for validation in validations {
|
||||
try validation.validateProperty(of: receipt)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a local receipt without any validation.
|
||||
///
|
||||
/// - Parameter origin: How to load the receipt.
|
||||
/// - Returns: The Parsed receipt.
|
||||
/// - Throws: A Error. Especially Error.couldNotFindReceipt if the receipt cannot be loaded/found.
|
||||
/// - Returns: The parsed receipt.
|
||||
/// - Throws: Especially Error.couldNotFindReceipt if the receipt cannot be loaded/found.
|
||||
public func parseReceipt(origin: Parameters.ReceiptOrigin) throws -> Receipt {
|
||||
guard let receiptData = origin.loadData() else { throw Error.couldNotFindReceipt }
|
||||
|
||||
let receiptContainer = try extractPKCS7Container(data: receiptData)
|
||||
return try parseReceipt(pkcs7: receiptContainer)
|
||||
let receiptContainer = try self.extractPKCS7Container(data: receiptData)
|
||||
return try parseReceipt(pkcs7: receiptContainer).receipt
|
||||
}
|
||||
|
||||
/// Parse the local receipt and it's unofficial attributes without any validation.
|
||||
///
|
||||
/// - Parameter origin: How to load the receipt.
|
||||
/// - Returns: The parsed receipt.
|
||||
/// - Throws: Especially Error.couldNotFindReceipt if the receipt cannot be loaded/found.
|
||||
public func parseUnofficialReceipt(origin: Parameters.ReceiptOrigin) throws -> (receipt: Receipt, unofficialReceipt: UnofficialReceipt) {
|
||||
guard let receiptData = origin.loadData() else { throw Error.couldNotFindReceipt }
|
||||
|
||||
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"
|
||||
@@ -86,7 +105,7 @@ public struct LocalReceiptValidator {
|
||||
private extension LocalReceiptValidator {
|
||||
|
||||
func validateHash(receipt: Receipt, deviceIdentifierData: Data) throws {
|
||||
// Make sure that the ParsedReceipt instances has non-nil values needed for hash comparison
|
||||
// Make sure that the Receipt instances has non-nil values needed for hash comparison
|
||||
guard let receiptOpaqueValueData = receipt.opaqueValue else { throw Error.incorrectHash }
|
||||
guard let receiptBundleIdData = receipt.bundleIdData else { throw Error.incorrectHash }
|
||||
guard let receiptHashData = receipt.sha1Hash else { throw Error.incorrectHash }
|
||||
@@ -181,69 +200,98 @@ private extension LocalReceiptValidator {
|
||||
private extension LocalReceiptValidator {
|
||||
|
||||
// swiftlint:disable:next cyclomatic_complexity
|
||||
func parseReceipt(pkcs7: PKCS7Wrapper) throws -> Receipt {
|
||||
func parseReceipt(pkcs7: PKCS7Wrapper, parseUnofficialParts: Bool = false) throws -> (receipt: Receipt, unofficialReceipt: UnofficialReceipt) {
|
||||
guard let contents = pkcs7.pkcs7.pointee.d.sign.pointee.contents, let octets = contents.pointee.d.data else { throw Error.malformedReceipt }
|
||||
guard let initialPointer = UnsafePointer(octets.pointee.data) else { throw Error.malformedReceipt }
|
||||
let length = Int(octets.pointee.length)
|
||||
var parsedReceipt = Receipt()
|
||||
var receipt = Receipt()
|
||||
var unofficialReceipt = UnofficialReceipt(entries: [])
|
||||
|
||||
try self.parseASN1Set(pointer: initialPointer, length: length) { attributeType, value in
|
||||
guard let attribute = KnownReceiptAttribute(rawValue: attributeType) else { return }
|
||||
guard let attribute = KnownReceiptAttribute(rawValue: attributeType) else {
|
||||
if parseUnofficialParts {
|
||||
let entry = parseUnofficialReceiptEntry(attributeType: attributeType, value: value)
|
||||
unofficialReceipt.entries.append(entry)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
switch attribute {
|
||||
case .bundleIdentifier:
|
||||
parsedReceipt.bundleIdData = value.dataValue
|
||||
parsedReceipt.bundleIdentifier = value.unwrappedStringValue
|
||||
receipt.bundleIdData = value.dataValue
|
||||
receipt.bundleIdentifier = value.unwrappedStringValue
|
||||
case .appVersion:
|
||||
parsedReceipt.appVersion = value.unwrappedStringValue
|
||||
receipt.appVersion = value.unwrappedStringValue
|
||||
case .opaqueValue:
|
||||
parsedReceipt.opaqueValue = value.dataValue
|
||||
receipt.opaqueValue = value.dataValue
|
||||
case .sha1Hash:
|
||||
parsedReceipt.sha1Hash = value.dataValue
|
||||
receipt.sha1Hash = value.dataValue
|
||||
case .inAppPurchaseReceipts:
|
||||
guard let pointer = value.valuePointer else { break }
|
||||
|
||||
let iapReceipt = try parseInAppPurchaseReceipt(pointer: pointer, length: value.length)
|
||||
parsedReceipt.inAppPurchaseReceipts.append(iapReceipt)
|
||||
receipt.inAppPurchaseReceipts.append(iapReceipt)
|
||||
case .receiptCreationDate:
|
||||
parsedReceipt.receiptCreationDate = value.unwrappedDateValue
|
||||
receipt.receiptCreationDate = value.unwrappedDateValue
|
||||
case .originalAppVersion:
|
||||
parsedReceipt.originalAppVersion = value.unwrappedStringValue
|
||||
receipt.originalAppVersion = value.unwrappedStringValue
|
||||
case .expirationDate:
|
||||
parsedReceipt.expirationDate = value.unwrappedDateValue
|
||||
break
|
||||
receipt.expirationDate = value.unwrappedDateValue
|
||||
}
|
||||
}
|
||||
return parsedReceipt
|
||||
|
||||
return (receipt: receipt, unofficialReceipt: unofficialReceipt)
|
||||
}
|
||||
|
||||
private func parseInAppPurchaseReceipt(pointer: UnsafePointer<UInt8>, length: Int) throws -> InAppPurchaseReceipt {
|
||||
var parsedInAppPurchaseReceipt = InAppPurchaseReceipt()
|
||||
var inAppPurchaseReceipt = InAppPurchaseReceipt()
|
||||
try self.parseASN1Set(pointer: pointer, length: length) { attributeType, value in
|
||||
guard let attribute = KnownInAppPurchaseAttribute(rawValue: attributeType) else { return }
|
||||
|
||||
switch attribute {
|
||||
case .quantity:
|
||||
parsedInAppPurchaseReceipt.quantity = value.intValue
|
||||
inAppPurchaseReceipt.quantity = value.intValue
|
||||
case .productIdentifier:
|
||||
parsedInAppPurchaseReceipt.productIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.productIdentifier = value.unwrappedStringValue
|
||||
case .transactionIdentifier:
|
||||
parsedInAppPurchaseReceipt.transactionIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.transactionIdentifier = value.unwrappedStringValue
|
||||
case .originalTransactionIdentifier:
|
||||
parsedInAppPurchaseReceipt.originalTransactionIdentifier = value.unwrappedStringValue
|
||||
inAppPurchaseReceipt.originalTransactionIdentifier = value.unwrappedStringValue
|
||||
case .purchaseDate:
|
||||
parsedInAppPurchaseReceipt.purchaseDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.purchaseDate = value.unwrappedDateValue
|
||||
case .originalPurchaseDate:
|
||||
parsedInAppPurchaseReceipt.originalPurchaseDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.originalPurchaseDate = value.unwrappedDateValue
|
||||
case .subscriptionExpirationDate:
|
||||
parsedInAppPurchaseReceipt.subscriptionExpirationDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.subscriptionExpirationDate = value.unwrappedDateValue
|
||||
case .cancellationDate:
|
||||
parsedInAppPurchaseReceipt.cancellationDate = value.unwrappedDateValue
|
||||
inAppPurchaseReceipt.cancellationDate = value.unwrappedDateValue
|
||||
case .webOrderLineItemId:
|
||||
parsedInAppPurchaseReceipt.webOrderLineItemId = value.intValue
|
||||
inAppPurchaseReceipt.webOrderLineItemId = value.intValue
|
||||
}
|
||||
}
|
||||
return parsedInAppPurchaseReceipt
|
||||
return inAppPurchaseReceipt
|
||||
}
|
||||
|
||||
private func parseUnofficialReceiptEntry(attributeType: Int32, value: ASN1Object) -> UnofficialReceipt.Entry {
|
||||
switch KnownUnofficialReceiptAttribute(rawValue: attributeType) {
|
||||
case .some(let meaning):
|
||||
switch meaning.parsingType {
|
||||
case .string:
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: meaning, value: value.unwrappedStringValue.map { UnofficialReceipt.Entry.Value.string($0) })
|
||||
case .date:
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: meaning, value: value.unwrappedDateValue.map { UnofficialReceipt.Entry.Value.date($0) })
|
||||
case .data:
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: meaning, value: value.dataValue.map { UnofficialReceipt.Entry.Value.bytes($0) })
|
||||
}
|
||||
case .none:
|
||||
if let string = value.unwrappedStringValue {
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: nil, value: .string(string))
|
||||
}
|
||||
if let string = value.stringValue {
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: nil, value: .string(string))
|
||||
}
|
||||
return UnofficialReceipt.Entry(attributeNumber: attributeType, meaning: nil, value: value.dataValue.map { UnofficialReceipt.Entry.Value.bytes($0) })
|
||||
}
|
||||
}
|
||||
|
||||
private func parseASN1Set(pointer initialPointer: UnsafePointer<UInt8>, length: Int, valueAttributeAction: (_ attributeType: Int32, _ value: ASN1Object) throws -> Void) throws {
|
||||
@@ -276,7 +324,7 @@ private extension LocalReceiptValidator {
|
||||
|
||||
private extension LocalReceiptValidator {
|
||||
|
||||
/// See ParsedReceipt.swift for details and a link to Apple reference
|
||||
/// See Receipt.swift for details and a link to Apple reference
|
||||
enum KnownReceiptAttribute: Int32 {
|
||||
case bundleIdentifier = 2
|
||||
case appVersion = 3
|
||||
@@ -286,16 +334,10 @@ private extension LocalReceiptValidator {
|
||||
case receiptCreationDate = 12
|
||||
case originalAppVersion = 19
|
||||
case expirationDate = 21
|
||||
|
||||
// Unofficial list found (not necessarily complete):
|
||||
// - 18: some date in the past
|
||||
// - 8: some date in the past, same as receiptCreationDate possibly
|
||||
// - 0: String, probably Provisioning-Type, Encountered Values: "Production", "ProductionSandbox"
|
||||
// - 10: String, probably Age Description, example Value "4+"
|
||||
// - and of unknown type 14(L=3), 25(L=3), 11(L=4), 13(L=4), 1(L=6), 9(L=6), 16(L=6), 15(L=8), 7(L=66), 6(L=69 variable)
|
||||
}
|
||||
|
||||
/// See ParsedReceipt.swift for details and a link to Apple reference
|
||||
|
||||
/// See Receipt.swift for details and a link to Apple reference
|
||||
enum KnownInAppPurchaseAttribute: Int32 {
|
||||
case quantity = 1701
|
||||
case productIdentifier = 1702
|
||||
@@ -315,12 +357,12 @@ extension LocalReceiptValidator {
|
||||
|
||||
public enum Result {
|
||||
|
||||
case success(Receipt)
|
||||
case error(LocalReceiptValidator.Error)
|
||||
case success(Receipt, receiptData: Data, deviceIdentifier: Data?)
|
||||
case error(LocalReceiptValidator.Error, receiptData: Data?, deviceIdentifier: Data?)
|
||||
|
||||
public var receipt: Receipt? {
|
||||
switch self {
|
||||
case .success(let receipt):
|
||||
case .success(let receipt, _, _):
|
||||
return receipt
|
||||
case .error:
|
||||
return nil
|
||||
@@ -331,10 +373,30 @@ extension LocalReceiptValidator {
|
||||
switch self {
|
||||
case .success:
|
||||
return nil
|
||||
case .error(let error):
|
||||
case .error(let error, _, _):
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
/// The receipt data if it could be loaded
|
||||
public var receiptData: Data? {
|
||||
switch self {
|
||||
case .success(_, let data, _):
|
||||
return data
|
||||
case .error(_, let data, _):
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
/// The device identifier if it could be determined
|
||||
public var deviceIdentifier: Data? {
|
||||
switch self {
|
||||
case .success(_, _, let data):
|
||||
return data
|
||||
case .error(_, _, let data):
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +415,8 @@ extension LocalReceiptValidator {
|
||||
case incorrectHash
|
||||
case deviceIdentifierNotDeterminable
|
||||
case malformedAppleRootCertificate
|
||||
case couldNotGetExpectedPropertyValue
|
||||
case propertyValueMismatch
|
||||
case unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public struct Receipt {
|
||||
/// The app’s bundle identifier as bytes, used, with other data, to compute the SHA-1 hash during validation.
|
||||
public internal(set) var bundleIdData: Data?
|
||||
|
||||
/// The app’s version number.
|
||||
/// The app’s version number. **This is platform dependent!**
|
||||
/// This corresponds to the value of `CFBundleVersion` (in iOS) or `CFBundleShortVersionString` (in macOS) in the Info.plist.
|
||||
/// ASN.1 Field Type 3.
|
||||
public internal(set) var appVersion: String?
|
||||
@@ -76,7 +76,7 @@ extension Receipt: AutoEquatable {}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension Receipt: CustomStringConvertible {
|
||||
extension Receipt: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
let formatter = StringFormatter()
|
||||
@@ -91,11 +91,15 @@ extension Receipt: CustomStringConvertible {
|
||||
("expirationDate", formatter.format(self.expirationDate)),
|
||||
("inAppPurchaseReceipts", formatter.format(self.inAppPurchaseReceipts))
|
||||
]
|
||||
return "ParsedReceipt(\n" + formatter.format(props) + "\n)"
|
||||
return "Receipt(\n" + formatter.format(props) + "\n)"
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - ParsedInAppPurchaseReceipt
|
||||
// MARK: - InAppPurchaseReceipt
|
||||
|
||||
/// An In-App-Purchase Receipt as Parsed from a receipt file.
|
||||
///
|
||||
@@ -169,7 +173,7 @@ public struct InAppPurchaseReceipt {
|
||||
/// This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
|
||||
public internal(set) var webOrderLineItemId: Int?
|
||||
|
||||
/// For documentation see ParsedInAppPurchaseReceipt itself.
|
||||
/// For documentation see InAppPurchaseReceipt itself.
|
||||
public init(quantity: Int?, productIdentifier: String?, transactionIdentifier: String?, originalTransactionIdentifier: String?, purchaseDate: Date?, originalPurchaseDate: Date?, subscriptionExpirationDate: Date?, cancellationDate: Date?, webOrderLineItemId: Int?) {
|
||||
self.quantity = quantity
|
||||
self.productIdentifier = productIdentifier
|
||||
@@ -191,7 +195,7 @@ extension InAppPurchaseReceipt: AutoEquatable {}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
extension InAppPurchaseReceipt: CustomStringConvertible {
|
||||
extension InAppPurchaseReceipt: CustomStringConvertible, CustomDebugStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
let formatter = StringFormatter()
|
||||
@@ -206,14 +210,18 @@ extension InAppPurchaseReceipt: CustomStringConvertible {
|
||||
("cancellationDate", formatter.format(self.cancellationDate)),
|
||||
("webOrderLineItemId", formatter.format(self.webOrderLineItemId))
|
||||
]
|
||||
return "ParsedInAppPurchaseReceipt(\n" + formatter.format(props) + "\n)"
|
||||
return "InAppPurchaseReceipt(\n" + formatter.format(props) + "\n)"
|
||||
}
|
||||
|
||||
public var debugDescription: String {
|
||||
return description
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Custom String Conversion
|
||||
|
||||
/// Private Helper for formatting the Receipts descriptions
|
||||
private struct StringFormatter {
|
||||
struct StringFormatter {
|
||||
|
||||
let fallback = "nil"
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
//
|
||||
// UnofficialReceipt.swift
|
||||
// Hekate
|
||||
//
|
||||
// Created by Hannes Oud on 15.12.17.
|
||||
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// This collects unofficial ASN1 values
|
||||
|
||||
public struct UnofficialReceipt {
|
||||
|
||||
public internal(set) var entries: [Entry]
|
||||
|
||||
public struct Entry {
|
||||
|
||||
public enum Value {
|
||||
case string(String)
|
||||
case date(Date)
|
||||
case bytes(Data)
|
||||
}
|
||||
|
||||
public internal(set) var attributeNumber: Int32
|
||||
public internal(set) var meaning: KnownUnofficialReceiptAttribute?
|
||||
public internal(set) var value: Value?
|
||||
}
|
||||
|
||||
public enum ProvisioningType: String {
|
||||
case production = "Production"
|
||||
case productionSandbox = "ProductionSandbox"
|
||||
case productionVPP = "ProductionVPP"
|
||||
}
|
||||
|
||||
/// Returns the provisioning type attribute's value, or nil if no entry was found at all.
|
||||
public var provisioningType: KnownOrUnknown<ProvisioningType>? {
|
||||
if let entry = entries.first(where: { $0.meaning == .provisioningType }), let value = entry.value, case .string(let stringValue) = value {
|
||||
return KnownOrUnknown(rawValue: stringValue)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public enum KnownUnofficialReceiptAttribute: Int32 {
|
||||
|
||||
case provisioningType = 0 // String, probably Provisioning-Type, Encountered Values: "Production", "ProductionSandbox", "ProductionVPP"
|
||||
case date1 = 8 // some date, same as receiptCreationDate possibly
|
||||
case ageRating = 10 // String, probably Age Description, example Value "4+"
|
||||
case date2 = 18 // some date, same as receiptCreationDate possibly
|
||||
case date3 = 22 // some date, same as receiptCreationDate possibly
|
||||
case clientName = 23 // String, probably VPP client name
|
||||
// - and of unknown type 14(L=3), 25(L=3), 11(L=4), 13(L=4), 1(L=6), 9(L=6), 16(L=6), 15(L=8), 7(L=66), 6(L=69 variable)
|
||||
|
||||
enum ParsingType {
|
||||
case string
|
||||
case date
|
||||
case data
|
||||
}
|
||||
|
||||
var parsingType: ParsingType {
|
||||
switch self {
|
||||
case .date1, .date2, .date3:
|
||||
return .string
|
||||
case .provisioningType, .ageRating, .clientName:
|
||||
return .string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
return "UnofficialReceipt([\n" + (entries.sorted(by: <).map { " \($0)" }.joined(separator: ",\n" )) + "\n])"
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt.Entry {
|
||||
|
||||
public static func < (lhs: UnofficialReceipt.Entry, rhs: UnofficialReceipt.Entry) -> Bool {
|
||||
if lhs.meaning != nil && rhs.meaning == nil {
|
||||
return true
|
||||
}
|
||||
if lhs.meaning == nil && rhs.meaning != nil {
|
||||
return false
|
||||
}
|
||||
return lhs.attributeNumber < rhs.attributeNumber
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt.Entry: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self.meaning {
|
||||
case .some(let attribute):
|
||||
return "\(attributeNumber)\t = \(attribute): \(formatValue(value))"
|
||||
case .none:
|
||||
return "\(attributeNumber)\t = unknown: \(formatValue(value))"
|
||||
}
|
||||
}
|
||||
|
||||
private func formatValue(_ value: Value?, fallback: String = "" ) -> String {
|
||||
guard let value = value else {
|
||||
return fallback
|
||||
}
|
||||
return value.description
|
||||
}
|
||||
}
|
||||
|
||||
extension UnofficialReceipt.Entry.Value: CustomStringConvertible {
|
||||
|
||||
public var description: String {
|
||||
switch self {
|
||||
|
||||
case .string(let value):
|
||||
return "\"\(value)\""
|
||||
case .date(let date):
|
||||
return LocalReceiptValidator.asn1DateFormatter.string(from: date)
|
||||
case .bytes(let bytes):
|
||||
if bytes.count == 2 && bytes.first == 12 && bytes.dropFirst().first == 0 {
|
||||
return "2 bytes (12, 0)"
|
||||
}
|
||||
if bytes.isEmpty {
|
||||
return "0 bytes"
|
||||
}
|
||||
if let utf8 = String(bytes: bytes, encoding: .utf8) {
|
||||
return "utf8: \"\(utf8)\""
|
||||
}
|
||||
return "len: \(bytes.count), b64: \(bytes.base64EncodedString())"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Generated using Sourcery 0.8.0 — https://github.com/krzysztofzablocki/Sourcery
|
||||
// Generated using Sourcery 0.9.0 — https://github.com/krzysztofzablocki/Sourcery
|
||||
// DO NOT EDIT
|
||||
|
||||
// swiftlint:disable file_length
|
||||
private func compareOptionals<T>(lhs: T?, rhs: T?, compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (lValue?, rValue?):
|
||||
@@ -23,7 +22,7 @@ private func compareArrays<T>(lhs: [T], rhs: [T], compare: (_ lhs: T, _ rhs: T)
|
||||
}
|
||||
|
||||
// MARK: - AutoEquatable for classes, protocols, structs
|
||||
// MARK: - ParsedInAppPurchaseReceipt AutoEquatable
|
||||
// MARK: - InAppPurchaseReceipt AutoEquatable
|
||||
extension InAppPurchaseReceipt: Equatable {}
|
||||
public func == (lhs: InAppPurchaseReceipt, rhs: InAppPurchaseReceipt) -> Bool {
|
||||
guard compareOptionals(lhs: lhs.quantity, rhs: rhs.quantity, compare: ==) else { return false }
|
||||
@@ -37,7 +36,7 @@ public func == (lhs: InAppPurchaseReceipt, rhs: InAppPurchaseReceipt) -> Bool {
|
||||
guard compareOptionals(lhs: lhs.webOrderLineItemId, rhs: rhs.webOrderLineItemId, compare: ==) else { return false }
|
||||
return true
|
||||
}
|
||||
// MARK: - ParsedReceipt AutoEquatable
|
||||
// MARK: - Receipt AutoEquatable
|
||||
extension Receipt: Equatable {}
|
||||
public func == (lhs: Receipt, rhs: Receipt) -> Bool {
|
||||
guard compareOptionals(lhs: lhs.bundleIdentifier, rhs: rhs.bundleIdentifier, compare: ==) else { return false }
|
||||
|
||||
@@ -1,28 +1,129 @@
|
||||
# Hekate
|
||||
|
||||
An iOS and macOS project intended for dealing with App Store receipts.
|
||||
An iOS and macOS project intended for dealing with App Store receipts, offering basic local retrieval, validation and parsing of receipt files.
|
||||
|
||||
[Hekate](https://en.wikipedia.org/wiki/Hecate) is the goddess of magic, crossroads, ghosts, and necromancy.
|
||||
|
||||
## Integration
|
||||
|
||||
Use carthage `github "IdeasOnCanvas/Hekate"`.
|
||||
|
||||
## Usage
|
||||
|
||||
### Just parsing a receipt
|
||||
|
||||
```swift
|
||||
let receiptValidator = LocalReceiptValidator()
|
||||
|
||||
let installedReceipt = receiptValidator.parseReceipt(origin: .installedInMainBundle)
|
||||
|
||||
let customReceipt = receiptValidator.parseReceipt(origin: .data(dataFromSomewhere))
|
||||
```
|
||||
|
||||
### Validating a receipt's signature and hash
|
||||
|
||||
```swift
|
||||
// Full validation of signature and hash based on installed receipt
|
||||
let result = receiptValidator.validateReceipt()
|
||||
|
||||
switch result {
|
||||
case .success(let receipt):
|
||||
print("receipt validated and parsed: \(receipt)")
|
||||
case .error(let validationError):
|
||||
print("not valid? \(validationError)")
|
||||
}
|
||||
```
|
||||
|
||||
### Customize validation dependencies or steps
|
||||
|
||||
Take `LocalReceiptValidator.Parameters.default` and customize it, then pass it to `validateReceipt(parameters:)`, like so:
|
||||
|
||||
```swift
|
||||
// Customizing validation parameters with configuration block, base on .default
|
||||
let parameters = LocalReceiptValidator.Parameters.default.with {
|
||||
$0.receiptOrigin = .data(myData)
|
||||
$0.shouldValidateSignaturePresence = false // skip signature presence validation
|
||||
$0.shouldValidateSignatureAuthenticity = false // skip signature authenticity validation
|
||||
$0.shouldValidateHash = false // skip hash validation
|
||||
$0.deviceIdentifier = .data(myCustomDeviceIdentifierData)
|
||||
$0.rootCertificateOrigin = .data(myAppleRootCertData)
|
||||
|
||||
// validate some string properties, this can also be done
|
||||
// independently with validateProperties(receipt:, validations:)
|
||||
// There are also shorthands for comparing with main bundle's
|
||||
// info.plist, e.g. bundleIdMatchingMainBundle and friends.
|
||||
// Note that appVersion meaning is platform specific.
|
||||
$0.propertyValidations = [
|
||||
.string(\.bundleIdentifier, expected: "my.bundle.identifier"),
|
||||
.string(\.appVersion, expected: "123"),
|
||||
.string(\.originalAppVersion, expected: "1")
|
||||
]
|
||||
}
|
||||
|
||||
let result = LocalReceiptValidator().validate(parameters: parameters)
|
||||
|
||||
switch result {
|
||||
case .success(let receipt):
|
||||
print("receipt validated and parsed: \(receipt)")
|
||||
case .error(let validationError):
|
||||
print("not valid? \(validationError)")
|
||||
}
|
||||
```
|
||||
|
||||
## Note
|
||||
|
||||
This framework currently doesn't
|
||||
|
||||
- deal with StoreKit at all.
|
||||
- the demo targets are pretty useless
|
||||
|
||||
The receipt file might not exist at all. See resources.
|
||||
|
||||
## How it Works
|
||||
|
||||
### Hekate Uses OpenSSL
|
||||
|
||||
OpenSSL is used for pkcs7 container parsing and signature validation, and then for parsing the ASN1 payload of the pkcs7, which contains the receipts attributes.
|
||||
|
||||
## Other Options
|
||||
|
||||
#### Alternatives to PKCS7 of OpenSSL
|
||||
|
||||
- `Security.framework` - `CMSDecoder` for PKCS7 interaction only available on macOS
|
||||
- `BoringSSL` instead of OpenSSL, Pod, only available on iOS (?)
|
||||
|
||||
#### Alternatives to ASN1 of OpenSSL
|
||||
|
||||
- [decoding-asn1-der-sequences-in-swift](http://nspasteboard.com/2016/10/23/decoding-asn1-der-sequences-in-swift/) implemented [here](https://gist.github.com/Jugale/2daaec0715d4f6d7347534d42bfa7110)
|
||||
- [Asn1Parser.swift](https://github.com/TakeScoop/SwiftyRSA/blob/03250be7319d8c54159234e5258ead395ea4de4c/SwiftyRSA/Asn1Parser.swift)
|
||||
|
||||
#### Validation Server to Server
|
||||
An app can send its receipt file to a backend from where Apples receipt API can be called. See Resources.
|
||||
|
||||
Advantages doing it locally:
|
||||
|
||||
- Works Offline
|
||||
- Validation mechanisms can be adjusted
|
||||
- Can be parsed without validation
|
||||
|
||||
## Resources
|
||||
|
||||
- [Apple guide](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Introduction.html)
|
||||
- [objc.io guide](https://www.objc.io/issues/17-security/receipt-validation/)
|
||||
- [Andrew Bancroft complete guide](https://www.andrewcbancroft.com/2017/08/01/local-receipt-validation-swift-start-finish/), or directly [ReceiptValidator.swift](https://github.com/andrewcbancroft/SwiftyLocalReceiptValidator/blob/master/ReceiptValidator.swift)
|
||||
- [Andrew Bancroft complete guide](https://www.andrewcbancroft.com/2017/08/01/local-receipt-validation-swift-start-finish/), or directly [ReceiptValidator.swift](https://github.com/andrewcbancroft/SwiftyLocalReceiptValidator/blob/master/ReceiptValidator.swift). This is what the Hekate implementation is loosely based on.
|
||||
- [OpenSSL-Universal Pod](https://github.com/krzyzanowskim/OpenSSL)
|
||||
- WWDC 2013 - 308 Using Receipts to Protect Your Digital Sales
|
||||
- WWDC 2014 - 305 Preventing Unauthorized Purchases with Receipts
|
||||
- WWDC 2016 - 702 Using Store Kit for In-App Purchases with Swift 3
|
||||
- WWDC 2017 - 304 What's New in Storekit
|
||||
- **WWDC 2017 - 304 What's New in Storekit**
|
||||
- **WWDC 2017 - 305 Advanced StoreKit**: Receipt checking and it's internals
|
||||
- [nsomar about Module Maps 1](http://nsomar.com/project-and-private-headers-in-a-swift-and-objective-c-framework/)
|
||||
- [nsomar about Module Maps 2](http://nsomar.com/modular-framework-creating-and-using-them/)
|
||||
|
||||
## Other Options
|
||||
|
||||
#### Alternatives to PKCS7 of OpenSSL
|
||||
- `Security.framework` - `CMSDecoder` for PKCS7 interaction only available on macOS
|
||||
- `BoringSSL` instead of OpenSSL, Pod, only available on iOS
|
||||
|
||||
#### Alternatives to ASN1 of OpenSSL
|
||||
- [decoding-asn1-der-sequences-in-swift](http://nspasteboard.com/2016/10/23/decoding-asn1-der-sequences-in-swift/) implemented [here](https://gist.github.com/Jugale/2daaec0715d4f6d7347534d42bfa7110)
|
||||
- [Asn1Parser.swift](https://github.com/TakeScoop/SwiftyRSA/blob/03250be7319d8c54159234e5258ead395ea4de4c/SwiftyRSA/Asn1Parser.swift)
|
||||
|
||||
## Updating OpenSSL
|
||||
|
||||
1. build or find prebuilt static libraries for iOS and macOS. They can for example be obtained from the [OpenSSL-Universal Pod](https://github.com/krzyzanowskim/OpenSSL).
|
||||
2. Replace the openssl related `.a` and `.h` files in the project
|
||||
3. When copying from the pod, make sure the .h files use direct includes like `#include "asn1.h"` instead of `#include "<OpenSSL/ans1.h>"` (use regex batch replace)
|
||||
4. Make sure the openssl related headers are in the *private* headers of the framework Hekate iOS and Hekate macOS targets respectively
|
||||
5. Make sure the openssl related headers are listed in the [Hekate.modulemap](Hekate/Hekate/Supporting%20Files/Hekate.modulemap) file
|
||||
|
||||
Reference in New Issue
Block a user