├── AppStoreReceipt.swift ├── README.md ├── main.swift ├── receipt_payload.sample └── run.sh /AppStoreReceipt.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppStoreReceipt.swift 3 | // Decode the decrypted payload data in App Store receipts. 4 | // 5 | // Copyright (c) 2015 Roopesh Chander. All rights reserved. 6 | // 7 | // Permission is hereby granted, free of charge, to any person obtaining a copy 8 | // of this software and associated documentation files (the "Software"), to deal 9 | // in the Software without restriction, including without limitation the rights 10 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | // copies of the Software, and to permit persons to whom the Software is 12 | // furnished to do so, subject to the following conditions: 13 | // 14 | // The above copyright notice and this permission notice shall be included in 15 | // all copies or substantial portions of the Software. 16 | // 17 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | // THE SOFTWARE. 24 | 25 | 26 | // The App Store receipt is signed with Apple's private key and 27 | // needs to be decrypted with Apple's public key to obtain the 28 | // payload data. The payload data is in ASN.1 DER format, which 29 | // can be decoded with this Swift class. 30 | // 31 | // See main.swift for example usage. 32 | 33 | 34 | import Foundation 35 | 36 | // Add '@objc' here if you'd like to use this from Objective-C 37 | class AppStoreReceipt { 38 | let _payloadData: NSData 39 | 40 | init(payloadData: NSData) { 41 | self._payloadData = payloadData 42 | } 43 | 44 | class func receiptWithPayloadData(payloadData: NSData) -> AppStoreReceipt { 45 | // Helper for using this class from Objective-C. 46 | // This is required because AppStoreReceipt is not an NSObject. 47 | return AppStoreReceipt(payloadData: payloadData) 48 | } 49 | 50 | func enumerateReceiptAttributes(block: (type: Int, version: Int, value: NSData) -> Void) -> Bool { 51 | return AppStoreReceipt.enumerateReceiptAttributesInASN1SetData(self._payloadData, block: block) 52 | } 53 | 54 | class func decodeASN1String(data: NSData) -> String? { 55 | let ptr = UnsafePointer(data.bytes) 56 | let len: Int = data.length 57 | var pos: Int = 0 58 | var numOfBytesInString: Int? = 0 59 | 60 | var decodedString = String() 61 | 62 | while (pos < len) { 63 | let typeByte = ptr[pos++] 64 | numOfBytesInString = self.decodeASN1Length(ptr, pos: &pos, bufferLength: len) 65 | if (numOfBytesInString == nil) { return nil } 66 | if (pos + numOfBytesInString! > len) { return nil } 67 | let encoding: NSStringEncoding 68 | if (typeByte == ASN1Tag.UTF8String.rawValue) { 69 | encoding = NSUTF8StringEncoding 70 | } else if (typeByte == ASN1Tag.IA5String.rawValue) { 71 | encoding = NSASCIIStringEncoding 72 | } else { 73 | // Other string types are not used in App Store receipts 74 | return nil 75 | } 76 | let str = NSString(bytes: UnsafePointer(ptr + pos), length: numOfBytesInString!, encoding: encoding) 77 | if let str = str as? String { 78 | decodedString += str 79 | } 80 | pos += numOfBytesInString! 81 | } 82 | 83 | return decodedString 84 | } 85 | 86 | class func decodeASN1Integer(data: NSData) -> Int? { 87 | let ptr = UnsafePointer(data.bytes) 88 | let len: Int = data.length 89 | var pos: Int = 0 90 | 91 | if (ptr[pos++] != ASN1Tag.Integer.rawValue) { return nil } 92 | if (pos >= len) { return nil } 93 | let numOfBytes: Int? = self.decodeASN1Length(ptr, pos: &pos, bufferLength: len) 94 | if (numOfBytes == nil) { return nil } 95 | if (pos + numOfBytes! > len) { return nil } 96 | 97 | return self.decodeASN1Integer(ptr, pos: &pos, numberOfBytes: numOfBytes!) 98 | } 99 | } 100 | 101 | // Private stuff follows 102 | 103 | private enum ASN1Tag: UInt8 { 104 | case Integer = 0x02 105 | case OctetString = 0x04 106 | case UTF8String = 0x0c 107 | case IA5String = 0x16 108 | case Sequence = 0x30 // 0x20 /*Compound*/ | 0x10 109 | case Set = 0x31 // 0x20 /*Compound*/ | 0x11 110 | } 111 | 112 | private typealias ReceiptAttribute = (type: Int, version: Int, value: NSData) 113 | 114 | extension AppStoreReceipt { 115 | private class func enumerateReceiptAttributesInASN1SetData(data: NSData, block: (type: Int, version: Int, value: NSData) -> Void) -> Bool { 116 | let ptr = UnsafePointer(data.bytes) 117 | let len: Int = data.length 118 | var pos: Int = 0 119 | var fieldIndex: Int = 0 120 | 121 | if (ptr[pos++] != ASN1Tag.Set.rawValue) { return false } 122 | if (pos >= len) { return false } 123 | 124 | let numOfBytesInSet = self.decodeASN1Length(ptr, pos: &pos, bufferLength: len) 125 | if (numOfBytesInSet == nil) { return false } 126 | 127 | let endOfSetContents = pos + numOfBytesInSet! 128 | if (len < endOfSetContents) { 129 | return false 130 | } 131 | 132 | while (pos < len) { 133 | if let receiptAttribute = self.decodeASN1ReceiptAttribute(ptr, pos: &pos, bufferLength: endOfSetContents) { 134 | block(type: receiptAttribute.type, version: receiptAttribute.version, value: receiptAttribute.value) 135 | } else { 136 | return false 137 | } 138 | } 139 | 140 | return true 141 | } 142 | } 143 | 144 | extension AppStoreReceipt { 145 | private class func decodeASN1Length(ptr: UnsafePointer, inout pos: Int, bufferLength length: Int) -> Int? { 146 | let byte = ptr[pos] 147 | if ((byte & 0x80) == 0x00) { 148 | // Short form 149 | pos++ 150 | return Int(byte) 151 | } else if ((byte & 0x7f) > 0x00) { 152 | // Long form 153 | var numOfLengthBytes = Int(byte & 0x7f) 154 | pos++ 155 | if (pos + numOfLengthBytes >= length) { return nil } 156 | return self.decodeASN1Integer(ptr, pos: &pos, numberOfBytes: numOfLengthBytes) 157 | } else { 158 | // Indefinite form is not expected in App Store receipts 159 | return nil 160 | } 161 | } 162 | 163 | private class func decodeASN1ReceiptAttribute(ptr: UnsafePointer, inout pos: Int, bufferLength len: Int) -> ReceiptAttribute? { 164 | if (ptr[pos++] != ASN1Tag.Sequence.rawValue) { return nil } 165 | if (pos >= len) { return nil } 166 | 167 | let numOfBytesInSequence = self.decodeASN1Length(ptr, pos: &pos, bufferLength: len) 168 | if (numOfBytesInSequence == nil) { return nil } 169 | 170 | let endOfSequenceContents = pos + numOfBytesInSequence! 171 | 172 | var numOfBytesInField: Int? = 0 173 | 174 | if (ptr[pos++] != ASN1Tag.Integer.rawValue) { return nil } 175 | if (pos >= endOfSequenceContents) { return nil } 176 | numOfBytesInField = self.decodeASN1Length(ptr, pos: &pos, bufferLength: endOfSequenceContents) 177 | if (numOfBytesInField == nil) { return nil } 178 | if (pos + numOfBytesInField! > endOfSequenceContents) { return nil } 179 | let first: Int = self.decodeASN1Integer(ptr, pos: &pos, numberOfBytes: numOfBytesInField!) 180 | 181 | if (ptr[pos++] != ASN1Tag.Integer.rawValue) { return nil } 182 | if (pos >= endOfSequenceContents) { return nil } 183 | numOfBytesInField = self.decodeASN1Length(ptr, pos: &pos, bufferLength: endOfSequenceContents) 184 | if (numOfBytesInField == nil) { return nil } 185 | if (pos + numOfBytesInField! > endOfSequenceContents) { return nil } 186 | let second: Int = self.decodeASN1Integer(ptr, pos: &pos, numberOfBytes: numOfBytesInField!) 187 | 188 | if (ptr[pos++] != ASN1Tag.OctetString.rawValue) { return nil } 189 | if (pos >= endOfSequenceContents) { return nil } 190 | numOfBytesInField = self.decodeASN1Length(ptr, pos: &pos, bufferLength: endOfSequenceContents) 191 | if (numOfBytesInField == nil) { return nil } 192 | if (pos + numOfBytesInField! > endOfSequenceContents) { return nil } 193 | let third: NSData = self.decodeASN1OctetString(ptr, pos: &pos, numberOfBytes: numOfBytesInField!) 194 | 195 | return (type: first, version: second, value: third) 196 | } 197 | 198 | private class func decodeASN1Integer(ptr: UnsafePointer, inout pos: Int, numberOfBytes: Int) -> Int { 199 | var result: UInt64 = 0 200 | for i in (0 ..< numberOfBytes) { 201 | let byte: UInt8 = ptr[pos + i] 202 | result |= (UInt64(byte) << UInt64((numberOfBytes - 1 - i) * 8)) 203 | } 204 | pos += numberOfBytes 205 | return Int(result) 206 | } 207 | 208 | private class func decodeASN1OctetString(ptr: UnsafePointer, inout pos: Int, numberOfBytes: Int) -> NSData { 209 | let data = NSData(bytesNoCopy: UnsafeMutablePointer(ptr + pos), length: numberOfBytes, freeWhenDone: false) 210 | pos += numberOfBytes 211 | return data 212 | } 213 | } 214 | 215 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## App Store Receipt Decoder 3 | 4 | The Apple App Store receipt is a PKCS7 container signed with Apple's 5 | private key and needs to be decrypted with Apple's public key to obtain 6 | the contained payload data. The payload data is in ASN.1 DER format. 7 | 8 | This project helps in decoding that format. 9 | 10 | ### Usage 11 | 12 | To try it, run `./run.sh` 13 | 14 | To use it in your Swift project, add `AppStoreReceipt.swift` to the project and use it like this: 15 | 16 | ~~~ Swift 17 | let data: NSData // data output from PKCS7_verify 18 | var bundleId: String? 19 | var bundleIdData: NSData? 20 | let receipt = AppStoreReceipt(payloadData: data) 21 | receipt.enumerateReceiptAttributes { (type, version, value) in 22 | switch (type) { 23 | case 2: // Bundle id 24 | bundleId = AppStoreReceipt.decodeASN1String(value) 25 | bundleIdData = value 26 | ... 27 | default: 28 | break 29 | } 30 | } 31 | ~~~ 32 | 33 | To use it in your Objective-C project, add `AppStoreReceipt.swift` to the project, add the `@objc` attribute to the `AppStoreReceipt` Swift class, import the Swift header (see 'Using Swift from Objective-C' in [_Using Swift with Cocoa and Objective-C_][swift-cocoa-book]), and use it like this: 34 | 35 | [swift-cocoa-book]: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/BuildingCocoaApps/ 36 | 37 | ~~~ Objective-C 38 | NSData* data; // data output from PKCS7_verify 39 | __block NSString* bundleId; 40 | __block NSData* bundleIdData; 41 | AppStoreReceipt *receipt = [AppStoreReceipt receiptWithPayloadData: data]; 42 | [receipt enumerateReceiptAttributes:^(NSInteger type, NSInteger version, NSData * __nonnull value) { 43 | switch (type) { 44 | case 2: 45 | bundleId = [AppStoreReceipt decodeASN1String:value]; 46 | bundleIdData = value; 47 | break; 48 | ... 49 | default: 50 | break; 51 | } 52 | }]; 53 | ~~~ 54 | 55 | See `main.swift` for a more detailed example. 56 | 57 | ### Why 58 | 59 | Integrating the [ASN.1 compiler][asn1c]-generated code with an iOS 60 | project introduces multiple source files and warnings, and is not Swift 61 | 1.2-friendly because of the use of function pointers. 62 | 63 | This project provides a clean one-file receipt decoder in Swift. 64 | 65 | [asn1c]: https://github.com/vlm/asn1c 66 | 67 | -------------------------------------------------------------------------------- /main.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | // For knowing which field type corresponds to what data, 4 | // please see "Receipt Fields" 5 | // https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ReceiptFields.html 6 | 7 | let data = NSData(contentsOfFile: "./receipt_payload.sample") 8 | if let data = data { 9 | let receipt = AppStoreReceipt(payloadData: data) 10 | receipt.enumerateReceiptAttributes { (type, version, value) in 11 | switch (type) { 12 | case 2: fallthrough 13 | case 3: fallthrough 14 | case 19: fallthrough 15 | case 21: 16 | println("type = \(type); data (\(value.length) bytes) = \(value)") 17 | if let str = AppStoreReceipt.decodeASN1String(value) { 18 | println(" string: \"\(str)\"") 19 | } 20 | println("") 21 | case 4: fallthrough 22 | case 5: 23 | println("type = \(type); data (\(value.length) bytes) = \(value)") 24 | println("") 25 | case 17: 26 | let inAppPurchaseReceipt = AppStoreReceipt(payloadData: value) 27 | inAppPurchaseReceipt.enumerateReceiptAttributes { (type, version, value) in 28 | switch (type) { 29 | case 1702: fallthrough 30 | case 1703: fallthrough 31 | case 1704: fallthrough 32 | case 1705: fallthrough 33 | case 1706: fallthrough 34 | case 1712: 35 | println("type = \(type); data (\(value.length) bytes) = \(value)") 36 | if let str = AppStoreReceipt.decodeASN1String(value) { 37 | println(" string: \"\(str)\"") 38 | } 39 | println("") 40 | case 1701: 41 | println("type = \(type); data (\(value.length) bytes) = \(value)") 42 | if let intVal = AppStoreReceipt.decodeASN1Integer(value) { 43 | println(" integer: [\(intVal)]") 44 | } 45 | println("") 46 | default: 47 | break 48 | } 49 | } 50 | default: 51 | break 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /receipt_payload.sample: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/roop/AppStoreReceiptDecoder/d82c53cce3471421ad014f6f69e5e6d084541f38/receipt_payload.sample -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | xcrun -sdk macosx swiftc AppStoreReceipt.swift main.swift -o ./decode_sample_receipt && ./decode_sample_receipt 2 | --------------------------------------------------------------------------------