|
| 1 | +"""Anisette provider in a Python package.""" |
| 2 | + |
| 3 | +from __future__ import annotations |
| 4 | + |
| 5 | +import base64 |
| 6 | +import logging |
| 7 | +from contextlib import ExitStack |
| 8 | +from ctypes import c_ulonglong |
| 9 | +from typing import TYPE_CHECKING, Any, BinaryIO |
| 10 | + |
| 11 | +from typing_extensions import Self |
| 12 | + |
| 13 | +from ._ani_provider import AnisetteProvider |
| 14 | +from ._fs import FSCollection |
| 15 | +from ._library import LibraryStore |
| 16 | +from ._util import open_file |
| 17 | + |
| 18 | +if TYPE_CHECKING: |
| 19 | + from pathlib import Path |
| 20 | + |
| 21 | + from ._device import AnisetteDeviceConfig |
| 22 | + |
| 23 | + |
| 24 | +class Anisette: |
| 25 | + """ |
| 26 | + The main Anisette provider class. |
| 27 | +
|
| 28 | + This is the main Anisette provider class, which provides the user-facing functionality of this package. |
| 29 | + Each instance of :class:`Anisette` represents a single Anisette session. |
| 30 | +
|
| 31 | + This class should not be instantiated directly through its __init__ method. |
| 32 | + Instead, you should use :meth:`Anisette.init` or :meth:`Anisette.load` depending on your use case. |
| 33 | + """ |
| 34 | + |
| 35 | + def __init__(self, ani_provider: AnisetteProvider) -> None: |
| 36 | + """ |
| 37 | + Init. |
| 38 | +
|
| 39 | + :meta private: |
| 40 | + """ |
| 41 | + self._ani_provider = ani_provider |
| 42 | + |
| 43 | + self._ds_id = c_ulonglong(-2).value |
| 44 | + |
| 45 | + @classmethod |
| 46 | + def init( |
| 47 | + cls, |
| 48 | + apk_file: BinaryIO | str | Path, |
| 49 | + default_device_config: AnisetteDeviceConfig | None = None, |
| 50 | + ) -> Self: |
| 51 | + """ |
| 52 | + Initialize a new Anisette session. |
| 53 | +
|
| 54 | + :param apk_file: |
| 55 | + :param default_device_config: |
| 56 | + :return: |
| 57 | + """ |
| 58 | + with open_file(apk_file, "rb") as apk: |
| 59 | + library_store = LibraryStore.from_apk(apk) |
| 60 | + |
| 61 | + fs_collection = FSCollection(libs=library_store) |
| 62 | + ani_provider = AnisetteProvider(fs_collection, default_device_config) |
| 63 | + |
| 64 | + return cls(ani_provider) |
| 65 | + |
| 66 | + @classmethod |
| 67 | + def load(cls, *files: BinaryIO | str | Path, default_device_config: AnisetteDeviceConfig | None = None) -> Self: |
| 68 | + """ |
| 69 | + Load a previously-initialized Anisette session. |
| 70 | +
|
| 71 | + :param files: File objects or paths that together form the provider's virtual file system. These can be obtained |
| 72 | + using the :meth:`Anisette.save_provisioning`, :meth:`Anisette.save_libs` |
| 73 | + and/or :meth:`Anisette.save_all` methods. |
| 74 | + :type files: BinaryIO, str, Path |
| 75 | + :return: An instance of :class:`Anisette`. |
| 76 | + :rtype: :class:`Anisette` |
| 77 | + """ |
| 78 | + with ExitStack() as stack: |
| 79 | + file_objs = [stack.enter_context(open_file(f, "rb")) for f in files] |
| 80 | + ani_provider = AnisetteProvider.load(*file_objs, default_device_config=default_device_config) |
| 81 | + |
| 82 | + return cls(ani_provider) |
| 83 | + |
| 84 | + def save_provisioning(self, file: BinaryIO | str | Path) -> None: |
| 85 | + """ |
| 86 | + Save provisioning data of this Anisette session to a file. |
| 87 | +
|
| 88 | + The size of this file is usually in the order of kilobytes. |
| 89 | +
|
| 90 | + Saving provisioning data is required if you want to re-use this session at a later time. |
| 91 | +
|
| 92 | + A session may be reconstructed from saved data using the :meth:`Anisette.load` method. |
| 93 | +
|
| 94 | + The advantage of using this method over :meth:`Anisette.save_all` is that it results in less overall disk usage |
| 95 | + when saving many sessions, since library data can be saved separately and may be re-used across sessions. |
| 96 | +
|
| 97 | + :param file: The file or path to save provisioning data to. |
| 98 | + :type file: BinaryIO, str, Path |
| 99 | + """ |
| 100 | + with open_file(file, "wb+") as f: |
| 101 | + self._ani_provider.save(f, exclude=["libs"]) |
| 102 | + |
| 103 | + def save_libs(self, file: BinaryIO | str | Path) -> None: |
| 104 | + """ |
| 105 | + Save library data to a file. |
| 106 | +
|
| 107 | + The size of this file is usually in the order of megabytes. |
| 108 | +
|
| 109 | + Library data is session-agnostic and may be used in as many sessions as you wish. |
| 110 | +
|
| 111 | + The advantage of using this method over :meth:`Anisette.save_all` is that it results in less overall disk usage |
| 112 | + when saving many sessions, since library data can be saved separately and may be re-used across sessions. |
| 113 | +
|
| 114 | + :param file: The file or path to save library data to. |
| 115 | + :type file: BinaryIO, str, Path |
| 116 | + """ |
| 117 | + with open_file(file, "wb+") as f: |
| 118 | + self._ani_provider.save(f, include=["libs"]) |
| 119 | + |
| 120 | + def save_all(self, file: BinaryIO | str | Path) -> None: |
| 121 | + """ |
| 122 | + Save a complete copy of this Anisette session to a file. |
| 123 | +
|
| 124 | + The size of this file is usually in the order of megabytes. |
| 125 | +
|
| 126 | + Saving session data is required if you want to re-use this session at a later time. |
| 127 | +
|
| 128 | + A session may be reconstructed from saved data using the :meth:`Anisette.load` method. |
| 129 | +
|
| 130 | + The advantage of using this method over :meth:`Anisette.save_provisioning` and :meth:`Anisette.save_libs` |
| 131 | + is that it is easier to use, since all information to reconstruct the session is contained in a single file. |
| 132 | +
|
| 133 | + :param file: The file or path to save session data to. |
| 134 | + :type file: BinaryIO, str, Path |
| 135 | + """ |
| 136 | + with open_file(file, "wb+") as f: |
| 137 | + self._ani_provider.save(f) |
| 138 | + |
| 139 | + def provision(self) -> None: |
| 140 | + """ |
| 141 | + Provision the virtual device, if it has not been provisioned yet. |
| 142 | +
|
| 143 | + In most cases it is not necessary to manually use this method, since :meth:`Anisette.get_data` |
| 144 | + will call it implicitly. |
| 145 | + """ |
| 146 | + if not self._ani_provider.adi.is_machine_provisioned(self._ds_id): |
| 147 | + logging.info("Provisioning...") |
| 148 | + self._ani_provider.provisioning_session.provision(self._ds_id) |
| 149 | + |
| 150 | + def get_data(self) -> dict[str, Any]: # FIXME: make TypedDict |
| 151 | + """ |
| 152 | + Obtain Anisette headers for this session. |
| 153 | +
|
| 154 | + :return: Anisette headers that may be used for authentication purposes. |
| 155 | + """ |
| 156 | + self.provision() |
| 157 | + otp = self._ani_provider.adi.request_otp(self._ds_id) |
| 158 | + |
| 159 | + # FIXME: return other fields as well |
| 160 | + return { |
| 161 | + "X-Apple-I-MD": base64.b64encode(bytes(otp.otp)).decode(), |
| 162 | + "X-Apple-I-MD-M": base64.b64encode(bytes(otp.machine_id)).decode(), |
| 163 | + } |
0 commit comments