// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Mobius Forensic Toolkit
// Copyright (C) 2008,2009,2010,2011,2012,2013,2014,2015,2016,2017,2018 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/>.
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
#include "turing.h"
#include <mobius/core/application.h>
#include <mobius/database/connection_pool.h>
#include <mobius/crypt/crypt.h>
#include <mobius/crypt/hash_md4.h>
#include <mobius/string_functions.h>
#include <string>

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
// Usage:
//
// mobius::turing::turing turing;
// auto connection = turing.new_connection ()    // only when running in a new thread!!!
// auto transaction = turing.new_transaction ()  // only when modifying data
// ...
// operations (new_hash, remove_hash, get_hash, get_hashes, ...)
// ...
// transaction.commit ();                        // only when have modified data
// connection.release ();                        // only when running in a new thread!!!
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

namespace mobius
{
namespace turing
{
namespace
{
static const std::string LM_NULL = "aad3b435b51404ee";

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief turing data structure
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
static struct data
{
  //! \brief module's connection pool
  mobius::database::connection_pool pool;

  // constructor
  data ();

private:
  static const std::string _get_database_path ();
} data_;

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief initialize data structure
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
data::data ()
  : pool (_get_database_path ())
{
  auto db = pool.get_database ();

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // check if there are old tables
  //! \deprecated since 1.4
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  bool flag_old = false;

  {
    auto stmt = db.new_statement (
                  "SELECT * "
                  "FROM sqlite_master "
                  "WHERE type = 'table' "
                  "AND name = 'win_hash_pending'");

    if (stmt.fetch_row ())
      flag_old = true;
  }

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // get data from old tables, if any
  //! \deprecated since 1.4
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  std::vector <std::tuple <std::string, std::string, bool, std::string>> data;

  if (flag_old)
    {
      auto stmt = db.new_statement (
                    "SELECT * "
                    "FROM hash");

      while (stmt.fetch_row ())
        {
          auto type = stmt.get_column_string (1);
          auto value = stmt.get_column_string (2);
          auto password = stmt.get_column_string (3);
          bool is_password_valid = !stmt.is_column_null (3);

          if (type == "win.nt")
            type = "nt";

          else if (type == "win.lm")
            {
              type = "lm";

              if (value.length () == 16)
                value += "aad3b435b51404ee";
            }

          else
            type = "";

          if (!type.empty ())
            data.push_back (std::make_tuple (type, value, is_password_valid, password));
        }
    }

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // remove old tables, if necessary
  //! \deprecated since 1.4
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  if (flag_old)
    {
      db.execute ("DROP TABLE win_hash_pending");
      db.execute ("DROP TABLE hash");
    }

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // create tables, if necessary
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  auto transaction = db.new_transaction ();

  db.execute (
    "CREATE TABLE IF NOT EXISTS hash"
    "(uid INTEGER PRIMARY KEY AUTOINCREMENT,"
    "type TEXT NOT NULL,"
    "value TEXT NOT NULL,"
    "password TEXT NULL,"
    "password_min_size INTEGER NULL,"
    "password_max_size INTEGER NULL,"
    "password_status INTEGER NULL)");

  db.execute ("CREATE UNIQUE INDEX IF NOT EXISTS idx_hash "
              "ON hash (type, value)");

  db.execute (
    "CREATE TABLE IF NOT EXISTS hash_args"
    "(uid INTEGER PRIMARY KEY AUTOINCREMENT,"
    "uid_hash INTEGER NOT NULL,"
    "name TEXT NOT NULL,"
    "value TEXT,"
    "FOREIGN KEY (uid_hash) REFERENCES hash (uid) ON DELETE CASCADE)");

  db.execute ("CREATE UNIQUE INDEX IF NOT EXISTS idx_hash_args "
              "ON hash_args (uid_hash, name)");

  db.execute ("PRAGMA foreign_keys = ON;");

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // write back old data, if necessary
  //! \deprecated since 1.4
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  for (auto t : data)
    {
      auto stmt = db.new_statement (
                    "INSERT INTO hash "
                    "VALUES (NULL, ?, ?, ?, ?, ?, ?)");

      stmt.bind (1, std::get<0> (t));
      stmt.bind (2, std::get<1> (t));

      if (std::get<2> (t))
        {
          stmt.bind (3, std::get<3> (t));
          auto l = std::get<3> (t).length ();
          stmt.bind (4, std::int64_t (l));      // password_min_size
          stmt.bind (5, std::int64_t (l));      // password_max_size
          stmt.bind (6, 2);                     // (2 - found)
        }

      else
        {
          stmt.bind_null (3);                   // password
          stmt.bind (4, 0);                     // password_min_size
          stmt.bind_null (5);                   // password_max_size
          stmt.bind (6, 0);                     // (0 - not found)
        }

      stmt.execute ();
    }

  transaction.commit ();
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief helper function: get database path
//! \return database full path
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
const std::string
data::_get_database_path ()
{
  mobius::core::application app;
  return app.get_config_path ("turing.sqlite");
}

} // namespace

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief get database object for current thread
//! \return database object
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
mobius::database::database
get_database ()
{
  return data_.pool.get_database ();
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief create new connection for Turing database
//! \return new connection object
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
mobius::database::connection_pool::connection
turing::new_connection ()
{
  return data_.pool.acquire ();
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief create new transaction for Turing database
//! \return new database transaction
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
mobius::database::transaction
turing::new_transaction ()
{
  auto db = get_database ();
  return db.new_transaction ();
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief get hash
//! \param type hash type
//! \param value hash value
//! \return hash object
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
hash
turing::get_hash (const std::string& type, const std::string& value) const
{
  auto db = get_database ();

  auto stmt = db.new_statement (
            "SELECT uid "
              "FROM hash "
             "WHERE type = ? "
               "AND value = ?");

  stmt.bind (1, type);
  stmt.bind (2, value);

  hash h;

  if (stmt.fetch_row ())
    {
      std::int64_t uid = stmt.get_column_int64 (0);
      h = hash (uid);
    }

  return h;
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief add hash
//! \param type hash type
//! \param value hash value
//! \return hash object
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
hash
turing::new_hash (const std::string& type, const std::string& value)
{
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // check if hash already exists
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  auto h = get_hash (type, value);

  if (h)
    return h;

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // set values
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  int password_min_size = 0;
  int password_max_size = -1;
  hash::password_status status = hash::password_status::not_found;
  std::string password;

  if (type == "lm")
    {
      if (value == "aad3b435b51404eeaad3b435b51404ee")
        {
          status = hash::password_status::found;
          password_max_size = 0;
        }

      else if (mobius::string::endswith (value, LM_NULL))
        {
          password_min_size = 1;
          password_max_size = 7;
        }

      else
        {
          password_min_size = 8;
          password_max_size = 14;
        }
    }

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // insert hash
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  auto db = get_database ();

  auto stmt = db.new_statement (
           "INSERT INTO hash "
                "VALUES (NULL, ?, ?, ?, ?, ?, ?)");

  stmt.bind (1, type);
  stmt.bind (2, value);

  if (status == hash::password_status::found)
    stmt.bind (3, password);
  else
    stmt.bind_null (3);

  stmt.bind (4, password_min_size);

  if (password_max_size == -1)
    stmt.bind_null (5);
  else
    stmt.bind (5, password_max_size);

  stmt.bind (6, static_cast <int> (status));
  stmt.execute ();

  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  // return hash object
  // =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
  auto uid = db.get_last_insert_row_id ();
  return hash (uid);
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief remove hash
//! \param type hash type
//! \param value hash value
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
void
turing::remove_hash (const std::string& type, const std::string& value)
{
  auto db = get_database ();

  auto stmt = db.new_statement (
          "DELETE FROM hash "
                "WHERE type = ? "
                  "AND value = ?");

  stmt.bind (1, type);
  stmt.bind (2, value);
  stmt.execute ();
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief get hashes
//! \return hashes
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
std::vector <hash>
turing::get_hashes () const
{
  auto db = get_database ();

  auto stmt = db.new_statement (
              "SELECT uid "
                "FROM hash");

  std::vector <hash> hashes;

  while (stmt.fetch_row ())
    {
      std::int64_t uid = stmt.get_column_int64 (0);
      hashes.push_back (hash (uid));
    }

  return hashes;
}

// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
//! \brief test keyword as password for hash database
//! \param keyword keyword
// =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
void
turing::test_keyword (const std::string& keyword)
{
  // calculate unsalted hashes
  const std::string nt_value = mobius::crypt::generate_hash_nt (keyword).to_hexstring ();
  const std::string lm_value = mobius::crypt::generate_hash_lm (keyword).to_hexstring ();
  
  mobius::crypt::hash_md4 md4;
  md4.update (keyword);
  const std::string md4_value = md4.get_hex_digest ();

  // check hashes
  auto db = get_database ();

  auto stmt = db.new_statement (
              "SELECT uid, type, value "
                "FROM hash "
               "WHERE password_status != 2");

  while (stmt.fetch_row ())
    {
      std::int64_t uid = stmt.get_column_int64 (0);
      std::string type = stmt.get_column_string (1);
      std::string value = stmt.get_column_string (2);
      hash h (uid);

      // NT hash
      if (type == "nt" && value == nt_value)
        h.set_password (keyword);

      // LM hash
      else if (type == "lm" && value == lm_value)
        h.set_password (keyword);

      // MD4 hash
      else if (type == "md4" && value == md4_value)
        h.set_password (keyword);

      // MSDCC1 hash
      else if (type == "msdcc1")
        {
          std::string username = h.get_argument ("username");
          mobius::bytearray hash_value = mobius::crypt::generate_hash_msdcc1 (keyword, username);

          if (hash_value.to_hexstring () == h.get_value ())
            h.set_password (keyword);
        }

      // MSDCC2 is too slow to test
    }
}

} // namespace turing
} // namespace mobius
