forked from googollee/iaplocal
/
receipt.go
258 lines (236 loc) · 6.2 KB
/
receipt.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
// Package iaplocal supports Apple Local In-App Purchase (IAP)
// receipt processing.
//
// It loads the receipt from binary, parses the receipt's
// attributes, and verifies the receipt signature and hash.
package iaplocal
import (
"crypto/sha1"
"crypto/x509"
"encoding/asn1"
"errors"
"time"
"github.com/fullsailor/pkcs7"
)
// Receipt is the receipt for an in-app purchase.
type Receipt struct {
Quantity int `json:"quantity"`
ProductID string `json:"product_id"`
TransactionID string `json:"transaction_date"`
PurchaseDate time.Time `json:"purchase_date"`
OriginalTransactionID string `json:"original_transaction_id"`
OriginalPurchaseDate time.Time `json:"original_purchase_date"`
ExpiresDate time.Time `json:"expires_date"`
WebOrderLineItemID int `json:"web_order_line_item_id"`
CancellationDate time.Time `json:"cancellation_date"`
IsInIntroPrice bool `json:"is_in_intro_offer_period"`
}
// Receipts is the app receipt.
type Receipts struct {
BundleID string `json:"bundle_id"`
ApplicationVersion string `json:"application_version"`
OpaqueValue []byte `json:"opaque_value"`
SHA1Hash []byte `json:"sha1hash"`
CreationDate time.Time `json:"creation_date"`
InApp []Receipt `json:"in_app"`
OriginalApplicationVersion string `json:"original_application_version"`
ExpirationDate time.Time `json:"expiration_date"`
rawBundleID []byte
}
var (
// ErrInvalidCertificate returns when parse a receipt
// with invalid certificate from given root certificate.
ErrInvalidCertificate = errors.New("iaplocal: invalid certificate in receipt")
// ErrInvalidSignature returns when parse a receipt
// which improperly signed.
ErrInvalidSignature = errors.New("iaplocal: invalid signature of receipt")
)
// Parse parses a receipt binary which certificates with
// root certificate.
// Need decode to DER binary if recevied a base64 file.
func Parse(root *x509.Certificate, data []byte) (Receipts, error) {
pkcs, err := pkcs7.Parse(data)
if err != nil {
return Receipts{}, err
}
if !verifyCertificates(root, pkcs) {
return Receipts{}, ErrInvalidCertificate
}
if !verifyPKCS(pkcs) {
return Receipts{}, ErrInvalidSignature
}
return parsePKCS(pkcs)
}
// Verify verifys the receipts with given guid.
// TestReceiptValidate shows how to get GUID from string.
// Check https://developer.apple.com/library/ios/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateLocally.html#//apple_ref/doc/uid/TP40010573-CH1-SW5
func (r *Receipts) Verify(guid []byte) bool {
hash := sha1.New()
hash.Write(guid)
hash.Write(r.OpaqueValue)
hash.Write([]byte(r.rawBundleID))
sign := hash.Sum(nil)
if len(sign) != len(r.SHA1Hash) {
return false
}
for i := range sign {
if sign[i] != r.SHA1Hash[i] {
return false
}
}
return true
}
func verifyCertificates(root *x509.Certificate, pkcs *pkcs7.PKCS7) bool {
roots := x509.NewCertPool()
roots.AddCert(root)
signer := pkcs.GetOnlySigner()
// prepare intermediate certs
intermediates := x509.NewCertPool()
for _, cert := range pkcs.Certificates {
if cert != signer && !cert.Equal(root) {
intermediates.AddCert(cert)
}
}
_, err := signer.Verify(x509.VerifyOptions{
Intermediates: intermediates,
Roots: roots,
})
if err != nil {
return false
}
return true
}
func verifyPKCS(pkcs *pkcs7.PKCS7) bool {
return pkcs.Verify() == nil
}
type attribute struct {
Type int
Version int
Value []byte
}
func parsePKCS(pkcs *pkcs7.PKCS7) (ret Receipts, err error) {
var r asn1.RawValue
_, err = asn1.Unmarshal(pkcs.Content, &r)
if err != nil {
return
}
rest := r.Bytes
for len(rest) > 0 {
var ra attribute
rest, err = asn1.Unmarshal(rest, &ra)
if err != nil {
return
}
switch ra.Type {
case 2:
if _, err = asn1.Unmarshal(ra.Value, &ret.BundleID); err != nil {
return
}
ret.rawBundleID = ra.Value
case 3:
if _, err = asn1.Unmarshal(ra.Value, &ret.ApplicationVersion); err != nil {
return
}
case 4:
ret.OpaqueValue = ra.Value
case 5:
ret.SHA1Hash = ra.Value
case 12:
ret.CreationDate, err = asn1ParseTime(ra.Value)
if err != nil {
return
}
case 17:
var inApp Receipt
inApp, err = parseInApp(ra.Value)
if err != nil {
return
}
ret.InApp = append(ret.InApp, inApp)
case 19:
if _, err = asn1.Unmarshal(ra.Value, &ret.OriginalApplicationVersion); err != nil {
return
}
case 21:
ret.ExpirationDate, err = asn1ParseTime(ra.Value)
if err != nil {
return
}
}
}
return
}
func parseInApp(data []byte) (ret Receipt, err error) {
var r asn1.RawValue
_, err = asn1.Unmarshal(data, &r)
if err != nil {
return
}
data = r.Bytes
for len(data) > 0 {
var ra attribute
data, err = asn1.Unmarshal(data, &ra)
if err != nil {
return
}
switch ra.Type {
case 1701:
if _, err = asn1.Unmarshal(ra.Value, &ret.Quantity); err != nil {
return
}
case 1702:
if _, err = asn1.Unmarshal(ra.Value, &ret.ProductID); err != nil {
return
}
case 1703:
if _, err = asn1.Unmarshal(ra.Value, &ret.TransactionID); err != nil {
return
}
case 1704:
ret.PurchaseDate, err = asn1ParseTime(ra.Value)
if err != nil {
return
}
case 1705:
if _, err = asn1.Unmarshal(ra.Value, &ret.OriginalTransactionID); err != nil {
return
}
case 1706:
ret.OriginalPurchaseDate, err = asn1ParseTime(ra.Value)
if err != nil {
return
}
case 1708:
ret.ExpiresDate, err = asn1ParseTime(ra.Value)
if err != nil {
return
}
case 1711:
if _, err = asn1.Unmarshal(ra.Value, &ret.WebOrderLineItemID); err != nil {
return
}
case 1712:
ret.CancellationDate, err = asn1ParseTime(ra.Value)
if err != nil {
return
}
case 1719:
var intRoprice int
if _, err = asn1.Unmarshal(ra.Value, &intRoprice); err != nil {
return
}
ret.IsInIntroPrice = (intRoprice != 0)
}
}
return
}
func asn1ParseTime(data []byte) (time.Time, error) {
var str string
if _, err := asn1.Unmarshal(data, &str); err != nil {
return time.Time{}, err
}
if str == "" {
return time.Time{}, nil
}
return time.Parse(time.RFC3339, str)
}