"""Plist parser plugin for MacOS user plist files.

Fields within the plist key:
  name: username.
  uid: user identifier (UID).
  passwordpolicyoptions: XML Plist structures with the timestamp.
  passwordLastSetTime: last time the password was changed.
  lastLoginTimestamp: last time the user was authenticated depending on
      the situation, these timestamps are reset (0 value). It is translated
      by the library as a 2001-01-01 00:00:00 (Cocoa zero time representation).
  failedLoginTimestamp: last time the login attempt failed.
  failedLoginCount: number of failed loging attempts.
"""

# TODO: Only plists from MacOS 10.8 and 10.9 were tested. Look at other
#       versions as well.

import codecs
import plistlib

from xml.parsers import expat

from defusedxml import ElementTree
from dfdatetime import time_elements as dfdatetime_time_elements

from plaso.containers import events
from plaso.parsers import logger
from plaso.parsers import plist
from plaso.parsers.plist_plugins import interface


class MacOSUserEventData(events.EventData):
    """MacOS user event data.

    Attributes:
      fullname (str): full name.
      home_directory (str): path of the home directory.
      last_login_attempt_time (dfdatetime.DateTimeValues): date and time of the last
          (failed) login attempt.
      last_login_time (dfdatetime.DateTimeValues): date and time of the last login.
      last_password_set_time (dfdatetime.DateTimeValues): date and time of the last
          password set.
      number_of_failed_login_attempts (str): number of failed login attempts.
      password_hash (str): password hash.
      user_identifier (str): user identifier.
      username (str): username.
    """

    DATA_TYPE = "macos:user:entry"

    def __init__(self):
        """Initializes event data."""
        super().__init__(data_type=self.DATA_TYPE)
        self.fullname = None
        self.home_directory = None
        self.last_login_attempt_time = None
        self.last_login_time = None
        self.last_password_set_time = None
        self.number_of_failed_login_attempts = None
        self.password_hash = None
        self.user_identifier = None
        self.username = None


class MacOSUserPlistPlugin(interface.PlistPlugin):
    """Plist parser plugin for MacOS user plist files."""

    NAME = "macuser"
    DATA_FORMAT = "MacOS user plist file"

    # The PLIST_PATH is dynamic, "user".plist is the name of the
    # MacOS user.
    PLIST_KEYS = frozenset(
        ["home", "name", "passwordpolicyoptions", "ShadowHashData", "uid"]
    )

    _ROOT = "/"

    def _GetDateTimeValueFromTimeString(
        self, parser_mediator, policy_values, value_name
    ):
        """Retrieves a date and time value from a time string value.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          policy_values (dict[str, object]): policy values.
          value_name (str): name of the value.

        Returns:
          tuple: containing:

              dfdatetime.TimeElements: date and time or None if not available.
              bool: value to indicate the date and time value was corrupted.
        """
        time_string = policy_values.get(value_name)
        if not time_string or time_string == "2001-01-01T00:00:00Z":
            return None, False

        date_time = dfdatetime_time_elements.TimeElements()

        try:
            date_time.CopyFromStringISO8601(time_string)
        except (TypeError, ValueError):
            parser_mediator.ProduceWarning(
                f"unable to parse value: {value_name:s} time string: {time_string!s}"
            )
            return None, True

        return date_time, False

    # pylint: disable=arguments-differ
    def _ParsePlist(self, parser_mediator, match=None, top_level=None, **unused_kwargs):
        """Extracts relevant user timestamp entries.

        Args:
          parser_mediator (ParserMediator): mediates interactions between parsers and
              other components, such as storage and dfVFS.
          match (Optional[dict[str: object]]): keys extracted from PLIST_KEYS.
          top_level (Optional[dict[str, object]]): plist top-level item.
        """
        if "name" not in match or "uid" not in match:
            return

        password_hash = None
        user_identifier = match["uid"][0]
        username = match["name"][0]

        shadow_hash_data = match.get("ShadowHashData")
        if isinstance(shadow_hash_data, (list, tuple)):
            # Extract the hash password information, which is stored in the attribute
            # ShadowHashData which is a binary plist data.
            try:
                property_list = plistlib.loads(shadow_hash_data[0])
            except plistlib.InvalidFileException as exception:
                parser_mediator.ProduceWarning(
                    f"unable to parse ShadowHashData with error: {exception!s}"
                )
                property_list = {}

            salted_hash = property_list.get("SALTED-SHA512-PBKDF2")
            if salted_hash:
                salt_hex_bytes = codecs.encode(salted_hash["salt"], "hex")
                salt_string = codecs.decode(salt_hex_bytes, "ascii")
                entropy_hex_bytes = codecs.encode(salted_hash["entropy"], "hex")
                entropy_string = codecs.decode(entropy_hex_bytes, "ascii")

                number_of_iterations = salted_hash["iterations"]
                password_hash = (
                    f"$ml${number_of_iterations:d}${salt_string:s}${entropy_string:s}"
                )

        for policy in match.get("passwordpolicyoptions", []):
            try:
                xml_policy = ElementTree.fromstring(policy)
            except (LookupError, ElementTree.ParseError, expat.ExpatError) as exception:
                logger.error(
                    f"Unable to parse XML structure for an user policy, username: "
                    f"{username:s} and UID: {user_identifier!s}, with error: "
                    f"{exception!s}"
                )
                continue

            for dict_elements in xml_policy.iterfind("dict"):
                key_values = [value.text for value in dict_elements]
                # Taking a list and converting it to a dict, using every other item
                # as the key and the other one as the value.
                policy_dict = dict(zip(key_values[0::2], key_values[1::2]))

            corrupted = False

            event_data = MacOSUserEventData()
            event_data.fullname = top_level.get("realname", [None])[0]
            event_data.home_directory = top_level.get("home", [None])[0]
            event_data.last_login_attempt_time, value_corrupted = (
                self._GetDateTimeValueFromTimeString(
                    parser_mediator, policy_dict, "failedLoginTimestamp"
                )
            )
            corrupted = corrupted or value_corrupted

            event_data.last_login_time, value_corrupted = (
                self._GetDateTimeValueFromTimeString(
                    parser_mediator, policy_dict, "lastLoginTimestamp"
                )
            )
            corrupted = corrupted or value_corrupted

            event_data.last_password_set_time, value_corrupted = (
                self._GetDateTimeValueFromTimeString(
                    parser_mediator, policy_dict, "passwordLastSetTime"
                )
            )
            corrupted = corrupted or value_corrupted

            event_data.number_of_failed_login_attempts = policy_dict.get(
                "failedLoginCount"
            )
            event_data.password_hash = password_hash
            event_data.user_identifier = user_identifier
            event_data.username = username

            parser_mediator.ProduceEventData(event_data, corrupted=corrupted)


plist.PlistParser.RegisterPlugin(MacOSUserPlistPlugin)
