/* g13-syshelp.c - Helper for disk key management with GnuPG
 * Copyright (C) 2015 Werner Koch
 *
 * This file is part of GnuPG.
 *
 * GnuPG 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 3 of the License, or
 * (at your option) any later version.
 *
 * GnuPG 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 <https://www.gnu.org/licenses/>.
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#include <config.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>
#include <unistd.h>
#include <fcntl.h>
#include <limits.h>
#ifdef HAVE_PWD_H
# include <pwd.h>
#endif
#include <unistd.h>

#define INCLUDED_BY_MAIN_MODULE 1
#include "g13-syshelp.h"

#include <gcrypt.h>
#include <assuan.h>

#include "../common/i18n.h"
#include "../common/sysutils.h"
#include "../common/asshelp.h"
#include "../common/init.h"
#include "keyblob.h"


enum cmd_and_opt_values {
  aNull = 0,
  oQuiet	= 'q',
  oVerbose	= 'v',
  oRecipient	= 'r',

  aGPGConfList  = 500,

  oDebug,
  oDebugLevel,
  oDebugAll,
  oDebugNone,
  oDebugWait,
  oDebugAllowCoreDump,
  oLogFile,
  oNoLogFile,
  oAuditLog,

  oOutput,

  oAgentProgram,
  oGpgProgram,
  oType,

  oDisplay,
  oTTYname,
  oTTYtype,
  oLCctype,
  oLCmessages,
  oXauthority,

  oStatusFD,
  oLoggerFD,

  oNoVerbose,
  oNoSecmemWarn,
  oHomedir,
  oDryRun,
  oNoDetach,

  oNoRandomSeedFile,
  oFakedSystemTime
 };


static ARGPARSE_OPTS opts[] = {

  ARGPARSE_s_n (oDryRun, "dry-run", N_("do not make any changes")),

  ARGPARSE_s_n (oVerbose, "verbose", N_("verbose")),
  ARGPARSE_s_n (oQuiet,	"quiet",  N_("be somewhat more quiet")),

  ARGPARSE_s_s (oDebug, "debug", "@"),
  ARGPARSE_s_s (oDebugLevel, "debug-level",
                N_("|LEVEL|set the debugging level to LEVEL")),
  ARGPARSE_s_n (oDebugAll, "debug-all", "@"),
  ARGPARSE_s_n (oDebugNone, "debug-none", "@"),
  ARGPARSE_s_i (oDebugWait, "debug-wait", "@"),
  ARGPARSE_s_n (oDebugAllowCoreDump, "debug-allow-core-dump", "@"),

  ARGPARSE_end ()
};


/* The list of supported debug flags.  */
static struct debug_flags_s debug_flags [] =
  {
    { DBG_MOUNT_VALUE  , "mount"  },
    { DBG_CRYPTO_VALUE , "crypto"  },
    { DBG_MEMORY_VALUE , "memory"  },
    { DBG_MEMSTAT_VALUE, "memstat" },
    { DBG_IPC_VALUE    , "ipc"     },
    { 0, NULL }
  };


/* The timer tick interval used by the idle task.  */
#define TIMERTICK_INTERVAL_SEC     (1)

/* It is possible that we are currently running under setuid permissions.  */
static int maybe_setuid = 1;

/* Helper to implement --debug-level and --debug.  */
static const char *debug_level;
static unsigned int debug_value;


/* Local prototypes.  */
static void g13_syshelp_deinit_default_ctrl (ctrl_t ctrl);
static void release_tab_items (tab_item_t tab);
static tab_item_t parse_g13tab (const char *username);



static const char *
my_strusage( int level )
{
  const char *p;

  switch (level)
    {
    case  9: p = "GPL-3.0-or-later"; break;
    case 11: p = "@G13@-syshelp (@GNUPG@)";
      break;
    case 13: p = VERSION; break;
    case 14: p = GNUPG_DEF_COPYRIGHT_LINE; break;
    case 17: p = PRINTABLE_OS_NAME; break;
    case 19: p = _("Please report bugs to <" PACKAGE_BUGREPORT ">.\n");
      break;
    case 1:
    case 40: p = _("Usage: @G13@-syshelp [options] [files] (-h for help)");
      break;
    case 41:
      p = _("Syntax: @G13@-syshelp [options] [files]\n"
            "Helper to perform root-only tasks for g13\n");
      break;

    case 31: p = "\nHome: "; break;
    case 32: p = gnupg_homedir (); break;

    default: p = NULL; break;
    }
  return p;
}


