├── usage.PNG
├── script.txt
├── README.md
├── NotificationParams.txt
├── iOSNotificationsParser.py
└── ccl_bplist.py
/usage.PNG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/abrignoni/iOS-Notifications-Parser/HEAD/usage.PNG
--------------------------------------------------------------------------------
/script.txt:
--------------------------------------------------------------------------------
1 |
46 |
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # iOS-Notifications-Parser
2 | Python script that generates a HTML triage report of iOS notifications content.
3 |
4 | 
5 |
6 | Usage:
7 | ~~python iOSNotificationsParser.py /path/to/data/directory~~
8 | Now supports iOS 11 notifications.
9 | python iOSNotificationsParser.py -v {11, 12} /path/to/data/directory
10 |
11 | See blog post here for more details:
12 | https://abrignoni.blogspot.com/2019/08/ios-12-notifications-triage-parser.html
13 |
14 | For details on the data source location for iO notifications see the blog post here:
15 | https://blog.d204n6.com/2019/08/ios-12-delivered-notifications-and-new.html
16 |
17 | Requisites:
18 | 1) Python 3 .
19 | 2) The ccl_bplist module is required for the script to work. It can be found here: https://github.com/cclgroupltd/ccl-bplist (But a version has been inluded in this repo) .
20 | 3) The included script.txt enables fields in the HTML report to be toggled between show and hide.
21 | 4) The included NotificationParams.txt defines which values to be toggled between show and hide. Add more as needed one value per line.
22 |
23 | After process is completed a folder will be created in the same directory where the script is located. The folder will be named TriageReports_script_run_timestamp.
24 |
25 | Caveat:
26 | Script depends on the UserNotification directory (where notifications on iOS are kept) to be at least one level down (or more) from the data directory provided to the script.
27 |
--------------------------------------------------------------------------------
/NotificationParams.txt:
--------------------------------------------------------------------------------
1 | AppNotificationMessage
2 | CriticalAlertSound
3 | ShouldHideTime
4 | AppNotificationMessageLocalizationArguments
5 | ShouldSuppressSyncDismissalWhenRemoved
6 | ToneAlertType
7 | BadgeApplicationIcon
8 | UNNotificationNotificationCenterDestination
9 | ShouldHideDate
10 | ShouldPreventNotificationDismissalAfterDefaultAction
11 | AppNotificationIdentifier
12 | ShouldIgnoreDowntime
13 | AppNotificationSummaryArgumentCount
14 | TriggerRepeats
15 | AppNotificationMessageLocazationKey
16 | UNNotificationUserInfo
17 | SoundMaximumDuration
18 | AppNotificationCreationDate
19 | UNNotificationDefaultDestinations
20 | UNNotificationAlertDestination
21 | ShouldSuppressScreenLightUp
22 | SoundShouldRepeat
23 | SoundShouldIgnoreRingerSwitch
24 | AppNotificationBadgeNumber
25 | HasDefaultActionKey
26 | ShouldPresentAlert
27 | ShouldPlaySound
28 | UNNotificationCarPlayDestination
29 | ShouldIgnoreDoNotDisturb
30 | TriggerTimeInterval
31 | UNNotificationLockScreenDestination
32 | ToneMediaLibraryItemIdentifier
33 | ShouldBackgroundDefaultAction
34 | AppNotificationContentAvailable
35 | ShouldAuthenticateDefaultAction
36 | SchemaVersion
37 | AppNotificationMutableContent
38 | UNNotificationTriggerType
39 | ShouldUseRequestIdentifierForDismissalSync
40 | TriggerRepeatInterval
41 | SBSPushStoreNotificationThreadKey
42 | AppNotificationAttachments
43 | ToneFileName
44 | Header
45 | AppNotificationSummaryArgument
46 | SBSPushStoreNotificationCategoryKey
47 | AppNotificationTitle
48 | ' '
49 | $null
--------------------------------------------------------------------------------
/iOSNotificationsParser.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import argparse
3 | from argparse import RawTextHelpFormatter
4 | from six.moves.configparser import RawConfigParser
5 | import sys
6 | import ccl_bplist
7 | import plistlib
8 | import io
9 | import os
10 | import glob
11 | import datetime
12 | import argparse
13 | from time import process_time
14 |
15 | parser = argparse.ArgumentParser(description="\
16 | iOS Notifications Traige Parser\
17 | \n\n Process iOS notification files for triage."
18 | , prog='iOSNotificationsParser.py'
19 | , formatter_class=RawTextHelpFormatter)
20 | parser.add_argument('-v', choices=['11','12'], required=True, action="store",help="iOS Version (required).")
21 | parser.add_argument('data_dir_to_analyze',help="Path to Data Directory.")
22 | args = parser.parse_args()
23 | version = args.v
24 | data_dir = args.data_dir_to_analyze
25 |
26 | print("\n--------------------------------------------------------------------------------------")
27 | print("iOS Notification Parser.")
28 | print("Objective: Triage iOS notifications content.")
29 | print("By: Alexis Brignoni | @AlexisBrignoni | abrignoni.com")
30 | print("Data Directory: " + data_dir)
31 | print("\n--------------------------------------------------------------------------------------")
32 | print("")
33 |
34 | start = process_time()
35 |
36 | #load common notification parameters
37 | with open('NotificationParams.txt', 'r') as f:
38 | notiparams = [line.strip() for line in f]
39 |
40 | #calculate timestamps
41 | unix = datetime.datetime(1970, 1, 1) # UTC
42 | cocoa = datetime.datetime(2001, 1, 1) # UTC
43 | delta = cocoa - unix
44 |
45 |
46 | def parse_ios11():
47 | pathfound = 0
48 | count = 0
49 | notdircount = 0
50 | exportedbplistcount = 0
51 |
52 | for root, dirs, filenames in os.walk(data_dir):
53 | for f in dirs:
54 | if f == "PushStore":
55 | pathfound = os.path.join(root, f)
56 |
57 | if pathfound == 0:
58 | print("No PushStore directory located")
59 | else:
60 | folder = ("TriageReports_" + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')) #add the date thing from phill
61 | os.makedirs( folder )
62 | print("Processing:")
63 | for filename in glob.iglob(pathfound+'\**', recursive=True):
64 | if os.path.isfile(filename): # filter dirs
65 | file_name = os.path.splitext(os.path.basename(filename))[0]
66 | #get extension and iterate on those files
67 | #file_extension = os.path.splitext(filename)
68 | #print(file_extension)
69 | #create directory
70 | if filename.endswith('pushstore'):
71 | #create directory where script is running from
72 | print (filename) #full path
73 | notdircount = notdircount + 1
74 | #print (os.path.basename(file_name)) #filename with no extension
75 | openplist = (os.path.basename(os.path.normpath(filename))) #filename with extension
76 | #print (openplist)
77 | #bundlepath = (os.path.basename(os.path.dirname(filename)))#previous directory
78 | bundlepath = file_name
79 | appdirect = (folder + "\\"+ bundlepath)
80 | #print(appdirect)
81 | os.makedirs( appdirect )
82 |
83 | #open the plist
84 | p = open(filename, 'rb')
85 | plist = ccl_bplist.load(p)
86 | plist2 = plist["$objects"]
87 |
88 | long = len(plist2)
89 | #print (long)
90 | h = open('./'+appdirect+'/DeliveredNotificationsReport.html', 'w') #write report
91 | h.write('
')
92 | h.write('iOS Delivered Notifications Triage Report
')
93 | h.write(filename)
94 | h.write('
')
95 | h.write ('')
96 | h.write('
')
97 |
98 | h.write('')
99 | h.write('')
100 |
101 | with open("script.txt") as f:
102 | for line in f:
103 | h.write(line)
104 |
105 | h.write('
')
106 | h.write('')
107 | h.write('')
108 | h.write('| Data type | ')
109 | h.write('Value | ')
110 | h.write('
')
111 |
112 | h.write('')
113 | h.write('| Plist | ')
114 | h.write('Initial Values | ')
115 | h.write('
')
116 |
117 | test = 0
118 | for i in range (0, long):
119 | try:
120 | if (plist2[i]['$classes']):
121 | h.write('')
122 | h.write('| $classes | ')
123 | ob6 = str(plist2[i]['$classes'])
124 | h.write('')
125 | h.write(str(ob6))
126 | h.write(' | ')
127 | h.write('
')
128 | test = 1
129 | except:
130 | pass
131 | try:
132 | if (plist2[i]['$class']):
133 | h.write('')
134 | h.write('| $class | ')
135 | ob5 = str(plist2[i]['$class'])
136 | h.write('')
137 | h.write(str(ob5))
138 | h.write(' | ')
139 | h.write('
')
140 | test = 1
141 | except:
142 | pass
143 | try:
144 | if (plist2[i]['NS.keys']):
145 | h.write('')
146 | h.write('| NS.keys | ')
147 | ob0 = str(plist2[i]['NS.keys'])
148 | h.write('')
149 | h.write(str(ob0))
150 | h.write(' | ')
151 | h.write('
')
152 | test = 1
153 | except:
154 | pass
155 | try:
156 | if (plist2[i]['NS.objects']):
157 | ob1 = str(plist2[i]['NS.objects'])
158 | h.write('')
159 | h.write('| NS.objects | ')
160 | h.write('')
161 | h.write(str(ob1))
162 | h.write(' | ')
163 | h.write('
')
164 |
165 | test = 1
166 | except:
167 | pass
168 | try:
169 | if (plist2[i]['NS.time']):
170 | dia = str(plist2[i]['NS.time'])
171 | dias = (dia.rsplit('.', 1)[0])
172 | timestamp = datetime.datetime.fromtimestamp(int(dias)) + delta
173 | #print (timestamp)
174 |
175 | h.write('')
176 | h.write('| Time UTC | ')
177 | h.write('')
178 | h.write(str(timestamp))
179 | #h.write(str(plist2[i]['NS.time']))
180 | h.write(' | ')
181 | h.write('
')
182 |
183 | test = 1
184 | except:
185 | pass
186 | try:
187 | if (plist2[i]['NS.base']):
188 | ob2 = str(plist2[i]['NS.objects'])
189 | h.write('')
190 | h.write('| NS.base | ')
191 | h.write('')
192 | h.write(str(ob2))
193 | h.write(' | ')
194 | h.write('
')
195 |
196 | test = 1
197 | except:
198 | pass
199 | try:
200 | if (plist2[i]['$classname']):
201 | ob3 = str(plist2[i]['$classname'])
202 | h.write('')
203 | h.write('| $classname | ')
204 | h.write('')
205 | h.write(str(ob3))
206 | h.write(' | ')
207 | h.write('
')
208 |
209 | test = 1
210 | except:
211 | pass
212 | try:
213 | if test == 0:
214 | if (plist2[i]) == "AppNotificationMessage":
215 | h.write('
')
216 | h.write('
')
217 | h.write('')
218 | h.write('')
219 | h.write('| Data type | ')
220 | h.write('Value | ')
221 | h.write('
')
222 |
223 | h.write('')
224 | h.write('| ASCII | ')
225 | h.write(''+str(plist2[i])+' | ')
226 | h.write('
')
227 |
228 |
229 | else:
230 | if plist2[i] in notiparams:
231 | h.write('')
232 | h.write('| ASCII | ')
233 | h.write(''+str(plist2[i])+' | ')
234 | h.write('
')
235 | elif plist2[i] == " ":
236 | h.write('')
237 | h.write('| Null | ')
238 | h.write(''+str(plist2[i])+' | ')
239 | h.write('
')
240 | else:
241 | h.write('')
242 | h.write('| ASCII | ')
243 | h.write(''+str(plist2[i])+' | ')
244 | h.write('
')
245 |
246 | except:
247 | pass
248 |
249 | test = 0
250 |
251 |
252 | #h.write('test')
253 |
254 |
255 | for dict in plist2:
256 | liste = dict
257 | types = (type(liste))
258 | #print (types)
259 | try:
260 | for k, v in liste.items():
261 | if k == 'NS.data':
262 | chk = str(v)
263 | reduced = (chk[2:8])
264 | #print (reduced)
265 | if reduced == "bplist":
266 | count = count + 1
267 | binfile = open('./'+appdirect+'/incepted'+str(count)+'.bplist', 'wb')
268 | binfile.write(v)
269 | binfile.close()
270 |
271 | procfile = open('./'+appdirect+'/incepted'+str(count)+'.bplist', 'rb')
272 | secondplist = ccl_bplist.load(procfile)
273 | secondplistint = secondplist["$objects"]
274 | print('Bplist processed and exported.')
275 | exportedbplistcount = exportedbplistcount + 1
276 | h.write('')
277 | h.write('| NS.data | ')
278 | h.write('')
279 | h.write(str(secondplistint))
280 | h.write(' | ')
281 | h.write('
')
282 |
283 | procfile.close()
284 | count = 0
285 | else:
286 | h.write('')
287 | h.write('| NS.data | ')
288 | h.write('')
289 | h.write(str(secondplistint))
290 | h.write(' | ')
291 | h.write('
')
292 | except:
293 | pass
294 | h.close()
295 | elif 'AttachmentsList' in file_name:
296 | test = 0 #future development
297 | end = process_time()
298 | time = start - end
299 | print(" ")
300 | print("Process completed.")
301 | print("Processing time: " + str(abs(time)) )
302 |
303 | print("Total notification directories processed:"+str(notdircount))
304 | print("Total exported bplists from notifications:"+str(exportedbplistcount))
305 |
306 | def parse_ios12():
307 | pathfound = 0
308 | count = 0
309 | notdircount = 0
310 | exportedbplistcount = 0
311 | for root, dirs, filenames in os.walk(data_dir):
312 | for f in dirs:
313 | if f == "UserNotifications":
314 | pathfound = os.path.join(root, f)
315 |
316 | if pathfound == 0:
317 | print("No UserNotifications directory located")
318 | else:
319 | folder = ("TriageReports_" + datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')) #add the date thing from phill
320 | os.makedirs( folder )
321 | print("Processing:")
322 | for filename in glob.iglob(pathfound+'\**', recursive=True):
323 | if os.path.isfile(filename): # filter dirs
324 | file_name = os.path.splitext(os.path.basename(filename))[0]
325 | #create directory
326 | if 'DeliveredNotifications' in file_name:
327 | #create directory where script is running from
328 | print (filename) #full path
329 | notdircount = notdircount + 1
330 | #print (os.path.basename(file_name)) #filename with no extension
331 | openplist = (os.path.basename(os.path.normpath(filename))) #filename with extension
332 | #print (openplist)
333 | bundlepath = (os.path.basename(os.path.dirname(filename)))#previous directory
334 | appdirect = (folder + "\\"+ bundlepath)
335 | #print(appdirect)
336 | os.makedirs( appdirect )
337 |
338 | #open the plist
339 | p = open(filename, 'rb')
340 | plist = ccl_bplist.load(p)
341 | plist2 = plist["$objects"]
342 |
343 | long = len(plist2)
344 | #print (long)
345 | h = open('./'+appdirect+'/DeliveredNotificationsReport.html', 'w') #write report
346 | h.write('')
347 | h.write('iOS Delivered Notifications Triage Report
')
348 | h.write ('')
349 | h.write(filename)
350 | h.write('
')
351 | h.write('
')
352 |
353 | h.write('')
354 | h.write('')
355 |
356 | with open("script.txt") as f:
357 | for line in f:
358 | h.write(line)
359 |
360 | h.write('
')
361 | h.write('')
362 | h.write('')
363 | h.write('| Data type | ')
364 | h.write('Value | ')
365 | h.write('
')
366 |
367 | h.write('')
368 | h.write('| Plist | ')
369 | h.write('Initial Values | ')
370 | h.write('
')
371 |
372 | test = 0
373 | for i in range (0, long):
374 | try:
375 | if (plist2[i]['$classes']):
376 | h.write('')
377 | h.write('| $classes | ')
378 | ob6 = str(plist2[i]['$classes'])
379 | h.write('')
380 | h.write(str(ob6))
381 | h.write(' | ')
382 | h.write('
')
383 | test = 1
384 | except:
385 | pass
386 | try:
387 | if (plist2[i]['$class']):
388 | h.write('')
389 | h.write('| $class | ')
390 | ob5 = str(plist2[i]['$class'])
391 | h.write('')
392 | h.write(str(ob5))
393 | h.write(' | ')
394 | h.write('
')
395 | test = 1
396 | except:
397 | pass
398 | try:
399 | if (plist2[i]['NS.keys']):
400 | h.write('')
401 | h.write('| NS.keys | ')
402 | ob0 = str(plist2[i]['NS.keys'])
403 | h.write('')
404 | h.write(str(ob0))
405 | h.write(' | ')
406 | h.write('
')
407 | test = 1
408 | except:
409 | pass
410 | try:
411 | if (plist2[i]['NS.objects']):
412 | ob1 = str(plist2[i]['NS.objects'])
413 | h.write('')
414 | h.write('| NS.objects | ')
415 | h.write('')
416 | h.write(str(ob1))
417 | h.write(' | ')
418 | h.write('
')
419 |
420 | test = 1
421 | except:
422 | pass
423 | try:
424 | if (plist2[i]['NS.time']):
425 | dia = str(plist2[i]['NS.time'])
426 | dias = (dia.rsplit('.', 1)[0])
427 | timestamp = datetime.datetime.fromtimestamp(int(dias)) + delta
428 | #print (timestamp)
429 |
430 | h.write('')
431 | h.write('| Time UTC | ')
432 | h.write('')
433 | h.write(str(timestamp))
434 | #h.write(str(plist2[i]['NS.time']))
435 | h.write(' | ')
436 | h.write('
')
437 |
438 | test = 1
439 | except:
440 | pass
441 | try:
442 | if (plist2[i]['NS.base']):
443 | ob2 = str(plist2[i]['NS.objects'])
444 | h.write('')
445 | h.write('| NS.base | ')
446 | h.write('')
447 | h.write(str(ob2))
448 | h.write(' | ')
449 | h.write('
')
450 |
451 | test = 1
452 | except:
453 | pass
454 | try:
455 | if (plist2[i]['$classname']):
456 | ob3 = str(plist2[i]['$classname'])
457 | h.write('')
458 | h.write('| $classname | ')
459 | h.write('')
460 | h.write(str(ob3))
461 | h.write(' | ')
462 | h.write('
')
463 |
464 | test = 1
465 | except:
466 | pass
467 | try:
468 | if test == 0:
469 | if (plist2[i]) == "AppNotificationMessage":
470 | h.write('
')
471 | h.write('
')
472 | h.write('')
473 | h.write('')
474 | h.write('| Data type | ')
475 | h.write('Value | ')
476 | h.write('
')
477 |
478 | h.write('')
479 | h.write('| ASCII | ')
480 | h.write(''+str(plist2[i])+' | ')
481 | h.write('
')
482 |
483 |
484 | else:
485 | if plist2[i] in notiparams:
486 | h.write('')
487 | h.write('| ASCII | ')
488 | h.write(''+str(plist2[i])+' | ')
489 | h.write('
')
490 | elif plist2[i] == " ":
491 | h.write('')
492 | h.write('| Null | ')
493 | h.write(''+str(plist2[i])+' | ')
494 | h.write('
')
495 | else:
496 | h.write('')
497 | h.write('| ASCII | ')
498 | h.write(''+str(plist2[i])+' | ')
499 | h.write('
')
500 |
501 | except:
502 | pass
503 |
504 | test = 0
505 |
506 |
507 | #h.write('test')
508 |
509 |
510 | for dict in plist2:
511 | liste = dict
512 | types = (type(liste))
513 | #print (types)
514 | try:
515 | for k, v in liste.items():
516 | if k == 'NS.data':
517 | chk = str(v)
518 | reduced = (chk[2:8])
519 | #print (reduced)
520 | if reduced == "bplist":
521 | count = count + 1
522 | binfile = open('./'+appdirect+'/incepted'+str(count)+'.bplist', 'wb')
523 | binfile.write(v)
524 | binfile.close()
525 |
526 | procfile = open('./'+appdirect+'/incepted'+str(count)+'.bplist', 'rb')
527 | secondplist = ccl_bplist.load(procfile)
528 | secondplistint = secondplist["$objects"]
529 | print('Bplist processed and exported.')
530 | exportedbplistcount = exportedbplistcount + 1
531 | h.write('')
532 | h.write('| NS.data | ')
533 | h.write('')
534 | h.write(str(secondplistint))
535 | h.write(' | ')
536 | h.write('
')
537 |
538 | procfile.close()
539 | count = 0
540 | else:
541 | h.write('')
542 | h.write('| NS.data | ')
543 | h.write('')
544 | h.write(str(secondplistint))
545 | h.write(' | ')
546 | h.write('
')
547 | except:
548 | pass
549 | h.close()
550 | elif 'AttachmentsList' in file_name:
551 | test = 0 #future development
552 | end = process_time()
553 | time = start - end
554 | print(" ")
555 | print("Process completed.")
556 | print("Processing time: " + str(abs(time)) )
557 |
558 | print("Total notification directories processed:"+str(notdircount))
559 | print("Total exported bplists from notifications:"+str(exportedbplistcount))
560 |
561 | if version == "11":
562 | parse_ios11()
563 | elif version == "12":
564 | parse_ios12()
--------------------------------------------------------------------------------
/ccl_bplist.py:
--------------------------------------------------------------------------------
1 | """
2 | Copyright (c) 2012-2016, CCL Forensics
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | * Neither the name of the CCL Forensics nor the
13 | names of its contributors may be used to endorse or promote products
14 | derived from this software without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL CCL FORENSICS BE LIABLE FOR ANY
20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | """
27 |
28 | import sys
29 | import os
30 | import struct
31 | import datetime
32 |
33 | __version__ = "0.21"
34 | __description__ = "Converts Apple binary PList files into a native Python data structure"
35 | __contact__ = "Alex Caithness"
36 |
37 | _object_converter = None
38 | def set_object_converter(function):
39 | """Sets the object converter function to be used when retrieving objects from the bplist.
40 | default is None (which will return objects in their raw form).
41 | A built in converter (ccl_bplist.NSKeyedArchiver_common_objects_convertor) which is geared
42 | toward dealling with common types in NSKeyedArchiver is available which can simplify code greatly
43 | when dealling with these types of files."""
44 | if not hasattr(function, "__call__"):
45 | raise TypeError("function is not a function")
46 | global _object_converter
47 | _object_converter = function
48 |
49 | class BplistError(Exception):
50 | pass
51 |
52 | class BplistUID:
53 | def __init__(self, value):
54 | self.value = value
55 |
56 | def __repr__(self):
57 | return "UID: {0}".format(self.value)
58 |
59 | def __str__(self):
60 | return self.__repr__()
61 |
62 | def __decode_multibyte_int(b, signed=True):
63 | if len(b) == 1:
64 | fmt = ">B" # Always unsigned?
65 | elif len(b) == 2:
66 | fmt = ">h"
67 | elif len(b) == 3:
68 | if signed:
69 | return ((b[0] << 16) | struct.unpack(">H", b[1:])[0]) - ((b[0] >> 7) * 2 * 0x800000)
70 | else:
71 | return (b[0] << 16) | struct.unpack(">H", b[1:])[0]
72 | elif len(b) == 4:
73 | fmt = ">i"
74 | elif len(b) == 8:
75 | fmt = ">q"
76 | elif len(b) == 16:
77 | # special case for BigIntegers
78 | high, low = struct.unpack(">QQ", b)
79 | result = (high << 64) | low
80 | if high & 0x8000000000000000 and signed:
81 | result -= 0x100000000000000000000000000000000
82 | return result
83 | else:
84 | raise BplistError("Cannot decode multibyte int of length {0}".format(len(b)))
85 |
86 | if signed and len(b) > 1:
87 | return struct.unpack(fmt.lower(), b)[0]
88 | else:
89 | return struct.unpack(fmt.upper(), b)[0]
90 |
91 | def __decode_float(b, signed=True):
92 | if len(b) == 4:
93 | fmt = ">f"
94 | elif len(b) == 8:
95 | fmt = ">d"
96 | else:
97 | raise BplistError("Cannot decode float of length {0}".format(len(b)))
98 |
99 | if signed:
100 | return struct.unpack(fmt.lower(), b)[0]
101 | else:
102 | return struct.unpack(fmt.upper(), b)[0]
103 |
104 | def __decode_object(f, offset, collection_offset_size, offset_table):
105 | # Move to offset and read type
106 | #print("Decoding object at offset {0}".format(offset))
107 | f.seek(offset)
108 | # A little hack to keep the script portable between py2.x and py3k
109 | if sys.version_info[0] < 3:
110 | type_byte = ord(f.read(1)[0])
111 | else:
112 | type_byte = f.read(1)[0]
113 | #print("Type byte: {0}".format(hex(type_byte)))
114 | if type_byte == 0x00: # Null 0000 0000
115 | return None
116 | elif type_byte == 0x08: # False 0000 1000
117 | return False
118 | elif type_byte == 0x09: # True 0000 1001
119 | return True
120 | elif type_byte == 0x0F: # Fill 0000 1111
121 | raise BplistError("Fill type not currently supported at offset {0}".format(f.tell())) # Not sure what to return really...
122 | elif type_byte & 0xF0 == 0x10: # Int 0001 xxxx
123 | int_length = 2 ** (type_byte & 0x0F)
124 | int_bytes = f.read(int_length)
125 | return __decode_multibyte_int(int_bytes)
126 | elif type_byte & 0xF0 == 0x20: # Float 0010 nnnn
127 | float_length = 2 ** (type_byte & 0x0F)
128 | float_bytes = f.read(float_length)
129 | return __decode_float(float_bytes)
130 | elif type_byte & 0xFF == 0x33: # Date 0011 0011
131 | date_bytes = f.read(8)
132 | date_value = __decode_float(date_bytes)
133 | try:
134 | result = datetime.datetime(2001,1,1) + datetime.timedelta(seconds = date_value)
135 | except OverflowError:
136 | result = datetime.datetime.min
137 | return result
138 | elif type_byte & 0xF0 == 0x40: # Data 0100 nnnn
139 | if type_byte & 0x0F != 0x0F:
140 | # length in 4 lsb
141 | data_length = type_byte & 0x0F
142 | else:
143 | # A little hack to keep the script portable between py2.x and py3k
144 | if sys.version_info[0] < 3:
145 | int_type_byte = ord(f.read(1)[0])
146 | else:
147 | int_type_byte = f.read(1)[0]
148 | if int_type_byte & 0xF0 != 0x10:
149 | raise BplistError("Long Data field definition not followed by int type at offset {0}".format(f.tell()))
150 | int_length = 2 ** (int_type_byte & 0x0F)
151 | int_bytes = f.read(int_length)
152 | data_length = __decode_multibyte_int(int_bytes, False)
153 | return f.read(data_length)
154 | elif type_byte & 0xF0 == 0x50: # ASCII 0101 nnnn
155 | if type_byte & 0x0F != 0x0F:
156 | # length in 4 lsb
157 | ascii_length = type_byte & 0x0F
158 | else:
159 | # A little hack to keep the script portable between py2.x and py3k
160 | if sys.version_info[0] < 3:
161 | int_type_byte = ord(f.read(1)[0])
162 | else:
163 | int_type_byte = f.read(1)[0]
164 | if int_type_byte & 0xF0 != 0x10:
165 | raise BplistError("Long ASCII field definition not followed by int type at offset {0}".format(f.tell()))
166 | int_length = 2 ** (int_type_byte & 0x0F)
167 | int_bytes = f.read(int_length)
168 | ascii_length = __decode_multibyte_int(int_bytes, False)
169 | return f.read(ascii_length).decode("ascii")
170 | elif type_byte & 0xF0 == 0x60: # UTF-16 0110 nnnn
171 | if type_byte & 0x0F != 0x0F:
172 | # length in 4 lsb
173 | utf16_length = (type_byte & 0x0F) * 2 # Length is characters - 16bit width
174 | else:
175 | # A little hack to keep the script portable between py2.x and py3k
176 | if sys.version_info[0] < 3:
177 | int_type_byte = ord(f.read(1)[0])
178 | else:
179 | int_type_byte = f.read(1)[0]
180 | if int_type_byte & 0xF0 != 0x10:
181 | raise BplistError("Long UTF-16 field definition not followed by int type at offset {0}".format(f.tell()))
182 | int_length = 2 ** (int_type_byte & 0x0F)
183 | int_bytes = f.read(int_length)
184 | utf16_length = __decode_multibyte_int(int_bytes, False) * 2
185 | return f.read(utf16_length).decode("utf_16_be")
186 | elif type_byte & 0xF0 == 0x80: # UID 1000 nnnn
187 | uid_length = (type_byte & 0x0F) + 1
188 | uid_bytes = f.read(uid_length)
189 | return BplistUID(__decode_multibyte_int(uid_bytes, signed=False))
190 | elif type_byte & 0xF0 == 0xA0: # Array 1010 nnnn
191 | if type_byte & 0x0F != 0x0F:
192 | # length in 4 lsb
193 | array_count = type_byte & 0x0F
194 | else:
195 | # A little hack to keep the script portable between py2.x and py3k
196 | if sys.version_info[0] < 3:
197 | int_type_byte = ord(f.read(1)[0])
198 | else:
199 | int_type_byte = f.read(1)[0]
200 | if int_type_byte & 0xF0 != 0x10:
201 | raise BplistError("Long Array field definition not followed by int type at offset {0}".format(f.tell()))
202 | int_length = 2 ** (int_type_byte & 0x0F)
203 | int_bytes = f.read(int_length)
204 | array_count = __decode_multibyte_int(int_bytes, signed=False)
205 | array_refs = []
206 | for i in range(array_count):
207 | array_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
208 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in array_refs]
209 | elif type_byte & 0xF0 == 0xC0: # Set 1010 nnnn
210 | if type_byte & 0x0F != 0x0F:
211 | # length in 4 lsb
212 | set_count = type_byte & 0x0F
213 | else:
214 | # A little hack to keep the script portable between py2.x and py3k
215 | if sys.version_info[0] < 3:
216 | int_type_byte = ord(f.read(1)[0])
217 | else:
218 | int_type_byte = f.read(1)[0]
219 | if int_type_byte & 0xF0 != 0x10:
220 | raise BplistError("Long Set field definition not followed by int type at offset {0}".format(f.tell()))
221 | int_length = 2 ** (int_type_byte & 0x0F)
222 | int_bytes = f.read(int_length)
223 | set_count = __decode_multibyte_int(int_bytes, signed=False)
224 | set_refs = []
225 | for i in range(set_count):
226 | set_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
227 | return [__decode_object(f, offset_table[obj_ref], collection_offset_size, offset_table) for obj_ref in set_refs]
228 | elif type_byte & 0xF0 == 0xD0: # Dict 1011 nnnn
229 | if type_byte & 0x0F != 0x0F:
230 | # length in 4 lsb
231 | dict_count = type_byte & 0x0F
232 | else:
233 | # A little hack to keep the script portable between py2.x and py3k
234 | if sys.version_info[0] < 3:
235 | int_type_byte = ord(f.read(1)[0])
236 | else:
237 | int_type_byte = f.read(1)[0]
238 | #print("Dictionary length int byte: {0}".format(hex(int_type_byte)))
239 | if int_type_byte & 0xF0 != 0x10:
240 | raise BplistError("Long Dict field definition not followed by int type at offset {0}".format(f.tell()))
241 | int_length = 2 ** (int_type_byte & 0x0F)
242 | int_bytes = f.read(int_length)
243 | dict_count = __decode_multibyte_int(int_bytes, signed=False)
244 | key_refs = []
245 | #print("Dictionary count: {0}".format(dict_count))
246 | for i in range(dict_count):
247 | key_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
248 | value_refs = []
249 | for i in range(dict_count):
250 | value_refs.append(__decode_multibyte_int(f.read(collection_offset_size), False))
251 |
252 | dict_result = {}
253 | for i in range(dict_count):
254 | #print("Key ref: {0}\tVal ref: {1}".format(key_refs[i], value_refs[i]))
255 | key = __decode_object(f, offset_table[key_refs[i]], collection_offset_size, offset_table)
256 | val = __decode_object(f, offset_table[value_refs[i]], collection_offset_size, offset_table)
257 | dict_result[key] = val
258 | return dict_result
259 |
260 |
261 | def load(f):
262 | """
263 | Reads and converts a file-like object containing a binary property list.
264 | Takes a file-like object (must support reading and seeking) as an argument
265 | Returns a data structure representing the data in the property list
266 | """
267 | # Check magic number
268 | if f.read(8) != b"bplist00":
269 | raise BplistError("Bad file header")
270 |
271 | # Read trailer
272 | f.seek(-32, os.SEEK_END)
273 | trailer = f.read(32)
274 | offset_int_size, collection_offset_size, object_count, top_level_object_index, offest_table_offset = struct.unpack(">6xbbQQQ", trailer)
275 |
276 | # Read offset table
277 | f.seek(offest_table_offset)
278 | offset_table = []
279 | for i in range(object_count):
280 | offset_table.append(__decode_multibyte_int(f.read(offset_int_size), False))
281 |
282 | return __decode_object(f, offset_table[top_level_object_index], collection_offset_size, offset_table)
283 |
284 |
285 | def NSKeyedArchiver_common_objects_convertor(o):
286 | """Built in converter function (suitable for submission to set_object_converter()) which automatically
287 | converts the following common data-types found in NSKeyedArchiver:
288 | NSDictionary/NSMutableDictionary;
289 | NSArray/NSMutableArray;
290 | NSSet/NSMutableSet
291 | NSString/NSMutableString
292 | NSDate
293 | $null strings"""
294 | # Conversion: NSDictionary
295 | if is_nsmutabledictionary(o):
296 | return convert_NSMutableDictionary(o)
297 | # Conversion: NSArray
298 | elif is_nsarray(o):
299 | return convert_NSArray(o)
300 | elif is_isnsset(o):
301 | return convert_NSSet(o)
302 | # Conversion: NSString
303 | elif is_nsstring(o):
304 | return convert_NSString(o)
305 | # Conversion: NSDate
306 | elif is_nsdate(o):
307 | return convert_NSDate(o)
308 | # Conversion: "$null" string
309 | elif isinstance(o, str) and o == "$null":
310 | return None
311 | # Fallback:
312 | else:
313 | return o
314 |
315 | def NSKeyedArchiver_convert(o, object_table):
316 | if isinstance(o, list):
317 | #return NsKeyedArchiverList(o, object_table)
318 | result = NsKeyedArchiverList(o, object_table)
319 | elif isinstance(o, dict):
320 | #return NsKeyedArchiverDictionary(o, object_table)
321 | result = NsKeyedArchiverDictionary(o, object_table)
322 | elif isinstance(o, BplistUID):
323 | #return NSKeyedArchiver_convert(object_table[o.value], object_table)
324 | result = NSKeyedArchiver_convert(object_table[o.value], object_table)
325 | else:
326 | #return o
327 | result = o
328 |
329 | if _object_converter:
330 | return _object_converter(result)
331 | else:
332 | return result
333 |
334 |
335 | class NsKeyedArchiverDictionary(dict):
336 | def __init__(self, original_dict, object_table):
337 | super(NsKeyedArchiverDictionary, self).__init__(original_dict)
338 | self.object_table = object_table
339 |
340 | def __getitem__(self, index):
341 | o = super(NsKeyedArchiverDictionary, self).__getitem__(index)
342 | return NSKeyedArchiver_convert(o, self.object_table)
343 |
344 | def get(self, key, default=None):
345 | return self[key] if key in self else default
346 |
347 | def values(self):
348 | for k in self:
349 | yield self[k]
350 |
351 | def items(self):
352 | for k in self:
353 | yield k, self[k]
354 |
355 | class NsKeyedArchiverList(list):
356 | def __init__(self, original_iterable, object_table):
357 | super(NsKeyedArchiverList, self).__init__(original_iterable)
358 | self.object_table = object_table
359 |
360 | def __getitem__(self, index):
361 | o = super(NsKeyedArchiverList, self).__getitem__(index)
362 | return NSKeyedArchiver_convert(o, self.object_table)
363 |
364 | def __iter__(self):
365 | for o in super(NsKeyedArchiverList, self).__iter__():
366 | yield NSKeyedArchiver_convert(o, self.object_table)
367 |
368 |
369 | def deserialise_NsKeyedArchiver(obj, parse_whole_structure=False):
370 | """Deserialises an NSKeyedArchiver bplist rebuilding the structure.
371 | obj should usually be the top-level object returned by the load()
372 | function."""
373 |
374 | # Check that this is an archiver and version we understand
375 | if not isinstance(obj, dict):
376 | raise TypeError("obj must be a dict")
377 | if "$archiver" not in obj or obj["$archiver"] not in ("NSKeyedArchiver", "NRKeyedArchiver"):
378 | raise ValueError("obj does not contain an '$archiver' key or the '$archiver' is unrecognised")
379 | if "$version" not in obj or obj["$version"] != 100000:
380 | raise ValueError("obj does not contain a '$version' key or the '$version' is unrecognised")
381 |
382 | object_table = obj["$objects"]
383 | if "root" in obj["$top"] and not parse_whole_structure:
384 | return NSKeyedArchiver_convert(obj["$top"]["root"], object_table)
385 | else:
386 | return NSKeyedArchiver_convert(obj["$top"], object_table)
387 |
388 | # NSMutableDictionary convenience functions
389 | def is_nsmutabledictionary(obj):
390 | if not isinstance(obj, dict):
391 | return False
392 | if "$class" not in obj.keys():
393 | return False
394 | if obj["$class"].get("$classname") not in ("NSMutableDictionary", "NSDictionary"):
395 | return False
396 | if "NS.keys" not in obj.keys():
397 | return False
398 | if "NS.objects" not in obj.keys():
399 | return False
400 |
401 | return True
402 |
403 | def convert_NSMutableDictionary(obj):
404 | """Converts a NSKeyedArchiver serialised NSMutableDictionary into
405 | a straight dictionary (rather than two lists as it is serialised
406 | as)"""
407 |
408 | # The dictionary is serialised as two lists (one for keys and one
409 | # for values) which obviously removes all convenience afforded by
410 | # dictionaries. This function converts this structure to an
411 | # actual dictionary so that values can be accessed by key.
412 |
413 | if not is_nsmutabledictionary(obj):
414 | raise ValueError("obj does not have the correct structure for a NSDictionary/NSMutableDictionary serialised to a NSKeyedArchiver")
415 | keys = obj["NS.keys"]
416 | vals = obj["NS.objects"]
417 |
418 | # sense check the keys and values:
419 | if not isinstance(keys, list):
420 | raise TypeError("The 'NS.keys' value is an unexpected type (expected list; actual: {0}".format(type(keys)))
421 | if not isinstance(vals, list):
422 | raise TypeError("The 'NS.objects' value is an unexpected type (expected list; actual: {0}".format(type(vals)))
423 | if len(keys) != len(vals):
424 | raise ValueError("The length of the 'NS.keys' list ({0}) is not equal to that of the 'NS.objects ({1})".format(len(keys), len(vals)))
425 |
426 | result = {}
427 | for i,k in enumerate(keys):
428 | if k in result:
429 | raise ValueError("The 'NS.keys' list contains duplicate entries")
430 | result[k] = vals[i]
431 |
432 | return result
433 |
434 | # NSArray convenience functions
435 | def is_nsarray(obj):
436 | if not isinstance(obj, dict):
437 | return False
438 | if "$class" not in obj.keys():
439 | return False
440 | if obj["$class"].get("$classname") not in ("NSArray", "NSMutableArray"):
441 | return False
442 | if "NS.objects" not in obj.keys():
443 | return False
444 |
445 | return True
446 |
447 | def convert_NSArray(obj):
448 | if not is_nsarray(obj):
449 | raise ValueError("obj does not have the correct structure for a NSArray/NSMutableArray serialised to a NSKeyedArchiver")
450 |
451 | return obj["NS.objects"]
452 |
453 | # NSSet convenience functions
454 | def is_isnsset(obj):
455 | if not isinstance(obj, dict):
456 | return False
457 | if "$class" not in obj.keys():
458 | return False
459 | if obj["$class"].get("$classname") not in ("NSSet", "NSMutableSet"):
460 | return False
461 | if "NS.objects" not in obj.keys():
462 | return False
463 |
464 | return True
465 |
466 | def convert_NSSet(obj):
467 | if not is_isnsset(obj):
468 | raise ValueError("obj does not have the correct structure for a NSSet/NSMutableSet serialised to a NSKeyedArchiver")
469 |
470 | return list(obj["NS.objects"])
471 |
472 | # NSString convenience functions
473 | def is_nsstring(obj):
474 | if not isinstance(obj, dict):
475 | return False
476 | if "$class" not in obj.keys():
477 | return False
478 | if obj["$class"].get("$classname") not in ("NSString", "NSMutableString"):
479 | return False
480 | if "NS.string" not in obj.keys():
481 | return False
482 | return True
483 |
484 | def convert_NSString(obj):
485 | if not is_nsstring(obj):
486 | raise ValueError("obj does not have the correct structure for a NSString/NSMutableString serialised to a NSKeyedArchiver")
487 |
488 | return obj["NS.string"]
489 |
490 | # NSDate convenience functions
491 | def is_nsdate(obj):
492 | if not isinstance(obj, dict):
493 | return False
494 | if "$class" not in obj.keys():
495 | return False
496 | if obj["$class"].get("$classname") not in ("NSDate"):
497 | return False
498 | if "NS.time" not in obj.keys():
499 | return False
500 |
501 | return True
502 |
503 | def convert_NSDate(obj):
504 | if not is_nsdate(obj):
505 | raise ValueError("obj does not have the correct structure for a NSDate serialised to a NSKeyedArchiver")
506 |
507 | return datetime.datetime(2001, 1, 1) + datetime.timedelta(seconds=obj["NS.time"])
508 |
--------------------------------------------------------------------------------