Compare commits

..

16 Commits

Author SHA1 Message Date
Hannes Oud 4bf518cd21 Merge pull request #34 from IdeasOnCanvas/core/feature/inofficialAttributes
Parse Unofficial Receipt attributes
2018-01-09 11:19:10 +01:00
Hannes Oud be46b25247 Deactivate autocorrect etc in iOS text view 2018-01-08 13:50:04 +01:00
Hannes Oud 2519fdfb23 Apply changes from code review 2018-01-08 13:47:48 +01:00
Hannes Oud 2f1ba126e7 Add KnownOrUnknown and use for provisioningType 2018-01-08 11:30:36 +01:00
Hannes Oud 8e9c94bb88 Improve attribute parsing and description 2017-12-15 19:38:28 +01:00
Hannes Oud ecee35a2e9 WIP Parse unofficial receipts attributes and log in mac app 2017-12-15 17:38:06 +01:00
Hannes Oud 9ef265073b Implement iOS demo app 2017-12-15 16:27:12 +01:00
Hannes Oud 3e69affe7e Implement mac demo app 2017-12-15 16:24:45 +01:00
Hannes Oud 3702ec3648 Fix Swiftlint warnings 2017-11-30 12:38:58 +01:00
Matthias Tretter eb78f3f493 Merge pull request #29 from IdeasOnCanvas/core/enhancement/improveInitialization
Rename allSteps to default, add a full initializer as well
2017-11-30 12:32:01 +01:00
Matthias Tretter 7477d83d4c Merge pull request #32 from IdeasOnCanvas/core/enhancement/removeUnusedFunctionality
Remove demo view model and view controller and receiptrefresher
2017-11-30 12:29:48 +01:00
Hannes Oud 5393889f39 Remove demo view model and view controller and receiptrefresher, as not usable for much 2017-11-30 12:19:17 +01:00
Hannes Oud be99440c4a Update readme 2017-11-27 10:15:09 +01:00
Hannes Oud bfa0c279cc Remove validateReceipt(configuration:) and add to tests as convenience instead 2017-11-24 16:33:50 +01:00
Hannes Oud 15144274a9 Rename allSteps to default, add a full initializer as well 2017-11-24 16:21:47 +01:00
Hannes Oud 3928822cbb Remove a unnecessary break 2017-11-24 16:13:15 +01:00
16 changed files with 590 additions and 278 deletions
@@ -1,33 +0,0 @@
//
// ReceiptRefresher+Convenience.swift
// Hekate
//
// Created by Hannes Oud on 19.09.17.
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
//
import Foundation
import Hekate
// MARK: - Convenience
extension ReceiptRefresher {
public func logIdentifierAndReceipt() {
if let deviceIdentifier = LocalReceiptValidator.Parameters.DeviceIdentifier.currentDevice.getData() {
print("Device Identifier (Base64):\n" + deviceIdentifier.base64EncodedString())
}
guard let data = self.receiptData else {
print("No receipt")
return
}
let base64 = data.base64EncodedString()
print("ReceiptData (Base64):\n" + base64)
}
public var receiptData: Data? {
guard let url = Bundle.main.appStoreReceiptURL else { return nil }
return try? Data(contentsOf: url)
}
}
@@ -1,57 +0,0 @@
//
// ReceiptRefresher.swift
// Hekate
//
// Created by Hannes Oud on 19.09.17.
// Copyright © 2017 IdeasOnCanvas GmbH. All rights reserved.
//
import StoreKit
@objc
public final class ReceiptRefresher: NSObject {
/// Refreshes the app store receipt using `SKReceiptRefreshRequest(receiptProperties: nil)`.
/// The instance of `ReceiptRefresher` on which this is called does not need to be kept around for this to complete.
/// - Parameters:
/// - queue: Queue on which the completion is called, defaults to main queue.
/// - completion: Called after the refresh has completed, gets passed the error on failure.
/// - Note: An iTunes Authentication alert will be presented by the system.
public func refreshReceipt(queue: DispatchQueue = .main, completion: ((NSError?) -> Void)?) {
let handler = RefreshCompletionHandler(queue: queue, completion: completion)
let request = SKReceiptRefreshRequest(receiptProperties: nil)
request.delegate = handler
request.start()
}
}
/// Encapsules SKRequestDelegate so it is not exposed at all.
///
/// - Note: Retains itself until the delegate is called.
private final class RefreshCompletionHandler: NSObject, SKRequestDelegate {
private let completion: ((NSError?) -> Void)?
private var retainedSelf: RefreshCompletionHandler?
private let queue: DispatchQueue
init(queue: DispatchQueue, completion: ((NSError?) -> Void)?) {
self.completion = completion
self.queue = queue
super.init()
self.retainedSelf = self
}
func requestDidFinish(_ request: SKRequest) {
self.queue.async {
self.completion?(nil)
self.retainedSelf = nil
}
}
func request(_ request: SKRequest, didFailWithError error: Error) {
self.queue.async {
self.completion?(error as NSError)
self.retainedSelf = nil
}
}
}
@@ -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,53 +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 refreshError: NSError?
var receiptIsValid: Bool {
guard let result = self.lastValidationResult else { return false }
switch result {
case .success:
return true
default:
return false
}
}
var descriptionText: String {
if let refreshError = refreshError {
return "Refresh Issue: " + refreshError.localizedDescription
}
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()
}
}
+78 -24
View File
@@ -7,38 +7,92 @@
//
import Hekate
import StoreKit
import UIKit
class ViewController: UIViewController {
private var receiptRefresher = ReceiptRefresher()
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()
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)
}
private func updateViewFromViewModel() {
textView.text = self.viewModel.descriptionText
receiptDataTextView.text = self.viewModel.receiptDataBase64Text
}
private func updateViewModel() {
viewModel.update()
}
@IBAction func refreshReceiptFromStoreTapped() {
receiptRefresher.refreshReceipt { error in
self.viewModel.refreshError = error
self.updateViewFromViewModel()
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
if self.autoPaste() {
return
} else {
self.inputTextView.becomeFirstResponder()
}
}
// MARK: - UITextViewDelegate
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>
+64 -1
View File
@@ -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)
}
}
@@ -74,7 +74,6 @@ class LocalReceiptValidationTests: XCTestCase {
XCTAssertEqual(receipt, expected)
}
func testMindNodeProMacReceiptParsing() {
guard let data = assertTestAsset(filename: "hannes_mac_mindnode_pro_receipt") else { return }
@@ -277,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")!)
}
@@ -286,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)\""
}
}
}
+12 -24
View File
@@ -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,6 +133,8 @@
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 */; };
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, ); }; };
@@ -223,10 +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 */; };
D1DD22421F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */; };
D1DD22431F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */; };
D1DD22451F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift */; };
D1DD22461F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.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 */; };
@@ -291,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>"; };
@@ -309,6 +307,7 @@
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>"; };
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>"; };
@@ -483,8 +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>"; };
D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiptRefresher.swift; sourceTree = "<group>"; };
D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ReceiptRefresher+Convenience.swift"; 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 */
@@ -854,7 +852,6 @@
D1D6F4FC1F5D696800E86FE1 /* Hekate */,
D1D6F4E51F5D691400E86FE1 /* Hekate Demo iOS */,
D19095821F6000A40095729B /* Hekate Demo macOS */,
D1DD22401F711E4400D111F4 /* Hekate Demo Shared */,
D19095951F6000A40095729B /* Hekate Tests macOS */,
D19095AA1F6001800095729B /* Hekate Tests iOS */,
D19095B61F6001C20095729B /* Hekate Tests Shared */,
@@ -880,7 +877,6 @@
children = (
D1D6F4E61F5D691400E86FE1 /* AppDelegate.swift */,
D1D6F4E81F5D691400E86FE1 /* ViewController.swift */,
D15358F01F62D43400F297D0 /* HekateDemoViewModel.swift */,
D1D6F4EA1F5D691400E86FE1 /* Main.storyboard */,
D1D6F4ED1F5D691400E86FE1 /* Assets.xcassets */,
D1D6F4EF1F5D691400E86FE1 /* LaunchScreen.storyboard */,
@@ -897,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 */,
@@ -951,19 +948,11 @@
children = (
D14FA72E1F6143C400545540 /* Date+Convenience.swift */,
D1D6F5491F5D9B1F00E86FE1 /* TestAssetLoading.swift */,
D1DFC5D920037B8400C7B99B /* KnownOrUnknown.swift */,
);
path = Tools;
sourceTree = "<group>";
};
D1DD22401F711E4400D111F4 /* Hekate Demo Shared */ = {
isa = PBXGroup;
children = (
D1DD22411F711E5E00D111F4 /* ReceiptRefresher.swift */,
D1DD22441F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift */,
);
path = "Hekate Demo Shared";
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@@ -1419,10 +1408,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
D1DD22431F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */,
D19095861F6000A40095729B /* ViewController.swift in Sources */,
D19095841F6000A40095729B /* AppDelegate.swift in Sources */,
D1DD22461F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1454,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 */,
@@ -1470,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 */,
@@ -1488,9 +1479,6 @@
files = (
D1D6F4E91F5D691400E86FE1 /* ViewController.swift in Sources */,
D1D6F4E71F5D691400E86FE1 /* AppDelegate.swift in Sources */,
D15358F11F62D43400F297D0 /* HekateDemoViewModel.swift in Sources */,
D1DD22421F711E5E00D111F4 /* ReceiptRefresher.swift in Sources */,
D1DD22451F7121FE00D111F4 /* ReceiptRefresher+Convenience.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -11,9 +11,11 @@ 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. By default, no `propertyValidations` are active.
/// 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
@@ -22,19 +24,31 @@ public extension LocalReceiptValidator {
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 {
var copy = self
block(&copy)
return copy
}
/// Use .allSteps to initialize
private init() {}
public static var allSteps: Parameters {
return Parameters()
}
}
}
@@ -128,7 +142,6 @@ extension LocalReceiptValidator.Parameters {
private class BundleToken {}
}
// MARK: - PropertyValidation
extension LocalReceiptValidator.Parameters {
+52 -22
View File
@@ -23,13 +23,8 @@ 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 {
@@ -48,7 +43,7 @@ public struct LocalReceiptValidator {
try self.checkSignatureAuthenticity(pkcs7: receiptContainer, appleRootCertificateData: appleRootCertificateData)
}
let receipt = try self.parseReceipt(pkcs7: receiptContainer)
let receipt = try self.parseReceipt(pkcs7: receiptContainer).receipt
try self.validateProperties(receipt: receipt, validations: parameters.propertyValidations)
@@ -73,13 +68,25 @@ public struct LocalReceiptValidator {
/// 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"
@@ -193,14 +200,21 @@ 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 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:
@@ -223,10 +237,10 @@ private extension LocalReceiptValidator {
receipt.originalAppVersion = value.unwrappedStringValue
case .expirationDate:
receipt.expirationDate = value.unwrappedDateValue
break
}
}
return receipt
return (receipt: receipt, unofficialReceipt: unofficialReceipt)
}
private func parseInAppPurchaseReceipt(pointer: UnsafePointer<UInt8>, length: Int) throws -> InAppPurchaseReceipt {
@@ -258,6 +272,28 @@ private extension LocalReceiptValidator {
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 {
var pointer: UnsafePointer<UInt8>? = initialPointer
let limit = initialPointer.advanced(by: length)
@@ -298,15 +334,9 @@ 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 Receipt.swift for details and a link to Apple reference
enum KnownInAppPurchaseAttribute: Int32 {
case quantity = 1701
+1 -1
View File
@@ -221,7 +221,7 @@ extension InAppPurchaseReceipt: CustomStringConvertible, CustomDebugStringConver
// MARK: - Custom String Conversion
/// Private Helper for formatting the Receipts descriptions
private struct StringFormatter {
struct StringFormatter {
let fallback = "nil"
+132
View File
@@ -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?):
+5 -3
View File
@@ -36,11 +36,11 @@ switch result {
### Customize validation dependencies or steps
Take `LocalReceiptValidator.Parameters.allSteps` and customize it, then pass it to `validateReceipt(parameters:)`, or use the shortcut which takes a configuration block, like so:
Take `LocalReceiptValidator.Parameters.default` and customize it, then pass it to `validateReceipt(parameters:)`, like so:
```swift
// Customizing validation parameters with configuration block, base on .allSteps
let result = receiptValidator.validateReceipt {
// 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
@@ -60,6 +60,8 @@ let result = receiptValidator.validateReceipt {
]
}
let result = LocalReceiptValidator().validate(parameters: parameters)
switch result {
case .success(let receipt):
print("receipt validated and parsed: \(receipt)")