diff --git a/appstoreserverlibrary/receipt_utility.py b/appstoreserverlibrary/receipt_utility.py index 7c66274b..be47cc30 100644 --- a/appstoreserverlibrary/receipt_utility.py +++ b/appstoreserverlibrary/receipt_utility.py @@ -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. @@ -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]: """ @@ -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() \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 56e100a5..610a3fbb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", ] diff --git a/requirements.txt b/requirements.txt index 70edeeaa..81519baf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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