25
25
import os
26
26
import os .path
27
27
from plistlib import readPlist
28
+ import lxml .html
29
+ import re
28
30
29
31
30
32
class TemporaryDirectory (object ):
@@ -58,9 +60,8 @@ def parse_options():
58
60
parser .add_option ('-o' , '--output' , help = 'the directory where the extracted files are placed to.' )
59
61
(options , args ) = parser .parse_args ()
60
62
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.' )
64
65
65
66
parser .destroy ()
66
67
@@ -80,12 +81,121 @@ def parse_options():
80
81
'iPod4,1' : 'iPod touch 4G' ,
81
82
}
82
83
84
+ _parenthesis_sub = re .compile ('\s|\([^)]+\)|\..+$' ).sub
85
+ _key_matcher = re .compile ('\s*([\w ]+):\s*([a-fA-F\d]+)' ).search
83
86
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' ]
86
119
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
88
152
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
+
89
199
output_dir = options .output
90
200
should_extract = True
91
201
@@ -99,41 +209,21 @@ def main():
99
209
100
210
101
211
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
+
119
217
build_manifest_file = os .path .join (output_dir , 'BuildManifest.plist' )
120
218
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 )
121
222
122
- product_type = plist_obj ['SupportedProductTypes' ][0 ]
123
- product_name = _products .get (product_type , product_type )
124
- version = plist_obj ['ProductVersion' ]
125
223
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' ]
131
224
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 ))
134
225
135
226
136
-
137
227
138
228
if __name__ == '__main__' :
139
229
main ()
0 commit comments