# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022,2023,2024 Eduardo Aguiar
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
from pymobius.registry import *
import pymobius
import struct
import mobius

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief SAM's F structure decoder
# @author Eduardo Aguiar
# @see http://www.beginningtoseethelight.org/ntsecurity/index.htm
# @see https://github.com/keydet89/RegRipper2.8
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class SAM_F (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief data structure
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  structure = [
    [ 0, 'revision', 'uint32_le', 'data structure version'],
    [ 4, 'unknown1', 'uint32_le', '??? (either 65538 or 65539)'],
    [ 8, 'last_logon_timestamp', 'uint64_le', 'last logon NT datetime'],
    [16, 'unknown2', 'uint64_le', '??? (always 0)'],
    [24, 'last_password_set_timestamp', 'uint64_le', 'last password set NT datetime'],
    [32, 'password_expiration_timestamp', 'uint64_le', '??? (always either 0 or 0x7fffffffffffffff'],
    [40, 'last_failed_logon_timestamp', 'uint64_le', 'last failed logon NT datetime'],
    [48, 'rid', 'uint32_le', 'user RID'],
    [52, 'gid', 'uint32_le', 'user primary group ID'],
    [56, 'flags', 'uint32_le', 'ACB flags'],
    [60, 'country_code', 'uint16_le', 'country code (0 = system default)'],
    [62, 'unknown3', 'uint16_le', '??? (always 0)'],
    [64, 'failed_logon_count', 'uint16_le', 'failed logon count'],
    [66, 'logon_count', 'uint16_le', 'successful logon count'],
    [68, 'unknown4', 'uint32_le', '??? (either 0 or 1)'],
  ]

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief initialize object
  # @param data F value data
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, data):

    # check if data can be parsed
    if len (data) < 72:
      self.__is_valid = False
      return

    # parse data structure
    self.revision,                      \
    dummy1,                             \
    last_logon_timestamp,               \
    last_logoff_timestamp,              \
    last_password_set_timestamp,        \
    password_expiration_timestamp,      \
    last_failed_logon_timestamp,        \
    self.rid,                           \
    self.gid,                           \
    self.flags,                         \
    self.country_code,                  \
    self.codepage,                      \
    self.failed_logon_count,            \
    self.logon_count,                   \
    self.dummy4 = struct.unpack ('<IIQQQQQIIIHHHHI', data[:72])

    if password_expiration_timestamp == 0x7fffffffffffffff:
      password_expiration_timestamp = 0

    # format data
    self.last_logon_datetime = get_nt_datetime (last_logon_timestamp)
    self.last_logoff_datetime = get_nt_datetime (last_logoff_timestamp)
    self.last_password_set_datetime = get_nt_datetime (last_password_set_timestamp)
    self.last_failed_logon_datetime = get_nt_datetime (last_failed_logon_timestamp)
    self.password_expiration_datetime = get_nt_datetime (password_expiration_timestamp)

    # @see https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-samr/b10cfda1-f24f-441b-8f43-80cb93e786ec
    self.flag_account_enabled = not (self.flags & 0x0001)
    self.flag_home_dir_required = bool (self.flags & 0x0002)
    self.flag_password_required = not (self.flags & 0x0004)
    self.flag_temporary_duplicated_account = bool (self.flags & 0x0008)
    self.flag_normal_user_account = bool (self.flags & 0x0010)
    self.flag_mns_logon_user_account = bool (self.flags & 0x0020)
    self.flag_interdomain_trust_account = bool (self.flags & 0x0040)
    self.flag_workstation_trust_account = bool (self.flags & 0x0080)
    self.flag_server_trust_account = bool (self.flags & 0x0100)
    self.flag_password_expires = not (self.flags & 0x0200)
    self.flag_account_auto_locked = bool (self.flags & 0x0400)

    self.__is_valid = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Check if object is valid
  # @return true/false
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def is_valid (self):
    return self.__is_valid

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief SAM's V structure decoder
# @author Eduardo Aguiar
# @see http://www.beginningtoseethelight.org/ntsecurity/index.htm
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class SAM_V (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief data structure
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  structure = [
    [  0, 'dummy1', 'uint32_le', '???'],
    [  4, 'var_field_offset', 'uint32_le', 'var fields offset'],
    [  8, 'revision', 'uint32_le', 'data structure version'],
    [ 12, 'name_offset', 'uint32_le', 'user name field offset'],
    [ 16, 'name_size', 'uint32_le', 'user name field size in bytes'],
    [ 20, 'dummy4', 'uint32_le', '???'],
    [ 24, 'fullname_offset', 'uint32_le', 'full user name field offset'],
    [ 28, 'fullname_size', 'uint32_le', 'full user name field size in bytes'],
    [ 32, 'dummy5', 'uint32_le', '???'],
    [ 36, 'comment_offset', 'uint32_le', 'comment field offset'],
    [ 40, 'comment_size', 'uint32_le', 'comment field size in bytes'],
    [ 44, 'dummy6', 'uint32_le', '???'],
    [ 48, 'user_comment_offset', 'uint32_le', 'user comment field offset'],
    [ 52, 'user_comment_size', 'uint32_le', 'user comment field size in bytes'],
    [ 56, 'dummy7', 'uint32_le', '???'],
    [ 60, 'ts_user_parm_offset', 'uint32_le', 'terminal server user parms field offset'],
    [ 64, 'ts_user_parm_size', 'uint32_le', 'terminal server user parms field size in bytes'],
    [ 68, 'dummy9', 'uint32_le', '???'],
    [ 72, 'home_dir_offset', 'uint32_le', 'home directory field offset'],
    [ 76, 'home_dir_size', 'uint32_le', 'home directory field size in bytes'],
    [ 80, 'dummy10', 'uint32_le', '???'],
    [ 84, 'home_dir_letter_offset', 'uint32_le', 'home directory letter field offset'],
    [ 88, 'home_dir_letter_size', 'uint32_le', 'home directory letter field size in bytes'],
    [ 92, 'dummy11', 'uint32_le', '???'],
    [ 96, 'logon_script_offset', 'uint32_le', 'logon script field offset'],
    [100, 'logon_script_size', 'uint32_le', 'logon script field size in bytes'],
    [104, 'dummy12', 'uint32_le', '???'],
    [108, 'profile_path_offset', 'uint32_le', 'profile path field offset'],
    [112, 'profile_path_size', 'uint32_le', 'profile path field size in bytes'],
    [116, 'dummy13', 'uint32_le', '???'],
    [120, 'workstation_offset', 'uint32_le', 'workstation field offset'],
    [124, 'workstation_size', 'uint32_le', 'workstation field size in bytes'],
    [128, 'dummy14', 'uint32_le', '???'],
    [132, 'hours_allowed_offset', 'uint32_le', 'Hours allowed offset'],
    [136, 'hours_allowed_size', 'uint32_le', 'Hours allowed size'],
    [140, 'dummy16', 'uint32_le', '???'],
    [144, 'dummy17_offset', 'uint32_le', '??? dummy17 field offset'],
    [148, 'dummy17_size', 'uint32_le', 'dummy17 field size in bytes'],
    [152, 'dummy18', 'uint32_le', '???'],
    [156, 'lm_hash_offset', 'uint32_le', 'LM hash field offset'],
    [160, 'lm_hash_size', 'uint32_le', 'LM hash field size in bytes'],
    [164, 'dummy19', 'uint32_le', '???'],
    [168, 'nt_hash_offset', 'uint32_le', 'NTLM hash field offset'],
    [172, 'nt_hash_size', 'uint32_le', 'NTLM hash field size in bytes'],
    [176, 'dummy20', 'uint32_le', '???'],
    [180, 'nt_hash_history_offset', 'uint32_le', 'NTLM hash history field offset'],
    [184, 'nt_hash_history_size', 'uint32_le', 'NTLM hash history field size in bytes'],
    [188, 'dummy22', 'uint32_le', '???'],
    [192, 'lm_hash_history_offset', 'uint32_le', 'LM hash history field offset'],
    [196, 'lm_hash_history_size', 'uint32_le', 'LM hash history field size in bytes'],
    [200, 'dummy24', 'uint32_le', '???'],
  ]

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Initialize object
  # @param data V value data
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, data):

    # check if data can be parsed
    if len (data) < 72:
      self.__is_valid = False
      return

    # parse data structure
    dummy1,                     \
    var_field_offset,           \
    self.revision,              \
    name_offset,                \
    name_size,                  \
    dummy4,                     \
    fullname_offset,            \
    fullname_size,              \
    dummy5,                     \
    admin_comment_offset,       \
    admin_comment_size,         \
    dummy6,                     \
    user_comment_offset,        \
    user_comment_size,          \
    dummy7,                     \
    ts_user_parm_offset,        \
    ts_user_parm_size,          \
    dummy9,                     \
    home_dir_offset,            \
    home_dir_size,              \
    dummy10,                    \
    home_dir_letter_offset,     \
    home_dir_letter_size,       \
    dummy11,                    \
    logon_script_offset,        \
    logon_script_size,          \
    dummy12,                    \
    profile_path_offset,        \
    profile_path_size,          \
    dummy13,                    \
    workstation_offset,         \
    workstation_size,           \
    dummy14,                    \
    hours_allowed_offset,       \
    hours_allowed_size,         \
    self.dummy16,               \
    dummy17_offset,             \
    dummy17_size,               \
    self.dummy18,               \
    lm_hash_offset,             \
    lm_hash_size,               \
    dummy19,                    \
    nt_hash_offset,             \
    nt_hash_size,               \
    dummy20,                    \
    nt_hash_history_offset,     \
    nt_hash_history_size,       \
    dummy22,                    \
    lm_hash_history_offset,     \
    lm_hash_history_size,       \
    dummy24 = struct.unpack ("<IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII", data[:204])

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # Parse data fields
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    self.username = self.__get_text (data, name_offset, name_size)
    self.fullname = self.__get_text (data, fullname_offset, fullname_size)
    self.admin_comment = self.__get_text (data, admin_comment_offset, admin_comment_size)
    self.user_comment = self.__get_text (data, user_comment_offset, user_comment_size)
    self.ts_user_parm = TS_User_Parameters (self.__get_field (data, ts_user_parm_offset, ts_user_parm_size))
    self.home_dir = self.__get_text (data, home_dir_offset, home_dir_size)
    self.home_dir_letter = self.__get_text (data, home_dir_letter_offset, home_dir_letter_size)
    self.logon_script = self.__get_text (data, logon_script_offset, logon_script_size)
    self.profile_path = self.__get_text (data, profile_path_offset, profile_path_size)
    self.workstation = self.__get_text (data, workstation_offset, workstation_size)
    self.hours_allowed = self.__get_field (data, hours_allowed_offset, hours_allowed_size)
    self.dummy17 = pymobius.dump (self.__get_field (data, dummy17_offset, dummy17_size))

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # Hashes
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    self.lm_hash = self.__get_field (data, lm_hash_offset, lm_hash_size)
    self.nt_hash = self.__get_field (data, nt_hash_offset, nt_hash_size)
    self.nt_hash_history = self.__get_field (data, nt_hash_history_offset, nt_hash_history_size)
    self.lm_hash_history = self.__get_field (data, lm_hash_history_offset, lm_hash_history_size)

    # account is valid
    self.__is_valid = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Check if object is valid
  # @return true/false
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def is_valid (self):
    return self.__is_valid

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Get UTF-16 field as UTF-8 text
  # @param data, offset, size
  # @return text
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __get_text (self, data, offset, size):
    pos = 0xcc
    return data[pos + offset:pos + offset + size].decode ('utf-16')

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief get data field
  # @param data, offset, size
  # @return data field
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __get_field (self, data, offset, size):
    pos = 0xcc
    return data[pos + offset:pos + offset + size]

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Terminal Server User Parameters decoder
# @author Eduardo Aguiar
# @see https://msdn.microsoft.com/en-us/library/ff635189.aspx
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class TS_User_Parameters (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief data structure
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  structure = [
    [  0, 'reserved', 'byte (96)', 'reserved data'],
    [ 96, 'signature', 'uint16_le', 'signature: letter "P" in UTF-16'],
    [ 98, 'property_count', 'uint16_le', 'property count']
  ]

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief initialize object
  # @param data data buffer
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, data):

    # check if data can be parsed
    if len (data) < 100:
      self.__is_valid = False
      return

    # parse data structure
    self.signature, property_count = struct.unpack ('96xHH', data[:100])
    self.properties = []
    pos = 100

    for i in range (property_count):
      prop = TS_Property (data[pos:])
      self.properties.append (prop)
      pos += prop.size

    self.__is_valid = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief check if object is valid
  # @return true/false
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def is_valid (self):
    return self.__is_valid

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Terminal Server Property decoder
# @author Eduardo Aguiar
# @see https://msdn.microsoft.com/en-us/library/ff635169.aspx
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class TS_Property (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief data structure
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  structure = [
    [  0, 'name_length', 'uint16_le', 'name length'],
    [  2, 'value_length', 'uint16_le', 'value length'],
    [  4, 'type', 'uint16_le', 'type'],
    [  6, 'name', 'var', 'property name'],
    [ -1, 'value', 'var', 'property value'],
  ]

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief initialize object
  # @param data data buffer
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, data):

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # check if data can be parsed
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    if len (data) < 6:
      self.__is_valid = False
      return

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # parse data structure
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    name_length, value_length, self.type = struct.unpack ('HHH', data[:6])
    pos = 6

    self.name = data[pos : pos + name_length].decode ('utf-16')
    pos += name_length

    value = mobius.encoder.hexstring (data[pos : pos + value_length])
    pos += value_length

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # format value according to var name
    # @see https://msdn.microsoft.com/en-us/library/ff635169.aspx
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    if self.name in ('CtxCfgPresent', 'CtxCfgFlags1', 'CtxCallBack', 'CtxKeyboardLayout', 'CtxNWLogonServer', 'CtxMaxConnectionTime', 'CtxMaxDisconnectionTime', 'CtxMaxIdleTime', 'CtxShadow'):
      self.value = struct.unpack ('<I', value)[0]

    elif self.name in ('CtxWFHomeDir', 'CtxWFHomeDirDrive', 'CtxInitialProgram', 'CtxWFProfilePath', 'CtxWorkDirectory', 'CtxCallbackNumber'):
      self.value = value.rstrip (' \0')

    elif self.name == 'CtxMinEncryptionLevel':
      self.value = ord (value[0])

    elif self.name == 'msNPAllowDialin':
      self.value = value.decode ('utf-16').rstrip ('\0 ')

    else:
      self.value = value

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # set size and is_valid flag
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    self.size = pos
    self.__is_valid = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Check if object is valid
  # @return true/false
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def is_valid (self):
    return self.__is_valid


# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Ggenerate SAM cryptographic key
# @param registry registry object
# @return SAM key or None
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def get_sam_key (registry):
  sam_key = None

  data = registry.get_data_by_path ('HKLM\\SAM\\SAM\\Domains\\Account\\F')

  if data and len (data.data) >= 4:
    syskey = registry.get_syskey ()
    f = data.data
    revision = struct.unpack ('<I', f[:4])[0]

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # Revision 0x10002 (up to Win8.1)
    # @see http://moyix.blogspot.com.br/2008/02/syskey-and-sam.html
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    if revision == 0x10002:
      md5 = mobius.crypt.hash ("md5")
      md5.update (f[0x70:0x80])
      md5.update (b'!@#$%^&*()qwertyUIOPAzxcvbnmQQQQQQQQQQQQ)(*@&%\0')
      md5.update (syskey)
      md5.update (b'0123456789012345678901234567890123456789\0')

      rc4 = mobius.crypt.new_cipher_stream ("rc4", md5.get_digest ())
      sam_key = rc4.encrypt (f[0x80:0xa0])[:16]

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # Revision 0x10003 (Win10)
    # @see https://github.com/gentilkiwi/mimikatz/wiki (kuhl_m_lsadump_getSamKey)
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    elif revision == 0x10003:
      struct_revision, struct_size, check_len, data_len, salt = struct.unpack ('<IIII16s', f[0x68:0x88])
      data = f[0x88:0x88 + data_len]            # followed by check

      c = mobius.crypt.new_cipher_cbc ("aes", syskey, salt)
      sam_key = c.decrypt (data)[:16]         # AES-128 (16 bytes key)

    else:
      mobius.core.logf ('DEV Domains\\Account\\F unknown revision: %08x' % revision)

  return sam_key


