Skip to content

Commit

Permalink
Worked on NSKeyedArchiver decode script
Browse files Browse the repository at this point in the history
  • Loading branch information
joachimmetz committed Feb 18, 2024
1 parent bc4bf6e commit 52dd23a
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 50 deletions.
75 changes: 49 additions & 26 deletions plistrc/decoders.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# -*- coding: utf-8 -*-
"""Property List decoders."""
"""Property list decoders."""

import base64
import plistlib
import uuid


class NSKeyedArchiverDecoder(object):
"""Decodes a NSKeyedArchiver encoded plist.
"""Decoder for NSKeyedArchiver encoded plists.
Also see:
https://developer.apple.com/documentation/foundation/nskeyedarchiver
Expand All @@ -22,6 +21,7 @@ class NSKeyedArchiverDecoder(object):
'NSNull': '_DecodeNSNull',
'NSObject': '_DecodeCompositeObject',
'NSSet': '_DecodeNSArray',
'NSString': '_DecodeNSString',
'NSURL': '_DecodeNSURL',
'NSUUID': '_DecodeNSUUID'}

Expand Down Expand Up @@ -122,7 +122,7 @@ def _DecodeNSData(self, plist_property, objects_array, parent_objects):
parent_objects (list[int]): parent object UIDs.
Returns:
str: decoded NSData.
bytes: decoded NSData.
Raises:
RuntimeError: if the NSData cannot be decoded.
Expand All @@ -139,7 +139,7 @@ def _DecodeNSData(self, plist_property, objects_array, parent_objects):
raise RuntimeError(
f'Unsupported type: {type_string!s} in {class_name:s}.NS.data.')

return str(base64.urlsafe_b64encode(ns_data))[2:-1]
return ns_data

def _DecodeNSDate(self, plist_property, objects_array, parent_objects):
"""Decodes a NSDate.
Expand Down Expand Up @@ -208,7 +208,15 @@ def _DecodeNSDictionary(self, plist_property, objects_array, parent_objects):
raise RuntimeError(
f'Missing UID in NS.keys[{index:d}] property of {class_name:s}.')

ns_key = objects_array[ns_key_plist_uid]
ns_key_referenced_property = objects_array[ns_key_plist_uid]

parent_objects.append(ns_key_plist_uid)

ns_key = self._DecodeObject(
ns_key_referenced_property, objects_array, parent_objects)

parent_objects.pop(-1)

if not ns_key:
raise RuntimeError((
f'Missing {class_name:s}.NS.keys[{index:d}] with UID: '
Expand Down Expand Up @@ -357,6 +365,34 @@ def _DecodeNSObject(self, plist_property, objects_array, parent_objects):

# pylint: disable=unused-argument

def _DecodeNSString(self, plist_property, objects_array, parent_objects):
"""Decodes a NSString.
Args:
plist_property (object): property containing the encoded NSString.
objects_array (list[object]): $objects array.
parent_objects (list[int]): parent object UIDs.
Returns:
str: decoded NSString.
Raises:
RuntimeError: if the NSString cannot be decoded.
"""
class_name = self._GetClassName(plist_property, objects_array)

if 'NS.string' not in plist_property:
raise RuntimeError(f'Missing NS.string in {class_name:s}')

ns_string = plist_property['NS.string']

if not isinstance(ns_string, str):
type_string = type(ns_string)
raise RuntimeError(
f'Unsupported type: {type_string!s} in {class_name:s}.NS.string.')

return ns_string

def _DecodeNSURL(self, plist_property, objects_array, parent_objects):
"""Decodes a NSURL.
Expand Down Expand Up @@ -436,11 +472,11 @@ def _DecodeNSUUID(self, plist_property, objects_array, parent_objects):

# pylint: enable=unused-argument

