Skip to content

Commit 524c727

Browse files
Merge pull request #180 from alexanderjordanbaker/AdoptASN1-3x
Adopting ASN1 3.x with much better native decoding support
2 parents fc45ef6 + a8c8332 commit 524c727

3 files changed

Lines changed: 27 additions & 80 deletions

File tree

appstoreserverlibrary/receipt_utility.py

Lines changed: 25 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313
ORIGINAL_TRANSACTION_IDENTIFIER = 1705
1414

1515
class ReceiptUtility:
16+
def _decode_octet_string(self, octet_string: bytes):
17+
decoder = asn1.Decoder()
18+
decoder.start(octet_string)
19+
_, value = decoder.read()
20+
return value
21+
1622
def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[str]:
1723
"""
1824
Extracts a transaction id from an encoded App Receipt. Throws if the receipt does not match the expected format.
@@ -21,68 +27,25 @@ def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[
2127
:param appReceipt: The unmodified app receipt
2228
:return: A transaction id from the array of in-app purchases, null if the receipt contains no in-app purchases
2329
"""
24-
decoder = IndefiniteFormAwareDecoder()
25-
decoder.start(b64decode(app_receipt, validate=True))
26-
tag = decoder.peek()
27-
if tag.typ != asn1.Types.Constructed or tag.nr != asn1.Numbers.Sequence:
28-
raise ValueError()
29-
decoder.enter()
30-
# PKCS#7 object
31-
tag, value = decoder.read()
32-
if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.ObjectIdentifier or value != PKCS7_OID:
33-
raise ValueError()
34-
# This is the PKCS#7 format, work our way into the inner content
35-
decoder.enter()
36-
decoder.enter()
37-
decoder.read()
38-
decoder.read()
39-
decoder.enter()
40-
decoder.read()
41-
decoder.enter()
42-
tag, value = decoder.read()
43-
# Xcode uses nested OctetStrings, we extract the inner string in this case
44-
if tag.typ == asn1.Types.Constructed and tag.nr == asn1.Numbers.OctetString:
45-
inner_decoder = asn1.Decoder()
46-
inner_decoder.start(value)
47-
tag, value = inner_decoder.read()
48-
if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.OctetString:
49-
raise ValueError()
50-
decoder = asn1.Decoder()
51-
decoder.start(value)
52-
tag = decoder.peek()
53-
if tag.typ != asn1.Types.Constructed or tag.nr != asn1.Numbers.Set:
54-
raise ValueError()
55-
decoder.enter()
56-
# We are in the top-level sequence, work our way to the array of in-apps
57-
while not decoder.eof():
58-
decoder.enter()
59-
tag, value = decoder.read()
60-
if tag.typ == asn1.Types.Primitive and tag.nr == asn1.Numbers.Integer and value == IN_APP_ARRAY:
61-
decoder.read()
62-
tag, value = decoder.read()
63-
if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.OctetString:
64-
raise ValueError()
65-
inapp_decoder = asn1.Decoder()
66-
inapp_decoder.start(value)
67-
inapp_decoder.enter()
68-
# In-app array
69-
while not inapp_decoder.eof():
70-
inapp_decoder.enter()
71-
tag, value = inapp_decoder.read()
72-
if (
73-
tag.typ == asn1.Types.Primitive
74-
and tag.nr == asn1.Numbers.Integer
75-
and (value == TRANSACTION_IDENTIFIER or value == ORIGINAL_TRANSACTION_IDENTIFIER)
76-
):
77-
inapp_decoder.read()
78-
tag, value = inapp_decoder.read()
79-
singleton_decoder = asn1.Decoder()
80-
singleton_decoder.start(value)
81-
tag, value = singleton_decoder.read()
82-
return value
83-
inapp_decoder.leave()
84-
decoder.leave()
85-
return None
30+
try:
31+
val = self._decode_octet_string(b64decode(app_receipt, validate=True))
32+
found_oid = val[0]
33+
if found_oid != PKCS7_OID:
34+
raise ValueError()
35+
inner_value = val[1][0][2][1][0]
36+
# Xcode uses nested OctetStrings, we extract the inner string in this case
37+
value = self._decode_octet_string(inner_value)
38+
# We are in the top-level sequence, work our way to the array of in-apps
39+
for inner_value in value:
40+
if inner_value[0] == IN_APP_ARRAY:
41+
array_values = self._decode_octet_string(inner_value[2])
42+
# In-app array
43+
for array_value in array_values:
44+
if array_value[0] == TRANSACTION_IDENTIFIER or array_value[0] == ORIGINAL_TRANSACTION_IDENTIFIER:
45+
return self._decode_octet_string(array_value[2])
46+
return None
47+
except Exception as e:
48+
raise ValueError(e)
8649

8750
def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: str) -> Optional[str]:
8851
"""
@@ -99,19 +62,3 @@ def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: s
9962
if inner_matching_result:
10063
return inner_matching_result.group(1)
10164
return None
102-
103-
class IndefiniteFormAwareDecoder(asn1.Decoder):
104-
def _read_length(self) -> int:
105-
index, input_data = self.m_stack[-1]
106-
try:
107-
byte = input_data[index]
108-
except IndexError:
109-
raise asn1.Error('Premature end of input.')
110-
if byte == 0x80:
111-
# Xcode receipts use indefinite length encoding, not supported by all parsers
112-
# Indefinite length encoding is only entered, but never left during parsing for receipts
113-
# We therefore round up indefinite length encoding to be the remaining length
114-
self._read_byte()
115-
index, input_data = self.m_stack[-1]
116-
return len(input_data) - index
117-
return super()._read_length()

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ dependencies = [
2020
"requests>=2.28.0,<3",
2121
"cryptography>=40.0.0",
2222
"pyOpenSSL>=23.1.1",
23-
"asn1==2.8.0",
23+
"asn1==3.2.0",
2424
"cattrs>=23.1.2",
2525
]
2626

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@ PyJWT >= 2.6.0, < 3
33
requests >= 2.28.0, < 3
44
cryptography >= 40.0.0
55
pyOpenSSL >= 23.1.1
6-
asn1==2.8.0
6+
asn1==3.2.0
77
cattrs >= 23.1.2
88
httpx==0.28.1

0 commit comments

Comments
 (0)