/* Setup the debugging.  With a DEBUG_LEVEL of NULL only the active
   debug flags are propagated to the subsystems.  With DEBUG_LEVEL
   set, a specific set of debug flags is set; and individual debugging
   flags will be added on top.  */
static void
set_debug (void)
{
  int numok = (debug_level && digitp (debug_level));
  int numlvl = numok? atoi (debug_level) : 0;

  if (!debug_level)
    ;
  else if (!strcmp (debug_level, "none") || (numok && numlvl < 1))
    opt.debug = 0;
  else if (!strcmp (debug_level, "basic") || (numok && numlvl <= 2))
    opt.debug = DBG_IPC_VALUE|DBG_MOUNT_VALUE;
  else if (!strcmp (debug_level, "advanced") || (numok && numlvl <= 5))
    opt.debug = DBG_IPC_VALUE|DBG_MOUNT_VALUE;
  else if (!strcmp (debug_level, "expert") || (numok && numlvl <= 8))
    opt.debug = (DBG_IPC_VALUE|DBG_MOUNT_VALUE|DBG_CRYPTO_VALUE);
  else if (!strcmp (debug_level, "guru") || numok)
    {
      opt.debug = ~0;
      /* if (numok) */
      /*   opt.debug &= ~(DBG_HASHING_VALUE); */
    }
  else
    {
      log_error (_("invalid debug-level '%s' given\n"), debug_level);
      g13_exit(2);
    }

  opt.debug |= debug_value;

  if (opt.debug && !opt.verbose)
    opt.verbose = 1;
  if (opt.debug)
    opt.quiet = 0;

  if (opt.debug & DBG_CRYPTO_VALUE )
    gcry_control (GCRYCTL_SET_DEBUG_FLAGS, 1);
  gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose);

  if (opt.debug)
    parse_debug_flag (NULL, &opt.debug, debug_flags);
}


