├── 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 | ![alt text](usage.PNG "Usage example") 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('') 109 | h.write('') 110 | h.write('') 111 | 112 | h.write('') 113 | h.write('') 114 | h.write('') 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('') 123 | ob6 = str(plist2[i]['$classes']) 124 | 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('') 135 | ob5 = str(plist2[i]['$class']) 136 | 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('') 147 | ob0 = str(plist2[i]['NS.keys']) 148 | 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('') 160 | 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('') 177 | 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('') 191 | 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('') 204 | 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('
Data typeValue
PlistInitial Values
$classes') 125 | h.write(str(ob6)) 126 | h.write('
$class') 137 | h.write(str(ob5)) 138 | h.write('
NS.keys') 149 | h.write(str(ob0)) 150 | h.write('
NS.objects') 161 | h.write(str(ob1)) 162 | h.write('
Time UTC') 178 | h.write(str(timestamp)) 179 | #h.write(str(plist2[i]['NS.time'])) 180 | h.write('
NS.base') 192 | h.write(str(ob2)) 193 | h.write('
$classname') 205 | h.write(str(ob3)) 206 | h.write('
') 216 | h.write('
') 217 | h.write('') 218 | h.write('') 219 | h.write('') 220 | h.write('') 221 | h.write('') 222 | 223 | h.write('') 224 | h.write('') 225 | h.write('') 226 | h.write('') 227 | 228 | 229 | else: 230 | if plist2[i] in notiparams: 231 | h.write('') 232 | h.write('') 233 | h.write('') 234 | h.write('') 235 | elif plist2[i] == " ": 236 | h.write('') 237 | h.write('') 238 | h.write('') 239 | h.write('') 240 | else: 241 | h.write('') 242 | h.write('') 243 | h.write('') 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('') 278 | h.write('') 281 | h.write('') 282 | 283 | procfile.close() 284 | count = 0 285 | else: 286 | h.write('') 287 | h.write('') 288 | 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('
Data typeValue
ASCII'+str(plist2[i])+'
ASCII'+str(plist2[i])+'
Null'+str(plist2[i])+'
ASCII'+str(plist2[i])+'
NS.data') 279 | h.write(str(secondplistint)) 280 | h.write('
NS.data') 289 | h.write(str(secondplistint)) 290 | h.write('
') 362 | h.write('') 363 | h.write('') 364 | h.write('') 365 | h.write('') 366 | 367 | h.write('') 368 | h.write('') 369 | h.write('') 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('') 378 | ob6 = str(plist2[i]['$classes']) 379 | 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('') 390 | ob5 = str(plist2[i]['$class']) 391 | 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('') 402 | ob0 = str(plist2[i]['NS.keys']) 403 | 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('') 415 | 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('') 432 | 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('') 446 | 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('') 459 | 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('
Data typeValue
PlistInitial Values
$classes') 380 | h.write(str(ob6)) 381 | h.write('
$class') 392 | h.write(str(ob5)) 393 | h.write('
NS.keys') 404 | h.write(str(ob0)) 405 | h.write('
NS.objects') 416 | h.write(str(ob1)) 417 | h.write('
Time UTC') 433 | h.write(str(timestamp)) 434 | #h.write(str(plist2[i]['NS.time'])) 435 | h.write('
NS.base') 447 | h.write(str(ob2)) 448 | h.write('
$classname') 460 | h.write(str(ob3)) 461 | h.write('
') 471 | h.write('
') 472 | h.write('') 473 | h.write('') 474 | h.write('') 475 | h.write('') 476 | h.write('') 477 | 478 | h.write('') 479 | h.write('') 480 | h.write('') 481 | h.write('') 482 | 483 | 484 | else: 485 | if plist2[i] in notiparams: 486 | h.write('') 487 | h.write('') 488 | h.write('') 489 | h.write('') 490 | elif plist2[i] == " ": 491 | h.write('') 492 | h.write('') 493 | h.write('') 494 | h.write('') 495 | else: 496 | h.write('') 497 | h.write('') 498 | h.write('') 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('') 533 | h.write('') 536 | h.write('') 537 | 538 | procfile.close() 539 | count = 0 540 | else: 541 | h.write('') 542 | h.write('') 543 | 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 | --------------------------------------------------------------------------------
Data typeValue
ASCII'+str(plist2[i])+'
ASCII'+str(plist2[i])+'
Null'+str(plist2[i])+'
ASCII'+str(plist2[i])+'
NS.data') 534 | h.write(str(secondplistint)) 535 | h.write('
NS.data') 544 | h.write(str(secondplistint)) 545 | h.write('