/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "util.h"

#include <fstream>
#include <iomanip>
#include <iostream>
#include <sstream>
#include <string>

#include <prerror.h>

#if defined(__unix__) || defined(__APPLE__)
#include <termios.h>
#include <unistd.h>
#elif defined(WIN32) || defined(_WIN64)
#include <Windows.h>
#endif

static std::string GetPassword(const std::string &prompt) {
  std::cout << prompt << std::endl;

#if defined(__unix__) || defined(__APPLE__)
  termios oldt;
  tcgetattr(STDIN_FILENO, &oldt);
  termios newt = oldt;
  newt.c_lflag &= ~ECHO;
  tcsetattr(STDIN_FILENO, TCSANOW, &newt);
#elif defined(WIN32) || defined(_WIN64)
  HANDLE hStdin = GetStdHandle(STD_INPUT_HANDLE);
  DWORD mode = 0;
  GetConsoleMode(hStdin, &mode);
  SetConsoleMode(hStdin, mode & (~ENABLE_ECHO_INPUT));
#endif

  std::string pw;
  std::getline(std::cin, pw);

#if defined(__unix__) || defined(__APPLE__)
  tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
#elif defined(WIN32) || defined(_WIN64)
  SetConsoleMode(hStdin, mode);
#endif

  return pw;
}

static char *GetModulePassword(PK11SlotInfo *slot, int retry, void *arg) {
  if (arg == nullptr) {
    return nullptr;
  }

  PwData *pwData = reinterpret_cast<PwData *>(arg);

  if (retry > 0) {
    std::cerr << "Incorrect password/PIN entered." << std::endl;
    return nullptr;
  }

  switch (pwData->source) {
    case PW_NONE:
    case PW_FROMFILE:
      std::cerr << "Password input method not supported." << std::endl;
      return nullptr;
    case PW_PLAINTEXT:
      return PL_strdup(pwData->data);
    default:
      break;
  }

  std::cerr << "Password check failed:  No password found." << std::endl;
  return nullptr;
}

static std::vector<uint8_t> ReadFromIstream(std::istream &is) {
  std::vector<uint8_t> data;
  while (is) {
    char buf[1024];
    is.read(buf, sizeof(buf));
    data.insert(data.end(), buf, buf + is.gcount());
  }

  return data;
}

static std::string GetNewPasswordFromUser(void) {
  std::string pw;

  while (true) {
    pw = GetPassword("Enter new password: ");
    if (pw == GetPassword("Re-enter password: ")) {
      break;
    }

    std::cerr << "Passwords do not match. Try again." << std::endl;
  }

  return pw;
}

bool InitSlotPassword(void) {
  ScopedPK11SlotInfo slot(PK11_GetInternalKeySlot());
  if (slot.get() == nullptr) {
    std::cerr << "Error: Init PK11SlotInfo failed!" << std::endl;
    return false;
  }

  std::cout << "Enter a password which will be used to encrypt your keys."
            << std::endl
            << std::endl;
  std::string pw = GetNewPasswordFromUser();

  SECStatus rv = PK11_InitPin(slot.get(), nullptr, pw.c_str());
  if (rv != SECSuccess) {
    std::cerr << "Init db password failed." << std::endl;
    return false;
  }

  return true;
}

bool ChangeSlotPassword(void) {
  ScopedPK11SlotInfo slot(PK11_GetInternalKeySlot());
  if (slot.get() == nullptr) {
    std::cerr << "Error: Init PK11SlotInfo failed!" << std::endl;
    return false;
  }

  // get old password and authenticate to db
  PK11_SetPasswordFunc(&GetModulePassword);
  std::string oldPw = GetPassword("Enter your current password: ");
  PwData pwData = {PW_PLAINTEXT, const_cast<char *>(oldPw.c_str())};
  SECStatus rv = PK11_Authenticate(slot.get(), false /*loadCerts*/, &pwData);
  if (rv != SECSuccess) {
    std::cerr << "Password incorrect." << std::endl;
    return false;
  }

  // get new password
  std::string newPw = GetNewPasswordFromUser();

  if (PK11_ChangePW(slot.get(), oldPw.c_str(), newPw.c_str()) != SECSuccess) {
    std::cerr << "Failed to change password." << std::endl;
    return false;
  }

  std::cout << "Password changed successfully." << std::endl;
  return true;
}

bool DBLoginIfNeeded(const ScopedPK11SlotInfo &slot) {
  if (!PK11_NeedLogin(slot.get())) {
    return true;
  }

  PK11_SetPasswordFunc(&GetModulePassword);
  std::string pw = GetPassword("Enter your password: ");
  PwData pwData = {PW_PLAINTEXT, const_cast<char *>(pw.c_str())};
  SECStatus rv = PK11_Authenticate(slot.get(), true /*loadCerts*/, &pwData);
  if (rv != SECSuccess) {
    std::cerr << "Could not authenticate to token "
              << PK11_GetTokenName(slot.get()) << ". Failed with error "
              << PR_ErrorToName(PR_GetError()) << std::endl;
    return false;
  }
  std::cout << std::endl;

  return true;
}

std::string StringToHex(const ScopedSECItem &input) {
  std::stringstream ss;
  ss << "0x";
  for (size_t i = 0; i < input->len; i++) {
    ss << std::hex << std::setfill('0') << std::setw(2)
       << static_cast<int>(input->data[i]);
  }

  return ss.str();
}

std::vector<uint8_t> ReadInputData(std::string dataPath) {
  std::vector<uint8_t> data;
  if (dataPath.empty()) {
    std::cout << "No input file path given, using stdin." << std::endl;
    data = ReadFromIstream(std::cin);
  } else {
    std::ifstream is(dataPath, std::ifstream::binary);
    if (is.good()) {
      data = ReadFromIstream(is);
    } else {
      std::cerr << "IO Error when opening " << dataPath << std::endl;
      std::cerr << "Input file does not exist or you don't have permissions."
                << std::endl;
    }
  }

  return data;
}

std::istream &GetStreamFromFileOrStdin(std::string &path, std::ifstream &ifs) {
  if (path.empty()) {
    return std::cin;
  }

  ifs.open(path, std::ifstream::binary);
  if (!ifs.good()) {
    std::cerr << "IO Error when opening " << path << std::endl;
    std::cerr << "Input file does not exist or you don't have permissions."
              << std::endl;
  }

  return ifs;
}
