/*
   BAREOS® - Backup Archiving REcovery Open Sourced

   Copyright (C) 2012-2012 Free Software Foundation Europe e.V.
   Copyright (C) 2011-2012 Planets Communications B.V.
   Copyright (C) 2013-2026 Bareos GmbH & Co. KG

   This program is Free Software; you can redistribute it and/or
   modify it under the terms of version three of the GNU Affero General Public
   License as published by the Free Software Foundation and included
   in the file LICENSE.

   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
   Affero General Public License for more details.

   You should have received a copy of the GNU Affero General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
   02110-1301, USA.
*/
// Marco van Wieringen, March 2012
/**
 * @file
 * Load and clear an SCSI encryption key used by a tape drive
 * using a lowlevel SCSI interface.
 */

#if defined(HAVE_MSVC)
#  include "getopt.h"
#else
#  include <unistd.h>
#endif
#include "include/fcntl_def.h"
#include "include/bareos.h"
#include "lib/cli.h"
#include "lib/crypto_cache.h"
#include "lib/crypto_wrap.h"
#include "lib/passphrase.h"
#include "lib/scsi_crypto.h"
#include "lib/base64.h"


static void TerminateBscrypto(int exitcode)
{
  TermMsg();
  exit(exitcode);
}

/**
 * Read the key bits from the keyfile.
 * - == stdin
 */
static void ReadKeyBits(const std::string& keyfile,
                        char* data,
                        size_t sizeof_data)
{
  int kfd = 0;
  if (bstrcmp(keyfile.c_str(), "-")) {
    kfd = 0;
    fprintf(stdout, T_("Enter Encryption Key (close with ^D): "));
    fflush(stdout);
  } else {
    kfd = open(keyfile.c_str(), O_RDONLY);
    if (kfd < 0) {
      fprintf(stderr, T_("Cannot open keyfile %s\n"), keyfile.c_str());
      TerminateBscrypto(1);
    }
  }
  Dmsg1(5, "data size = %zu\n", sizeof_data);
  if (read(kfd, data, sizeof_data) == 0) {
    fprintf(stderr, T_("Cannot read from keyfile %s\n"), keyfile.c_str());
    TerminateBscrypto(1);
  }
  if (kfd > 0) { close(kfd); }
  StripTrailingJunk(data);
  Dmsg1(10, "Key data = %s\n", data);
}

static void wrap_key(const std::string& wrap_keyfile,
                     const char* passphrase,
                     char* keydata,
                     size_t keydata_len)
{
  char wrapdata[64];
  memset(wrapdata, 0, sizeof(wrapdata));
  ReadKeyBits(wrap_keyfile, wrapdata, sizeof(wrapdata));

  memset(keydata, 0, keydata_len);
  if (auto error
      = AesWrap((unsigned char*)wrapdata, DEFAULT_PASSPHRASE_LENGTH / 8,
                (unsigned char*)passphrase, (unsigned char*)keydata)) {
    fprintf(stderr, T_("Cannot wrap passphrase ERR=%s\n"), error->c_str());
    TerminateBscrypto(1);
  }
}

static void unwrap_key(char* keydata,
                       const std::string& keyfile,
                       const std::string& wrap_keyfile)
{
  char wrapdata[64];
  memset(wrapdata, 0, sizeof(wrapdata));
  ReadKeyBits(wrap_keyfile, wrapdata, sizeof(wrapdata));

  /* A wrapped key is base64 encoded after it was wrapped so first
   * convert it from base64 to bin. As we first go from base64 to bin
   * and the Base64ToBin has a check if the decoded string will fit
   * we need to allocate some more bytes for the decoded buffer to be
   * sure it will fit. */
  size_t length = DEFAULT_PASSPHRASE_LENGTH + 12;
  char* wrapped_passphrase = (char*)malloc(length);
  memset(wrapped_passphrase, 0, length);
  if (Base64ToBin(wrapped_passphrase, length, keydata, strlen(keydata)) == 0) {
    fprintf(stderr,
            T_("Failed to base64 decode the keydata read from %s, "
               "aborting...\n"),
            keyfile.c_str());
    free(wrapped_passphrase);
    TerminateBscrypto(0);
  }

  length = DEFAULT_PASSPHRASE_LENGTH;

  if (auto error = AesUnwrap((unsigned char*)wrapdata, length / 8,
                             (unsigned char*)wrapped_passphrase,
                             (unsigned char*)keydata)) {
    fprintf(stderr,
            T_("Failed to aes unwrap the keydata read from %s using the "
               "wrap data from %s ERR=%s, aborting...\n"),
            keyfile.c_str(), wrap_keyfile.c_str(), error->c_str());
    free(wrapped_passphrase);
    TerminateBscrypto(0);
  }
  keydata[DEFAULT_PASSPHRASE_LENGTH] = '\0';
  free(wrapped_passphrase);
}