int
main ( int argc, char **argv)
{
  ARGPARSE_ARGS pargs;
  int orig_argc;
  char **orig_argv;
  gpg_error_t err = 0;
  /* const char *fname; */
  int may_coredump;
  char *last_configname = NULL;
  const char *configname = NULL;
  int debug_argparser = 0;
  int no_more_options = 0;
  char *logfile = NULL;
  /* int debug_wait = 0; */
  int use_random_seed = 1;
  /* int nodetach = 0; */
  /* int nokeysetup = 0; */
  struct server_control_s ctrl;

  /*mtrace();*/

  early_system_init ();
  gnupg_reopen_std (G13_NAME "-syshelp");
  set_strusage (my_strusage);
  gcry_control (GCRYCTL_SUSPEND_SECMEM_WARN);

  log_set_prefix (G13_NAME "-syshelp", GPGRT_LOG_WITH_PREFIX);

  /* Make sure that our subsystems are ready.  */
  i18n_init ();
  init_common_subsystems (&argc, &argv);

  /* Take extra care of the random pool.  */
  gcry_control (GCRYCTL_USE_SECURE_RNDPOOL);

  may_coredump = disable_core_dumps ();

  g13_init_signals ();

  dotlock_create (NULL, 0); /* Register locking cleanup.  */

  opt.session_env = session_env_new ();
  if (!opt.session_env)
    log_fatal ("error allocating session environment block: %s\n",
               strerror (errno));

  /* First check whether we have a debug option on the commandline.  */
  orig_argc = argc;
  orig_argv = argv;
  pargs.argc = &argc;
  pargs.argv = &argv;
  pargs.flags= (ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_NOVERSION);
  while (gnupg_argparse (NULL, &pargs, opts))
    {
      switch (pargs.r_opt)
        {
        case oDebug:
        case oDebugAll:
          debug_argparser++;
          break;
        }
    }
  /* Reset the flags.  */
  pargs.flags &= ~(ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_NOVERSION);

  /* Initialize the secure memory. */
  gcry_control (GCRYCTL_INIT_SECMEM, 16384, 0);
  maybe_setuid = 0;

  /*
   * Now we are now working under our real uid
   */

  /* Setup malloc hooks. */
  {
    struct assuan_malloc_hooks malloc_hooks;

    malloc_hooks.malloc = gcry_malloc;
    malloc_hooks.realloc = gcry_realloc;
    malloc_hooks.free = gcry_free;
    assuan_set_malloc_hooks (&malloc_hooks);
  }

  /* Prepare libassuan.  */
  assuan_set_gpg_err_source (GPG_ERR_SOURCE_DEFAULT);
  /*assuan_set_system_hooks (ASSUAN_SYSTEM_NPTH);*/
  setup_libassuan_logging (&opt.debug, NULL);

  /* Setup a default control structure for command line mode.  */
  memset (&ctrl, 0, sizeof ctrl);
  g13_syshelp_init_default_ctrl (&ctrl);
  ctrl.no_server = 1;
  ctrl.status_fd = -1; /* No status output. */

  /* The configuraton directories for use by gpgrt_argparser.  */
  gnupg_set_confdir (GNUPG_CONFDIR_SYS, gnupg_sysconfdir ());
  gnupg_set_confdir (GNUPG_CONFDIR_USER, gnupg_homedir ());

  argc        = orig_argc;
  argv        = orig_argv;
  pargs.argc  = &argc;
  pargs.argv  = &argv;
  pargs.flags |=  (ARGPARSE_FLAG_RESET
                   | ARGPARSE_FLAG_KEEP
                   | ARGPARSE_FLAG_SYS
                   | ARGPARSE_FLAG_USER);

  while (!no_more_options
         && gnupg_argparser (&pargs, opts, G13_NAME"-syshelp" EXTSEP_S "conf"))
    {
      switch (pargs.r_opt)
        {
        case ARGPARSE_CONFFILE:
          {
            if (debug_argparser)
              log_info (_("reading options from '%s'\n"),
                        pargs.r_type? pargs.r.ret_str: "[cmdline]");
            if (pargs.r_type)
              {
                xfree (last_configname);
                last_configname = xstrdup (pargs.r.ret_str);
                configname = last_configname;
              }
            else
              configname = NULL;
          }
          break;

        case oQuiet: opt.quiet = 1; break;

        case oDryRun: opt.dry_run = 1; break;

        case oVerbose:
          opt.verbose++;
          gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose);
          break;
        case oNoVerbose:
          opt.verbose = 0;
          gcry_control (GCRYCTL_SET_VERBOSITY, (int)opt.verbose);
          break;

        case oLogFile: logfile = pargs.r.ret_str; break;
        case oNoLogFile: logfile = NULL; break;

        case oNoDetach: /*nodetach = 1; */break;

        case oDebug:
          if (parse_debug_flag (pargs.r.ret_str, &opt.debug, debug_flags))
            {
              pargs.r_opt = ARGPARSE_INVALID_ARG;
              pargs.err = ARGPARSE_PRINT_ERROR;
            }
            break;
        case oDebugAll: debug_value = ~0; break;
        case oDebugNone: debug_value = 0; break;
        case oDebugLevel: debug_level = pargs.r.ret_str; break;
        case oDebugWait: /*debug_wait = pargs.r.ret_int; */break;
        case oDebugAllowCoreDump:
          may_coredump = enable_core_dumps ();
          break;

        case oStatusFD: ctrl.status_fd = pargs.r.ret_int; break;
        case oLoggerFD: log_set_fd (pargs.r.ret_int ); break;

        case oHomedir: gnupg_set_homedir (pargs.r.ret_str); break;

        case oFakedSystemTime:
          {
            time_t faked_time = isotime2epoch (pargs.r.ret_str);
            if (faked_time == (time_t)(-1))
              faked_time = (time_t)strtoul (pargs.r.ret_str, NULL, 10);
            gnupg_set_time (faked_time, 0);
          }
          break;

        case oNoSecmemWarn: gcry_control (GCRYCTL_DISABLE_SECMEM_WARN); break;

        case oNoRandomSeedFile: use_random_seed = 0; break;

        default:
          pargs.err = configname? ARGPARSE_PRINT_WARNING:ARGPARSE_PRINT_ERROR;
          break;
	}
    }
  gnupg_argparse (NULL, &pargs, NULL);  /* Release internal state.  */

  if (!last_configname)
    opt.config_filename = make_filename (gnupg_homedir (),
                                         G13_NAME"-syshelp" EXTSEP_S "conf",
                                         NULL);
  else
    {
      opt.config_filename = last_configname;
      last_configname = NULL;
     }

  if (log_get_errorcount(0))
    g13_exit(2);

  /* Now that we have the options parsed we need to update the default
     control structure.  */
  g13_syshelp_init_default_ctrl (&ctrl);

  if (may_coredump && !opt.quiet)
    log_info (_("WARNING: program may create a core file!\n"));

  if (logfile)
    {
      log_set_file (logfile);
      log_set_prefix (NULL, GPGRT_LOG_WITH_PREFIX | GPGRT_LOG_WITH_TIME | GPGRT_LOG_WITH_PID);
    }

  if (gnupg_faked_time_p ())
    {
      gnupg_isotime_t tbuf;

      log_info (_("WARNING: running with faked system time: "));
      gnupg_get_isotime (tbuf);
      dump_isotime (tbuf);
      log_printf ("\n");
    }

  /* Print any pending secure memory warnings.  */
  gcry_control (GCRYCTL_RESUME_SECMEM_WARN);

  /* Setup the debug flags for all subsystems.  */
  set_debug ();

  /* Install a regular exit handler to make real sure that the secure
     memory gets wiped out.  */
  g13_install_emergency_cleanup ();

  /* Terminate if we found any error until now.  */
  if (log_get_errorcount(0))
    g13_exit (2);

  /* Set the standard GnuPG random seed file.  */
  if (use_random_seed)
    {
      char *p = make_filename (gnupg_homedir (), "random_seed", NULL);
      gcry_control (GCRYCTL_SET_RANDOM_SEED_FILE, p);
      xfree(p);
    }

  /* Get the UID of the caller.  */
