/*
 * pam_fprint: PAM module for fingerprint authentication through fprintd
 * Copyright (C) 2007 Daniel Drake <dsd@gentoo.org>
 * Copyright (C) 2008-2014, 2017-2020 Bastien Nocera <hadess@hadess.net>
 *
 * 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 of the License, 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, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

#include <config.h>

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <syslog.h>
#include <errno.h>

#include <libintl.h>
#include <systemd/sd-bus.h>
#include <systemd/sd-login.h>

#define PAM_SM_AUTH
#include <security/pam_modules.h>
#include <security/pam_ext.h>

#define _(s) ((char *) dgettext (GETTEXT_PACKAGE, s))
#define TR(s) dgettext (GETTEXT_PACKAGE, s)
#define N_(s) (s)

#include "fingerprint-strings.h"
#include "pam_fprintd_autoptrs.h"

#define DEFAULT_MAX_TRIES 3
#define DEFAULT_TIMEOUT 30
#define MIN_TIMEOUT 10

#define DEBUG_MATCH "debug="
#define MAX_TRIES_MATCH "max-tries="
#define TIMEOUT_MATCH "timeout="

static bool debug = false;
static unsigned max_tries = DEFAULT_MAX_TRIES;
static unsigned timeout = DEFAULT_TIMEOUT;

#define USEC_PER_SEC ((uint64_t) 1000000ULL)
#define NSEC_PER_USEC ((uint64_t) 1000ULL)

static size_t user_enrolled_prints_num (pam_handle_t *pamh,
                                        sd_bus       *bus,
                                        const char   *dev,
                                        const char   *username);

static uint64_t
now (void)
{
  struct timespec ts;

  clock_gettime (CLOCK_MONOTONIC, &ts);
  return (uint64_t) ts.tv_sec * USEC_PER_SEC + (uint64_t) ts.tv_nsec / NSEC_PER_USEC;
}

static bool
str_has_prefix (const char *s, const char *prefix)
{
  if (s == NULL || prefix == NULL)
    return false;
  return strncmp (s, prefix, strlen (prefix)) == 0;
}

static bool
send_msg (pam_handle_t *pamh, const char *msg, int style)
{
  const struct pam_message mymsg = {
    .msg_style = style,
    .msg = msg,
  };
  const struct pam_message *msgp = &mymsg;
  const struct pam_conv *pc;
  struct pam_response *resp;

  if (pam_get_item (pamh, PAM_CONV, (const void **) &pc) != PAM_SUCCESS)
    return false;

  if (!pc || !pc->conv)
    return false;

  return pc->conv (1, &msgp, &resp, pc->appdata_ptr) == PAM_SUCCESS;
}

static bool
send_info_msg (pam_handle_t *pamh, const char *msg)
{
  return send_msg (pamh, msg, PAM_TEXT_INFO);
}

static bool
send_err_msg (pam_handle_t *pamh, const char *msg)
{
  return send_msg (pamh, msg, PAM_ERROR_MSG);
}

static char *
open_device (pam_handle_t *pamh,
             sd_bus       *bus,
             const char   *username,
             bool         *has_multiple_devices)
{
  pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;
  pf_autoptr (sd_bus_message) m = NULL;
  size_t num_devices;
  size_t max_prints;
  const char *path = NULL;
  const char *s;
  int r;

  *has_multiple_devices = false;

  if (sd_bus_call_method (bus,
                          "net.reactivated.Fprint",
                          "/net/reactivated/Fprint/Manager",
                          "net.reactivated.Fprint.Manager",
                          "GetDevices",
                          &error,
                          &m,
                          NULL) < 0)
    {
      pam_syslog (pamh, LOG_ERR, "GetDevices failed: %s", error.message);
      return NULL;
    }

  r = sd_bus_message_enter_container (m, 'a', "o");
  if (r < 0)
    {
      pam_syslog (pamh, LOG_ERR, "Failed to parse answer from GetDevices(): %d", r);
      return NULL;
    }

  num_devices = 0;
  max_prints = 0;
  while ((r = sd_bus_message_read_basic (m, 'o', &s)) > 0)
    {
      size_t enrolled_prints = user_enrolled_prints_num (pamh, bus, s, username);

      if (debug)
        pam_syslog (pamh, LOG_DEBUG, "%s prints registered: %" PRIu64, s, enrolled_prints);

      if (enrolled_prints > max_prints)
        {
          max_prints = enrolled_prints;
          path = s;
        }

      num_devices++;
    }
  *has_multiple_devices = (num_devices > 1);
  if (debug)
    pam_syslog (pamh, LOG_DEBUG, "Using device %s (out of %ld devices)", path, num_devices);

  sd_bus_message_exit_container (m);

  return path ? strdup (path) : NULL;
}

typedef struct
{
  char         *dev;
  bool          has_multiple_devices;

  unsigned      max_tries;
  char         *result;
  bool          timed_out;
  bool          is_swipe;
  bool          verify_started;
  int           verify_ret;
  pam_handle_t *pamh;

  char         *driver;
} verify_data;

static void
verify_data_free (verify_data *data)
{
  free (data->result);
  free (data->driver);
  free (data->dev);
  free (data);
}

PF_DEFINE_AUTOPTR_CLEANUP_FUNC (verify_data, verify_data_free)

static int
verify_result (sd_bus_message *m,
               void           *userdata,
               sd_bus_error   *ret_error)
{
  verify_data *data = userdata;
  const char *msg;
  const char *result = NULL;
  /* see https://github.com/systemd/systemd/issues/14643 */
  uint64_t done = false;
  int r;

  if (!sd_bus_message_is_signal (m, "net.reactivated.Fprint.Device", "VerifyStatus"))
    {
      pam_syslog (data->pamh, LOG_ERR, "Not the signal we expected (iface: %s, member: %s)",
                  sd_bus_message_get_interface (m),
                  sd_bus_message_get_member (m));
      return 0;
    }

  if ((r = sd_bus_message_read (m, "sb", &result, &done)) < 0)
    {
      pam_syslog (data->pamh, LOG_ERR, "Failed to parse VerifyResult signal: %d", r);
      data->verify_ret = PAM_AUTHINFO_UNAVAIL;
      return 0;
    }

  if (!data->verify_started)
    {
      pam_syslog (data->pamh, LOG_ERR, "Unexpected VerifyResult '%s', %" PRIu64 " signal", result, done);
      return 0;
    }

  if (debug)
    pam_syslog (data->pamh, LOG_DEBUG, "Verify result: %s (done: %d)", result, done ? 1 : 0);

  if (data->result)
    {
      free (data->result);
      data->result = NULL;
    }

  if (done && result)
    {
      data->result = strdup (result);
      return 0;
    }

  msg = verify_result_str_to_msg (result, data->is_swipe);
  if (!msg)
    {
      data->result = strdup ("Protocol error with fprintd!");
      return 0;
    }
  send_err_msg (data->pamh, msg);

  return 0;
}