int main(int argc, char* const* argv)
{
  setlocale(LC_ALL, "");
  tzset();
  bindtextdomain("bareos", LOCALEDIR);
  textdomain("bareos");

  CLI::App bscrypto_app;
  InitCLIApp(bscrypto_app, "The Bareos encryption tool.");

  bool base64_transform = false;
  bscrypto_app.add_flag("-b,--base64", base64_transform,
                        "Perform base64 encoding of keydata.");

  bool clear_encryption = false;
  auto option_clear_encryption = bscrypto_app.add_flag(
      "-c,--clear-encryption", clear_encryption, "Clear encryption key.");

  bool dump_cache = false;
  std::string cache_file{};
  auto option_dump_cache
      = bscrypto_app
            .add_option(
                "-D,--dump-cache",
                [&dump_cache, &cache_file](std::vector<std::string> val) {
                  dump_cache = true;
                  cache_file = val[0];
                  return true;
                },
                "Dump content of given cachefile.")
            ->type_name("<cachefile>")
            ->excludes(option_clear_encryption);

  AddDebugOptions(bscrypto_app);

  bool drive_encryption_status = false;
  auto option_drive_encryption_status
      = bscrypto_app
            .add_flag("-e,--drive-encryption-status", drive_encryption_status,
                      "Show drive encryption status.")
            ->excludes(option_clear_encryption)
            ->excludes(option_dump_cache);

  bool generate_passphrase = false;
  std::string keyfile{};
  auto option_generate_pass
      = bscrypto_app
            .add_option(
                "-g,--generate-passphrase",
                [&generate_passphrase, &keyfile](std::vector<std::string> val) {
                  generate_passphrase = true;
                  keyfile = val[0];
                  return true;
                },
                "Generate new encryption passphrase in keyfile.")
            ->type_name("<keyfile>")
            ->excludes(option_clear_encryption)
            ->excludes(option_drive_encryption_status);

  bool show_keydata = false;
  auto option_show_keyfile
      = bscrypto_app
            .add_option(
                "-k,--show-keyfile",
                [&show_keydata, &keyfile](std::vector<std::string> val) {
                  show_keydata = true;
                  keyfile = val[0];
                  return true;
                },
                "Show content of keyfile.")
            ->type_name("<keyfile>")
            ->excludes(option_generate_pass)
            ->excludes(option_clear_encryption)
            ->excludes(option_drive_encryption_status);

  bool populate_cache = false;
  auto option_populate_cache
      = bscrypto_app
            .add_option(
                "-p,--populate-cachefile",
                [&populate_cache, &cache_file](std::vector<std::string> val) {
                  populate_cache = true;
                  cache_file = val[0];
                  return true;
                },
                "Populate given cachefile with crypto keys.")
            ->type_name("<cachefile>")
            ->excludes(option_clear_encryption)
            ->excludes(option_drive_encryption_status);

  bool reset_cache = false;
  auto option_reset_cache
      = bscrypto_app
            .add_option(
                "-r,--reset-expiry-time",
                [&reset_cache, &cache_file](std::vector<std::string> val) {
                  reset_cache = true;
                  cache_file = val[0];
                  return true;
                },
                "Reset expiry time for entries of given cachefile.")
            ->type_name("<cachefile>")
            ->excludes(option_clear_encryption)
            ->excludes(option_drive_encryption_status);

  bool set_encryption = false;
  auto option_set_encryption
      = bscrypto_app
            .add_option(
                "-s,--set-encryption-key",
                [&set_encryption, &keyfile](std::vector<std::string> val) {
                  set_encryption = true;
                  keyfile = val[0];
                  return true;
                },
                "Set encryption key loaded from keyfile.")
            ->type_name("<keyfile>")
            ->excludes(option_show_keyfile)
            ->excludes(option_clear_encryption)
            ->excludes(option_reset_cache)
            ->excludes(option_dump_cache)
            ->excludes(option_drive_encryption_status)
            ->excludes(option_generate_pass);

  bool volume_encryption_status = false;
  bscrypto_app
      .add_flag("-v,--volume-encryption-status", volume_encryption_status,
                "Show volume encryption status.")
      ->excludes(option_clear_encryption)
      ->excludes(option_set_encryption)
      ->excludes(option_generate_pass)
      ->excludes(option_show_keyfile)
      ->excludes(option_dump_cache)
      ->excludes(option_populate_cache)
      ->excludes(option_reset_cache);

  bool wrapped_keys = false;
  std::string wrap_keyfile{};
  bscrypto_app
      .add_option(
          "-w,--wrap-unwrap-key",
          [&wrapped_keys, &wrap_keyfile](std::vector<std::string> val) {
            wrapped_keys = true;
            wrap_keyfile = val[0];
            return true;
          },
          "Wrap/Unwrap the key using RFC3394 aes-(un)wrap using the key in "
          "keyfile as a Key Encryption.")
      ->type_name("<keyfile>");

  std::string device_name{};
  bscrypto_app.add_option("device_name", device_name, "Name of device.");

  CLI11_PARSE(bscrypto_app, argc, argv);

  InitMsg(nullptr, nullptr);

  if (!generate_passphrase && !show_keydata && !dump_cache && !populate_cache
      && !reset_cache && device_name.empty()) {
    fprintf(stderr, T_("Missing device_name argument for this option\n"));
    TerminateBscrypto(1);
  }

  if (generate_passphrase && show_keydata) {
    fprintf(stderr, T_("Either use -g or -k not both\n"));
    TerminateBscrypto(1);
  }

  if (clear_encryption && set_encryption) {
    fprintf(stderr, T_("Either use -c or -s not both\n"));
    TerminateBscrypto(1);
  }

  if ((clear_encryption || set_encryption)
      && (drive_encryption_status || volume_encryption_status)) {
    fprintf(
        stderr,
        T_("Either set or clear the crypto key or ask for status not both\n"));
    TerminateBscrypto(1);
  }

  if ((clear_encryption || set_encryption || drive_encryption_status
       || volume_encryption_status)
      && (generate_passphrase || show_keydata || dump_cache || populate_cache
          || reset_cache)) {
    fprintf(stderr, T_("Don't mix operations which are incompatible "
                       "e.g. generate/show vs set/clear etc.\n"));
    TerminateBscrypto(1);
  }

  OSDependentInit();

  if (dump_cache) {
    // Load any keys currently in the cache.
    ReadCryptoCache(cache_file.c_str());

    // Dump the content of the cache.
    DumpCryptoCache(STDOUT_FILENO);

    FlushCryptoCache();
    TerminateBscrypto(0);
  }

  if (populate_cache) {
    char *VolumeName, *EncrKey;
    char new_cache_entry[256];

    // Load any keys currently in the cache.
    ReadCryptoCache(cache_file.c_str());

    /* Read new entries from stdin and parse them to update
     * the cache. */
    fprintf(stdout, T_("Enter cache entries (close with ^D): "));
    fflush(stdout);

    memset(new_cache_entry, 0, sizeof(new_cache_entry));
    while (read(1, new_cache_entry, sizeof(new_cache_entry)) > 0) {
      StripTrailingJunk(new_cache_entry);

      // Try to parse the entry.
      VolumeName = new_cache_entry;
      EncrKey = strchr(new_cache_entry, '\t');
      if (!EncrKey) { break; }

      *EncrKey++ = '\0';
      UpdateCryptoCache(VolumeName, EncrKey);
      memset(new_cache_entry, 0, sizeof(new_cache_entry));
    }

    // Write out the new cache entries.
    WriteCryptoCache(cache_file.c_str());

    FlushCryptoCache();
    TerminateBscrypto(0);
  }

  if (reset_cache) {
    // Load any keys currently in the cache.
    ReadCryptoCache(cache_file.c_str());

    // Reset all entries.
    ResetCryptoCache();

    // Write out the new cache entries.
    WriteCryptoCache(cache_file.c_str());

    FlushCryptoCache();
    TerminateBscrypto(0);
  }


  char keydata[64];
  memset(keydata, 0, sizeof(keydata));
  /* Generate a new passphrase allow it to be wrapped using the given wrapkey
   * and base64 if specified or when wrapped. */
  int length = DEFAULT_PASSPHRASE_LENGTH;
  if (generate_passphrase) {
    int cnt;
    char* passphrase;

    passphrase = generate_crypto_passphrase(DEFAULT_PASSPHRASE_LENGTH);
    if (!passphrase) { TerminateBscrypto(1); }

    Dmsg1(10, T_("Generated passphrase = %.*s\n"), DEFAULT_PASSPHRASE_LENGTH,
          passphrase);

    // See if we need to wrap the passphrase.
    if (wrapped_keys) {
      wrap_key(wrap_keyfile, passphrase, keydata, sizeof(keydata));
      ASSERT(strlen(keydata) <= sizeof(keydata));
      length = DEFAULT_PASSPHRASE_LENGTH + 8;
      free(passphrase);
      passphrase = (char*)malloc(length);
      memcpy(passphrase, keydata, length);
    } else {
      length = DEFAULT_PASSPHRASE_LENGTH;
    }

    /* See where to write the key.
     * - == stdout */
    int kfd;
    if (bstrcmp(keyfile.c_str(), "-")) {
      kfd = 1;
    } else {
      kfd = open(keyfile.c_str(), O_WRONLY | O_CREAT, 0644);
      if (kfd < 0) {
        fprintf(stderr, T_("Cannot open keyfile %s\n"), keyfile.c_str());
        free(passphrase);
        TerminateBscrypto(1);
      }
    }

    if (base64_transform || wrapped_keys) {
      cnt = BinToBase64(keydata, sizeof(keydata), passphrase, length, true);
      if (write(kfd, keydata, cnt) != cnt) {
        fprintf(stderr, T_("Failed to write %d bytes to keyfile %s\n"), cnt,
                keyfile.c_str());
      }
    } else {
      cnt = DEFAULT_PASSPHRASE_LENGTH;
      if (write(kfd, passphrase, cnt) != cnt) {
        fprintf(stderr, T_("Failed to write %d bytes to keyfile %s\n"), cnt,
                keyfile.c_str());
      }
    }

    Dmsg1(10, "Keydata = %s\n", keydata);

    if (kfd > 1) {
      close(kfd);
    } else {
      if (write(kfd, "\n", 1) == 0) {
        free(passphrase);
        TerminateBscrypto(0);
      }
    }
    free(passphrase);
    TerminateBscrypto(0);
  }

  if (show_keydata) {
    ReadKeyBits(keyfile, keydata, sizeof(keydata));
    char* passphrase;

    // See if we need to unwrap the passphrase.
    if (wrapped_keys) {
      unwrap_key(keydata, keyfile, wrap_keyfile);
      length = strnlen(keydata, length);
      passphrase = (char*)calloc(length, 1);
      memcpy(passphrase, keydata, length);
    } else {
      if (base64_transform) {
        /* As we first go from base64 to bin and the Base64ToBin has a check
         * if the decoded string will fit we need to allocate some more bytes
         * for the decoded buffer to be sure it will fit. */
        length = DEFAULT_PASSPHRASE_LENGTH + 4;
        passphrase = (char*)calloc(length, 1);
        Base64ToBin(passphrase, length, keydata, strlen(keydata));
      } else {
        length = DEFAULT_PASSPHRASE_LENGTH;
        // do not use bstrncpy here since it copies a 31 characters and appends
        // a terminating NUL whereas passphrase is not a cstring and consists
        // of 32 copied characters of keydata
        length = strnlen(keydata, length);
        passphrase = (char*)calloc(length, 1);
        memcpy(passphrase, keydata, length);
      }
    }

    Dmsg1(10, "Unwrapped passphrase = %.*s\n", DEFAULT_PASSPHRASE_LENGTH,
          passphrase);
    fprintf(stdout, T_("%.*s\n"), DEFAULT_PASSPHRASE_LENGTH, passphrase);

    free(passphrase);
    TerminateBscrypto(0);
  }

  // Clear the loaded encryption key of the given drive.
  if (clear_encryption) {
    if (ClearScsiEncryptionKey(-1, device_name.c_str())) {
      TerminateBscrypto(0);
    } else {
      TerminateBscrypto(1);
    }
  }

  // Get the drive encryption status of the given drive.
  if (drive_encryption_status) {
    POOLMEM* encryption_status = GetPoolMemory(PM_MESSAGE);

    if (GetScsiDriveEncryptionStatus(-1, device_name.c_str(), encryption_status,
                                     0)) {
      fprintf(stdout, T_("%s"), encryption_status);
      FreePoolMemory(encryption_status);
    } else {
      FreePoolMemory(encryption_status);
      TerminateBscrypto(1);
    }
  }

  // Load a new encryption key onto the given drive.
  if (set_encryption) {
    ReadKeyBits(keyfile, keydata, sizeof(keydata));

    if (wrapped_keys) { unwrap_key(keydata, keyfile, wrap_keyfile); }

    Dmsg1(10, "keydata: %s\n", keydata);
    if (SetScsiEncryptionKey(-1, device_name.c_str(), keydata)) {
      TerminateBscrypto(0);
    } else {
      TerminateBscrypto(1);
    }
  }

  /* Get the volume encryption status of volume currently loaded in the given
   * drive. */
  if (volume_encryption_status) {
    POOLMEM* encryption_status = GetPoolMemory(PM_MESSAGE);

    if (GetScsiVolumeEncryptionStatus(-1, device_name.c_str(),
                                      encryption_status, 0)) {
      fprintf(stdout, T_("%s"), encryption_status);
      FreePoolMemory(encryption_status);
    } else {
      FreePoolMemory(encryption_status);
      TerminateBscrypto(1);
    }
  }
  TermMsg();
  TerminateBscrypto(0);
}
