/*
 * 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"

#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 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;
	int r;

	r = pam_get_item(pamh, PAM_CONV, (const void **) &pc);
	if (r != 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,
	     bool            *has_multiple_devices)
{
	sd_bus_error error = SD_BUS_ERROR_NULL;
	sd_bus_message *m = NULL;
	size_t num_devices;
	const char *path = NULL;
	char *ret;
	const char *s;
	int r;

	*has_multiple_devices = false;

	r = sd_bus_call_method (bus,
				"net.reactivated.Fprint",
				"/net/reactivated/Fprint/Manager",
				"net.reactivated.Fprint.Manager",
				"GetDevices",
				&error,
				&m,
				NULL);
	if (r < 0) {
		pam_syslog (pamh, LOG_ERR, "GetDevices failed: %s", error.message);
		sd_bus_error_free (&error);
		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);
		goto out;
	}

	r = sd_bus_message_read_basic (m, 'o', &path);
	if (r < 0)
		goto out;

	num_devices = 1;
	while ((r = sd_bus_message_read_basic(m, 'o', &s)) > 0)
		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);

out:
	ret = path ? strdup (path) : NULL;
	sd_bus_message_unref (m);
	return ret;
}

typedef struct {
	unsigned max_tries;
	char *result;
	bool timed_out;
	bool is_swipe;
	pam_handle_t *pamh;

	char *driver;
} verify_data;

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);
		return 0;
	}

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

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

	msg = _(verify_result_str_to_msg (result, data->is_swipe));
	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;
	char *msg;

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

	msg = finger_str_to_msg(finger_name, data->driver, data->is_swipe);
	if (debug)
		pam_syslog (data->pamh, LOG_DEBUG, "verify_finger_selected %s", msg);
	send_info_msg (data->pamh, msg);
	free (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) {

	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)
		goto fail;

	r = sd_bus_message_read_basic(reply, 's', &s);
	if (r < 0)
		goto fail;

	n = strdup(s);
	if (!n) {
		r = -ENOMEM;
		goto fail;
	}

	sd_bus_message_unref (reply);

	*ret = n;
	return 0;

fail:
	if (reply != NULL)
		sd_bus_message_unref (reply);
	return sd_bus_error_set_errno(error, r);
}

static int
do_verify (pam_handle_t *pamh,
	   sd_bus       *bus,
	   const char   *dev,
	   bool          has_multiple_devices)
{
	verify_data *data;
	sd_bus_slot *verify_status_slot, *verify_finger_selected_slot;
	char *scan_type = NULL;
	int ret;
	int r;

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

	/* Get some properties for the device */
	r = get_property_string (bus,
				 "net.reactivated.Fprint",
				 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", dev, r);
	if (debug)
		pam_syslog (data->pamh, LOG_DEBUG, "scan-type for %s: %s", dev, scan_type);
	if (str_equal (scan_type, "swipe"))
		data->is_swipe = true;
	free (scan_type);

	if (has_multiple_devices) {
		get_property_string (bus,
				     "net.reactivated.Fprint",
				     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", dev, r);
		if (debug && r == 0)
			pam_syslog (data->pamh, LOG_DEBUG, "driver name for %s: %s", dev, data->driver);
	}

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

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

	ret = PAM_AUTH_ERR;

	while (ret == PAM_AUTH_ERR && data->max_tries > 0) {
		uint64_t verification_end = now () + (timeout * USEC_PER_SEC);
		sd_bus_message *m = NULL;
		sd_bus_error error = SD_BUS_ERROR_NULL;

		data->timed_out = false;

		r = sd_bus_call_method (bus,
					"net.reactivated.Fprint",
					dev,
					"net.reactivated.Fprint.Device",
					"VerifyStart",
					&error,
					&m,
					"s",
					"any");

		if (r < 0) {
			if (sd_bus_error_has_name (&error, "net.reactivated.Fprint.Error.NoEnrolledPrints"))
				ret = PAM_USER_UNKNOWN;

			if (debug)
				pam_syslog (pamh, LOG_DEBUG, "VerifyStart failed: %s", error.message);
			sd_bus_error_free (&error);
			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->result != NULL)
				break;
			if (r == 0) {
				if (debug) {
					pam_syslog(pamh, LOG_DEBUG, "Waiting for %"PRId64" seconds (%"PRId64" usecs)",
						   wait_time / USEC_PER_SEC,
						   wait_time);
				}
				r = sd_bus_wait (bus, wait_time);
				if (r < 0)
					break;
			}
		}

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

		/* Ignore errors from VerifyStop */
		sd_bus_call_method (bus,
				    "net.reactivated.Fprint",
				    dev,
				    "net.reactivated.Fprint.Device",
				    "VerifyStop",
				    NULL,
				    NULL,
				    NULL,
				    NULL);

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

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

	sd_bus_slot_unref (verify_status_slot);
	sd_bus_slot_unref (verify_finger_selected_slot);

	if (data->result)
		free (data->result);
	free (data->driver);
	free (data);

	return ret;
}

static bool
user_has_prints (pam_handle_t *pamh,
		 sd_bus       *bus,
		 const char   *dev,
		 const char   *username)
{
	sd_bus_error error = SD_BUS_ERROR_NULL;
	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);
		}
		sd_bus_error_free (&error);
		return false;
	}

	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);
		goto out;
	}

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

out:
	sd_bus_message_unref (m);
	return (num_fingers > 0);
}

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

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

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

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

	return true;
}

static int do_auth(pam_handle_t *pamh, const char *username)
{
	char *dev;
	bool have_prints;
	bool has_multiple_devices;
	int ret = PAM_AUTHINFO_UNAVAIL;
	sd_bus *bus = NULL;

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

	dev = open_device (pamh, bus, &has_multiple_devices);
	if (dev == NULL) {
		sd_bus_unref (bus);
		return PAM_AUTHINFO_UNAVAIL;
	}

	have_prints = user_has_prints (pamh, bus, dev, username);
	if (debug)
		pam_syslog (pamh, LOG_DEBUG, "prints registered: %s\n", have_prints ? "yes" : "no");

	if (!have_prints)
		goto out;

	if (claim_device (pamh, bus, dev, username)) {
		ret = do_verify (pamh, bus, dev, has_multiple_devices);
		release_device (pamh, bus, dev);
	}

out:
	free (dev);
	sd_bus_unref (bus);

	return ret;
}

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;
	unsigned i;
	int r;

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

	if (is_remote (pamh))
		return PAM_AUTHINFO_UNAVAIL;

	r = pam_get_user(pamh, &username, NULL);
	if (r != 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);
				}
			}
		}
	}

	r = do_auth(pamh, username);

	return r;
}

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;
}