def _DecodeObject(self, encoded_object, objects_array, parent_objects):
def _DecodeObject(self, plist_property, objects_array, parent_objects):
"""Decodes an object.
Args:
encoded_object (object): encoded object.
plist_property (object): property containing the encoded NSUUID.
objects_array (list[object]): $objects array.
parent_objects (list[int]): parent object UIDs.
Expand All @@ -450,26 +486,13 @@ def _DecodeObject(self, encoded_object, objects_array, parent_objects):
Raises:
RuntimeError: if the object cannot be decoded.
"""
if (encoded_object is None or
isinstance(encoded_object, (bool, int, float))):
return encoded_object

if isinstance(encoded_object, bytes):
return str(base64.urlsafe_b64encode(encoded_object))[2:-1]

if isinstance(encoded_object, dict) and '$class' not in encoded_object:
return encoded_object

if isinstance(encoded_object, list):
return encoded_object

if isinstance(encoded_object, str):
if encoded_object == '$null':
return None
if isinstance(plist_property, dict) and '$class' in plist_property:
return self._DecodeNSObject(plist_property, objects_array, parent_objects)

return encoded_object
if isinstance(plist_property, str) and plist_property == '$null':
return None

return self._DecodeNSObject(encoded_object, objects_array, parent_objects)
return plist_property

def _GetClassName(self, plist_property, objects_array):
"""Retrieves a class name.
Expand Down
46 changes: 23 additions & 23 deletions plistrc/schema_extractor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
"""Property List file schema extractor."""
"""Property list file schema extractor."""

import datetime
import logging
Expand All @@ -18,7 +18,7 @@


class PropertyListSchemaExtractor(object):
"""Property List file schema extractor."""
"""Property list file schema extractor."""

_COMPOSITE_VALUE_TYPES = frozenset(['array', 'dict'])

Expand All @@ -33,7 +33,7 @@ class PropertyListSchemaExtractor(object):
_UTF32LE_BYTE_ORDER_MARK = b'\xff\xfe\x00\x00'

def __init__(self, artifact_definitions, mediator=None):
"""Initializes a Property List file schema extractor.
"""Initializes a property list file schema extractor.
Args:
artifact_definitions (str): path to a single artifact definitions
Expand All @@ -54,7 +54,7 @@ def __init__(self, artifact_definitions, mediator=None):
self._artifacts_registry.ReadFromFile(reader, artifact_definitions)

def _CheckByteOrderMark(self, data):
"""Determines if a Property List starts with a byte-order-mark.
"""Determines if a property list starts with a byte-order-mark.
Args:
data (bytes): data.
Expand Down Expand Up @@ -84,10 +84,10 @@ def _CheckSignature(self, file_object):
"""Checks the signature of a given file-like object.
Args:
file_object (dfvfs.FileIO): file-like object of the Property List.
file_object (dfvfs.FileIO): file-like object of the property list.
Returns:
bool: True if the signature matches that of a Property List, False
bool: True if the signature matches that of a property list, False
otherwise.
"""
if not file_object:
Expand Down Expand Up @@ -160,7 +160,7 @@ def _FormatSchemaAsYAML(self, schema):
tables.append(table)

lines = [
'# PList-kb Property List schema.',
'# PList-kb property list schema.',
'---']

for table in sorted(tables):
Expand All @@ -184,23 +184,23 @@ def _GetDictPropertyDefinitions(self, property_definition):
value_property_definition)

def _GetPropertyListKeyPath(self, key_path_segments):
"""Retrieves a Property List key path.
"""Retrieves a property list key path.
Args:
key_path_segments (list[str]): Property List key path segments.
key_path_segments (list[str]): property list key path segments.
Returns:
str: Property List key path.
str: property list key path.
"""
# TODO: escape '.' in path segments
return '.'.join(key_path_segments)

def _GetPropertyListSchemaFromItem(self, item, key_path_segments):
"""Retrieves schema from given Property List item.
"""Retrieves schema from given property list item.
Args:
item (object): Property List item.
key_path_segments (list[str]): Property List key path segments.
item (object): property list item.
key_path_segments (list[str]): property list key path segments.
Returns:
PropertyDefinition: property definition of the item.
Expand Down Expand Up @@ -245,10 +245,10 @@ def _GetPropertyListSchemaFromItem(self, item, key_path_segments):
return property_definition

