summaryrefslogtreecommitdiff
path: root/lib/auth/src/pam.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/auth/src/pam.c')
-rw-r--r--lib/auth/src/pam.c524
1 files changed, 524 insertions, 0 deletions
diff --git a/lib/auth/src/pam.c b/lib/auth/src/pam.c
new file mode 100644
index 0000000..d0afec4
--- /dev/null
+++ b/lib/auth/src/pam.c
@@ -0,0 +1,524 @@
+#include <pwd.h>
+#include <security/_pam_types.h>
+#include <security/pam_appl.h>
+
+#include "astal-auth.h"
+
+struct _AstalAuthPam {
+ GObject parent_instance;
+
+ gchar *username;
+ gchar *service;
+};
+
+typedef struct {
+ GTask *task;
+ GMainContext *context;
+ GMutex data_mutex;
+ GCond data_cond;
+
+ gchar *secret;
+ gboolean secret_set;
+} AstalAuthPamPrivate;
+
+typedef struct {
+ AstalAuthPam *pam;
+ guint signal_id;
+ gchar *msg;
+} AstalAuthPamSignalEmitData;
+
+static void astal_auth_pam_signal_emit_data_free(AstalAuthPamSignalEmitData *data) {
+ g_free(data->msg);
+ g_free(data);
+}
+
+typedef enum {
+ ASTAL_AUTH_PAM_SIGNAL_PROMPT_VISIBLE,
+ ASTAL_AUTH_PAM_SIGNAL_PROMPT_HIDDEN,
+ ASTAL_AUTH_PAM_SIGNAL_INFO,
+ ASTAL_AUTH_PAM_SIGNAL_ERROR,
+ ASTAL_AUTH_PAM_SIGNAL_SUCCESS,
+ ASTAL_AUTH_PAM_SIGNAL_FAIL,
+ ASTAL_AUTH_PAM_N_SIGNALS
+} AstalAuthPamSignals;
+
+typedef enum {
+ ASTAL_AUTH_PAM_PROP_USERNAME = 1,
+ ASTAL_AUTH_PAM_PROP_SERVICE,
+ ASTAL_AUTH_PAM_N_PROPERTIES
+} AstalAuthPamProperties;
+
+static guint astal_auth_pam_signals[ASTAL_AUTH_PAM_N_SIGNALS] = {
+ 0,
+};
+static GParamSpec *astal_auth_pam_properties[ASTAL_AUTH_PAM_N_PROPERTIES] = {
+ NULL,
+};
+
+G_DEFINE_TYPE_WITH_PRIVATE(AstalAuthPam, astal_auth_pam, G_TYPE_OBJECT);
+
+/**
+ *
+ * AstalAuthPam
+ *
+ * For simple authentication using only a password, using the [[email protected]]
+ * method is recommended. Look at the simple examples for how to use it.
+ *
+ * There is also a way to get access to the pam conversation, to allow for a more complex
+ * authentication process, like using multiple factor authentication. Generally it can be used like
+ * this:
+ *
+ * 1. create the Pam object.
+ * 2. set username and service if so required. It has sane defaults, so in most cases you can skip
+ * this.
+ * 3. connect to the signals.
+ * After an `auth-*` signal is emitted, it has to be responded with exactly one
+ * [[email protected]_secret] call. The secret is a string containing the user input. For
+ * [auth-info][[email protected]::auth-info:] and [auth-error][[email protected]::auth-error:]
+ * it should be `NULL`. Not connecting those signals, is equivalent to calling
+ * [[email protected]_secret] with `NULL` immediately after the signal is emitted.
+ * 4. start authentication process using [[email protected]_authenticate].
+ * 5. it is possible to reuse the same Pam object for multiple sequential authentication attempts.
+ * Just call [[email protected]_authenticate] again after the `success` or `fail` signal
+ * was emitted.
+ *
+ */
+
+/**
+ * astal_auth_pam_set_username
+ * @self: a AstalAuthPam object
+ * @username: the new username
+ *
+ * Sets the username to be used for authentication. This must be set to
+ * before calling start_authenticate.
+ * Changing it afterwards has no effect on the authentication process.
+ *
+ * Defaults to the owner of the process.
+ *
+ */
+void astal_auth_pam_set_username(AstalAuthPam *self, const gchar *username) {
+ g_return_if_fail(ASTAL_AUTH_IS_PAM(self));
+ g_return_if_fail(username != NULL);
+
+ g_free(self->username);
+ self->username = g_strdup(username);
+ g_object_notify(G_OBJECT(self), "username");
+}
+
+/**
+ * astal_auth_pam_supply_secret
+ * @self: a AstalAuthPam Object
+ * @secret: (nullable): the secret to be provided to pam. Can be NULL.
+ *
+ * provides pam with a secret. This method must be called exactly once after a
+ * auth-* signal is emitted.
+ */
+void astal_auth_pam_supply_secret(AstalAuthPam *self, const gchar *secret) {
+ g_return_if_fail(ASTAL_AUTH_IS_PAM(self));
+ AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self);
+
+ g_mutex_lock(&priv->data_mutex);
+ g_free(priv->secret);
+ priv->secret = g_strdup(secret);
+ priv->secret_set = TRUE;
+ g_cond_signal(&priv->data_cond);
+ g_mutex_unlock(&priv->data_mutex);
+}
+
+/**
+ * astal_auth_pam_set_service
+ * @self: a AstalAuthPam object
+ * @service: the pam service used for authentication
+ *
+ * Sets the service to be used for authentication. This must be set to
+ * before calling start_authenticate.
+ * Changing it afterwards has no effect on the authentication process.
+ *
+ * Defaults to `astal-auth`.
+ *
+ */
+void astal_auth_pam_set_service(AstalAuthPam *self, const gchar *service) {
+ g_return_if_fail(ASTAL_AUTH_IS_PAM(self));
+ g_return_if_fail(service != NULL);
+
+ g_free(self->service);
+ self->service = g_strdup(service);
+ g_object_notify(G_OBJECT(self), "service");
+}
+
+/**
+ * astal_auth_pam_get_username
+ * @self: a AstalAuthPam object
+ *
+ * Fetches the username from AsalAuthPam object.
+ *
+ * Returns: the username of the AsalAuthPam object. This string is
+ * owned by the object and must not be modified or freed.
+ */
+
+const gchar *astal_auth_pam_get_username(AstalAuthPam *self) {
+ g_return_val_if_fail(ASTAL_AUTH_IS_PAM(self), NULL);
+ return self->username;
+}
+
+/**
+ * astal_auth_pam_get_service
+ * @self: a AstalAuthPam
+ *
+ * Fetches the service from AsalAuthPam object.
+ *
+ * Returns: the service of the AsalAuthPam object. This string is
+ * owned by the object and must not be modified or freed.
+ */
+const gchar *astal_auth_pam_get_service(AstalAuthPam *self) {
+ g_return_val_if_fail(ASTAL_AUTH_IS_PAM(self), NULL);
+ return self->service;
+}
+
+static void astal_auth_pam_set_property(GObject *object, guint property_id, const GValue *value,
+ GParamSpec *pspec) {
+ AstalAuthPam *self = ASTAL_AUTH_PAM(object);
+
+ switch (property_id) {
+ case ASTAL_AUTH_PAM_PROP_USERNAME:
+ astal_auth_pam_set_username(self, g_value_get_string(value));
+ break;
+ case ASTAL_AUTH_PAM_PROP_SERVICE:
+ astal_auth_pam_set_service(self, g_value_get_string(value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void astal_auth_pam_get_property(GObject *object, guint property_id, GValue *value,
+ GParamSpec *pspec) {
+ AstalAuthPam *self = ASTAL_AUTH_PAM(object);
+
+ switch (property_id) {
+ case ASTAL_AUTH_PAM_PROP_USERNAME:
+ g_value_set_string(value, self->username);
+ break;
+ case ASTAL_AUTH_PAM_PROP_SERVICE:
+ g_value_set_string(value, self->service);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
+ break;
+ }
+}
+
+static void astal_auth_pam_callback(GObject *object, GAsyncResult *res, gpointer user_data) {
+ AstalAuthPam *self = ASTAL_AUTH_PAM(object);
+ AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self);
+
+ GTask *task = g_steal_pointer(&priv->task);
+
+ GError *error = NULL;
+ g_task_propagate_int(task, &error);
+
+ if (error == NULL) {
+ g_signal_emit(self, astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_SUCCESS], 0);
+ } else {
+ g_signal_emit(self, astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_FAIL], 0, error->message);
+ g_error_free(error);
+ }
+ g_object_unref(task);
+}
+
+static gboolean astal_auth_pam_emit_signal_in_context(gpointer user_data) {
+ AstalAuthPamSignalEmitData *data = user_data;
+ g_signal_emit(data->pam, data->signal_id, 0, data->msg);
+ return G_SOURCE_REMOVE;
+}
+
+static void astal_auth_pam_emit_signal(AstalAuthPam *pam, guint signal, const gchar *msg) {
+ GSource *emit_source;
+ AstalAuthPamSignalEmitData *data;
+
+ data = g_new0(AstalAuthPamSignalEmitData, 1);
+ data->pam = pam;
+ data->signal_id = astal_auth_pam_signals[signal];
+ data->msg = g_strdup(msg);
+
+ emit_source = g_idle_source_new();
+ g_source_set_callback(emit_source, astal_auth_pam_emit_signal_in_context, data,
+ (GDestroyNotify)astal_auth_pam_signal_emit_data_free);
+ g_source_set_priority(emit_source, G_PRIORITY_DEFAULT);
+ g_source_attach(emit_source,
+ ((AstalAuthPamPrivate *)astal_auth_pam_get_instance_private(pam))->context);
+ g_source_unref(emit_source);
+}
+
+int astal_auth_pam_handle_conversation(int num_msg, const struct pam_message **msg,
+ struct pam_response **resp, void *appdata_ptr) {
+ AstalAuthPam *self = appdata_ptr;
+ AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self);
+
+ struct pam_response *replies = NULL;
+ if (num_msg <= 0 || num_msg > PAM_MAX_NUM_MSG) {
+ return PAM_CONV_ERR;
+ }
+ replies = (struct pam_response *)calloc(num_msg, sizeof(struct pam_response));
+ if (replies == NULL) {
+ return PAM_BUF_ERR;
+ }
+ for (int i = 0; i < num_msg; ++i) {
+ guint signal;
+ switch (msg[i]->msg_style) {
+ case PAM_PROMPT_ECHO_OFF:
+ signal = ASTAL_AUTH_PAM_SIGNAL_PROMPT_HIDDEN;
+ break;
+ case PAM_PROMPT_ECHO_ON:
+ signal = ASTAL_AUTH_PAM_SIGNAL_PROMPT_VISIBLE;
+ break;
+ case PAM_ERROR_MSG:
+ signal = ASTAL_AUTH_PAM_SIGNAL_ERROR;
+ ;
+ break;
+ case PAM_TEXT_INFO:
+ signal = ASTAL_AUTH_PAM_SIGNAL_INFO;
+ break;
+ default:
+ g_free(replies);
+ return PAM_CONV_ERR;
+ break;
+ }
+ guint signal_id = astal_auth_pam_signals[signal];
+ if (g_signal_has_handler_pending(self, signal_id, 0, FALSE)) {
+ astal_auth_pam_emit_signal(self, signal, msg[i]->msg);
+ g_mutex_lock(&priv->data_mutex);
+ while (!priv->secret_set) {
+ g_cond_wait(&priv->data_cond, &priv->data_mutex);
+ }
+ replies[i].resp_retcode = 0;
+ replies[i].resp = g_strdup(priv->secret);
+ g_free(priv->secret);
+ priv->secret = NULL;
+ priv->secret_set = FALSE;
+ g_mutex_unlock(&priv->data_mutex);
+ }
+ }
+ *resp = replies;
+ return PAM_SUCCESS;
+}
+
+static void astal_auth_pam_thread(GTask *task, gpointer object, gpointer task_data,
+ GCancellable *cancellable) {
+ AstalAuthPam *self = g_task_get_source_object(task);
+
+ pam_handle_t *pamh = NULL;
+ const struct pam_conv conv = {
+ .conv = astal_auth_pam_handle_conversation,
+ .appdata_ptr = self,
+ };
+
+ int retval;
+ retval = pam_start(self->service, self->username, &conv, &pamh);
+ if (retval == PAM_SUCCESS) {
+ retval = pam_authenticate(pamh, 0);
+ pam_end(pamh, retval);
+ }
+ if (retval != PAM_SUCCESS) {
+ g_task_return_new_error(task, G_IO_ERROR, G_IO_ERROR_FAILED, "%s",
+ pam_strerror(pamh, retval));
+ } else {
+ g_task_return_int(task, retval);
+ }
+}
+
+gboolean astal_auth_pam_start_authenticate_with_callback(AstalAuthPam *self,
+ GAsyncReadyCallback result_callback,
+ gpointer user_data) {
+ g_return_val_if_fail(ASTAL_AUTH_IS_PAM(self), FALSE);
+ AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self);
+ g_return_val_if_fail(priv->task == NULL, FALSE);
+
+ priv->task = g_task_new(self, NULL, result_callback, user_data);
+ g_task_set_priority(priv->task, 0);
+ g_task_set_name(priv->task, "[AstalAuth] authenticate");
+ g_task_run_in_thread(priv->task, astal_auth_pam_thread);
+
+ return TRUE;
+}
+
+/**
+ * astal_auth_pam_start_authenticate:
+ * @self: a AstalAuthPam Object
+ *
+ * starts a new authentication process using the PAM (Pluggable Authentication Modules) system.
+ * Note that this will cancel an already running authentication process
+ * associated with this AstalAuthPam object.
+ */
+gboolean astal_auth_pam_start_authenticate(AstalAuthPam *self) {
+ return astal_auth_pam_start_authenticate_with_callback(
+ self, (GAsyncReadyCallback)astal_auth_pam_callback, NULL);
+}
+
+static void astal_auth_pam_on_hidden(AstalAuthPam *pam, const gchar *msg, gchar *password) {
+ astal_auth_pam_supply_secret(pam, password);
+ g_free(password);
+}
+
+/**
+ * astal_auth_pam_authenticate:
+ * @password: the password to be authenticated
+ * @result_callback: (scope async) (closure user_data): a GAsyncReadyCallback
+ * to call when the request is satisfied
+ * @user_data: the data to pass to callback function
+ *
+ * Requests authentication of the provided password using the PAM (Pluggable Authentication Modules)
+ * system.
+ */
+gboolean astal_auth_pam_authenticate(const gchar *password, GAsyncReadyCallback result_callback,
+ gpointer user_data) {
+ AstalAuthPam *pam = g_object_new(ASTAL_AUTH_TYPE_PAM, NULL);
+ g_signal_connect(pam, "auth-prompt-hidden", G_CALLBACK(astal_auth_pam_on_hidden),
+ (void *)g_strdup(password));
+
+ gboolean started =
+ astal_auth_pam_start_authenticate_with_callback(pam, result_callback, user_data);
+ g_object_unref(pam);
+ return started;
+}
+
+gssize astal_auth_pam_authenticate_finish(GAsyncResult *res, GError **error) {
+ return g_task_propagate_int(G_TASK(res), error);
+}
+
+static void astal_auth_pam_init(AstalAuthPam *self) {
+ AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self);
+
+ priv->secret = NULL;
+
+ g_cond_init(&priv->data_cond);
+ g_mutex_init(&priv->data_mutex);
+
+ priv->context = g_main_context_get_thread_default();
+}
+
+static void astal_auth_pam_finalize(GObject *gobject) {
+ AstalAuthPam *self = ASTAL_AUTH_PAM(gobject);
+ AstalAuthPamPrivate *priv = astal_auth_pam_get_instance_private(self);
+
+ g_free(self->username);
+ g_free(self->service);
+
+ g_free(priv->secret);
+
+ g_cond_clear(&priv->data_cond);
+ g_mutex_clear(&priv->data_mutex);
+
+ G_OBJECT_CLASS(astal_auth_pam_parent_class)->finalize(gobject);
+}
+
+static void astal_auth_pam_class_init(AstalAuthPamClass *class) {
+ GObjectClass *object_class = G_OBJECT_CLASS(class);
+
+ object_class->get_property = astal_auth_pam_get_property;
+ object_class->set_property = astal_auth_pam_set_property;
+
+ object_class->finalize = astal_auth_pam_finalize;
+
+ struct passwd *passwd = getpwuid(getuid());
+
+ /**
+ * AstalAuthPam:username:
+ *
+ * The username used for authentication.
+ * Changing the value of this property has no affect on an already started authentication
+ * process.
+ *
+ * Defaults to the user that owns this process.
+ */
+ astal_auth_pam_properties[ASTAL_AUTH_PAM_PROP_USERNAME] =
+ g_param_spec_string("username", "username", "username used for authentication",
+ passwd->pw_name, G_PARAM_CONSTRUCT | G_PARAM_READWRITE);
+ /**
+ * AstalAuthPam:service:
+ *
+ * The pam service used for authentication.
+ * Changing the value of this property has no affect on an already started authentication
+ * process.
+ *
+ * Defaults to the astal-auth pam service.
+ */
+ astal_auth_pam_properties[ASTAL_AUTH_PAM_PROP_SERVICE] =
+ g_param_spec_string("service", "service", "the pam service to use", "astal-auth",
+ G_PARAM_CONSTRUCT | G_PARAM_READWRITE);
+
+ g_object_class_install_properties(object_class, ASTAL_AUTH_PAM_N_PROPERTIES,
+ astal_auth_pam_properties);
+ /**
+ * AstalAuthPam::auth-prompt-visible:
+ * @pam: the object which received the signal.
+ * @msg: the prompt to be shown to the user
+ *
+ * This signal is emitted when user input is required. The input should be visible
+ * when entered (e.g., for One-Time Passwords (OTP)).
+ *
+ * This signal has to be matched with exaclty one supply_secret call.
+ */
+ astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_PROMPT_VISIBLE] =
+ g_signal_new("auth-prompt-visible", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL,
+ NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
+ /**
+ * AstalAuthPam::auth-prompt-hidden:
+ * @pam: the object which received the signal.
+ * @msg: the prompt to be shown to the user
+ *
+ * This signal is emitted when user input is required. The input should be hidden
+ * when entered (e.g., for passwords).
+ *
+ * This signal has to be matched with exaclty one supply_secret call.
+ */
+ astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_PROMPT_HIDDEN] =
+ g_signal_new("auth-prompt-hidden", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL,
+ NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
+ /**
+ * AstalAuthPam::auth-info:
+ * @pam: the object which received the signal.
+ * @msg: the info mssage to be shown to the user
+ *
+ * This signal is emitted when the user should receive an information (e.g., tell the user to
+ * touch a security key, or the remaining time pam has been locked after multiple failed
+ * attempts)
+ *
+ * This signal has to be matched with exaclty one supply_secret call.
+ */
+ astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_INFO] =
+ g_signal_new("auth-info", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+ /**
+ * AstalAuthPam::auth-error:
+ * @pam: the object which received the signal.
+ * @msg: the error message
+ *
+ * This signal is emitted when an authentication error has occured.
+ *
+ * This signal has to be matched with exaclty one supply_secret call.
+ */
+ astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_ERROR] =
+ g_signal_new("auth-error", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL,
+ NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
+ /**
+ * AstalAuthPam::success:
+ * @pam: the object which received the signal.
+ *
+ * This signal is emitted after successful authentication
+ */
+ astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_SUCCESS] =
+ g_signal_new("success", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+ /**
+ * AstalAuthPam::fail:
+ * @pam: the object which received the signal.
+ * @msg: the authentication failure message
+ *
+ * This signal is emitted when authentication failed.
+ */
+ astal_auth_pam_signals[ASTAL_AUTH_PAM_SIGNAL_FAIL] =
+ g_signal_new("fail", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL,
+ G_TYPE_NONE, 1, G_TYPE_STRING);
+}