static int
verify_finger_selected (sd_bus_message *m,
                        void           *userdata,
                        sd_bus_error   *ret_error)
{
  verify_data *data = userdata;
  const char *finger_name = NULL;
  pf_autofree char *msg = NULL;

  if (sd_bus_message_read_basic (m, 's', &finger_name) < 0)
    {
      pam_syslog (data->pamh, LOG_ERR, "Failed to parse VerifyFingerSelected signal: %d", errno);
      data->verify_ret = PAM_AUTHINFO_UNAVAIL;
      return 0;
    }

  if (!data->verify_started)
    {
      pam_syslog (data->pamh, LOG_ERR, "Unexpected VerifyFingerSelected %s signal", finger_name);
      return 0;
    }

  msg = finger_str_to_msg (finger_name, data->driver, data->is_swipe);
  if (!msg)
    {
      data->result = strdup ("Protocol error with fprintd!");
      return 0;
    }
  if (debug)
    pam_syslog (data->pamh, LOG_DEBUG, "verify_finger_selected %s", msg);
  send_info_msg (data->pamh, msg);

  return 0;
}

/* See https://github.com/systemd/systemd/issues/14636 */
static int
get_property_string (sd_bus       *bus,
                     const char   *destination,
                     const char   *path,
                     const char   *interface,
                     const char   *member,
                     sd_bus_error *error,
                     char        **ret)
{

  pf_autoptr (sd_bus_message) reply = NULL;
  const char *s;
  char *n;
  int r;

  r = sd_bus_call_method (bus, destination, path, "org.freedesktop.DBus.Properties", "Get", error, &reply, "ss", interface, member);
  if (r < 0)
    return r;

  r = sd_bus_message_enter_container (reply, 'v', "s");
  if (r < 0)
    return sd_bus_error_set_errno (error, r);

  r = sd_bus_message_read_basic (reply, 's', &s);
  if (r < 0)
    return sd_bus_error_set_errno (error, r);

  n = strdup (s);
  if (!n)
    return sd_bus_error_set_errno (error, -ENOMEM);

  *ret = n;
  return 0;
}


