session server UPDATE add system authentication
diff --git a/src/session_server_ssh.c b/src/session_server_ssh.c
index f79f788..91fb17a 100644
--- a/src/session_server_ssh.c
+++ b/src/session_server_ssh.c
@@ -15,11 +15,14 @@
 
 #define _GNU_SOURCE
 
-#include "config.h" /* Expose HAVE_LIBPAM */
+#include "config.h" /* Expose HAVE_LIBPAM and HAVE_SHADOW */
 
 #ifdef HAVE_LIBPAM
 #   include <security/pam_appl.h>
 #endif
+#ifdef HAVE_SHADOW
+#   include <shadow.h>
+#endif
 
 #include <arpa/inet.h>
 #include <assert.h>
@@ -231,6 +234,46 @@
     return auth_ret;
 }
 
+/* get answers to kbdint prompts on the given libssh session and return the number of them, -1 on timeout/dc */
+static int
+nc_server_ssh_kbdint_get_nanswers(struct nc_session *session, ssh_session libssh_session, uint16_t auth_timeout)
+{
+    int ret = 0;
+    struct timespec ts_timeout = {0};
+    ssh_message reply;
+
+    if (auth_timeout) {
+        nc_timeouttime_get(&ts_timeout, auth_timeout * 1000);
+    }
+
+    /* wait for answers from the client */
+    do {
+        if (!nc_session_is_connected(session)) {
+            ERR(NULL, "SSH communication socket unexpectedly closed.");
+            ret = -1;
+            goto cleanup;
+        }
+
+        reply = ssh_message_get(libssh_session);
+        if (reply) {
+            break;
+        }
+
+        usleep(NC_TIMEOUT_STEP);
+    } while (auth_timeout && (nc_timeouttime_cur_diff(&ts_timeout) >= 1));
+    if (!reply) {
+        ERR(NULL, "Authentication timeout.");
+        ret = -1;
+        goto cleanup;
+    }
+
+    ret = ssh_userauth_kbdint_getnanswers(libssh_session);
+
+cleanup:
+    ssh_message_free(reply);
+    return ret;
+}
+
 #ifdef HAVE_LIBPAM
 
 /**
@@ -253,7 +296,6 @@
     ssh_message reply = NULL;
     struct nc_pam_thread_arg *clb_data = appdata_ptr;
     ssh_session libssh_session;
-    struct timespec ts_timeout;
     uint16_t auth_timeout;
 
     libssh_session = clb_data->session->ti.libssh.session;
@@ -261,7 +303,7 @@
 
     /* PAM_MAX_NUM_MSG == 32 by default */
     if ((n_messages <= 0) || (n_messages >= PAM_MAX_NUM_MSG)) {
-        ERR(NULL, "Bad number of PAM messages (#%d).", n_messages);
+        ERR(clb_data->session, "Bad number of PAM messages (#%d).", n_messages);
         r = PAM_CONV_ERR;
         goto cleanup;
     }