#if defined(HAVE_PWD_H) && defined(HAVE_GETPWUID)
  {
    const char *uidstr;
    struct passwd *pwd = NULL;

    uidstr = getenv ("USERV_UID");

    /* Print a quick note if we are not started via userv.  */
    if (!uidstr)
      {
        if (getuid ())
          {
            log_info ("WARNING: Not started via userv\n");
            ctrl.fail_all_cmds = 1;
          }
        ctrl.client.uid = getuid ();
      }
    else
      {
        unsigned long myuid;

        errno = 0;
        myuid = strtoul (uidstr, NULL, 10);
        if (myuid == ULONG_MAX && errno)
          {
            log_info ("WARNING: Started via broken userv: %s\n",
                      strerror (errno));
            ctrl.fail_all_cmds = 1;
            ctrl.client.uid = getuid ();
          }
        else
          ctrl.client.uid = (uid_t)myuid;
      }

    pwd = getpwuid (ctrl.client.uid);
    if (!pwd || !*pwd->pw_name)
      {
        log_info ("WARNING: Name for UID not found: %s\n", strerror (errno));
        ctrl.fail_all_cmds = 1;
        ctrl.client.uname = xstrdup ("?");
      }
    else
      ctrl.client.uname = xstrdup (pwd->pw_name);

    /* Check that the user name does not contain a directory
       separator. */
    if (strchr (ctrl.client.uname, '/'))
      {
        log_info ("WARNING: Invalid user name passed\n");
        ctrl.fail_all_cmds = 1;
      }
  }
#else /*!HAVE_PWD_H || !HAVE_GETPWUID*/
  log_info ("WARNING: System does not support required syscalls\n");
  ctrl.fail_all_cmds = 1;
  ctrl.client.uid = getuid ();
  ctrl.client.uname = xstrdup ("?");
#endif /*!HAVE_PWD_H || !HAVE_GETPWUID*/

  /* Read the table entries for this user.  */
  if (!ctrl.fail_all_cmds
      && !(ctrl.client.tab = parse_g13tab (ctrl.client.uname)))
    ctrl.fail_all_cmds = 1;

  /* Start the server.  */
  err = syshelp_server (&ctrl);
  if (err)
    log_error ("server exited with error: %s <%s>\n",
               gpg_strerror (err), gpg_strsource (err));

  /* Cleanup.  */
  g13_syshelp_deinit_default_ctrl (&ctrl);
  g13_exit (0);
  return 8; /*NOTREACHED*/
}


/* Store defaults into the per-connection CTRL object.  */
void
g13_syshelp_init_default_ctrl (ctrl_t ctrl)
{
  ctrl->conttype = CONTTYPE_DM_CRYPT;
}

/* Release all resources allocated by default in the CTRl object.  */
static void
g13_syshelp_deinit_default_ctrl (ctrl_t ctrl)
{
  xfree (ctrl->client.uname);
  release_tab_items (ctrl->client.tab);
}


/* Release the list of g13tab itejms at TAB.  */
static void
release_tab_items (tab_item_t tab)
{
  while (tab)
    {
      tab_item_t next = tab->next;
      xfree (tab->mountpoint);
      xfree (tab);
      tab = next;
    }
}


void
g13_syshelp_i_know_what_i_am_doing (void)
{
  const char * const yesfile = "Yes-g13-I-know-what-I-am-doing";
  char *fname;

  fname = make_filename (gnupg_sysconfdir (), yesfile, NULL);
  if (gnupg_access (fname, F_OK))
    {
      log_info ("*******************************************************\n");
      log_info ("* The G13 support for DM-Crypt is new and not matured.\n");
      log_info ("* Bugs or improper use may delete all your disks!\n");
      log_info ("* To confirm that you are ware of this risk, create\n");
      log_info ("* the file '%s'.\n", fname);
      log_info ("*******************************************************\n");
      exit (1);
    }
  xfree (fname);
}