static int
verify_started_cb (sd_bus_message *m,
                   void           *userdata,
                   sd_bus_error   *ret_error)
{
  const sd_bus_error *error = sd_bus_message_get_error (m);
  verify_data *data = userdata;

  if (error)
    {
      if (sd_bus_error_has_name (error, "net.reactivated.Fprint.Error.NoEnrolledPrints"))
        {
          pam_syslog (data->pamh, LOG_DEBUG, "No prints enrolled");
          data->verify_ret = PAM_USER_UNKNOWN;
        }
      else
        {
          data->verify_ret = PAM_AUTH_ERR;
        }

      if (debug)
        pam_syslog (data->pamh, LOG_DEBUG, "VerifyStart failed: %s", error->message);

      return 1;
    }

  if (debug)
    pam_syslog (data->pamh, LOG_DEBUG, "VerifyStart completed successfully");

  data->verify_started = true;

  return 1;
}

static int
do_verify (sd_bus      *bus,
           verify_data *data)
{
  pf_autoptr (sd_bus_slot) verify_status_slot = NULL;
  pf_autoptr (sd_bus_slot) verify_finger_selected_slot = NULL;
  pf_autofree char *scan_type = NULL;
  int r;

  /* Get some properties for the device */
  r = get_property_string (bus,
                           "net.reactivated.Fprint",
                           data->dev,
                           "net.reactivated.Fprint.Device",
                           "scan-type",
                           NULL,
                           &scan_type);
  if (r < 0)
    pam_syslog (data->pamh, LOG_ERR, "Failed to get scan-type for %s: %d", data->dev, r);
  if (debug)
    pam_syslog (data->pamh, LOG_DEBUG, "scan-type for %s: %s", data->dev, scan_type);
  if (str_equal (scan_type, "swipe"))
    data->is_swipe = true;

  if (data->has_multiple_devices)
    {
      get_property_string (bus,
                           "net.reactivated.Fprint",
                           data->dev,
                           "net.reactivated.Fprint.Device",
                           "name",
                           NULL,
                           &data->driver);
      if (r < 0)
        pam_syslog (data->pamh, LOG_ERR, "Failed to get driver name for %s: %d", data->dev, r);
      if (debug && r == 0)
        pam_syslog (data->pamh, LOG_DEBUG, "driver name for %s: %s", data->dev, data->driver);
    }

  sd_bus_match_signal (bus,
                       &verify_status_slot,
                       "net.reactivated.Fprint",
                       data->dev,
                       "net.reactivated.Fprint.Device",
                       "VerifyStatus",
                       verify_result,
                       data);

  sd_bus_match_signal (bus,
                       &verify_finger_selected_slot,
                       "net.reactivated.Fprint",
                       data->dev,
                       "net.reactivated.Fprint.Device",
                       "VerifyFingerSelected",
                       verify_finger_selected,
                       data);

  while (data->max_tries > 0)
    {
      uint64_t verification_end = now () + (timeout * USEC_PER_SEC);

      data->timed_out = false;
      data->verify_started = false;
      data->verify_ret = PAM_INCOMPLETE;

      free (data->result);
      data->result = NULL;

      if (debug)
        pam_syslog (data->pamh, LOG_DEBUG, "About to call VerifyStart");

      r = sd_bus_call_method_async (bus,
                                    NULL,
                                    "net.reactivated.Fprint",
                                    data->dev,
                                    "net.reactivated.Fprint.Device",
                                    "VerifyStart",
                                    verify_started_cb,
                                    data,
                                    "s",
                                    "any");

      if (r < 0)
        {
          if (debug)
            pam_syslog (data->pamh, LOG_DEBUG, "VerifyStart call failed: %d", r);
          break;
        }

      for (;;)
        {
          int64_t wait_time;

          wait_time = verification_end - now ();
          if (wait_time <= 0)
            break;

          r = sd_bus_process (bus, NULL);
          if (r < 0)
            break;
          if (data->verify_ret != PAM_INCOMPLETE)
            break;
          if (!data->verify_started)
            continue;
          if (data->result != NULL)
            break;
          if (r == 0)
            {
              if (debug)
                {
                  pam_syslog (data->pamh, LOG_DEBUG,
                              "Waiting for %" PRId64 " seconds (%" PRId64 " usecs)",
                              wait_time / USEC_PER_SEC,
                              wait_time);
                }
              if (sd_bus_wait (bus, wait_time) < 0)
                break;
            }
        }

      if (data->verify_ret != PAM_INCOMPLETE)
        return data->verify_ret;

      if (now () >= verification_end)
        {
          data->timed_out = true;
          send_info_msg (data->pamh, _("Verification timed out"));
        }

      /* Ignore errors from VerifyStop */
      data->verify_started = false;
      sd_bus_call_method (bus,
                          "net.reactivated.Fprint",
                          data->dev,
                          "net.reactivated.Fprint.Device",
                          "VerifyStop",
                          NULL,
                          NULL,
                          NULL,
                          NULL);

      if (data->timed_out)
        {
          return PAM_AUTHINFO_UNAVAIL;
        }
      else
        {
          if (str_equal (data->result, "verify-no-match"))
            {
              send_err_msg (data->pamh, "Failed to match fingerprint");
            }
          else if (str_equal (data->result, "verify-match"))
            {
              return PAM_SUCCESS;
            }
          else if (str_equal (data->result, "verify-unknown-error"))
            {
              return PAM_AUTHINFO_UNAVAIL;
            }
          else if (str_equal (data->result, "verify-disconnected"))
            {
              return PAM_AUTHINFO_UNAVAIL;
            }
          else
            {
              send_err_msg (data->pamh, _("An unknown error occurred"));
              return PAM_AUTH_ERR;
            }
        }
      data->max_tries--;
    }

  if (data->max_tries == 0)
    return PAM_MAXTRIES;

  return PAM_AUTH_ERR;
}

