# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Mobius Forensic Toolkit
# Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018,2019,2020,2021,2022 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/>.
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
import datetime
import struct
import traceback
import mobius

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Entry
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
class Entry (object):

  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  # @brief initialize object
  # =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  def __init__ (self):
    self.path = None
    self.name = None
    self.size = None
    self.seeds = None
    self.filetype = None
    self.hash_sha1 = None
    self.evaluated_hash_sha1 = None
    self.title = None
    self.url = None
    self.comment = None
    self.flag_verified = False

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief get data as UNIX datetime
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def get_unix_datetime (timestamp):

  if timestamp > 0:
    delta = datetime.timedelta (seconds=timestamp)
    date = datetime.datetime (1970, 1, 1, 0, 0, 0) + delta
  else:
    date = None

  return date

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief decrypt data
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def decrypt (data, seed):
  out = b''
  for c in data:
    out += bytes ([c ^ (seed >> 8)])
    seed = ((c + seed) * 23219 + 36126) & 0xffff
  return out

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief get file type according to amime field
# @see const_ares.pas and helper_mimetypes.pas
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def get_file_type (mime):
  return {
      1 : 'Audio',
      2 : 'Audio',
      3 : 'Software',
      4 : 'Audio',
      5 : 'Video',
      6 : 'Document',
      7 : 'Image'}.get (mime, 'Other')

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Decode file
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def decode (f):
  try:
    return decode_file (f)
  except Exception as e:
    mobius.core.logf ('WRN %s %s' % (str (e), traceback.format_exc ()))

# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# @brief Decode data from TorrentH.dat
# @see tthread_dht.getMagnetFiles - DHT/thread_dht.pas (line 284)
# =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
def decode_file (f):

  # try to open file
  reader = f.new_reader ()
  if not reader:
    return

  # decode header
  decoder = mobius.decoder.data_decoder (reader)
  signature = decoder.get_string_by_size (14)

  # @see DHT/thread_dht.pas (line 319)
  if signature != '__ARESDB1.02H_':
    return

  # get TorrentH.dat metadata
  torrenth_path = f.path.replace ('/', '\\')
  torrenth_creation_time = f.creation_time
  torrenth_last_modification_time = f.modification_time

  # decode entries
  pos = decoder.tell ()
  size = decoder.get_size ()
  entries = []

  while pos < size:

    # decrypt entry header
    # @see DHT/thread_dht.pas (line 335)
    enc_data = decoder.get_bytearray_by_size (39)
    pos += 39
    data = decrypt (enc_data, 12971)

    # decode entry header
    # @see DHT/thread_dht.pas (line 338)
    entry = Entry ()
    entry.torrenth_path = torrenth_path
    entry.torrenth_creation_time = torrenth_creation_time
    entry.torrenth_last_modification_time = torrenth_last_modification_time

    timestamp,		\
    mime,		\
    entry.size,		\
    entry.seeds,	\
    hash_sha1,		\
    details_size = struct.unpack ('<IBQI20sH', data)

    # DHT Magnet file added date/time
    # @see BitTorrent/thread_bitTorrent.pas
    entry.timestamp = get_unix_datetime (timestamp)

    entry.filetype = get_file_type (mime)
    entry.hash_sha1 = mobius.encoder.hexstring (hash_sha1)

    # @see https://en.wikipedia.org/wiki/Magnet_URI_scheme
    entry.url = 'magnet:?xt=urn:btih:' + entry.hash_sha1

    # entry metadata
    if details_size > 0:

      # decrypt entry metadata
      # @see DHT/thread_dht.pas (line 355)
      enc_data = decoder.get_bytearray_by_size (details_size)
      data = decrypt (enc_data, 13175)
      pos += details_size

      # decode entry metadata
      # @see DHT/thread_dht.pas (line 359)
      i = 0
      while i + 2 < len (data):
        type_detail, len_detail = struct.unpack ('<BH', data[i:i + 3])
        value_detail = data[i + 3:i + 3 + len_detail]

        # @see DHT/thread_dht.pas (line 369)
        if type_detail == 1:
          entry.title = value_detail.decode ('utf-8')
          entry.name = value_detail.decode ('utf-8')

        # @see DHT/thread_dht.pas (line 370)
        elif type_detail == 2:
          entry.comment = value_detail.replace (b'\0', b' ').decode ('utf-8')

        # @see DHT/thread_dht.pas (line 371)
        elif type_detail == 3:
          entry.evaluated_hash_sha1 = mobius.encoder.hexstring (value_detail)
          entry.flag_verified = (entry.evaluated_hash_sha1 == entry.hash_sha1)

        else:
          mobius.core.logf ('DEV Unknown detail type (%d)' % type_detail)		

        i = i + 3 + len_detail

    # add entry
    entries.append (entry)

  return entries
