Compare commits

...

52 Commits

Author SHA1 Message Date
Hannes Oud d57c87e69d Merge pull request #60 from ianbytchek/patch-1
Remove redundant print statement
2019-11-04 10:58:08 +01:00
Ian Bytchek c3eef703ab Remove redundant print statement
Fixes #59.
2019-11-02 18:10:05 +00:00
Michael Schwarz 538302d69c Merge pull request #58 from IdeasOnCanvas/enhancement/localMacAddressDemo
Add local MAC Address to macOS Demo App
2019-10-17 13:55:12 +02:00
Hannes Oud 9c87bd3f71 Improve naming 2019-10-15 11:53:48 +02:00
Hannes Oud 3d7fe8e045 Display local MAC Address in mac demo app 2019-10-15 11:42:10 +02:00
Hannes Oud f2fbd4227a Show local device identifier 2019-10-15 11:25:58 +02:00
Hannes Oud 86ee3389bb Touch storyboard 2019-10-15 11:03:58 +02:00
Michael Schwarz 29f22e4c7d Merge pull request #56 from IdeasOnCanvas/enhancement/dynamicData
Add dynamic data retriever as Receipt Origin
2019-08-27 17:03:25 +02:00
Hannes Oud 6f15dda97a Remove unnecacessary swiftlint disabling 2019-08-27 17:00:02 +02:00
Hannes Oud f7c3e26500 Add dynamic ReceiptOrigin, useful for tests 2019-08-27 16:59:29 +02:00
Hannes Oud 61794a63e3 Fix swift 5.0 badge in readme 2019-05-31 15:10:24 +02:00
Hannes Oud 137a767294 Merge pull request #53 from IdeasOnCanvas/enhancement/intParsingAndDescriptions
Fix quantity and webOrderLineItemID parsing, update for Xcode 10.2, improve convenience
2019-05-31 13:29:13 +02:00
Hannes Oud 11b8faf96c Fix swiftlint warnings 2019-05-31 13:28:24 +02:00
Hannes Oud ee521f9429 Switch to Int64 for ASN1 integer fields, as webOrderLineItemID requires this
# Conflicts:
#	AppReceiptValidator/AppReceiptValidator/OpenSSL/ASN1Helpers.swift
#	AppReceiptValidator/AppReceiptValidator/Receipt.swift
2019-05-31 13:18:57 +02:00
Hannes Oud bf7010545b Update readme for Swift 5.0 2019-05-31 10:32:28 +02:00
Hannes Oud 6a1b919093 Update travis file for Xcode 10.2 2019-05-31 10:29:51 +02:00
Hannes Oud e8146f7122 Disable an unnecessary swiftlint warning 2019-05-23 17:28:30 +02:00
Hannes Oud 2f54c5b042 Update macOS project for Xcode 10.2 / Swift 5 2019-05-23 17:26:16 +02:00
Hannes Oud f0ba73b45e Modernize Data initializer to fix deprectation warning 2019-05-23 17:25:56 +02:00
Hannes Oud 0ff100054d Add Stringy convenience initializers to Receipt and InAppPurchaseReceipt 2019-05-23 17:25:21 +02:00
Hannes Oud 15e3bb5471 Fix test-data after fixing quantity and webOrderLineItemId parsing 2019-05-23 16:47:03 +02:00
Hannes Oud c42f8d9ac9 Fix deprecation warnings using data.withUnsafeBytes 2019-05-23 14:32:07 +02:00
Hannes Oud 0efe3dd839 Update for Xcode 10.2, Swift 5 2019-05-23 14:31:47 +02:00
Hannes Oud cb4690217b Add quotes around strings and dates when creating description 2019-05-23 14:15:23 +02:00
Hannes Oud 8266af7edb Assert octet string when unwrapping and add comments 2019-05-23 14:15:06 +02:00
Hannes Oud 51d190ece0 Unwrap int values correctly (quantity, web order line item) 2019-05-23 14:14:45 +02:00
Hannes Oud 26385ee11b Merge pull request #52 from lightsprint09/extension-save
Support App extensions
2019-03-26 10:32:04 +01:00
Lukas Schmidt a96f16969d support app extensions to silence warning when use in a extension 2019-03-10 12:54:43 +01:00
Hannes Oud 1ba0b31095 Merge pull request #51 from IdeasOnCanvas/enhancement/deviceIdentifierFromMacAddress
Support DeviceIdentifier from MAC Address String
2018-11-22 16:11:06 +01:00
Hannes Oud 5121e901f5 Remove unneeded prefix 2018-11-22 16:10:44 +01:00
Hannes Oud d31ab05fe2 Update gif 2018-11-22 12:29:06 +01:00
Hannes Oud c4769bc42a Update mac demo app to support identifiers 2018-11-22 12:22:58 +01:00
Hannes Oud 6605544598 Add tests for DeviceIdentifier 2018-11-22 11:54:19 +01:00
Hannes Oud 3bb92db368 Add DeviceIdentifier initializer with macAddress string 2018-11-22 11:48:28 +01:00
Hannes Oud 97b0adff0a Make primary mac address function internal (testable) 2018-11-22 11:48:13 +01:00
Hannes Oud bf875864ea Fix typo 2018-11-22 11:47:50 +01:00
Hannes Oud c536866906 Merge pull request #50 from IdeasOnCanvas/chore/Xcode10
Update to Xcode 10 and Swift 4.2
2018-09-24 17:20:23 +02:00
Hannes Oud ede64d2e3b Update Swift version in Readme 2018-09-24 16:10:10 +02:00
Hannes Oud 112a5a9e4f Update travis.yml for Xcode 10 and iOS 12 2018-09-24 16:09:19 +02:00
Hannes Oud 7d8cb7bd06 Update schemes, parrallelize tests and make execution order random 2018-09-24 16:06:41 +02:00
Hannes Oud da06a099ce Make tests unhosted 2018-09-24 16:04:18 +02:00
Hannes Oud 95337f7544 Upgrade to Swift 4.2 and Xcode 10 2018-09-24 16:00:56 +02:00
Hannes Oud 7ee443a4af Merge pull request #49 from IdeasOnCanvas/chore/core/moveSwiftlintFromFrameworkTarget
Move Swiftlint run script phases to example project
2018-07-05 11:03:21 +02:00
Hannes Oud ae082be181 Remove placeholder comment from script phase 2018-07-05 11:01:48 +02:00
Hannes Oud bcb0ee2c27 Move Swiftlint run script phases to example project 2018-07-03 15:23:48 +02:00
Hannes Oud f8dae53736 Merge pull request #48 from IdeasOnCanvas/enhancement/modernizeForSwift41
Remove Sourcery and update README with new references
2018-04-24 11:43:57 +02:00
Hannes Oud 5c90e58864 Update README, add links to AppStoreReceiptChecker 2018-04-16 18:22:13 +02:00
Hannes Oud df47710990 Remove sourcery from README 2018-04-16 18:12:30 +02:00
Hannes Oud 9b03d4b850 Remove sourcery from .swiftlint.yml 2018-04-16 18:08:42 +02:00
Hannes Oud 06b43589d6 Use two char indentation in swiftlint run phase, add SRCROOT as input to prepare for new build system 2018-04-16 18:06:23 +02:00
Hannes Oud 1722f04928 Remove vertical whitespaces 2018-04-16 18:06:23 +02:00
Hannes Oud 16ef7de4bf Remove Sourcery 2018-04-16 17:42:45 +02:00
27 changed files with 473 additions and 341 deletions
+2 -2
View File
@@ -1,5 +1,5 @@
language: objective-c
osx_image: xcode9.2
osx_image: xcode10.2
env:
global:
@@ -7,7 +7,7 @@ env:
- LANG=en_US.UTF-8
- PROJECT=AppReceiptValidator/AppReceiptValidator.xcodeproj
matrix:
- DESTINATION="OS=11.2,name=iPhone X" SCHEME="AppReceiptValidator Demo iOS"
- DESTINATION="OS=12.2,name=iPhone X" SCHEME="AppReceiptValidator Demo iOS"
- DESTINATION="arch=x86_64" SCHEME="AppReceiptValidator Demo macOS"
script:
-7
View File
@@ -1,7 +0,0 @@
sources:
- AppReceiptValidator
- Sourcery/Protocols
templates:
- Sourcery/Templates
output:
Sourcery/Generated
+1 -1
View File
@@ -4,7 +4,7 @@ disabled_rules:
- nesting
- todo
excluded:
- Sourcery/Generated
- excluded_dir_example
file_length:
warning: 500
large_tuple:
@@ -13,7 +13,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
return true
}
}
@@ -9,9 +9,8 @@
import AppReceiptValidator
import UIKit
// MARK: - ViewController
/// Displays two textfields. One to paste a receipt into as base64 string, the other displaying the parsed receipt.
/// A device identifier for validation is not supported, have a look at the mac demo instead.
class ViewController: UIViewController, UITextViewDelegate {
@IBOutlet private var inputTextView: UITextView!
@@ -24,8 +23,8 @@ class ViewController: UIViewController, UITextViewDelegate {
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)
NotificationCenter.default.addObserver(self, selector: #selector(triggerAutoPaste), name: UIApplication.willEnterForegroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(triggerAutoPaste), name: UIPasteboard.changedNotification, object: nil)
}
override func viewDidAppear(_ animated: Bool) {
@@ -1,9 +1,8 @@
<?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">
<document type="com.apple.InterfaceBuilder3.Cocoa.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" initialViewController="B8D-0N-5wS">
<dependencies>
<deployment identifier="macosx"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="13771"/>
<capability name="Constraints with non-1.0 multipliers" minToolsVersion="5.1"/>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="15400"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -621,7 +620,7 @@
<menuItem title="Show Sidebar" keyEquivalent="s" id="kIP-vf-haE">
<modifierMask key="keyEquivalentModifierMask" control="YES" command="YES"/>
<connections>
<action selector="toggleSourceList:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
<action selector="toggleSidebar:" target="Ady-hI-5gd" id="iwa-gc-5KM"/>
</connections>
</menuItem>
<menuItem title="Enter Full Screen" keyEquivalent="f" id="4J7-dP-txa">
@@ -686,7 +685,7 @@
<scene sceneID="R2V-B0-nI4">
<objects>
<windowController id="B8D-0N-5wS" sceneMemberID="viewController">
<window key="window" title="AppReceiptValidator Receipt Parser" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" releasedWhenClosed="NO" showsToolbarButton="NO" visibleAtLaunch="NO" animationBehavior="default" id="IQv-IB-iLA">
<window key="window" title="AppReceiptValidator Receipt Parser" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="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"/>
@@ -708,82 +707,163 @@
<objects>
<viewController id="XfG-lQ-9wD" customClass="ViewController" customModule="AppReceiptValidator_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="631" height="270"/>
<rect key="frame" x="0.0" y="0.0" width="698" 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"/>
<rect key="frame" x="0.0" y="71" width="349" height="199"/>
<clipView key="contentView" drawsBackground="NO" id="yEL-yO-YvB">
<rect key="frame" x="1" y="1" width="347" height="197"/>
<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="DropAcceptingTextView" customModule="AppReceiptValidator_Demo_macOS" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="314" height="268"/>
<rect key="frame" x="0.0" y="0.0" width="347" height="197"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
<size key="minSize" width="314" height="268"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="347" height="197"/>
<size key="maxSize" width="629" height="10000000"/>
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</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">
<scroller key="horizontalScroller" hidden="YES" wantsLayer="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"/>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="hkR-e4-WtN">
<rect key="frame" x="332" y="1" width="16" height="197"/>
<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"/>
<rect key="frame" x="349" y="0.0" width="349" height="270"/>
<clipView key="contentView" drawsBackground="NO" id="YwZ-F9-Cvh">
<rect key="frame" x="1" y="1" width="347" 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="AppReceiptValidator_Demo_macOS" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="313" height="268"/>
<rect key="frame" x="0.0" y="0.0" width="347" 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"/>
<color key="textColor" name="textColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
<size key="minSize" width="347" height="268"/>
<size key="maxSize" width="463" height="10000000"/>
<color key="insertionPointColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<color key="insertionPointColor" name="textColor" catalog="System" colorSpace="catalog"/>
</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">
<scroller key="horizontalScroller" hidden="YES" wantsLayer="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"/>
<scroller key="verticalScroller" wantsLayer="YES" verticalHuggingPriority="750" doubleValue="1" horizontal="NO" id="uk9-Sg-RUp">
<rect key="frame" x="332" y="1" width="16" height="268"/>
<autoresizingMask key="autoresizingMask"/>
</scroller>
</scrollView>
<stackView distribution="fill" orientation="vertical" alignment="leading" spacing="0.0" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="X6E-h2-FTp">
<rect key="frame" x="0.0" y="0.0" width="349" height="71"/>
<subviews>
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yvX-By-nNw">
<rect key="frame" x="0.0" y="50" width="316" height="21"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="Optional DeviceIdentifier for Validation (MAC Address, UUID, Base64)" drawsBackground="YES" id="Z2r-sB-vIS">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<action selector="identifierDidChange:" target="XfG-lQ-9wD" id="ccM-mH-NPQ"/>
</connections>
</textField>
<customView verticalHuggingPriority="251" verticalCompressionResistancePriority="749" translatesAutoresizingMaskIntoConstraints="NO" id="MIY-2Q-BBh">
<rect key="frame" x="0.0" y="0.0" width="276" height="50"/>
<subviews>
<stackView distribution="fill" orientation="horizontal" alignment="top" spacing="14" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="3Fk-0l-MkU">
<rect key="frame" x="5" y="2" width="264" height="48"/>
<subviews>
<textField horizontalHuggingPriority="252" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="3HO-XF-hNE">
<rect key="frame" x="-2" y="32" width="122" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Local MAC Address" id="cII-wV-fEg">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="MYU-Qf-vgG">
<rect key="frame" x="130" y="32" width="37" height="16"/>
<textFieldCell key="cell" lineBreakMode="clipping" selectable="YES" title="Label" id="toH-92-xds">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="7M8-eM-sps">
<rect key="frame" x="173" y="20" width="97" height="32"/>
<buttonCell key="cell" type="push" title="Reload" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nhJ-Nu-6Mt">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
</buttonCell>
<connections>
<action selector="determineDeviceIdentifier:" target="XfG-lQ-9wD" id="0Iu-Ou-tt2"/>
</connections>
</button>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="3Fk-0l-MkU" firstAttribute="top" secondItem="MIY-2Q-BBh" secondAttribute="top" id="G9h-eg-R7c"/>
<constraint firstItem="3Fk-0l-MkU" firstAttribute="leading" secondItem="MIY-2Q-BBh" secondAttribute="leading" constant="5" id="XhE-X8-Yll"/>
<constraint firstAttribute="bottom" secondItem="3Fk-0l-MkU" secondAttribute="bottom" constant="2" id="eS8-I6-F8I"/>
<constraint firstAttribute="trailing" secondItem="3Fk-0l-MkU" secondAttribute="trailing" constant="7" id="u5d-kZ-Hiq"/>
</constraints>
</customView>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
</subviews>
<constraints>
<constraint firstItem="6mt-ay-mAL" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="CdY-lv-wee"/>
<constraint firstItem="X6E-h2-FTp" firstAttribute="top" secondItem="ynK-dw-fFc" secondAttribute="bottom" id="0st-m4-dJZ"/>
<constraint firstItem="ynK-dw-fFc" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="1Pf-3A-rBH"/>
<constraint firstAttribute="bottom" secondItem="X6E-h2-FTp" secondAttribute="bottom" id="1kz-OO-S1i"/>
<constraint firstItem="6mt-ay-mAL" firstAttribute="leading" secondItem="X6E-h2-FTp" secondAttribute="trailing" id="9Vb-5X-3ef"/>
<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 firstItem="6mt-ay-mAL" firstAttribute="top" secondItem="m2S-Jp-Qdl" secondAttribute="top" id="clC-xc-JDh"/>
<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"/>
<constraint firstItem="X6E-h2-FTp" firstAttribute="leading" secondItem="m2S-Jp-Qdl" secondAttribute="leading" id="ysr-U6-n9c"/>
</constraints>
</view>
<connections>
<outlet property="dropReceivingView" destination="0xW-mT-lME" id="cuR-H1-BKN"/>
<outlet property="identifierTextField" destination="yvX-By-nNw" id="pW5-lJ-8Cy"/>
<outlet property="inputTextView" destination="0xW-mT-lME" id="8Y7-yb-63r"/>
<outlet property="localDeviceIdentifierLabel" destination="MYU-Qf-vgG" id="cqz-uf-16l"/>
<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="76" y="726"/>
<point key="canvasLocation" x="75" y="726"/>
</scene>
</scenes>
</document>
@@ -43,7 +43,7 @@ final class DropAcceptingTextView: NSTextView {
fileprivate extension NSDraggingInfo {
var fileURLs: [URL] {
let asStrings = self.draggingPasteboard().propertyList(forType: makeFileNameType()) as? [String] ?? []
let asStrings = self.draggingPasteboard.propertyList(forType: makeFileNameType()) as? [String] ?? []
return asStrings.map { URL(fileURLWithPath: $0) }
}
}
@@ -9,14 +9,16 @@
import AppReceiptValidator
import Cocoa
/// Displays two textfields. One to paste a receipt into as base64 string, the other displaying the parsed receipt.
/// A device identifier can be added in a third field, which is then used to validate the receipt.
class ViewController: NSViewController, NSTextViewDelegate, NSTextFieldDelegate {
// MARK: - ViewController
class ViewController: NSViewController, NSTextViewDelegate {
private var textFieldObserver: Any?
@IBOutlet private var inputTextView: NSTextView!
@IBOutlet private var identifierTextField: NSTextField!
@IBOutlet private var outputTextView: NSTextView!
@IBOutlet private var dropReceivingView: DropAcceptingTextView!
@IBOutlet private var localDeviceIdentifierLabel: NSTextField!
// MARK: - Lifecycle
@@ -28,12 +30,25 @@ class ViewController: NSViewController, NSTextViewDelegate {
self.dropReceivingView.handleDroppedFile = { [unowned self] url in
self.update(url: url)
}
self.textFieldObserver = NotificationCenter.default.addObserver(forName: NSTextField.textDidChangeNotification, object: self.identifierTextField, queue: .main) { [weak self] _ in
guard let self = self else { return }
self.identifierDidChange(self.identifierTextField)
}
self.renderLocalDeviceIdentifierText()
}
// MARK: - NSTextViewDelegate
func textDidChange(_ notification: Notification) {
let string = inputTextView.string
let string = self.inputTextView.string
self.update(base64String: string)
}
// MARK: - Identifier Textfield
@IBAction func identifierDidChange(_ sender: NSTextField) {
let string = self.inputTextView.string
self.update(base64String: string)
}
@@ -42,6 +57,10 @@ class ViewController: NSViewController, NSTextViewDelegate {
func paste(_ sender: Any) {
self.inputTextView.paste(sender)
}
@IBAction func determineDeviceIdentifier(_ sender: Any) {
self.renderLocalDeviceIdentifierText()
}
}
// MARK: - Private
@@ -57,7 +76,8 @@ private extension ViewController {
}
do {
let result = try AppReceiptValidator().parseUnofficialReceipt(origin: .data(data))
self.render(string: "\(result.receipt)\n\(result.unofficialReceipt)")
let validationResult = self.validateReceiptIfNecessary(data: data, macAddress: self.identifierTextField.stringValue) ?? "<Receipt not Validated, No Identifier provided, Supported: UUID, base64, MAC-Address>"
self.render(string: "\(validationResult)\n\n✅ Receipt Parsed\n\(result.receipt)\n\(result.unofficialReceipt)")
} catch {
self.render(string: "\(error)")
}
@@ -79,9 +99,41 @@ private extension ViewController {
}
}
func validateReceiptIfNecessary(data: Data, macAddress: String?) -> String? {
guard let macAddress = macAddress, macAddress.isEmpty == false else { return nil }
let sanitized = macAddress.trimmingCharacters(in: .whitespacesAndNewlines)
let deviceIdentifier = AppReceiptValidator.Parameters.DeviceIdentifier(macAddress: sanitized) ??
(UUID(uuidString: sanitized).flatMap { AppReceiptValidator.Parameters.DeviceIdentifier(uuid: $0) }) ??
AppReceiptValidator.Parameters.DeviceIdentifier(base64Encoded: sanitized)
guard let identifier = deviceIdentifier else { return "<Receipt not Validated\nIdentifier not parseable>" }
let parameters = AppReceiptValidator.Parameters(receiptOrigin: .data(data), shouldValidateSignaturePresence: true, signatureValidation: .shouldValidate(rootCertificateOrigin: .cerFileBundledWithAppReceiptValidator), shouldValidateHash: true, deviceIdentifier: identifier, propertyValidations: [])
let result = AppReceiptValidator().validateReceipt(parameters: parameters)
switch result {
case .success:
return "✅ Receipt Validated\nSignature and Hash Check successful"
case .error(let error, _, _):
return "❌ Receipt Invalid\n\(error)"
}
}
func localDeviceIdentifierString() -> String {
guard let device = AppReceiptValidator.Parameters.DeviceIdentifier.getPrimaryNetworkMACAddress() else { return "DeviceIdentifier could not be determined" }
return "\(device.addressString) (HEX), \(device.data.base64EncodedString()) (B64)"
}
func render(string: String) {
self.outputTextView.string = string
}
func renderLocalDeviceIdentifierText() {
NSLog("Local MAC Address: " + localDeviceIdentifierString())
self.localDeviceIdentifierLabel.attributedStringValue =
NSAttributedString(string: localDeviceIdentifierString())
}
}
// MARK: - TextView
@@ -22,7 +22,6 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
} catch {
XCTFail("Unexpectedly failed parsing a receipt \(error)")
}
}
func testNonMindNodeReceiptParsingWithMultipleInAppPurchases() {
@@ -55,7 +54,7 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
expirationDate: nil,
inAppPurchaseReceipts: [
InAppPurchaseReceipt(
quantity: nil,
quantity: 1,
productIdentifier: "consumable",
transactionIdentifier: "1000000166865231",
originalTransactionIdentifier: "1000000166865231",
@@ -63,10 +62,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-07T20:37:55Z"),
subscriptionExpirationDate: nil,
cancellationDate: nil,
webOrderLineItemId: nil
webOrderLineItemId: 0
),
InAppPurchaseReceipt(
quantity: nil,
quantity: 1,
productIdentifier: "monthly",
transactionIdentifier: "1000000166965150",
originalTransactionIdentifier: "1000000166965150",
@@ -74,10 +73,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:49:33Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T06:54:32Z"),
cancellationDate: nil,
webOrderLineItemId: nil
webOrderLineItemId: 1000000030274153
),
InAppPurchaseReceipt( // restores
quantity: nil,
InAppPurchaseReceipt(
quantity: 1,
productIdentifier: "monthly",
transactionIdentifier: "1000000166965327",
originalTransactionIdentifier: "1000000166965150",
@@ -85,10 +84,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:53:18Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T06:59:32Z"),
cancellationDate: nil,
webOrderLineItemId: nil
webOrderLineItemId: 1000000030274154
),
InAppPurchaseReceipt(
quantity: nil,
quantity: 1,
productIdentifier: "monthly",
transactionIdentifier: "1000000166965895",
originalTransactionIdentifier: "1000000166965150",
@@ -96,10 +95,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T06:57:34Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:04:32Z"),
cancellationDate: nil,
webOrderLineItemId: nil
webOrderLineItemId: 1000000030274165
),
InAppPurchaseReceipt(
quantity: nil,
quantity: 1,
productIdentifier: "monthly",
transactionIdentifier: "1000000166967152",
originalTransactionIdentifier: "1000000166965150",
@@ -107,10 +106,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:02:33Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:09:32Z"),
cancellationDate: nil,
webOrderLineItemId: nil
webOrderLineItemId: 1000000030274192
),
InAppPurchaseReceipt(
quantity: nil,
quantity: 1,
productIdentifier: "monthly",
transactionIdentifier: "1000000166967484",
originalTransactionIdentifier: "1000000166965150",
@@ -118,10 +117,10 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:08:30Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:14:32Z"),
cancellationDate: nil,
webOrderLineItemId: nil
webOrderLineItemId: 1000000030274219
),
InAppPurchaseReceipt(
quantity: nil,
quantity: 1,
productIdentifier: "monthly",
transactionIdentifier: "1000000166967782",
originalTransactionIdentifier: "1000000166965150",
@@ -129,8 +128,9 @@ class AppReceiptValidationInAppPurchaseTests: XCTestCase {
originalPurchaseDate: Date.demoDate(string: "2015-08-10T07:12:34Z"),
subscriptionExpirationDate: Date.demoDate(string: "2015-08-10T07:19:32Z"),
cancellationDate: nil,
webOrderLineItemId: nil
webOrderLineItemId: 1000000030274249
)
])
]
)
}
}
@@ -0,0 +1,56 @@
//
// DeviceIdentifierTests.swift
// AppReceiptValidator
//
// Created by Hannes Oud on 22.11.18.
// Copyright © 2018 IdeasOnCanvas GmbH. All rights reserved.
//
@testable import AppReceiptValidator
import Foundation
import XCTest
final class DeviceIdentifierTests: XCTestCase {
func testBase64Initializer() {
let deviceIdentifier = AppReceiptValidator.Parameters.DeviceIdentifier(base64Encoded: "bEAItZRe")
XCTAssertNotNil(deviceIdentifier)
}
func testMacAddressInitializer() {
XCTAssertNotNil(AppReceiptValidator.Parameters.DeviceIdentifier(macAddress: "00:0d:3f:cd:02:5f"))
}
func testMacAddressInitializerOtherSeparator() {
XCTAssertNotNil(AppReceiptValidator.Parameters.DeviceIdentifier(macAddress: "00-0d-3f-cd-02-5f", separator: "-"))
}
func testUUIDInitializer() {
XCTAssertNotNil(AppReceiptValidator.Parameters.DeviceIdentifier(uuid: UUID()))
}
func testCurrent() {
let deviceIdentifierData = AppReceiptValidator.Parameters.DeviceIdentifier.currentDevice.getData()
XCTAssertNotNil(deviceIdentifierData)
}
#if os(macOS)
func testMacAddressRetrieval() {
guard let (data, string) = AppReceiptValidator.Parameters.DeviceIdentifier.getPrimaryNetworkMACAddress() else {
XCTFail("Failed to get device mac address")
return
}
guard let deviceIdentifierData = AppReceiptValidator.Parameters.DeviceIdentifier.currentDevice.getData() else {
XCTFail("Failed to get device mac address")
return
}
XCTAssertEqual(data, deviceIdentifierData)
guard let deviceIdentifierFromString = AppReceiptValidator.Parameters.DeviceIdentifier(macAddress: string) else {
XCTFail("Failed to get device identifier from mac address string")
return
}
XCTAssertEqual(deviceIdentifierFromString.getData(), deviceIdentifierData)
}
#endif
}
@@ -47,8 +47,8 @@ extension KnownOrUnknown: RawRepresentable {
extension KnownOrUnknown: Hashable {
public var hashValue: Int {
return self.rawValue.hashValue
public func hash(into hasher: inout Hasher) {
hasher.combine(self.rawValue.hashValue)
}
public static func == (lhs: KnownOrUnknown<Known>, rhs: KnownOrUnknown<Known>) -> Bool {
@@ -7,13 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
D114544621A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */; };
D114544721A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */; };
D1239FFF1F6A7B5000D0421E /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */; };
D123A0001F6A7CCF00D0421E /* AppleIncRootCertificate.cer in Resources */ = {isa = PBXBuildFile; fileRef = D19095C41F601DEA0095729B /* AppleIncRootCertificate.cer */; };
D13E5B7D20331B9B001880F0 /* DropAcceptingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D13E5B7C20331B9B001880F0 /* DropAcceptingTextView.swift */; };
D14FA7261F61350F00545540 /* AutoEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14FA71F1F6134CF00545540 /* AutoEquatable.swift */; };
D14FA7271F61351000545540 /* AutoEquatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14FA71F1F6134CF00545540 /* AutoEquatable.swift */; };
D14FA7291F61351400545540 /* AutoEquatable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14FA71E1F6134CF00545540 /* AutoEquatable.generated.swift */; };
D14FA72A1F61351500545540 /* AutoEquatable.generated.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14FA71E1F6134CF00545540 /* AutoEquatable.generated.swift */; };
D14FA7321F61476700545540 /* mac_mindnode_rebought_receipt in Resources */ = {isa = PBXBuildFile; fileRef = D14FA7311F61472400545540 /* mac_mindnode_rebought_receipt */; };
D14FA7331F61476800545540 /* mac_mindnode_rebought_receipt in Resources */ = {isa = PBXBuildFile; fileRef = D14FA7311F61472400545540 /* mac_mindnode_rebought_receipt */; };
D14FA7381F6181C700545540 /* OpenSSLWrappers.swift in Sources */ = {isa = PBXBuildFile; fileRef = D14FA7371F6181C700545540 /* OpenSSLWrappers.swift */; };
@@ -279,10 +277,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceIdentifierTests.swift; sourceTree = "<group>"; };
D13E5B7C20331B9B001880F0 /* DropAcceptingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DropAcceptingTextView.swift; sourceTree = "<group>"; };
D14FA71E1F6134CF00545540 /* AutoEquatable.generated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoEquatable.generated.swift; sourceTree = "<group>"; };
D14FA71F1F6134CF00545540 /* AutoEquatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoEquatable.swift; sourceTree = "<group>"; };
D14FA7221F6134CF00545540 /* AutoEquatable.stencil */ = {isa = PBXFileReference; lastKnownFileType = text; path = AutoEquatable.stencil; sourceTree = "<group>"; };
D14FA72E1F6143C400545540 /* Date+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Date+Convenience.swift"; sourceTree = "<group>"; };
D14FA7311F61472400545540 /* mac_mindnode_rebought_receipt */ = {isa = PBXFileReference; lastKnownFileType = file; path = mac_mindnode_rebought_receipt; sourceTree = "<group>"; };
D14FA7371F6181C700545540 /* OpenSSLWrappers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenSSLWrappers.swift; sourceTree = "<group>"; };
@@ -542,40 +538,6 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
D14FA71B1F6134CF00545540 /* Sourcery */ = {
isa = PBXGroup;
children = (
D14FA7231F6134EC00545540 /* Protocols */,
D14FA71D1F6134CF00545540 /* Generated */,
D14FA7201F6134CF00545540 /* Templates */,
);
path = Sourcery;
sourceTree = "<group>";
};
D14FA71D1F6134CF00545540 /* Generated */ = {
isa = PBXGroup;
children = (
D14FA71E1F6134CF00545540 /* AutoEquatable.generated.swift */,
);
path = Generated;
sourceTree = "<group>";
};
D14FA7201F6134CF00545540 /* Templates */ = {
isa = PBXGroup;
children = (
D14FA7221F6134CF00545540 /* AutoEquatable.stencil */,
);
path = Templates;
sourceTree = "<group>";
};
D14FA7231F6134EC00545540 /* Protocols */ = {
isa = PBXGroup;
children = (
D14FA71F1F6134CF00545540 /* AutoEquatable.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
D14FA7341F614A9C00545540 /* OpenSSL */ = {
isa = PBXGroup;
children = (
@@ -644,6 +606,7 @@
D1D6F5411F5D8A3800E86FE1 /* AppReceiptValidationTests.swift */,
D1AA845A1F6ABB31007F2558 /* AppReceiptPropertyValidationTests.swift */,
D150A0ED1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift */,
D114544521A6BDE6001BEC61 /* DeviceIdentifierTests.swift */,
D1D6F5481F5D9B1100E86FE1 /* Tools */,
D1D6F5431F5D8DBC00E86FE1 /* Test Assets */,
);
@@ -851,7 +814,6 @@
D1D6F4921F5D67E600E86FE1 = {
isa = PBXGroup;
children = (
D14FA71B1F6134CF00545540 /* Sourcery */,
D1D6F4FC1F5D696800E86FE1 /* AppReceiptValidator */,
D1D6F4E51F5D691400E86FE1 /* AppReceiptValidator Demo iOS */,
D19095821F6000A40095729B /* AppReceiptValidator Demo macOS */,
@@ -1135,6 +1097,7 @@
D190957D1F6000A40095729B /* Sources */,
D190957E1F6000A40095729B /* Frameworks */,
D190957F1F6000A40095729B /* Resources */,
D1DEE48D20EBAEE800F95036 /* Swiftlint */,
);
buildRules = (
);
@@ -1190,7 +1153,6 @@
D1D6F4B11F5D684C00E86FE1 /* Frameworks */,
D1D6F4B21F5D684C00E86FE1 /* Headers */,
D1D6F4B31F5D684C00E86FE1 /* Resources */,
D1D6F52D1F5D872200E86FE1 /* Swiftlint */,
);
buildRules = (
);
@@ -1209,7 +1171,6 @@
D1D6F4BE1F5D687400E86FE1 /* Frameworks */,
D1D6F4BF1F5D687400E86FE1 /* Headers */,
D1D6F4C01F5D687400E86FE1 /* Resources */,
D1D6F52E1F5D872B00E86FE1 /* Swiftlint */,
);
buildRules = (
);
@@ -1228,6 +1189,7 @@
D1D6F4E11F5D691400E86FE1 /* Frameworks */,
D1D6F4E21F5D691400E86FE1 /* Resources */,
D1D6F5071F5D696800E86FE1 /* Embed Frameworks */,
D1DEE48C20EBAEC800F95036 /* Swiftlint */,
);
buildRules = (
);
@@ -1251,6 +1213,7 @@
TargetAttributes = {
D19095801F6000A40095729B = {
CreatedOnToolsVersion = 9.0;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.Sandbox = {
@@ -1260,27 +1223,27 @@
};
D19095911F6000A40095729B = {
CreatedOnToolsVersion = 9.0;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
TestTargetID = D19095801F6000A40095729B;
};
D19095A81F6001800095729B = {
CreatedOnToolsVersion = 9.0;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
TestTargetID = D1D6F4E31F5D691400E86FE1;
};
D1D6F4B41F5D684C00E86FE1 = {
CreatedOnToolsVersion = 9.0;
LastSwiftMigration = 0900;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
};
D1D6F4C11F5D687400E86FE1 = {
CreatedOnToolsVersion = 9.0;
LastSwiftMigration = 0900;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
};
D1D6F4E31F5D691400E86FE1 = {
CreatedOnToolsVersion = 9.0;
LastSwiftMigration = 0900;
LastSwiftMigration = 1020;
ProvisioningStyle = Automatic;
};
};
@@ -1381,33 +1344,43 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
D1D6F52D1F5D872200E86FE1 /* Swiftlint */ = {
D1DEE48C20EBAEC800F95036 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)",
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
D1D6F52E1F5D872B00E86FE1 /* Swiftlint */ = {
D1DEE48D20EBAEE800F95036 /* Swiftlint */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"$(SRCROOT)",
);
name = Swiftlint;
outputFileListPaths = (
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "if which swiftlint >/dev/null; then\n swiftlint\nelse\n echo \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi";
shellScript = "if which swiftlint >/dev/null; then\nswiftlint\nelse\necho \"warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint\"\nfi\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -1430,6 +1403,7 @@
D19095CD1F601E960095729B /* AppReceiptValidationTests.swift in Sources */,
D1AA845D1F6ABB59007F2558 /* AppReceiptPropertyValidationTests.swift in Sources */,
D150A0EF1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift in Sources */,
D114544721A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */,
D150A0F01F67E0990026ED04 /* Date+Convenience.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1442,6 +1416,7 @@
D19095CE1F601E980095729B /* AppReceiptValidationTests.swift in Sources */,
D1AA845C1F6ABB59007F2558 /* AppReceiptPropertyValidationTests.swift in Sources */,
D150A0EE1F669A880026ED04 /* AppReceiptValidationInAppPurchaseTests.swift in Sources */,
D114544621A6BDE6001BEC61 /* DeviceIdentifierTests.swift in Sources */,
D150A0F11F67E0990026ED04 /* Date+Convenience.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1454,8 +1429,6 @@
D1D6F53F1F5D89D000E86FE1 /* AppReceiptValidator.swift in Sources */,
D1FE34401F604F540029576B /* AppReceiptValidator+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 */,
@@ -1473,11 +1446,9 @@
D1FE34411F604F540029576B /* AppReceiptValidator+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 */,
D19095BD1F6004D10095729B /* pkcs7_union_accessors.c in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -1558,7 +1529,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -1576,7 +1547,7 @@
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SDKROOT = macosx;
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
};
name = Release;
};
@@ -1584,7 +1555,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
@@ -1595,8 +1565,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-Demo-macOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_VERSION = 4.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppReceiptValidator Demo macOS.app/Contents/MacOS/AppReceiptValidator Demo macOS";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
@@ -1604,7 +1573,6 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
@@ -1615,8 +1583,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-Demo-macOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = macosx;
SWIFT_VERSION = 4.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppReceiptValidator Demo macOS.app/Contents/MacOS/AppReceiptValidator Demo macOS";
SWIFT_VERSION = 5.0;
};
name = Release;
};
@@ -1629,9 +1596,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-iOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppReceiptValidator Demo iOS.app/AppReceiptValidator Demo iOS";
};
name = Debug;
};
@@ -1644,9 +1610,8 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator-iOSTests";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AppReceiptValidator Demo iOS.app/AppReceiptValidator Demo iOS";
};
name = Release;
};
@@ -1768,6 +1733,7 @@
D1D6F4BB1F5D684C00E86FE1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -1789,6 +1755,7 @@
PRODUCT_NAME = AppReceiptValidator;
SKIP_INSTALL = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -1798,6 +1765,7 @@
D1D6F4BC1F5D684C00E86FE1 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
@@ -1818,6 +1786,7 @@
PRODUCT_BUNDLE_IDENTIFIER = com.ideasoncanvas.AppReceiptValidator;
PRODUCT_NAME = AppReceiptValidator;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
@@ -1827,6 +1796,7 @@
D1D6F4C81F5D687400E86FE1 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
@@ -1849,6 +1819,7 @@
PRODUCT_NAME = AppReceiptValidator;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
@@ -1857,6 +1828,7 @@
D1D6F4C91F5D687400E86FE1 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
APPLICATION_EXTENSION_API_ONLY = YES;
CODE_SIGN_IDENTITY = "";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
@@ -1879,6 +1851,7 @@
PRODUCT_NAME = AppReceiptValidator;
SDKROOT = macosx;
SKIP_INSTALL = YES;
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
@@ -1896,7 +1869,7 @@
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator.Demo-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
@@ -1912,7 +1885,7 @@
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
PRODUCT_BUNDLE_IDENTIFIER = "com.ideasoncanvas.AppReceiptValidator.Demo-iOS";
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 4.0;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
@@ -43,7 +43,9 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D19095A81F6001800095729B"
@@ -43,7 +43,9 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D19095911F6000A40095729B"
@@ -28,7 +28,28 @@
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D19095A81F6001800095729B"
BuildableName = "AppReceiptValidator Tests iOS.xctest"
BlueprintName = "AppReceiptValidator Tests iOS"
ReferencedContainer = "container:AppReceiptValidator.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1D6F4B41F5D684C00E86FE1"
BuildableName = "AppReceiptValidator.framework"
BlueprintName = "AppReceiptValidator iOS"
ReferencedContainer = "container:AppReceiptValidator.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
</TestAction>
@@ -20,20 +20,6 @@
ReferencedContainer = "container:AppReceiptValidator.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1D6F5321F5D894900E86FE1"
BuildableName = "AppReceiptValidator_macOSTests.xctest"
BlueprintName = "AppReceiptValidator_macOSTests"
ReferencedContainer = "container:AppReceiptValidator.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
@@ -43,12 +29,14 @@
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "D1D6F5321F5D894900E86FE1"
BuildableName = "AppReceiptValidator_macOSTests.xctest"
BlueprintName = "AppReceiptValidator_macOSTests"
BlueprintIdentifier = "D19095911F6000A40095729B"
BuildableName = "AppReceiptValidator Tests macOS.xctest"
BlueprintName = "AppReceiptValidator Tests macOS"
ReferencedContainer = "container:AppReceiptValidator.xcodeproj">
</BuildableReference>
</TestableReference>
@@ -12,7 +12,7 @@ public extension AppReceiptValidator {
/// Describes how to validate a receipt, and how/where to obtain the dependencies (receipt, deviceIdentifier, apple root certificate)
/// Use .default to initialize the standard parameters. By default, no `propertyValidations` are active.
public struct Parameters {
struct Parameters {
// MARK: - Properties
@@ -62,11 +62,14 @@ extension AppReceiptValidator.Parameters {
case installedInMainBundle
case data(Data)
case dynamic(() -> Data?)
public func loadData() -> Data? {
switch self {
case .data(let data):
return data
case .dynamic(let maker):
return maker()
case .installedInMainBundle:
guard let receiptUrl = Bundle.main.appStoreReceiptURL else { return nil }
guard (try? receiptUrl.checkResourceIsReachable()) ?? false else { return nil }
@@ -83,9 +86,9 @@ public extension AppReceiptValidator.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
/// - currentDevice: Obtains it from the system location: MAC Address on macOS, deviceIdentifierForVendor on iOS
/// - data: Specific Data to use
public enum DeviceIdentifier {
enum DeviceIdentifier {
case currentDevice
case data(Data)
@@ -99,6 +102,20 @@ public extension AppReceiptValidator.Parameters {
self = .data(uuid.data)
}
/// Returns .data by parsing a MAC Address String
///
/// - Parameters:
/// - macAddress: A MAC Address of the form "00:0d:3f:cd:02:5f"
/// - separator: Defaults to `":"`
///
/// - Note: on macOS the MAC Addresses can be read from terminal command `ifconfig` looking for ther `ether` entry of `5e`.
public init?(macAddress: String, separator: String = ":") {
let bytes = macAddress.components(separatedBy: separator).compactMap { UInt8($0, radix: 16) }
guard bytes.count == 6 else { return nil }
self = .data(Data(bytes))
}
public func getData() -> Data? {
switch self {
case .data(let data):
@@ -117,15 +117,14 @@ private extension AppReceiptValidator {
var sha1Context = SHA_CTX()
SHA1_Init(&sha1Context)
deviceIdentifierData.withUnsafeBytes { pointer -> Void in
SHA1_Update(&sha1Context, pointer, deviceIdentifierData.count)
_ = deviceIdentifierData.withUnsafeBytes { pointer -> Void in
SHA1_Update(&sha1Context, pointer.baseAddress, deviceIdentifierData.count)
}
receiptOpaqueValueData.withUnsafeBytes { pointer -> Void in
SHA1_Update(&sha1Context, pointer, receiptOpaqueValueData.count)
_ = receiptOpaqueValueData.withUnsafeBytes { pointer -> Void in
SHA1_Update(&sha1Context, pointer.baseAddress, receiptOpaqueValueData.count)
}
receiptBundleIdData.withUnsafeBytes { pointer -> Void in
SHA1_Update(&sha1Context, pointer, receiptBundleIdData.count)
_ = receiptBundleIdData.withUnsafeBytes { pointer -> Void in
SHA1_Update(&sha1Context, pointer.baseAddress, receiptBundleIdData.count)
}
SHA1_Final(&computedHash, &sha1Context)
@@ -243,28 +242,30 @@ private extension AppReceiptValidator {
return (receipt: receipt, unofficialReceipt: unofficialReceipt)
}
// swiftlint:disable:next cyclomatic_complexity
private func parseInAppPurchaseReceipt(pointer: UnsafePointer<UInt8>, length: Int) throws -> InAppPurchaseReceipt {
var inAppPurchaseReceipt = InAppPurchaseReceipt()
try self.parseASN1Set(pointer: pointer, length: length) { attributeType, value in
guard let attribute = KnownInAppPurchaseAttribute(rawValue: attributeType) else { return }
guard let value = value.unwrapped else { return } // always unwrap set members
switch attribute {
case .quantity:
inAppPurchaseReceipt.quantity = value.intValue
case .productIdentifier:
inAppPurchaseReceipt.productIdentifier = value.unwrappedStringValue
inAppPurchaseReceipt.productIdentifier = value.stringValue
case .transactionIdentifier:
inAppPurchaseReceipt.transactionIdentifier = value.unwrappedStringValue
inAppPurchaseReceipt.transactionIdentifier = value.stringValue
case .originalTransactionIdentifier:
inAppPurchaseReceipt.originalTransactionIdentifier = value.unwrappedStringValue
inAppPurchaseReceipt.originalTransactionIdentifier = value.stringValue
case .purchaseDate:
inAppPurchaseReceipt.purchaseDate = value.unwrappedDateValue
inAppPurchaseReceipt.purchaseDate = value.dateValue
case .originalPurchaseDate:
inAppPurchaseReceipt.originalPurchaseDate = value.unwrappedDateValue
inAppPurchaseReceipt.originalPurchaseDate = value.dateValue
case .subscriptionExpirationDate:
inAppPurchaseReceipt.subscriptionExpirationDate = value.unwrappedDateValue
inAppPurchaseReceipt.subscriptionExpirationDate = value.dateValue
case .cancellationDate:
inAppPurchaseReceipt.cancellationDate = value.unwrappedDateValue
inAppPurchaseReceipt.cancellationDate = value.dateValue
case .webOrderLineItemId:
inAppPurchaseReceipt.webOrderLineItemId = value.intValue
}
@@ -336,7 +337,6 @@ private extension AppReceiptValidator {
case expirationDate = 21
}
/// See Receipt.swift for details and a link to Apple reference
enum KnownInAppPurchaseAttribute: Int32 {
case quantity = 1701
@@ -20,7 +20,7 @@ extension AppReceiptValidator.Parameters.DeviceIdentifier {
/// Original implementation https://gist.github.com/mminer/82975d3781e2f42fc644d7fbfbf4f905
///
/// - Returns: The MAC Address as Data and String representation
private static func getPrimaryNetworkMACAddress() -> (data: Data, addressString: String)? {
public static func getPrimaryNetworkMACAddress() -> (data: Data, addressString: String)? {
let matching = IOServiceMatching("IOEthernetInterface") as NSMutableDictionary
matching[kIOPropertyMatchKey] = ["IOPrimaryInterface": true]
var servicesIterator: io_iterator_t = 0
@@ -53,6 +53,6 @@ extension AppReceiptValidator.Parameters.DeviceIdentifier {
.map { String(format: "%02x", $0) }
.joined(separator: ":")
return (data: Data(bytes: address), addressString: addressString)
return (data: Data(address), addressString: addressString)
}
}
@@ -9,6 +9,8 @@
import AppReceiptValidator.OpenSSL
import Foundation
// A resource for ASN1 can be https://www.oss.com/asn1/resources/reference/asn1-reference-card.html
/// An ASN1 Sequence Object. Of interest are the attributeType and the valueObject.
/// The attributeType determines how to interpret the valueObject.
///
@@ -116,7 +118,11 @@ extension ASN1Object {
extension ASN1Object {
/// Unwraps an OCTET_STRING, which is a binary container, that can contain another ASN1Object.
/// This is the case when we are looking at an entry in an ASN1Set
var unwrapped: ASN1Object? {
guard self.type == V_ASN1_OCTET_STRING else { return nil }
guard let endPointer = valuePointer?.advanced(by: length) else { return nil }
var innerPointer = valuePointer
@@ -155,7 +161,11 @@ extension ASN1Object {
extension ASN1Object {
var intValue: Int? {
var unwrappedIntValue: Int64? {
return self.unwrapped?.intValue
}
var intValue: Int64? {
guard self.type == V_ASN1_INTEGER else { return nil }
var pointer = self.valuePointer
@@ -163,11 +173,11 @@ extension ASN1Object {
defer {
ASN1_INTEGER_free(integer)
}
let result = ASN1_INTEGER_get(integer)
let result = Int64(ASN1_INTEGER_get(integer))
return result
}
func intValue(byAdvancingPointer pointer: inout UnsafePointer<UInt8>?, length: Int? = nil) -> Int? {
func intValue(byAdvancingPointer pointer: inout UnsafePointer<UInt8>?, length: Int? = nil) -> Int64? {
let length = length ?? self.length
pointer = pointer?.advanced(by: length)
guard let intValue = self.intValue else { return nil }
@@ -14,8 +14,8 @@ final class BIOWrapper {
let bio = BIO_new(BIO_s_mem())
init(data: Data) {
data.withUnsafeBytes { pointer -> Void in
BIO_write(self.bio, pointer, Int32(data.count))
_ = data.withUnsafeBytes { pointer in
BIO_write(self.bio, pointer.baseAddress, Int32(data.count))
}
}
@@ -11,7 +11,7 @@ import Foundation
/// Receipts are made up of a number of fields. This represents all fields that are available locally when parsing a receipt file in ASN.1 form.
///
/// See [Apple Reference](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html)
public struct Receipt {
public struct Receipt: Equatable {
/// The apps bundle identifier. This corresponds to the value of `CFBundleIdentifier` in the Info.plist file.
/// Use this value to validate if the receipt was indeed generated for your app. ASN.1 Field Type 2.
@@ -55,6 +55,7 @@ public struct Receipt {
/// The in-app purchase receipt for a non-consumable product, auto-renewable subscription, non-renewing subscription, or free subscription remains in the receipt indefinitely.
public internal(set) var inAppPurchaseReceipts: [InAppPurchaseReceipt] = []
/// For documentation of parameters, have a look at the documented properties of `Receipt`
public init(bundleIdentifier: String?, bundleIdData: Data?, appVersion: String?, opaqueValue: Data?, sha1Hash: Data?, originalAppVersion: String?, receiptCreationDate: Date?, expirationDate: Date?, inAppPurchaseReceipts: [InAppPurchaseReceipt]) {
self.bundleIdentifier = bundleIdentifier
self.bundleIdData = bundleIdData
@@ -67,17 +68,36 @@ public struct Receipt {
self.inAppPurchaseReceipts = inAppPurchaseReceipts
}
/// Convenience initializer with stringified parameters for `Date` and `Data` type parameters.
/// When logging a `Receipt`'s (`CustomStringConvertible`) description, you can use that as swift source code calling this initializer,
/// which can simplify creating tests.
///
/// For documentation of parameters, have a look at the documented properties of `Receipt`.
///
/// - Note: Dates must be formatted as "2017-01-01T12:00:00Z", otherwise will be assigned `nil`.
/// Data must be formatted as Base64, otherwise will be assigned `nil`.
/// Correct formatting will be asserted in DEBUG builds.
public init(bundleIdentifier: String, bundleIdData: String, appVersion: String, opaqueValue: String, sha1Hash: String, originalAppVersion: String, receiptCreationDate: String, expirationDate: String?, inAppPurchaseReceipts: [InAppPurchaseReceipt]) {
self.init(bundleIdentifier: bundleIdentifier,
bundleIdData: parseBase64(string: bundleIdData),
appVersion: appVersion,
opaqueValue: parseBase64(string: opaqueValue),
sha1Hash: parseBase64(string: sha1Hash),
originalAppVersion: originalAppVersion,
receiptCreationDate: parseDate(string: receiptCreationDate),
expirationDate: expirationDate.flatMap { parseDate(string: $0) },
inAppPurchaseReceipts: inAppPurchaseReceipts)
}
public init() {}
}
// MARK: - Equatable
extension Receipt: AutoEquatable {}
// MARK: - CustomStringConvertible
extension Receipt: CustomStringConvertible, CustomDebugStringConvertible {
// This description is carefully matched to match the stringy convenience initializer of `Receipt`
public var description: String {
let formatter = StringFormatter()
let props: [(String, String)] = [
@@ -105,7 +125,7 @@ extension Receipt: CustomStringConvertible, CustomDebugStringConvertible {
///
/// Documentation was obtained from: https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html
///
/// The following fields are part of JSON communication but not part of the parsed version (matched Sept 2017):
/// The following fields are part of JSON communication but not part of the parsed version (matched May 2019):
/// - Subscription Expiration Intent
/// - Subscription Retry Flag
/// - Subscription Trial Period
@@ -115,11 +135,11 @@ extension Receipt: CustomStringConvertible, CustomDebugStringConvertible {
/// - Subscription Auto Renew Status
/// - Subscription Auto Renew Preference
/// - Subscription Price Consent Status
public struct InAppPurchaseReceipt {
public struct InAppPurchaseReceipt: Equatable {
/// The number of items purchased. ASN.1 Field Type 1701.
/// This value corresponds to the quantity property of the `SKPayment` object stored in the transactions payment property.
public internal(set) var quantity: Int?
public internal(set) var quantity: Int64?
/// The product identifier of the item that was purchased. ASN.1 Field Type 1702.
/// This value corresponds to the `productIdentifier` property of the `SKPayment` object stored in the transactions `payment` property.
@@ -171,10 +191,10 @@ public struct InAppPurchaseReceipt {
/// The primary key for identifying subscription purchases. ASN.1 Field Type 1711.
/// This value is a unique ID that identifies purchase events across devices, including subscription renewal purchase events.
public internal(set) var webOrderLineItemId: Int?
public internal(set) var webOrderLineItemId: Int64?
/// 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?) {
/// For documentation of parameters, have a look at the documented properties of `InAppPurchaseReceipt`.
public init(quantity: Int64?, productIdentifier: String?, transactionIdentifier: String?, originalTransactionIdentifier: String?, purchaseDate: Date?, originalPurchaseDate: Date?, subscriptionExpirationDate: Date?, cancellationDate: Date?, webOrderLineItemId: Int64?) {
self.quantity = quantity
self.productIdentifier = productIdentifier
self.transactionIdentifier = transactionIdentifier
@@ -186,17 +206,36 @@ public struct InAppPurchaseReceipt {
self.webOrderLineItemId = webOrderLineItemId
}
/// Convenience initializer with stringified parameters for `Date` and `Data` type parameters.
/// When logging a `Receipt`'s (`CustomStringConvertible`) description, you can use that as swift source code calling this initializer,
/// which can simplify creating tests.
///
/// For documentation of parameters, have a look at the documented properties of `InAppPurchaseReceipt`.
///
/// - Note: Dates must be formatted as "2017-01-01T12:00:00Z", otherwise will be assigned `nil`.
/// Data must be formatted as Base64, otherwise will be assigned `nil`.
/// Correct formatting will be asserted in DEBUG builds.
public init(quantity: Int64?, productIdentifier: String, transactionIdentifier: String, originalTransactionIdentifier: String, purchaseDate: String, originalPurchaseDate: String, subscriptionExpirationDate: String?, cancellationDate: String?, webOrderLineItemId: Int64?) {
self.init(quantity: quantity,
productIdentifier: productIdentifier,
transactionIdentifier: transactionIdentifier,
originalTransactionIdentifier: originalTransactionIdentifier,
purchaseDate: parseDate(string: purchaseDate),
originalPurchaseDate: parseDate(string: originalPurchaseDate),
subscriptionExpirationDate: subscriptionExpirationDate.flatMap { parseDate(string: $0) },
cancellationDate: cancellationDate.flatMap { parseDate(string: $0) },
webOrderLineItemId: webOrderLineItemId)
}
public init() {}
}
// MARK: - Equatable
extension InAppPurchaseReceipt: AutoEquatable {}
// MARK: - CustomStringConvertible
extension InAppPurchaseReceipt: CustomStringConvertible, CustomDebugStringConvertible {
// This description is carefully matched to match the stringy convenience initializer of `InAppPurchaseReceipt`
public var description: String {
let formatter = StringFormatter()
let props: [(String, String)] = [
@@ -236,7 +275,7 @@ private struct StringFormatter {
return pairs.map { self.format(key: $0, value: $1) }.joined(separator: ",\n")
}
func format(_ int: Int?) -> String {
func format(_ int: Int64?) -> String {
guard let int = int else { return fallback }
return "\(int)"
@@ -249,16 +288,40 @@ private struct StringFormatter {
func format(_ data: Data?) -> String {
guard let data = data else { return fallback }
return data.base64EncodedString()
return quoted(data.base64EncodedString())
}
func format(_ date: Date?) -> String {
guard let date = date else { return fallback }
return AppReceiptValidator.asn1DateFormatter.string(from: date)
return quoted(AppReceiptValidator.asn1DateFormatter.string(from: date))
}
func format(_ string: String?) -> String {
return string ?? fallback
return string.map(quoted) ?? fallback
}
/// Surrounds a string with quotes ""
func quoted(_ string: String) -> String {
return "\"" + string + "\""
}
}
private func parseBase64(string: String) -> Data? {
guard let data = Data(base64Encoded: string) else {
assertionFailure("Data could not be parsed from string '\(string)', make sure it is non-empty nad base64 encoded.")
return nil
}
return data
}
// Parses a string of type "2017-01-01T12:00:00Z"
private func parseDate(string: String) -> Date? {
guard let date = AppReceiptValidator.asn1DateFormatter.date(from: string) else {
assertionFailure("Date could not be parsed from string '\(string)', make sure it has a correct format, example `2017-01-01T12:00:00Z`")
return nil
}
return date
}
@@ -1,56 +0,0 @@
// Generated using Sourcery 0.10.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?):
return compare(lValue, rValue)
case (nil, nil):
return true
default:
return false
}
}
private func compareArrays<T>(lhs: [T], rhs: [T], compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool {
guard lhs.count == rhs.count else { return false }
for (idx, lhsItem) in lhs.enumerated() {
guard compare(lhsItem, rhs[idx]) else { return false }
}
return true
}
// MARK: - AutoEquatable for classes, protocols, structs
// 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 }
guard compareOptionals(lhs: lhs.productIdentifier, rhs: rhs.productIdentifier, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.transactionIdentifier, rhs: rhs.transactionIdentifier, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.originalTransactionIdentifier, rhs: rhs.originalTransactionIdentifier, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.purchaseDate, rhs: rhs.purchaseDate, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.originalPurchaseDate, rhs: rhs.originalPurchaseDate, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.subscriptionExpirationDate, rhs: rhs.subscriptionExpirationDate, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.cancellationDate, rhs: rhs.cancellationDate, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.webOrderLineItemId, rhs: rhs.webOrderLineItemId, compare: ==) else { return false }
return true
}
// 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 }
guard compareOptionals(lhs: lhs.bundleIdData, rhs: rhs.bundleIdData, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.appVersion, rhs: rhs.appVersion, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.opaqueValue, rhs: rhs.opaqueValue, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.sha1Hash, rhs: rhs.sha1Hash, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.originalAppVersion, rhs: rhs.originalAppVersion, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.receiptCreationDate, rhs: rhs.receiptCreationDate, compare: ==) else { return false }
guard compareOptionals(lhs: lhs.expirationDate, rhs: rhs.expirationDate, compare: ==) else { return false }
guard lhs.inAppPurchaseReceipts == rhs.inAppPurchaseReceipts else { return false }
return true
}
// MARK: - AutoEquatable for Enums
@@ -1,8 +0,0 @@
//
// AutoEquatable.swift
//
//
// Created by Hannes Oud on 07.09.17.
//
protocol AutoEquatable {}
@@ -1,62 +0,0 @@
// swiftlint:disable file_length
fileprivate func compareOptionals<T>(lhs: T?, rhs: T?, compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool {
switch (lhs, rhs) {
case let (lValue?, rValue?):
return compare(lValue, rValue)
case (nil, nil):
return true
default:
return false
}
}
fileprivate func compareArrays<T>(lhs: [T], rhs: [T], compare: (_ lhs: T, _ rhs: T) -> Bool) -> Bool {
guard lhs.count == rhs.count else { return false }
for (idx, lhsItem) in lhs.enumerated() {
guard compare(lhsItem, rhs[idx]) else { return false }
}
return true
}
{% macro compareVariables variables %}
{% for variable in variables where variable.readAccess != "private" and variable.readAccess != "fileprivate" %}{% if not variable.annotations.skipEquality %}guard {% if not variable.isOptional %}{% if not variable.annotations.arrayEquality %}lhs.{{ variable.name }} == rhs.{{ variable.name }}{% else %}compareArrays(lhs: lhs.{{ variable.name }}, rhs: rhs.{{ variable.name }}, compare: ==){% endif %}{% else %}compareOptionals(lhs: lhs.{{ variable.name }}, rhs: rhs.{{ variable.name }}, compare: ==){% endif %} else { return false }{% endif %}
{% endfor %}
{% endmacro %}
// MARK: - AutoEquatable for classes, protocols, structs
{% for type in types.implementing.AutoEquatable|!enum %}
// MARK: - {{ type.name }} AutoEquatable
{% if not type.kind == "protocol" %}extension {{ type.name }}: Equatable {}{% endif %}
{% if type.supertype.based.Equatable or type.supertype.implements.AutoEquatable %}THIS WONT COMPILE, WE DONT SUPPORT INHERITANCE for AutoEquatable{% endif %}
{{ type.accessLevel }} func == (lhs: {{ type.name }}, rhs: {{ type.name }}) -> Bool {
{% if not type.kind == "protocol" %}
{% call compareVariables type.storedVariables %}
{% else %}
{% call compareVariables type.allVariables %}
{% endif %}
return true
}
{% endfor %}
// MARK: - AutoEquatable for Enums
{% for type in types.implementing.AutoEquatable|enum %}
// MARK: - {{ type.name }} AutoEquatable
extension {{ type.name }}: Equatable {}
{{ type.accessLevel }} func == (lhs: {{ type.name }}, rhs: {{ type.name }}) -> Bool {
switch (lhs, rhs) {
{% for case in type.cases %}
{% if case.hasAssociatedValue %}case (.{{ case.name }}(let lhs), .{{ case.name }}(let rhs)):{% else %}case (.{{ case.name }}, .{{ case.name }}):{% endif %}
{% ifnot case.hasAssociatedValue %}return true{% else %}
{% if case.associatedValues.count == 1 %}
return lhs == rhs
{% else %}
{% for associated in case.associatedValues %}if lhs.{{ associated.externalName }} != rhs.{{ associated.externalName }} { return false }
{% endfor %}return true
{% endif %}
{% endif %}
{% endfor %}
{% if type.cases.count > 1 %}default: return false{% endif %}
}
}
{% endfor %}
+8 -6
View File
@@ -2,7 +2,7 @@
[![Carthage Compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)
![Platforms iOS, macOS](https://img.shields.io/badge/Platform-iOS%20|%20macOS-blue.svg "Platforms iOS, macOS")
![Language Swift](https://img.shields.io/badge/Language-Swift%204.1-orange.svg "Swift 4.1")
![Language Swift](https://img.shields.io/badge/Language-Swift%205.0-orange.svg "Swift 5.0")
[![License Apache 2.0 + OpenSSL](https://img.shields.io/badge/License-Apache%202.0%20|%20OpenSSL%20-aaaaff.svg "License")](LICENSE)
[![Build Status](https://travis-ci.org/IdeasOnCanvas/AppReceiptValidator.svg?branch=master)](https://travis-ci.org/IdeasOnCanvas/AppReceiptValidator)
[![Twitter: @hannesoid](https://img.shields.io/badge/Twitter-@hannesoid-red.svg?style=flat)](https://twitter.com/hannesoid)
@@ -73,7 +73,7 @@ Receipt(
)
```
**Receipt** is *Equatable*, thanks [Sourcery](https://github.com/krzysztofzablocki/Sourcery), so you can do comparisons in Unit Tests.
**Receipt** is *Equatable*, so you can do comparisons in Unit Tests.
There are also some opt-in unofficial attributes, but this is experimental and should not be used in production.
### Validating a receipt's signature and hash
@@ -150,19 +150,20 @@ If you have no receipt (happens in development builds) or your receipt is invali
### AppReceiptValidator Uses OpenSSL
OpenSSL is used for pkcs7 container parsing and signature validation, and also for parsing the ASN1 payload of the pkcs7, which contains the receipts attributes.
OpenSSL is used for PKCS#7 container parsing and signature validation, and also for parsing the ASN1 payload of the PKCS#7, which contains the receipts attributes.
### Other Options
##### Alternatives to PKCS7 of OpenSSL
##### Alternatives to PKCS#7 of OpenSSL
- `Security.framework` - `CMSDecoder` for PKCS7 interaction *only available on macOS*
- `BoringSSL` instead of OpenSSL, Pod, *only available on iOS (?)*
- `Security.framework` - `CMSDecoder` for PKCS#7 interaction *only available on macOS*, [AppStoreReceiptChecker](https://github.com/delicious-monster/AppStoreReceiptChecker) uses this.
- `BoringSSL` instead of OpenSSL, seems included as frameworks in modern iOS and macOS, but not officially supported?
##### 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)
- [AppStoreReceiptChecker](https://github.com/delicious-monster/AppStoreReceiptChecker)
##### Validation Server to Server
An app can send its receipt file to a backend from where Apples receipt API can be called. See Resources.
@@ -187,6 +188,7 @@ Advantages doing it locally:
- [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/)
- [SwiftyStoreKit](https://github.com/bizz84/SwiftyStoreKit)
- [AppStoreReceiptChecker](https://github.com/delicious-monster/AppStoreReceiptChecker) - macOS, uses CMSDecoder and a Swift ASN1 Implementation
## Updating Apple Root Certificate
For convenience, AppReceiptValidator contains a copy of apples root certificate to validate the signature against. If uncomfortable with this, you can specify your own by changing the parameters like this:
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 299 KiB

After

Width:  |  Height:  |  Size: 463 KiB