static size_t
user_enrolled_prints_num (pam_handle_t *pamh,
                          sd_bus       *bus,
                          const char   *dev,
                          const char   *username)
{
  pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;
  pf_autoptr (sd_bus_message) m = NULL;
  size_t num_fingers = 0;
  const char *s;
  int r;

  r = sd_bus_call_method (bus,
                          "net.reactivated.Fprint",
                          dev,
                          "net.reactivated.Fprint.Device",
                          "ListEnrolledFingers",
                          &error,
                          &m,
                          "s",
                          username);
  if (r < 0)
    {
      /* If ListEnrolledFingers fails then verification should
       * also fail (both use the same underlying call), so we
       * report false here and bail out early.  */
      if (debug)
        pam_syslog (pamh, LOG_DEBUG, "ListEnrolledFingers failed for %s: %s",
                    username, error.message);
      return 0;
    }

  r = sd_bus_message_enter_container (m, 'a', "s");
  if (r < 0)
    {
      pam_syslog (pamh, LOG_ERR, "Failed to parse answer from ListEnrolledFingers(): %d", r);
      return 0;
    }

  while ((r = sd_bus_message_read_basic (m, 's', &s)) > 0)
    num_fingers++;
  sd_bus_message_exit_container (m);

  return num_fingers;
}

