Skip to content

Commit d8ebc58

Browse files
committed
ipsw_decrypt: Get and match keys from theiphonewiki
1 parent 0d88bcd commit d8ebc58

File tree

1 file changed

+124
-34
lines changed

1 file changed

+124
-34
lines changed

ipsw_decrypt.py

+124-34
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
import os
2626
import os.path
2727
from plistlib import readPlist
28+
import lxml.html
29+
import re
2830

2931

3032
class TemporaryDirectory(object):
@@ -58,9 +60,8 @@ def parse_options():
5860
parser.add_option('-o', '--output', help='the directory where the extracted files are placed to.')
5961
(options, args) = parser.parse_args()
6062

61-
if not args:
62-
parser.error('Please supply the path to the IPSW file.')
63-
options = None
63+
if not args and not os.path.isdir(options.output):
64+
parser.error('Please supply the path to the IPSW file or an existing output directory that contains the extracted firmware.')
6465

6566
parser.destroy()
6667

@@ -80,12 +81,121 @@ def parse_options():
8081
'iPod4,1': 'iPod touch 4G',
8182
}
8283

84+
_parenthesis_sub = re.compile('\s|\([^)]+\)|\..+$').sub
85+
_key_matcher = re.compile('\s*([\w ]+):\s*([a-fA-F\d]+)').search
8386

84-
def main():
85-
(options, args) = parse_options()
87+
def extract_zipfile(ipsw_path):
88+
with TemporaryDirectory() as td:
89+
print("<Info> Extracting content from {0}, it may take a minute...".format(ipsw_path))
90+
with closing(ZipFile(ipsw_path)) as zipfile:
91+
zipfile.extractall(td.directory)
92+
93+
if output_dir is None:
94+
build_manifest_file = os.path.join(td.directory, 'BuildManifest.plist')
95+
plist_obj = readPlist(build_manifest_file)
96+
product_type = plist_obj['SupportedProductTypes'][0]
97+
product_name = _products.get(product_type, product_type)
98+
version = plist_obj['ProductVersion']
99+
build = plist_obj['ProductBuildVersion']
100+
101+
output_dir = '{0}, {1} ({2})'.format(product_name, version, build)
102+
103+
td.move(output_dir)
104+
print("<Info> Extracted firmware to '{0}'. You may use the '-o \"{0}\"' switch in the future to skip this step.".format(output_dir))
105+
106+
107+
_header_replacement_get = {
108+
'mainfilesystem': 'os',
109+
'rootfilesystem': 'os',
110+
'glyphcharging': 'batterycharging',
111+
'glyphplugin': 'batteryplugin',
112+
}.get
113+
114+
115+
def get_decryption_info(plist_obj, output_dir, url=None):
116+
product_type = plist_obj['SupportedProductTypes'][0]
117+
product_name = _products.get(product_type, product_type)
118+
version = plist_obj['ProductVersion']
86119

87-
ipsw_path = args[0]
120+
build_info = plist_obj['BuildIdentities'][0]['Info']
121+
build_train = build_info['BuildTrain']
122+
build_number = build_info['BuildNumber']
123+
device_class = build_info['DeviceClass']
124+
125+
print("<Info> {0} ({1}), class {2}".format(product_name, product_type, device_class))
126+
print("<Info> iOS version {0}, build {1} {2}".format(version, build_train, build_number))
127+
128+
if url is None:
129+
url = 'http://theiphonewiki.com/wiki/index.php?title={0}_{1}_({2})'.format(build_train, build_number, product_name.translate({0x20:'_'}))
130+
131+
print("<Info> Downloading decryption keys from '{0}'...".format(url))
132+
133+
try:
134+
htmldoc = lxml.html.parse(url)
135+
except IOError as e:
136+
print("<Error> {1}".format(url, e))
137+
return None
138+
139+
headers = htmldoc.iterfind('//h3/span[@class="mw-headline"]')
140+
key_map = {}
141+
for tag in headers:
142+
header_name = _parenthesis_sub('', tag.text_content()).strip().lower()
143+
header_name = _header_replacement_get(header_name, header_name)
144+
ul = tag.getparent().getnext()
145+
keys = {}
146+
for li in ul.iterchildren('li'):
147+
m = _key_matcher(li.text_content())
148+
if m:
149+
(key_type, key_value) = m.groups()
150+
keys[key_type] = key_value
151+
key_map[header_name] = keys
88152