/* Parse the /etc/gnupg/g13tab for user USERNAME.  Return a table for
   the user on success.  Return NULL on error and print
   diagnostics. */
static tab_item_t
parse_g13tab (const char *username)
{
  gpg_error_t err;
  int c, n;
  char line[512];
  char *p;
  char *fname;
  estream_t fp;
  int lnr;
  char **words = NULL;
  tab_item_t table = NULL;
  tab_item_t *tabletail, ti;

  fname = make_filename (gnupg_sysconfdir (), G13_NAME"tab", NULL);
  fp = es_fopen (fname, "r");
  if (!fp)
    {
      err = gpg_error_from_syserror ();
      log_error (_("error opening '%s': %s\n"), fname, gpg_strerror (err));
      goto leave;
    }

  tabletail = &table;
  err = 0;
  lnr = 0;
  while (es_fgets (line, DIM(line)-1, fp))
    {
      lnr++;
      n = strlen (line);
      if (!n || line[n-1] != '\n')
        {
          /* Eat until end of line. */
          while ((c=es_getc (fp)) != EOF && c != '\n')
            ;
          err = gpg_error (*line? GPG_ERR_LINE_TOO_LONG
                           : GPG_ERR_INCOMPLETE_LINE);
          log_error (_("file '%s', line %d: %s\n"),
                     fname, lnr, gpg_strerror (err));
          continue;
        }
      line[--n] = 0; /* Chop the LF. */
      if (n && line[n-1] == '\r')
        line[--n] = 0; /* Chop an optional CR. */

      /* Allow for empty lines and spaces */
      for (p=line; spacep (p); p++)
        ;
      if (!*p || *p == '#')
        continue;

      /* Parse the line.  The format is
       * <username> <blockdev> [<label>|"-" [<mountpoint>]]
       */
      xfree (words);
      words = strtokenize (p, " \t");
      if (!words)
        {
          err = gpg_error_from_syserror ();
          break;
        }
      if (!words[0] || !words[1])
        {
          log_error (_("file '%s', line %d: %s\n"),
                     fname, lnr, gpg_strerror (GPG_ERR_SYNTAX));
          continue;
        }
      if (!(*words[1] == '/'
            || !strncmp (words[1], "PARTUUID=", 9)
            || !strncmp (words[1], "partuuid=", 9)))
        {
          log_error (_("file '%s', line %d: %s\n"),
                     fname, lnr, "Invalid block device syntax");
          continue;
        }
      if (words[2])
        {
          if (strlen (words[2]) > 16 || strchr (words[2], '/'))
            {
              log_error (_("file '%s', line %d: %s\n"),
                         fname, lnr, "Label too long or invalid syntax");
              continue;
            }

          if (words[3] && *words[3] != '/')
            {
              log_error (_("file '%s', line %d: %s\n"),
                         fname, lnr, "Invalid mountpoint syntax");
              continue;
            }
        }
      if (strcmp (words[0], username))
        continue; /* Skip entries for other usernames!  */

      ti = xtrymalloc (sizeof *ti + strlen (words[1]));
      if (!ti)
        {
          err = gpg_error_from_syserror ();
          break;
        }
      ti->next = NULL;
      ti->label = NULL;
      ti->mountpoint = NULL;
      strcpy (ti->blockdev, *words[1]=='/'? words[1] : words[1]+9);
      if (words[2])
        {
          if (strcmp (words[2], "-")
              && !(ti->label = xtrystrdup (words[2])))
            {
              err = gpg_error_from_syserror ();
              xfree (ti);
              break;
            }
          if (words[3] && !(ti->mountpoint = xtrystrdup (words[3])))
            {
              err = gpg_error_from_syserror ();
              xfree (ti->label);
              xfree (ti);
              break;
            }
        }
      *tabletail = ti;
      tabletail = &ti->next;
    }

  if (!err && !es_feof (fp))
    err = gpg_error_from_syserror ();
  if (err)
    log_error (_("error reading '%s', line %d: %s\n"),
               fname, lnr, gpg_strerror (err));

 leave:
  xfree (words);
  es_fclose (fp);
  xfree (fname);
  if (err)
    {
      release_tab_items (table);
      return NULL;
    }
  return table;
}