@@ -270,7 +312,7 @@
     for (i = 0; i < n_messages; i++) {
         t = msg[i]->msg_style;
         if ((t != PAM_PROMPT_ECHO_OFF) && (t != PAM_PROMPT_ECHO_ON) && (t != PAM_TEXT_INFO) && (t != PAM_ERROR_MSG)) {
-            ERR(NULL, "PAM conversation callback received an unexpected type of message.");
+            ERR(clb_data->session, "PAM conversation callback received an unexpected type of message.");
             r = PAM_CONV_ERR;
             goto cleanup;
         }
@@ -279,11 +321,11 @@
     /* display messages with errors and/or some information and count the amount of actual authentication challenges */
     for (i = 0; i < n_messages; i++) {
         if (msg[i]->msg_style == PAM_TEXT_INFO) {
-            VRB(NULL, "PAM conversation callback received a message with some information for the client (%s).", msg[i]->msg);
+            VRB(clb_data->session, "PAM conversation callback received a message with some information for the client (%s).", msg[i]->msg);
             n_requests--;
         }
         if (msg[i]->msg_style == PAM_ERROR_MSG) {
-            ERR(NULL, "PAM conversation callback received an error message (%s).", msg[i]->msg);
+            ERR(clb_data->session, "PAM conversation callback received an error message (%s).", msg[i]->msg);
             r = PAM_CONV_ERR;
             goto cleanup;
         }
@@ -324,41 +366,19 @@
     /* print all the keyboard-interactive challenges to the user */
     r = ssh_message_auth_interactive_request(clb_data->msg, name, instruction, n_requests, prompts, echo);
     if (r != SSH_OK) {
-        ERR(NULL, "Failed to send an authentication request.");
+        ERR(clb_data->session, "Failed to send an authentication request.");
         r = PAM_CONV_ERR;
         goto cleanup;
     }
 
-    if (auth_timeout) {
-        nc_timeouttime_get(&ts_timeout, auth_timeout * 1000);
-    }
-
-    /* get user's replies */
-    do {
-        if (!nc_session_is_connected(clb_data->session)) {
-            ERR(NULL, "Communication SSH socket unexpectedly closed.");
-            r = PAM_CONV_ERR;
-            goto cleanup;
-        }
-
-        reply = ssh_message_get(libssh_session);
-        if (reply) {
-            break;
-        }
-
-        usleep(NC_TIMEOUT_STEP);
-    } while (auth_timeout && (nc_timeouttime_cur_diff(&ts_timeout) >= 1));
-
-    if (!reply) {
-        ERR(NULL, "Authentication timeout.");
+    n_answers = nc_server_ssh_kbdint_get_nanswers(clb_data->session, libssh_session, auth_timeout);
+    if (n_answers < 0) {
+        /* timeout or dc */
         r = PAM_CONV_ERR;
         goto cleanup;
-    }
-
-    /* check if the amount of replies matches the amount of requests */
-    n_answers = ssh_userauth_kbdint_getnanswers(libssh_session);
-    if (n_answers != n_requests) {
-        ERR(NULL, "Expected %d response(s), got %d.", n_requests, n_answers);
+    } else if (n_answers != n_requests) {
+        /* check if the number of answers and requests matches */
+        ERR(clb_data->session, "Expected %d response(s), got %d.", n_requests, n_answers);
         r = PAM_CONV_ERR;
         goto cleanup;
     }
@@ -412,7 +432,7 @@
     conv.appdata_ptr = &clb_data;
 
     if (!server_opts.pam_config_name) {
-        ERR(NULL, "PAM configuration filename not set.");
+        ERR(session, "PAM configuration filename not set.");
         ret = 1;
         goto cleanup;
     }
@@ -420,7 +440,7 @@
     /* initialize PAM and see if the given configuration file exists */
     ret = pam_start(server_opts.pam_config_name, client->username, &conv, &pam_h);
     if (ret != PAM_SUCCESS) {
-        ERR(NULL, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
+        ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
         goto cleanup;
     }
 
@@ -428,10 +448,10 @@
     ret = pam_authenticate(pam_h, 0);
     if (ret != PAM_SUCCESS) {
         if (ret == PAM_ABORT) {
-            ERR(NULL, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
+            ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
             goto cleanup;
         } else {
-            VRB(NULL, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
+            VRB(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
             goto cleanup;
         }
     }
@@ -439,18 +459,18 @@
     /* correct token entered, check other requirements(the time of the day, expired token, ...) */
     ret = pam_acct_mgmt(pam_h, 0);
     if ((ret != PAM_SUCCESS) && (ret != PAM_NEW_AUTHTOK_REQD)) {
-        VRB(NULL, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
+        VRB(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
         goto cleanup;
     }
 
     /* if a token has expired a new one will be generated */
     if (ret == PAM_NEW_AUTHTOK_REQD) {
-        VRB(NULL, "PAM warning occurred (%s).", pam_strerror(pam_h, ret));
+        VRB(session, "PAM warning occurred (%s).", pam_strerror(pam_h, ret));
         ret = pam_chauthtok(pam_h, PAM_CHANGE_EXPIRED_AUTHTOK);
         if (ret == PAM_SUCCESS) {
-            VRB(NULL, "The authentication token of user \"%s\" updated successfully.", client->username);
+            VRB(session, "The authentication token of user \"%s\" updated successfully.", client->username);
         } else {
-            ERR(NULL, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
+            ERR(session, "PAM error occurred (%s).", pam_strerror(pam_h, ret));
             goto cleanup;
         }
     }
@@ -463,7 +483,189 @@
     return ret;
 }
 
-#endif /* HAVE_LIBPAM */
+#elif defined (HAVE_SHADOW)
+
+static struct passwd *
+nc_server_ssh_getpwnam(const char *username, struct passwd *pwd_buf, char **buf, size_t *buf_size)
+{
+    struct passwd *pwd = NULL;
+    char *mem;
+    int r = 0;
+
+    do {
+        r = getpwnam_r(username, pwd_buf, *buf, *buf_size, &pwd);
+        if (pwd) {
+            /* entry found */
+            break;
+        }
+
+        if (r == ERANGE) {
+            /* small buffer, enlarge */
+            *buf_size <<= 2;
+            mem = realloc(*buf, *buf_size);
+            if (!mem) {
+                ERRMEM;
+                return NULL;
+            }
+            *buf = mem;
+        }
+    } while (r == ERANGE);
+
+    return pwd;
+}
+
+static struct spwd *
+nc_server_ssh_getspnam(const char *username, struct spwd *spwd_buf, char **buf, size_t *buf_size)
+{
+    struct spwd *spwd = NULL;
+    char *mem;
+    int r = 0;
+
+    do {
+# ifndef __QNXNTO__
+        r = getspnam_r(username, spwd_buf, *buf, *buf_size, &spwd);
+# else
+        spwd = getspnam_r(username, spwd_buf, *buf, *buf_size);
+# endif
+        if (spwd) {
+            /* entry found */
+            break;
+        }
+
+        if (r == ERANGE) {
+            /* small buffer, enlarge */
+            *buf_size <<= 2;
+            mem = realloc(*buf, *buf_size);
+            if (!mem) {
+                ERRMEM;
+                return NULL;
+            }
+            *buf = mem;
+        }
+    } while (r == ERANGE);
+
+    return spwd;
+}
+
+static char *
+nc_server_ssh_get_pwd_hash(const char *username)
+{
+    struct passwd *pwd, pwd_buf;
+    struct spwd *spwd, spwd_buf;
+    char *pass_hash = NULL, *buf = NULL;
+    size_t buf_size = 256;
+
+    buf = malloc(buf_size);
+    NC_CHECK_ERRMEM_GOTO(!buf, , error);
+
+    pwd = nc_server_ssh_getpwnam(username, &pwd_buf, &buf, &buf_size);
+    if (!pwd) {
+        VRB(NULL, "User \"%s\" not found locally.", username);
+        goto error;
+    }
+
+    if (!strcmp(pwd->pw_passwd, "x")) {
+        spwd = nc_server_ssh_getspnam(username, &spwd_buf, &buf, &buf_size);
+        if (!spwd) {
+            VRB(NULL, "Failed to retrieve the shadow entry for \"%s\".", username);
+            goto error;
+        } else if ((spwd->sp_expire > -1) && (spwd->sp_expire <= (time(NULL) / (60 * 60 * 24)))) {
+            WRN(NULL, "User \"%s\" account has expired.", username);
+            goto error;
+        }
+
+        pass_hash = spwd->sp_pwdp;
+    } else {
+        pass_hash = pwd->pw_passwd;
+    }
+
+    if (!pass_hash) {
+        ERR(NULL, "No password could be retrieved for \"%s\".", username);
+        goto error;
+    }
+
+    /* check the hash structure for special meaning */
+    if (!strcmp(pass_hash, "*") || !strcmp(pass_hash, "!")) {
+        VRB(NULL, "User \"%s\" is not allowed to authenticate using a password.", username);
+        goto error;
+    }
+    if (!strcmp(pass_hash, "*NP*")) {
+        VRB(NULL, "Retrieving password for \"%s\" from a NIS+ server not supported.", username);
+        goto error;
+    }
+
+    pass_hash = strdup(pass_hash);
+    free(buf);
+    return pass_hash;
+
+error:
+    free(buf);
+    return NULL;
+}
+
+/**
+ * @brief Authenticate using locally stored credentials.
+ *
+ * @param[in] session Session to authenticate on.
+ * @param[in] client Client to authenticate.
+ * @param[in] auth_timeout Authentication timeout.
+ * @param[in] msg SSH message that originally requested kbdint authentication.
+ *
+ * @return 0 on success, non-zero otherwise.
+ */
+static int
+nc_server_ssh_system_auth(struct nc_session *session, struct nc_auth_client *client, uint16_t auth_timeout, ssh_message msg)
+{
+    int ret = 0, n_answers;
+    const char *name = "Keyboard-Interactive Authentication";
+    const char *instruction = "Please enter your authentication token";
+    char *prompt = NULL, *local_pw = NULL, *received_pw = NULL;
+    char echo[] = {0};
+
+    /* try to get the client's locally stored pw hash */
+    local_pw = nc_server_ssh_get_pwd_hash(client->username);
+    if (!local_pw) {
+        ERR(session, "Unable to get %s's credentials.", client->username);
+        ret = 1;
+        goto cleanup;
+    }
+
+    ret = asprintf(&prompt, "%s's password:", client->username);
+    NC_CHECK_ERRMEM_GOTO(ret == -1, prompt = NULL; ret = 1, cleanup);
+
+    /* send the password prompt to the client */
+    ret = ssh_message_auth_interactive_request(msg, name, instruction, 1, (const char **) &prompt, echo);
+    if (ret) {
+        ERR(session, "Failed to send an authentication request to client \"%s\".", client->username);
+        goto cleanup;
+    }
+
+    /* get the reply */
+    n_answers = nc_server_ssh_kbdint_get_nanswers(session, session->ti.libssh.session, auth_timeout);
+    if (n_answers < 0) {
+        /* timeout or dc */
+        ret = 1;
+        goto cleanup;
+    } else if (n_answers != 1) {
+        /* only expecting a single answer */
+        ERR(session, "Unexpected amount of answers in system auth. Expected 1, got \"%d\".", n_answers);
+        ret = 1;
+        goto cleanup;
+    }
+    received_pw = strdup(ssh_userauth_kbdint_getanswer(session->ti.libssh.session, 0));
+    NC_CHECK_ERRMEM_GOTO(!received_pw, ret = 1, cleanup);
+
+    /* cmp the pw hashes */
+    ret = auth_password_compare_pwd(local_pw, received_pw);
+
+cleanup:
+    free(local_pw);
+    free(received_pw);
+    free(prompt);
+    return ret;
+}
+
+#endif
 
 static int
 nc_sshcb_auth_kbdint(struct nc_session *session, struct nc_auth_client *client, uint16_t auth_timeout, ssh_message msg)
@@ -474,18 +676,25 @@
         auth_ret = server_opts.interactive_auth_clb(session, session->ti.libssh.session, msg, server_opts.interactive_auth_data);
     } else {
 #ifdef HAVE_LIBPAM
-        if (nc_pam_auth(session, client, auth_timeout, msg) == PAM_SUCCESS) {
+        /* authenticate using PAM */
+        if (!nc_pam_auth(session, client, auth_timeout, msg)) {
+            auth_ret = 0;
+        }
+#elif defined (HAVE_SHADOW)
+        /* authenticate using locally configured users */
+        if (!nc_server_ssh_system_auth(session, client, auth_timeout, msg)) {
             auth_ret = 0;
         }
 #else
-        ERR(session, "PAM-based SSH authentication is not supported.");
+        (void) auth_timeout;
+        ERR(NULL, "Keyboard-interactive method not supported.");
 #endif
     }
 
     /* Authenticate message based on outcome */
     if (auth_ret) {
         ++session->opts.server.ssh_auth_attempts;
-        VRB(session, "Failed user \"%s\" authentication attempt (#%d).", session->username,
+        VRB(session, "Failed user \"%s\" authentication attempt (#%d).", client->username,
                 session->opts.server.ssh_auth_attempts);
         ssh_message_reply_default(msg);
     }
@@ -497,9 +706,15 @@
 nc_server_ssh_set_interactive_auth_clb(int (*interactive_auth_clb)(const struct nc_session *session, ssh_session ssh_sess, ssh_message msg, void *user_data),
         void *user_data, void (*free_user_data)(void *user_data))
 {
+    /* CONFIG LOCK */
+    pthread_rwlock_wrlock(&server_opts.config_lock);
+
     server_opts.interactive_auth_clb = interactive_auth_clb;
     server_opts.interactive_auth_data = user_data;
     server_opts.interactive_auth_data_free = free_user_data;
+
+    /* CONFIG UNLOCK */
+    pthread_rwlock_unlock(&server_opts.config_lock);
 }
 
 #ifdef HAVE_LIBPAM
@@ -507,12 +722,23 @@
 API int
 nc_server_ssh_set_pam_conf_filename(const char *filename)
 {
+    int ret = 0;
+
     NC_CHECK_ARG_RET(NULL, filename, 1);
 
+    /* CONFIG LOCK */
+    pthread_rwlock_wrlock(&server_opts.config_lock);
+
     free(server_opts.pam_config_name);
     server_opts.pam_config_name = strdup(filename);
-    NC_CHECK_ERRMEM_RET(!server_opts.pam_config_name, 1);
-    return 0;
+    if (!server_opts.pam_config_name) {
+        ERRMEM;
+        ret = 1;
+    }
+
+    /* CONFIG UNLOCK */
+    pthread_rwlock_unlock(&server_opts.config_lock);
+    return ret;
 }
 
 #else