153+
print("<Info> Retrieved {0} keys.".format(len(key_map)))
154+
return key_map
155+
156+
157+
def decrypted_filename(path):
158+
(root, ext) = os.path.splitext(path)
159+
return root + '.decrypted' + ext
160+
161+
162+
def build_file_decryption_map(plist_obj, key_map):
163+
for identity in plist_obj['BuildIdentities']:
164+
behavior = identity['Info']['RestoreBehavior']
165+
for key, content in identity['Manifest'].items():
166+
key_lower = key.lower()
167+
if key_lower.startswith('restore'):
168+
if key_lower == 'restoreramdisk' and behavior == 'Update':
169+
key_lower = 'updateramdisk'
170+
else:
171+
continue
172+
173+
path = os.path.join(output_dir, content['Info']['Path'])
174+
dec_path = decrypted_filename(path)
175+
176+
skip_reason = None
177+
level = 'Notice'
178+
if key_lower not in key_map:
179+
skip_reason = 'No decryption key'
180+
elif not os.path.exists(path):
181+
if os.path.exists(dec_path):
182+
skip_reason = 'Already decrypted'
183+
level = 'Info'
184+
else:
185+
skip_reason = 'File does not exist'
186+
187+
if skip_reason:
188+
print("<{3}> Skipping {0} at '{1}': {2}".format(key, content['Info']['Path'], skip_reason, level))
189+
else:
190+
file_key_map[path] = {'dec_path': dec_path, 'keys': key_map[key_lower]}
191+
192+
return file_key_map
193+
194+
195+
196+
def main():
197+
(options, args) = parse_options()
198+
89199
output_dir = options.output
90200
should_extract = True
91201

@@ -99,41 +209,21 @@ def main():
99209

100210

101211
if should_extract:
102-
with TemporaryDirectory() as td:
103-
print("<Info> Extracting content from {0}, it may take a minute...".format(ipsw_path))
104-
with closing(ZipFile(ipsw_path)) as zipfile:
105-
zipfile.extractall(td.directory)
106-
107-
if output_dir is None:
108-
build_manifest_file = os.path.join(td.directory, 'BuildManifest.plist')
109-
plist_obj = readPlist(build_manifest_file)
110-
product_type = plist_obj['SupportedProductTypes'][0]
111-
product_name = _products.get(product_type, product_type)
112-
version = plist_obj['ProductVersion']
113-
build = plist_obj['ProductBuildVersion']
114-
115-
output_dir = '{0}, {1} ({2})'.format(product_name, version, build)
116-
117-
td.move(output_dir)
118-
212+
if not args:
213+
print("<Error> Please supply the path to the IPSW file.")
214+
return
215+
extract_zipfile(args[0])
216+
119217
build_manifest_file = os.path.join(output_dir, 'BuildManifest.plist')
120218
plist_obj = readPlist(build_manifest_file)
219+
220+
key_map = get_decryption_info(plist_obj, output_dir, options.url)
221+
file_key_map = build_file_decryption_map(plist_obj, key_map)
121222

122-
product_type = plist_obj['SupportedProductTypes'][0]
123-
product_name = _products.get(product_type, product_type)
124-
version = plist_obj['ProductVersion']
125223

126-
build_identity = plist_obj['BuildIdentities'][0]
127-
build_info = build_identity['Info']
128-
build_train = build_info['BuildTrain']
129-
build_number = build_info['BuildNumber']
130-
device_class = build_info['DeviceClass']
131224

132-
print("<Info> {0} ({1}), class {2}".format(product_name, product_type, device_class))
133-
print("<Info> iOS version {0}, build {1} {2}".format(version, build_train, build_number))
134225

135226

136-
137227

138228
if __name__ == '__main__':
139229
main()

0 commit comments

Comments
 (0)