Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 25 additions & 78 deletions appstoreserverlibrary/receipt_utility.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
ORIGINAL_TRANSACTION_IDENTIFIER = 1705

class ReceiptUtility:
def _decode_octet_string(self, octet_string: bytes):
decoder = asn1.Decoder()
decoder.start(octet_string)
_, value = decoder.read()
return value

def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[str]:
"""
Extracts a transaction id from an encoded App Receipt. Throws if the receipt does not match the expected format.
Expand All @@ -21,68 +27,25 @@ def extract_transaction_id_from_app_receipt(self, app_receipt: str) -> Optional[
:param appReceipt: The unmodified app receipt
:return: A transaction id from the array of in-app purchases, null if the receipt contains no in-app purchases
"""
decoder = IndefiniteFormAwareDecoder()
decoder.start(b64decode(app_receipt, validate=True))
tag = decoder.peek()
if tag.typ != asn1.Types.Constructed or tag.nr != asn1.Numbers.Sequence:
raise ValueError()
decoder.enter()
# PKCS#7 object
tag, value = decoder.read()
if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.ObjectIdentifier or value != PKCS7_OID:
raise ValueError()
# This is the PKCS#7 format, work our way into the inner content
decoder.enter()
decoder.enter()
decoder.read()
decoder.read()
decoder.enter()
decoder.read()
decoder.enter()
tag, value = decoder.read()
# Xcode uses nested OctetStrings, we extract the inner string in this case
if tag.typ == asn1.Types.Constructed and tag.nr == asn1.Numbers.OctetString:
inner_decoder = asn1.Decoder()
inner_decoder.start(value)
tag, value = inner_decoder.read()
if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.OctetString:
raise ValueError()
decoder = asn1.Decoder()
decoder.start(value)
tag = decoder.peek()
if tag.typ != asn1.Types.Constructed or tag.nr != asn1.Numbers.Set:
raise ValueError()
decoder.enter()
# We are in the top-level sequence, work our way to the array of in-apps
while not decoder.eof():
decoder.enter()
tag, value = decoder.read()
if tag.typ == asn1.Types.Primitive and tag.nr == asn1.Numbers.Integer and value == IN_APP_ARRAY:
decoder.read()
tag, value = decoder.read()
if tag.typ != asn1.Types.Primitive or tag.nr != asn1.Numbers.OctetString:
raise ValueError()
inapp_decoder = asn1.Decoder()
inapp_decoder.start(value)
inapp_decoder.enter()
# In-app array
while not inapp_decoder.eof():
inapp_decoder.enter()
tag, value = inapp_decoder.read()
if (
tag.typ == asn1.Types.Primitive
and tag.nr == asn1.Numbers.Integer
and (value == TRANSACTION_IDENTIFIER or value == ORIGINAL_TRANSACTION_IDENTIFIER)
):
inapp_decoder.read()
tag, value = inapp_decoder.read()
singleton_decoder = asn1.Decoder()
singleton_decoder.start(value)
tag, value = singleton_decoder.read()
return value
inapp_decoder.leave()
decoder.leave()
return None
try:
val = self._decode_octet_string(b64decode(app_receipt, validate=True))
found_oid = val[0]
if found_oid != PKCS7_OID:
raise ValueError()
inner_value = val[1][0][2][1][0]
# Xcode uses nested OctetStrings, we extract the inner string in this case
value = self._decode_octet_string(inner_value)
# We are in the top-level sequence, work our way to the array of in-apps
for inner_value in value:
if inner_value[0] == IN_APP_ARRAY:
array_values = self._decode_octet_string(inner_value[2])
# In-app array
for array_value in array_values:
if array_value[0] == TRANSACTION_IDENTIFIER or array_value[0] == ORIGINAL_TRANSACTION_IDENTIFIER:
return self._decode_octet_string(array_value[2])
return None
except Exception as e:
raise ValueError(e)

def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: str) -> Optional[str]:
"""
Expand All @@ -99,19 +62,3 @@ def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: s
if inner_matching_result:
return inner_matching_result.group(1)
return None

class IndefiniteFormAwareDecoder(asn1.Decoder):
def _read_length(self) -> int:
index, input_data = self.m_stack[-1]
try:
byte = input_data[index]
except IndexError:
raise asn1.Error('Premature end of input.')
if byte == 0x80:
# Xcode receipts use indefinite length encoding, not supported by all parsers
# Indefinite length encoding is only entered, but never left during parsing for receipts
# We therefore round up indefinite length encoding to be the remaining length
self._read_byte()
index, input_data = self.m_stack[-1]
return len(input_data) - index
return super()._read_length()
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ dependencies = [
"requests>=2.28.0,<3",
"cryptography>=40.0.0",
"pyOpenSSL>=23.1.1",
"asn1==2.8.0",
"asn1==3.2.0",
"cattrs>=23.1.2",
]

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ PyJWT >= 2.6.0, < 3
requests >= 2.28.0, < 3
cryptography >= 40.0.0
pyOpenSSL >= 23.1.1
asn1==2.8.0
asn1==3.2.0
cattrs >= 23.1.2
httpx==0.28.1