├── README.md
├── pyproject.toml
├── LICENSE
├── .gitignore
├── pdm.lock
└── getnonce.py
/README.md:
--------------------------------------------------------------------------------
1 | # getnonce
2 |
3 | Get an apnonce/generator pair from any\* iDevice (including A12+) without jailbreak
4 |
5 | \* May not work on really old devices, but you only really need this on A12+ anyway.
6 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "getnonce"
3 | version = "0.1.0"
4 | description = "Get an apnonce/generator pair from any iDevice (including A12+) without jailbreak"
5 | authors = [
6 | { name = "nyuszika7h", email = "nyuszika7h@gmail.com" },
7 | ]
8 | license = { text = "MIT" }
9 | requires-python = "~= 3.8"
10 | dependencies = [
11 | "rich ~= 12.5",
12 | ]
13 |
14 | [project.optional-dependencies]
15 | dev = [
16 | "Flake8-pyproject ~= 1.1",
17 | ]
18 |
19 | [build-system]
20 | requires = ["pdm-pep517"]
21 | build-backend = "pdm.pep517.api"
22 |
23 | [tool.flake8]
24 | max-line-length = 120
25 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 nyuszika7h
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | #.idea/
161 |
--------------------------------------------------------------------------------
/pdm.lock:
--------------------------------------------------------------------------------
1 | [[package]]
2 | name = "commonmark"
3 | version = "0.9.1"
4 | summary = "Python parser for the CommonMark Markdown spec"
5 |
6 | [[package]]
7 | name = "flake8"
8 | version = "5.0.4"
9 | requires_python = ">=3.6.1"
10 | summary = "the modular source code checker: pep8 pyflakes and co"
11 | dependencies = [
12 | "mccabe<0.8.0,>=0.7.0",
13 | "pycodestyle<2.10.0,>=2.9.0",
14 | "pyflakes<2.6.0,>=2.5.0",
15 | ]
16 |
17 | [[package]]
18 | name = "flake8-pyproject"
19 | version = "1.1.0.post0"
20 | requires_python = ">=3.6"
21 | summary = "Flake8 plug-in loading the configuration from pyproject.toml"
22 | dependencies = [
23 | "Flake8<6,>=5",
24 | "TOMLi; python_version < \"3.11\"",
25 | ]
26 |
27 | [[package]]
28 | name = "mccabe"
29 | version = "0.7.0"
30 | requires_python = ">=3.6"
31 | summary = "McCabe checker, plugin for flake8"
32 |
33 | [[package]]
34 | name = "pycodestyle"
35 | version = "2.9.1"
36 | requires_python = ">=3.6"
37 | summary = "Python style guide checker"
38 |
39 | [[package]]
40 | name = "pyflakes"
41 | version = "2.5.0"
42 | requires_python = ">=3.6"
43 | summary = "passive checker of Python programs"
44 |
45 | [[package]]
46 | name = "pygments"
47 | version = "2.13.0"
48 | requires_python = ">=3.6"
49 | summary = "Pygments is a syntax highlighting package written in Python."
50 |
51 | [[package]]
52 | name = "rich"
53 | version = "12.6.0"
54 | requires_python = ">=3.6.3,<4.0.0"
55 | summary = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
56 | dependencies = [
57 | "commonmark<0.10.0,>=0.9.0",
58 | "pygments<3.0.0,>=2.6.0",
59 | "typing-extensions<5.0,>=4.0.0; python_version < \"3.9\"",
60 | ]
61 |
62 | [[package]]
63 | name = "tomli"
64 | version = "2.0.1"
65 | requires_python = ">=3.7"
66 | summary = "A lil' TOML parser"
67 |
68 | [[package]]
69 | name = "typing-extensions"
70 | version = "4.4.0"
71 | requires_python = ">=3.7"
72 | summary = "Backported and Experimental Type Hints for Python 3.7+"
73 |
74 | [metadata]
75 | lock_version = "4.0"
76 | content_hash = "sha256:9f0e4e49d9f02d741e169c3ddef055584526a514387ba1427af3d387c66dbbad"
77 |
78 | [metadata.files]
79 | "commonmark 0.9.1" = [
80 | {url = "https://files.pythonhosted.org/packages/60/48/a60f593447e8f0894ebb7f6e6c1f25dafc5e89c5879fdc9360ae93ff83f0/commonmark-0.9.1.tar.gz", hash = "sha256:452f9dc859be7f06631ddcb328b6919c67984aca654e5fefb3914d54691aed60"},
81 | {url = "https://files.pythonhosted.org/packages/b1/92/dfd892312d822f36c55366118b95d914e5f16de11044a27cf10a7d71bbbf/commonmark-0.9.1-py2.py3-none-any.whl", hash = "sha256:da2f38c92590f83de410ba1a3cbceafbc74fee9def35f9251ba9a971d6d66fd9"},
82 | ]
83 | "flake8 5.0.4" = [
84 | {url = "https://files.pythonhosted.org/packages/ad/00/9808c62b2d529cefc69ce4e4a1ea42c0f855effa55817b7327ec5b75e60a/flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
85 | {url = "https://files.pythonhosted.org/packages/cf/a0/b881b63a17a59d9d07f5c0cc91a29182c8e8a9aa2bde5b3b2b16519c02f4/flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
86 | ]
87 | "flake8-pyproject 1.1.0.post0" = [
88 | {url = "https://files.pythonhosted.org/packages/e8/21/570600cb8862c2bfbe8926ad9b3e13b0d7e12b7a232e4849439b7816465c/flake8_pyproject-1.1.0.post0-py3-none-any.whl", hash = "sha256:55bc7cbb4272dca45ba42521954452be9d443237fa9b78a09d0424bbd5c94fab"},
89 | ]
90 | "mccabe 0.7.0" = [
91 | {url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
92 | {url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
93 | ]
94 | "pycodestyle 2.9.1" = [
95 | {url = "https://files.pythonhosted.org/packages/67/e4/fc77f1039c34b3612c4867b69cbb2b8a4e569720b1f19b0637002ee03aff/pycodestyle-2.9.1-py2.py3-none-any.whl", hash = "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b"},
96 | {url = "https://files.pythonhosted.org/packages/b6/83/5bcaedba1f47200f0665ceb07bcb00e2be123192742ee0edfb66b600e5fd/pycodestyle-2.9.1.tar.gz", hash = "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785"},
97 | ]
98 | "pyflakes 2.5.0" = [
99 | {url = "https://files.pythonhosted.org/packages/07/92/f0cb5381f752e89a598dd2850941e7f570ac3cb8ea4a344854de486db152/pyflakes-2.5.0.tar.gz", hash = "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3"},
100 | {url = "https://files.pythonhosted.org/packages/dc/13/63178f59f74e53acc2165aee4b002619a3cfa7eeaeac989a9eb41edf364e/pyflakes-2.5.0-py2.py3-none-any.whl", hash = "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2"},
101 | ]
102 | "pygments 2.13.0" = [
103 | {url = "https://files.pythonhosted.org/packages/4f/82/672cd382e5b39ab1cd422a672382f08a1fb3d08d9e0c0f3707f33a52063b/Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"},
104 | {url = "https://files.pythonhosted.org/packages/e0/ef/5905cd3642f2337d44143529c941cc3a02e5af16f0f65f81cbef7af452bb/Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"},
105 | ]
106 | "rich 12.6.0" = [
107 | {url = "https://files.pythonhosted.org/packages/11/23/814edf09ec6470d52022b9e95c23c1bef77f0bc451761e1504ebd09606d3/rich-12.6.0.tar.gz", hash = "sha256:ba3a3775974105c221d31141f2c116f4fd65c5ceb0698657a11e9f295ec93fd0"},
108 | {url = "https://files.pythonhosted.org/packages/32/60/81ac2e7d1e3b861ab478a72e3b20fc91c4302acd2274822e493758941829/rich-12.6.0-py3-none-any.whl", hash = "sha256:a4eb26484f2c82589bd9a17c73d32a010b1e29d89f1604cd9bf3a2097b81bb5e"},
109 | ]
110 | "tomli 2.0.1" = [
111 | {url = "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
112 | {url = "https://files.pythonhosted.org/packages/c0/3f/d7af728f075fb08564c5949a9c95e44352e23dee646869fa104a3b2060a3/tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
113 | ]
114 | "typing-extensions 4.4.0" = [
115 | {url = "https://files.pythonhosted.org/packages/0b/8e/f1a0a5a76cfef77e1eb6004cb49e5f8d72634da638420b9ea492ce8305e8/typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
116 | {url = "https://files.pythonhosted.org/packages/e3/a7/8f4e456ef0adac43f452efc2d0e4b242ab831297f1bac60ac815d37eb9cf/typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
117 | ]
118 |
--------------------------------------------------------------------------------
/getnonce.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 | import atexit
4 | import base64
5 | import os
6 | import plistlib
7 | import shutil
8 | import signal
9 | import subprocess
10 | import sys
11 | import time
12 |
13 | from rich.console import Console
14 | from rich.markup import escape
15 | from rich.prompt import Confirm
16 |
17 |
18 | signal.signal(signal.SIGINT, signal.SIG_DFL)
19 |
20 | console = Console(highlight=False)
21 | print = console.print
22 |
23 |
24 | @atexit.register
25 | def finish():
26 | print("\nPress Enter to exit.")
27 | input()
28 |
29 |
30 | def run_process(command, *args, silence_errors=False, timeout=None):
31 | """Run a command with the specified arguments."""
32 | try:
33 | p = subprocess.run(
34 | [command, *args], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", timeout=timeout
35 | )
36 | except subprocess.TimeoutExpired:
37 | return None
38 |
39 | if p.returncode != 0:
40 | if silence_errors:
41 | return None
42 | else:
43 | print(f"[red]{escape(p.stdout)}[/red]")
44 | sys.exit(1)
45 |
46 | return p.stdout.strip()
47 |
48 |
49 | def wait_for_device(mode):
50 | """Wait for a device to be connected over USB and unlocked."""
51 |
52 | if mode == "normal":
53 | print("[yellow]Waiting for device to be connected and unlocked[/yellow]", end="")
54 |
55 | while not run_process("idevicediagnostics", "diagnostics", silence_errors=True):
56 | print("[yellow].[/yellow]", end="")
57 | time.sleep(1)
58 | elif mode == "recovery":
59 | print("Waiting for device", end="")
60 |
61 | while not run_process("irecovery", "-m", silence_errors=True):
62 | print(".", end="")
63 |
64 | print()
65 |
66 |
67 | def lockdownd_read_int(key):
68 | """Read an integer from lockdownd."""
69 |
70 | value = run_process("ideviceinfo", "-k", key).encode()
71 | return int(value) if value else None
72 |
73 |
74 | def _format_bytes(value, endianness):
75 | return "{:x}".format(int.from_bytes(value, endianness)) if value else None
76 |
77 |
78 | def lockdownd_read_bytes(key, endianness):
79 | """Read bytes with the specified endianness from lockdownd and return it as a hex string."""
80 |
81 | value = base64.b64decode(run_process("ideviceinfo", "-k", key).encode())
82 | return _format_bytes(value, endianness)
83 |
84 |
85 | def mobilegestalt_read_bytes(key, endianness):
86 | """Read bytes with the specified endianness from MobileGestalt and return it as a hex string."""
87 |
88 | value = plistlib.loads(run_process("idevicediagnostics", "mobilegestalt", key).encode())["MobileGestalt"][key]
89 | return _format_bytes(value, endianness)
90 |
91 |
92 | def pad_apnonce(apnonce):
93 | """Pad an ApNonce to 64 characters (A10 and above) or 40 characters (A9 and below)."""
94 |
95 | padded = apnonce.zfill(64)
96 |
97 | if padded[0:24] == "000000000000000000000000":
98 | return padded[24:]
99 | else:
100 | return padded
101 |
102 |
103 | def get_recovery_apnonce(old_apnonce):
104 | """Read the ApNonce in recovery mode."""
105 |
106 | apnonce = None
107 |
108 | wait_for_device(mode="recovery")
109 |
110 | info = run_process("irecovery", "-q")
111 |
112 | if not info:
113 | print("[red]ERROR: Unable to read ApNonce[/red]")
114 | sys.exit(1)
115 |
116 | for line in info.splitlines():
117 | key, value = line.split(": ")
118 | if key == "NONC":
119 | apnonce = value
120 | break
121 |
122 | if apnonce:
123 | print(f"[green]ApNonce = [bold]{apnonce}[/bold][/green]")
124 | else:
125 | print("[red]ERROR: Unable to read ApNonce[/red]")
126 | sys.exit(1)
127 |
128 | if old_apnonce and apnonce != old_apnonce:
129 | print("[red]ERROR: ApNonce does not match[/red]")
130 |
131 | print("Exiting recovery mode")
132 | run_process("irecovery", "-n")
133 |
134 | sys.exit(1)
135 |
136 | return apnonce
137 |
138 |
139 | if __name__ == "__main__":
140 | os.environ["PATH"] = os.pathsep.join([".", os.environ["PATH"]])
141 | for binary in ["idevice_id", "idevicediagnostics", "ideviceinfo", "irecovery"]:
142 | if not shutil.which(binary):
143 | print(
144 | f"[red]ERROR: {binary} not found. Please place the binary in your PATH "
145 | f"or the same folder as the script and try again.[/red]"
146 | )
147 | sys.exit(1)
148 |
149 | jailbroken = Confirm.ask("Is your device jailbroken?")
150 |
151 | # If the device is in recovery mode, exit it.
152 | print("\n[bold]\\[1/5] Checking device state[/bold]")
153 | if run_process("irecovery", "-m", silence_errors=True, timeout=1) == "Recovery Mode":
154 | print("Exiting recovery mode")
155 | run_process("irecovery", "-n")
156 | wait_for_device(mode="normal")
157 |
158 | print("\n[bold]\\[2/5] Getting ECID[/bold]")
159 | ecid = lockdownd_read_int("UniqueChipID")
160 | print(f"[green]ECID (hex) = [bold]{ecid:X}[/bold][/green]")
161 |
162 | # If the device is not jailbroken, get the ApNonce in normal mode, which will set a new random generator.
163 | print("\n[bold]\\[3/5] Getting ApNonce[/bold]")
164 | if jailbroken:
165 | print("Skipping on jailbroken device")
166 | apnonce = None
167 | else:
168 | apnonce = lockdownd_read_bytes("ApNonce", "big")
169 | if apnonce:
170 | apnonce = pad_apnonce(apnonce)
171 | print(f"[green]ApNonce = [bold]{apnonce}[/bold][/green]")
172 | else:
173 | print("[red]ERROR: Unable to read ApNonce[/red]")
174 | sys.exit(1)
175 |
176 | print("\n[bold]\\[4/5] Getting generator[/bold]")
177 | cpid = lockdownd_read_int("ChipID")
178 | if 0x8020 <= cpid < 0x8720:
179 | # A12+ device, we can take a shortcut and avoid rebooting
180 | # Note: This value is only available via MobileGestalt and not the regular lockdownd interface
181 | generator = mobilegestalt_read_bytes("ApNonceRetrieve", "little")
182 | else:
183 | # A11- device, we must reboot to obtain the up to date generator value
184 | print("Rebooting device")
185 | run_process("idevicediagnostics", "restart")
186 | wait_for_device(mode="normal")
187 | generator = lockdownd_read_bytes("BootNonce", "little")
188 | if generator:
189 | print(f"[green]Generator = [bold]0x{generator:016}[/bold][/green]")
190 | else:
191 | print("[red]ERROR: Unable to read generator[/red]")
192 | sys.exit(1)
193 |
194 | # Read the ApNonce in recovery mode to confirm it matches.
195 | print("\n[bold]\\[5/5] Verifying ApNonce[/bold]")
196 | print("Entering recovery mode")
197 | udid = run_process("idevice_id", "-l")
198 | if not udid:
199 | print("[red]ERROR: Unable to find device[/red]")
200 | sys.exit(1)
201 | run_process("ideviceenterrecovery", udid)
202 | apnonce = get_recovery_apnonce(apnonce)
203 |
204 | # Reset and read it again to make sure the generator was set properly.
205 | print("Rebooting device")
206 | run_process("irecovery", "-c", "reset")
207 | time.sleep(5) # A delay is needed here to make sure it doesn't catch the device before it started exiting recovery
208 | apnonce = get_recovery_apnonce(apnonce)
209 |
210 | # Return to normal mode.
211 | print("Exiting recovery mode")
212 | run_process("irecovery", "-n")
213 |
214 | print("\n[bold]All done! You can go to https://tsssaver.1conan.com/v2/ to save blobs.[/bold]")
215 | print("Make sure to specify the ECID, ApNonce and generator values you got above.")
216 |
--------------------------------------------------------------------------------