static void
release_device (pam_handle_t *pamh,
                sd_bus       *bus,
                const char   *dev)
{
  pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;

  if (sd_bus_call_method (bus,
                          "net.reactivated.Fprint",
                          dev,
                          "net.reactivated.Fprint.Device",
                          "Release",
                          &error,
                          NULL,
                          NULL,
                          NULL) < 0)
    pam_syslog (pamh, LOG_ERR, "ReleaseDevice failed: %s", error.message);
}

static bool
claim_device (pam_handle_t *pamh,
              sd_bus       *bus,
              const char   *dev,
              const char   *username)
{
  pf_auto (sd_bus_error) error = SD_BUS_ERROR_NULL;

  if (sd_bus_call_method (bus,
                          "net.reactivated.Fprint",
                          dev,
                          "net.reactivated.Fprint.Device",
                          "Claim",
                          &error,
                          NULL,
                          "s",
                          username) < 0)
    {
      if (debug)
        pam_syslog (pamh, LOG_DEBUG, "failed to claim device %s", error.message);
      return false;
    }

  return true;
}

static int
name_owner_changed (sd_bus_message *m,
                    void           *userdata,
                    sd_bus_error   *ret_error)
{
  verify_data *data = userdata;
  const char *name = NULL;
  const char *old_owner = NULL;
  const char *new_owner = NULL;

  if (sd_bus_message_read (m, "sss", &name, &old_owner, &new_owner) < 0)
    {
      pam_syslog (data->pamh, LOG_ERR, "Failed to parse NameOwnerChanged signal: %d", errno);
      data->verify_ret = PAM_AUTHINFO_UNAVAIL;
      return 0;
    }

  if (strcmp (name, "net.reactivated.Fprint") != 0)
    return 0;

  /* Name owner for fprintd changed, give up as we might start listening
   * to events from a new name owner otherwise. */
  data->verify_ret = PAM_AUTHINFO_UNAVAIL;

  pam_syslog (data->pamh, LOG_WARNING, "fprintd name owner changed during operation!");

  return 0;
}

static int
do_auth (pam_handle_t *pamh, const char *username)
{
  pf_autoptr (verify_data) data = NULL;
  pf_autoptr (sd_bus) bus = NULL;
  pf_autoptr (sd_bus_slot) name_owner_changed_slot = NULL;

  data = calloc (1, sizeof (verify_data));
  data->max_tries = max_tries;
  data->pamh = pamh;

  if (sd_bus_open_system (&bus) < 0)
    {
      pam_syslog (pamh, LOG_ERR, "Error with getting the bus: %d", errno);
      return PAM_AUTHINFO_UNAVAIL;
    }

  data->dev = open_device (pamh, bus, username, &data->has_multiple_devices);
  if (data->dev == NULL)
    return PAM_AUTHINFO_UNAVAIL;

  /* Only connect to NameOwnerChanged when needed. In case of automatic startup
   * we rely on the fact that we never see those signals.
   */
  name_owner_changed_slot = NULL;
  sd_bus_match_signal (bus,
                       &name_owner_changed_slot,
                       "org.freedesktop.DBus",
                       "/org/freedesktop/DBus",
                       "org.freedesktop.DBus",
                       "NameOwnerChanged",
                       name_owner_changed,
                       data);

  if (claim_device (pamh, bus, data->dev, username))
    {
      int ret = do_verify (bus, data);
      release_device (pamh, bus, data->dev);
      return ret;
    }

  return PAM_AUTHINFO_UNAVAIL;
}