def _GetPropertyListValueType(self, item):
"""Retrieves Property List value type.
"""Retrieves property list value type.
Args:
item (object): Property List item.
item (object): property list item.
Yields:
str: value type.
Expand Down Expand Up @@ -305,18 +305,18 @@ def GetDisplayPath(self, path_segments, data_stream_name=None):
return display_path or '/'

def ExtractSchemas(self, path, options=None):
"""Extracts Property List schemas from the path.
"""Extracts property list schemas from the path.
Args:
path (str): path of a Property List file or storage media image containing
Property List files.
path (str): path of a property list file or storage media image containing
property list files.
options (Optional[dfvfs.VolumeScannerOptions]): volume scanner options. If
None the default volume scanner options are used, which are defined in
the dfVFS VolumeScannerOptions class.
Yields:
tuple[str, dict[str, str]]: known Property List type identifier or the
name of the Property List file if not known and schema.
tuple[str, dict[str, str]]: known property list type identifier or the
name of the property list file if not known and schema.
"""
entry_lister = file_entry_lister.FileEntryLister(mediator=self._mediator)

Expand All @@ -341,14 +341,14 @@ def ExtractSchemas(self, path, options=None):
# Skip Cocoa nib files for now https://developer.apple.com/library/
# archive/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/
# CocoaNibs.html
# if path_segments[-1].endswith('.nib'):
# continue
if path_segments[-1].endswith('.nib'):
continue

display_path = self.GetDisplayPath(path_segments)
# logging.info(f'Extracting schema from plist file: {display_path:s}')

# Note that plistlib assumes the file-like object current offset is at
# the start of the Property List.
# the start of the property list.
file_object.seek(0, os.SEEK_SET)

try:
Expand Down
23 changes: 22 additions & 1 deletion scripts/decode.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,33 @@
"""Script to decodes a NSKeyedArchiver encoded plist."""

import argparse
import base64
import json
import plistlib
import sys

from plistrc import decoders


class NSKeyedArchiverJSONEncoder(json.JSONEncoder):
"""JSON encoder for decoded NSKeyedArchiver encoded plists."""

def default(self, o):
"""Encodes an object as JSON.
Args:
o (object): object to encode.
Returns:
object: JSON encoded object.
"""
if isinstance(o, bytes):
encoded_bytes = base64.urlsafe_b64encode(o)
return encoded_bytes.decode('latin1')

return super(NSKeyedArchiverJSONEncoder, self).default(o)


def Main():
"""The main program function.
Expand Down Expand Up @@ -43,7 +63,8 @@ def Main():
print(f'[WARNING] {exception!s}')
return False

print(json.dumps(decoded_plist))
json_string = json.dumps(decoded_plist, cls=NSKeyedArchiverJSONEncoder)
print(json_string)

return True

Expand Down
Binary file added test_data/NSKeyedArchiver.plist
Binary file not shown.
Binary file removed test_data/Printing.nib
Binary file not shown.
36 changes: 36 additions & 0 deletions tests/decoders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""Tests for the property list decoders."""

import plistlib
import unittest

from plistrc import decoders

from tests import test_lib


class NSKeyedArchiverDecoderTest(test_lib.BaseTestCase):
"""Tests for the decoder for NSKeyedArchiver encoded plists."""

def testDecode(self):
"""Tests the Decode function."""
test_file_path = self._GetTestFilePath(['NSKeyedArchiver.plist'])
self._SkipIfPathNotExists(test_file_path)

test_decoder = decoders.NSKeyedArchiverDecoder()

with open(test_file_path, 'rb') as file_object:
encoded_plist = plistlib.load(file_object)

decoded_plist = test_decoder.Decode(encoded_plist)
self.assertIsNotNone(decoded_plist)
self.assertIn('root', decoded_plist)

root_item = decoded_plist['root']
self.assertIn('MyString', root_item)
self.assertEqual(root_item['MyString'], 'Some string')


if __name__ == '__main__':
unittest.main()

0 comments on commit 52dd23a

Please sign in to comment.