class passwd_hash (object):
  pass

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief OS User account
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class user_account (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self, sam_key, f, v):
    self.__sam_key = sam_key
    f = SAM_F (f.data)
    v = SAM_V (v.data)

    if not f.is_valid () or not v.is_valid ():
      self.__is_valid = False
      return

    # set data from F value
    self.f_data_revision = f.revision
    self.last_logon_datetime = f.last_logon_datetime
    self.last_password_set_datetime = f.last_password_set_datetime
    self.last_failed_logon_datetime = f.last_failed_logon_datetime
    self.password_expiration_datetime = f.password_expiration_datetime
    self.rid = f.rid
    self.gid = f.gid
    self.country_code = f.country_code
    self.failed_logon_count = f.failed_logon_count
    self.logon_count = f.logon_count
    self.acb_flags = f.flags
    self.flag_account_enabled = f.flag_account_enabled
    self.flag_home_dir_required = f.flag_home_dir_required
    self.flag_password_required = f.flag_password_required
    self.flag_temporary_duplicated_account = f.flag_temporary_duplicated_account
    self.flag_normal_user_account = f.flag_normal_user_account
    self.flag_mns_logon_user_account = f.flag_mns_logon_user_account
    self.flag_interdomain_trust_account = f.flag_interdomain_trust_account
    self.flag_workstation_trust_account = f.flag_workstation_trust_account
    self.flag_server_trust_account = f.flag_server_trust_account
    self.flag_password_expires = f.flag_password_expires
    self.flag_account_auto_locked = f.flag_account_auto_locked

    # set data from V value
    self.v_data_revision = v.revision
    self.username = v.username
    self.fullname = v.fullname
    self.admin_comment = v.admin_comment
    self.user_comment = v.user_comment
    self.home_dir = v.home_dir
    self.home_dir_letter = v.home_dir_letter
    self.logon_script = v.logon_script
    self.profile_path = v.profile_path
    self.workstation = v.workstation

    if v.ts_user_parm.is_valid ():
      self.ts_properties = v.ts_user_parm.properties
    else:
      self.ts_properties = []

    # decrypt password hashes
    self.hashes = []

    if v.nt_hash:
      self.hashes += self.__decrypt_user_hash (v.nt_hash, "nt", True)

    if v.lm_hash:
      self.hashes += self.__decrypt_user_hash (v.lm_hash, "lm", True)

    if v.nt_hash_history:
      self.hashes += self.__decrypt_user_hash (v.nt_hash_history, "nt", False)

    if v.lm_hash_history:
      self.hashes += self.__decrypt_user_hash (v.lm_hash_history, "lm", False)

    self.__is_valid = True

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Check if object is valid
  # @return true/false
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def is_valid (self):
    return self.__is_valid

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief Decrypt user hash
  # @param hdata hash encrypted data
  # @param type (NT, LM)
  # @todo revision >= 0x20000 (AES)
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __decrypt_user_hash (self, data, htype, is_current):
    pekid, revision = struct.unpack ('<HH', data[:4])

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # DES final key1 and key2 values
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    rid_data = struct.pack ('<I', self.rid)
    key1 = rid_data + rid_data[:3]
    key2 = rid_data[3:4] + rid_data + rid_data[:2]

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # Revision 0x01 - Up to Win8.1
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    if revision == 1:
      if htype == "nt":
        if is_current:
          ntlm_str = b"NTPASSWORD\0"
        else:
          ntlm_str = b"NTPASSWORDHISTORY\0"

      elif htype == "lm":
        if is_current:
          ntlm_str = b"LMPASSWORD\0"
        else:
          ntlm_str = b"LMPASSWORDHISTORY\0"

      md5 = mobius.crypt.hash ("md5")
      md5.update (self.__sam_key)
      md5.update (rid_data)
      md5.update (ntlm_str)

      rc4 = mobius.crypt.new_cipher_stream ("rc4", md5.get_digest ())
      obfkey = rc4.decrypt (data[4:])

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # Revision 0x02 - Win10
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    elif revision == 2:
      aes_revision, data_size = struct.unpack ('<II', data[:8])

      SALT_LENGTH = 16
      if data_size:
        salt = data[8:24]
        hdata = data[24:24 + data_size]
        c = mobius.crypt.new_cipher_cbc ("aes", self.__sam_key, salt)
        obfkey = c.decrypt (hdata)

      else:
        obfkey = b''

    else:
      mobius.core.logf ('DEV SAM_HASH unknown revision: %d' % revision)

    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    # Get hashes from obfkey
    # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
    NTLM_HASH_SIZE = 16
    hashes = []

    for i in range (0, len (obfkey), NTLM_HASH_SIZE):
      h = passwd_hash ()
      h.revision = revision
      h.pekid = pekid
      h.type = htype
      h.is_current = is_current

      des1 = mobius.crypt.new_cipher_ecb ("des", key1)
      des2 = mobius.crypt.new_cipher_ecb ("des", key2)
      h.value = des1.decrypt (obfkey[i:i + 8]) + des2.decrypt (obfkey[i + 8:i + NTLM_HASH_SIZE])
      hashes.append (h)

    return hashes

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief get user accounts
# @param registry registry object
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def get (registry):
  accounts = []
  sam_key = get_sam_key (registry)

  for key in registry.get_key_by_mask ('HKLM\\SAM\\SAM\\Domains\\Account\\Users\\[0-9]*'):
    f = key.get_data_by_name ('F')
    v = key.get_data_by_name ('V')

    if f and v:
      account = user_account (sam_key, f, v)
      account.username, account.hashes
      accounts.append (account)

  return accounts