static bool
is_remote (pam_handle_t *pamh)
{
  const char *rhost = NULL;

  pam_get_item (pamh, PAM_RHOST, (const void **) (const void *) &rhost);

  /* NULL or empty rhost if the host information is not available or set.
   * "localhost" if the host is local.
   * We want to not run for known remote hosts */
  if (rhost != NULL &&
      *rhost != '\0' &&
      strcmp (rhost, "localhost") != 0)
    return true;

  if (sd_session_is_remote (NULL) > 0)
    return true;

  return false;
}

PAM_EXTERN int
pam_sm_authenticate (pam_handle_t *pamh, int flags, int argc,
                     const char **argv)
{
  const char *username;
  int i;

  bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
  bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");

  if (is_remote (pamh))
    return PAM_AUTHINFO_UNAVAIL;

  if (pam_get_user (pamh, &username, NULL) != PAM_SUCCESS)
    return PAM_AUTHINFO_UNAVAIL;

  for (i = 0; i < argc; i++)
    {
      if (argv[i] != NULL)
        {
          if (str_equal (argv[i], "debug"))
            {
              pam_syslog (pamh, LOG_DEBUG, "debug on");
              debug = true;
            }
          else if (str_has_prefix (argv[i], DEBUG_MATCH))
            {
              pam_syslog (pamh, LOG_DEBUG, "debug on");
              const char *value;

              value = argv[i] + strlen (DEBUG_MATCH);
              if (str_equal (value, "on") ||
                  str_equal (value, "true") ||
                  str_equal (value, "1"))
                {
                  pam_syslog (pamh, LOG_DEBUG, "debug on");
                  debug = true;
                }
              else if (str_equal (value, "off") ||
                       str_equal (value, "false") ||
                       str_equal (value, "0"))
                {
                  debug = false;
                }
              else
                {
                  pam_syslog (pamh, LOG_DEBUG, "invalid debug value '%s', disabling", value);
                }
            }
          else if (str_has_prefix (argv[i], MAX_TRIES_MATCH) && strlen (argv[i]) == strlen (MAX_TRIES_MATCH) + 1)
            {
              max_tries = atoi (argv[i] + strlen (MAX_TRIES_MATCH));
              if (max_tries < 1)
                {
                  if (debug)
                    pam_syslog (pamh, LOG_DEBUG, "invalid max tries '%s', using %d",
                                argv[i] + strlen (MAX_TRIES_MATCH), DEFAULT_MAX_TRIES);
                  max_tries = DEFAULT_MAX_TRIES;
                }
              if (debug)
                pam_syslog (pamh, LOG_DEBUG, "max_tries specified as: %d", max_tries);
            }
          else if (str_has_prefix (argv[i], TIMEOUT_MATCH) && strlen (argv[i]) <= strlen (TIMEOUT_MATCH) + 2)
            {
              timeout = atoi (argv[i] + strlen (TIMEOUT_MATCH));
              if (timeout < MIN_TIMEOUT)
                {
                  if (debug)
                    pam_syslog (pamh, LOG_DEBUG, "timeout %d secs too low, using %d",
                                timeout, MIN_TIMEOUT);
                  timeout = MIN_TIMEOUT;
                }
              else if (debug)
                {
                  pam_syslog (pamh, LOG_DEBUG, "timeout specified as: %d secs", timeout);
                }
            }
        }
    }

  return do_auth (pamh, username);
}

PAM_EXTERN int
pam_sm_setcred (pam_handle_t *pamh, int flags, int argc,
                const char **argv)
{
  return PAM_SUCCESS;
}

PAM_EXTERN int
pam_sm_chauthtok (pam_handle_t *pamh, int flags, int argc,
                  const char **argv)
{
  return PAM_SUCCESS;
}
