Source code for appstoreserverlibrary.receipt_utility
# Copyright (c) 2023 Apple Inc. Licensed under MIT License.
from base64 import b64decode
from typing import Optional
import asn1
import base64
import re
PKCS7_OID = "1.2.840.113549.1.7.2"
IN_APP_ARRAY = 17
TRANSACTION_IDENTIFIER = 1703
ORIGINAL_TRANSACTION_IDENTIFIER = 1705
[docs]
class ReceiptUtility:
[docs]
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.
*NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API.
: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
[docs]
def extract_transaction_id_from_transaction_receipt(self, transaction_receipt: str) -> Optional[str]:
"""
Extracts a transaction id from an encoded transactional receipt. Throws if the receipt does not match the expected format.
*NO validation* is performed on the receipt, and any data returned should only be used to call the App Store Server API.
:param transactionReceipt: The unmodified transactionReceipt
:return: A transaction id, or null if no transactionId is found in the receipt
"""
decoded_top_level = base64.b64decode(transaction_receipt).decode('utf-8')
matching_result = re.search('"purchase-info"\s+=\s+"([a-zA-Z0-9+/=]+)";', decoded_top_level)
if matching_result:
decoded_inner_level = base64.b64decode(matching_result.group(1)).decode('utf-8')
inner_matching_result = re.search('"transaction-id"\s+=\s+"([a-zA-Z0-9+/=]+)";', decoded_inner_level)
if inner_matching_result:
return inner_matching_result.group(1)
return None
[docs]
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()