From eda645f7e0af2dcb02a13e1a1ce6c56049c30b83 Mon Sep 17 00:00:00 2001 From: Maxime de Roucy Date: Sun, 4 Jan 2015 12:47:03 +0100 Subject: [PATCH 1/3] usersfile : rewrite modify the usersfile inplace to keep owner and permissions --- NEWS | 3 + liboath/errors.c | 54 ++-- liboath/oath.h.in | 71 +++-- liboath/tests/tst_usersfile.c | 2 +- liboath/totp.c | 6 +- liboath/usersfile.c | 701 ++++++++++++++++++++++++------------------ pam_oath/README | 2 +- 7 files changed, 481 insertions(+), 358 deletions(-) diff --git a/NEWS b/NEWS index 559d9d8..d84f8bf 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,9 @@ Copyright (C) 2009-2014 Simon Josefsson. Licensed under the GPLv3+. * Version 2.6.0 (unreleased) +** liboath: usersfile is now updated inplace, so there is no owner/permission +modifications. + ** liboath: Support TOTP with HMAC-SHA256 and HMAC-SHA512. This adds new APIs oath_totp_generate2, oath_totp_validate4 and oath_totp_validate4_callback. diff --git a/liboath/errors.c b/liboath/errors.c index 87292e1..1257bad 100644 --- a/liboath/errors.c +++ b/liboath/errors.c @@ -31,32 +31,34 @@ typedef struct } err_t; static const err_t errors[] = { - ERR (OATH_OK, "Successful return"), - ERR (OATH_CRYPTO_ERROR, "Internal error in crypto functions"), - ERR (OATH_INVALID_DIGITS, "Unsupported number of OTP digits"), - ERR (OATH_PRINTF_ERROR, "Error from system printf call"), - ERR (OATH_INVALID_HEX, "Hex string is invalid"), - ERR (OATH_TOO_SMALL_BUFFER, "The output buffer is too small"), - ERR (OATH_INVALID_OTP, "The OTP is not valid"), - ERR (OATH_REPLAYED_OTP, "The OTP has been replayed"), - ERR (OATH_BAD_PASSWORD, "The password does not match"), - ERR (OATH_INVALID_COUNTER, "The counter value is corrupt"), - ERR (OATH_INVALID_TIMESTAMP, "The timestamp is corrupt"), - ERR (OATH_NO_SUCH_FILE, "The supplied filename does not exist"), - ERR (OATH_UNKNOWN_USER, "Cannot find information about user"), - ERR (OATH_FILE_SEEK_ERROR, "System error when seeking in file"), - ERR (OATH_FILE_CREATE_ERROR, "System error when creating file"), - ERR (OATH_FILE_LOCK_ERROR, "System error when locking file"), - ERR (OATH_FILE_RENAME_ERROR, "System error when renaming file"), - ERR (OATH_FILE_UNLINK_ERROR, "System error when removing file"), - ERR (OATH_TIME_ERROR, "System error for time manipulation"), - ERR (OATH_STRCMP_ERROR, "A strcmp callback returned an error"), - ERR (OATH_INVALID_BASE32, "Base32 string is invalid"), - ERR (OATH_BASE32_OVERFLOW, "Base32 encoding would overflow"), - ERR (OATH_MALLOC_ERROR, "Memory allocation failed"), - ERR (OATH_FILE_FLUSH_ERROR, "System error when flushing file buffer"), - ERR (OATH_FILE_SYNC_ERROR, "System error when syncing file to disk"), - ERR (OATH_FILE_CLOSE_ERROR, "System error when closing file") + ERR (OATH_OK, " Successful return"), + ERR (OATH_CRYPTO_ERROR, " Internal error in crypto functions"), + ERR (OATH_INVALID_DIGITS, " Unsupported number of OTP digits"), + ERR (OATH_INVALID_HEX, " Hex string is invalid"), + ERR (OATH_TOO_SMALL_BUFFER, " The output buffer is too small"), + ERR (OATH_INVALID_OTP, " The OTP is not valid"), + ERR (OATH_REPLAYED_OTP, " The OTP has been replayed"), + ERR (OATH_BAD_PASSWORD, " The password does not match"), + ERR (OATH_INVALID_COUNTER, " The counter value is corrupt"), + ERR (OATH_INVALID_TIMESTAMP, " The timestamp is corrupt"), + ERR (OATH_UNKNOWN_USER, " Cannot find information about user"), + ERR (OATH_WRONG_TOKEN_TYPE, " Bad formated token type"), + ERR (OATH_NO_USERNAME, " Can't read username from file"), + ERR (OATH_NO_PASSWORD, " Can't read password from file"), + ERR (OATH_NO_SECRET, " Can't read secret key from file"), + ERR (OATH_PRINTF_ERROR, " Error from system *printf call"), + ERR (OATH_FILE_OPEN_ERROR, " System error when opening file"), + ERR (OATH_FILE_READ_ERROR, " System error when reading in file"), + ERR (OATH_FILE_WRITE_ERROR, " System error when writting in file"), + ERR (OATH_FILE_TELL_ERROR, " Error from system ftell call"), + ERR (OATH_FILE_SEEK_ERROR, " System error when seeking in file"), + ERR (OATH_FILE_TRUNCATE_ERROR, " System error when truncating file"), + ERR (OATH_FILE_LOCK_ERROR, " System error when locking file"), + ERR (OATH_TIME_ERROR, " System error for time manipulation"), + ERR (OATH_STRCMP_ERROR, " A strcmp callback returned an error"), + ERR (OATH_INVALID_BASE32, " Base32 string is invalid"), + ERR (OATH_BASE32_OVERFLOW, " Base32 encoding would overflow"), + ERR (OATH_MALLOC_ERROR, " Memory allocation failed") }; /** diff --git a/liboath/oath.h.in b/liboath/oath.h.in index 173592a..a6bb8d1 100644 --- a/liboath/oath.h.in +++ b/liboath/oath.h.in @@ -68,7 +68,6 @@ extern "C" * @OATH_OK: Successful return * @OATH_CRYPTO_ERROR: Internal error in crypto functions * @OATH_INVALID_DIGITS: Unsupported number of OTP digits - * @OATH_PRINTF_ERROR: Error from system printf call * @OATH_INVALID_HEX: Hex string is invalid * @OATH_TOO_SMALL_BUFFER: The output buffer is too small * @OATH_INVALID_OTP: The OTP is not valid @@ -76,22 +75,26 @@ extern "C" * @OATH_BAD_PASSWORD: The password does not match * @OATH_INVALID_COUNTER: The counter value is corrupt * @OATH_INVALID_TIMESTAMP: The timestamp is corrupt - * @OATH_NO_SUCH_FILE: The supplied filename does not exist * @OATH_UNKNOWN_USER: Cannot find information about user + * @OATH_WRONG_TOKEN_TYPE: Bad formated token type + * @OATH_NO_USERNAME: Can't read username from file + * @OATH_NO_PASSWORD: Can't read password from file + * @OATH_NO_SECRET: Can't read secret key from file + * @OATH_PRINTF_ERROR: Error from system *printf call + * @OATH_FILE_OPEN_ERROR: System error when opening file + * @OATH_FILE_READ_ERROR: System error when reading in file + * @OATH_FILE_WRITE_ERROR: System error when writting in file + * @OATH_FILE_TELL_ERROR: Error from system ftell call * @OATH_FILE_SEEK_ERROR: System error when seeking in file - * @OATH_FILE_CREATE_ERROR: System error when creating file + * @OATH_FILE_TRUNCATE_ERROR: System error when truncating file * @OATH_FILE_LOCK_ERROR: System error when locking file - * @OATH_FILE_RENAME_ERROR: System error when renaming file - * @OATH_FILE_UNLINK_ERROR: System error when removing file * @OATH_TIME_ERROR: System error for time manipulation * @OATH_STRCMP_ERROR: A strcmp callback returned an error * @OATH_INVALID_BASE32: Base32 string is invalid * @OATH_BASE32_OVERFLOW: Base32 encoding would overflow * @OATH_MALLOC_ERROR: Memory allocation failed - * @OATH_FILE_FLUSH_ERROR: System error when flushing file buffer - * @OATH_FILE_SYNC_ERROR: System error when syncing file to disk - * @OATH_FILE_CLOSE_ERROR: System error when closing file * @OATH_LAST_ERROR: Meta-error indicating the last error code, for use + * * when iterating over all error codes or similar. * * Return codes for OATH functions. All return codes are negative @@ -106,32 +109,34 @@ typedef enum OATH_OK = 0, OATH_CRYPTO_ERROR = -1, OATH_INVALID_DIGITS = -2, - OATH_PRINTF_ERROR = -3, - OATH_INVALID_HEX = -4, - OATH_TOO_SMALL_BUFFER = -5, - OATH_INVALID_OTP = -6, - OATH_REPLAYED_OTP = -7, - OATH_BAD_PASSWORD = -8, - OATH_INVALID_COUNTER = -9, - OATH_INVALID_TIMESTAMP = -10, - OATH_NO_SUCH_FILE = -11, - OATH_UNKNOWN_USER = -12, - OATH_FILE_SEEK_ERROR = -13, - OATH_FILE_CREATE_ERROR = -14, - OATH_FILE_LOCK_ERROR = -15, - OATH_FILE_RENAME_ERROR = -16, - OATH_FILE_UNLINK_ERROR = -17, - OATH_TIME_ERROR = -18, - OATH_STRCMP_ERROR = -19, - OATH_INVALID_BASE32 = -20, - OATH_BASE32_OVERFLOW = -21, - OATH_MALLOC_ERROR = -22, - OATH_FILE_FLUSH_ERROR = -23, - OATH_FILE_SYNC_ERROR = -24, - OATH_FILE_CLOSE_ERROR = -25, + OATH_INVALID_HEX = -3, + OATH_TOO_SMALL_BUFFER = -4, + OATH_INVALID_OTP = -5, + OATH_REPLAYED_OTP = -6, + OATH_BAD_PASSWORD = -7, + OATH_INVALID_COUNTER = -8, + OATH_INVALID_TIMESTAMP = -9, + OATH_UNKNOWN_USER = -10, + OATH_WRONG_TOKEN_TYPE = -11, + OATH_NO_USERNAME = -12, + OATH_NO_PASSWORD = -13, + OATH_NO_SECRET = -14, + OATH_PRINTF_ERROR = -15, + OATH_FILE_OPEN_ERROR = -16, + OATH_FILE_READ_ERROR = -17, + OATH_FILE_WRITE_ERROR = -18, + OATH_FILE_TELL_ERROR = -19, + OATH_FILE_SEEK_ERROR = -20, + OATH_FILE_TRUNCATE_ERROR = -21, + OATH_FILE_LOCK_ERROR = -22, + OATH_TIME_ERROR = -23, + OATH_STRCMP_ERROR = -24, + OATH_INVALID_BASE32 = -25, + OATH_BASE32_OVERFLOW = -26, + OATH_MALLOC_ERROR = -27, /* When adding anything here, update OATH_LAST_ERROR, errors.c - and tests/tst_errors.c. */ - OATH_LAST_ERROR = -25 + * and tests/tst_errors.c. */ + OATH_LAST_ERROR = -27 } oath_rc; /* Global */ diff --git a/liboath/tests/tst_usersfile.c b/liboath/tests/tst_usersfile.c index b018707..615e376 100644 --- a/liboath/tests/tst_usersfile.c +++ b/liboath/tests/tst_usersfile.c @@ -53,7 +53,7 @@ main (void) rc = oath_authenticate_usersfile ("no-such-file", "joe", "755224", 0, "1234", &last_otp); - if (rc != OATH_NO_SUCH_FILE) + if (rc != OATH_FILE_OPEN_ERROR) { printf ("oath_authenticate_usersfile[1]: %s (%d)\n", oath_strerror_name (rc), rc); diff --git a/liboath/totp.c b/liboath/totp.c index 0d796ae..2b3be04 100644 --- a/liboath/totp.c +++ b/liboath/totp.c @@ -162,8 +162,10 @@ oath_totp_validate (const char *secret, unsigned time_step_size, time_t start_offset, size_t window, const char *otp) { - return oath_totp_validate3 (secret, secret_length, now, time_step_size, - start_offset, window, NULL, NULL, otp); + return oath_totp_validate4_callback (secret, secret_length, now, + time_step_size, start_offset, + strlen (otp), window, NULL, NULL, 0, + _oath_strcmp_callback, (void *) otp); } /** diff --git a/liboath/usersfile.c b/liboath/usersfile.c index 8dfeffa..816053d 100644 --- a/liboath/usersfile.c +++ b/liboath/usersfile.c @@ -31,8 +31,25 @@ #include /* For errno. */ #include /* For S_IRUSR, S_IWUSR. */ +#define IF_ERROR_GOTO(test, rc_value, goto_label) if (test) { rc = rc_value; goto goto_label; } + +static const char *whitespace = " \t\r\n"; +#define TIME_FORMAT_STRING "%Y-%m-%dT%H:%M:%SL" +#define TIME_BUFFER_SIZE 30 +#define BUFFER_SIZE 1024 + +/* + * parse_usersfile : + * @str: string with token type to parse + * @digits: output variable holding the length of OTP (6, 7 or 8) + * @totpstepsize: output variable holding the interval (in second) of TOTP + * + * internal fonction + * + * Returns: 0 on success, -1 on error + **/ static int -parse_type (const char *str, unsigned *digits, unsigned *totpstepsize) +parse_type (const char *str, unsigned int *digits, unsigned int *totpstepsize) { *totpstepsize = 0; if (strcmp (str, "HOTP/E/6") == 0 @@ -72,59 +89,127 @@ parse_type (const char *str, unsigned *digits, unsigned *totpstepsize) return 0; } -static const char *whitespace = " \t\r\n"; -#define TIME_FORMAT_STRING "%Y-%m-%dT%H:%M:%SL" +/* compute the timestamp */ +static int +compute_timestamp (char timestamp[]) +{ + struct tm now; + time_t t; + size_t l; + + if (time (&t) == (time_t) - 1) + return OATH_TIME_ERROR; + + if (localtime_r (&t, &now) == NULL) + return OATH_TIME_ERROR; + + l = strftime (timestamp, TIME_BUFFER_SIZE, TIME_FORMAT_STRING, &now); + if (l != 20) + return OATH_TIME_ERROR; + + return OATH_OK; +} + + +/* + * parse_usersfile : + * @username: string with name of user + * @otp: string with one-time password to authenticate + * @window: how many past/future OTPs to search + * @passwd: string with password, or NULL if password checking is disabled + * @usersfile_fd: input file descriptor for usersfile + * @last_otp_timestamp: output variable holding last successful authentication timestamp + * @old_log_start: output variable holding the file position of the beginning of the "log" of the validation line + * @old_log_end: output variable holding the file position of the end of the "log" of the validation line + * + * internal fonction + * + * by "log" (old_log_start/end) I mean the end of line that should contain a moving_factor, an otp and a timestamp. + * + * Returns: %OATH_OK on success, negative value on error (see oath.h) + **/ static int parse_usersfile (const char *username, const char *otp, - size_t window, + const size_t window, const char *passwd, - time_t * last_otp, - FILE * infh, - char **lineptr, size_t * n, uint64_t * new_moving_factor, - size_t * skipped_users) + FILE * usersfile_fd, + time_t * last_otp_timestamp, + off_t * old_log_start, + off_t * old_log_end, char *new_log_buffer) { - int bad_password = 0; + int rc = OATH_OK; + + // to record the fact we read a line matching the username + int matching_user_line = 0; + // to record the fact we read a line matching the username and the passwd + int matching_user_and_passwd_line = 0; - *skipped_users = 0; + char *line_buffer = NULL; + size_t line_buffer_size = 0; + ssize_t line_size = 0; - while (getline (lineptr, n, infh) != -1) + // for each line of userfile + while ((line_size = + getline (&line_buffer, &line_buffer_size, usersfile_fd)) != -1) { + // used internally by strtok_r in order to maintain context + // between successive calls that parse the same string. char *saveptr; - char *p = strtok_r (*lineptr, whitespace, &saveptr); - unsigned digits, totpstepsize; - char secret[32]; - size_t secret_length = sizeof (secret); - uint64_t start_moving_factor = 0; - int rc = 0; - char *prev_otp = NULL; - if (p == NULL) + // read the first token/element of the line + char *p = strtok_r (line_buffer, whitespace, &saveptr); + + if (p == NULL || *p == '#') + // blank line or comment continue; - /* Read token type */ + // read token type + unsigned int digits, totpstepsize; if (parse_type (p, &digits, &totpstepsize) != 0) - continue; + { + // wrong formated token type + rc = OATH_WRONG_TOKEN_TYPE; + break; + } - /* Read username */ + // read username p = strtok_r (NULL, whitespace, &saveptr); - if (p == NULL || strcmp (p, username) != 0) + if (p == NULL) + { + // there is no username in the current line of usersfile + rc = OATH_NO_USERNAME; + break; + } + + if (strcmp (p, username) != 0) + // username doesn't match continue; - /* Read password. */ + // we record the fact we read a line matching the username + matching_user_line = 1; + + // read password p = strtok_r (NULL, whitespace, &saveptr); + if (passwd) { + // password checking is enabled + if (p == NULL) - continue; + { + // there is no password in the usersfile + rc = OATH_NO_PASSWORD; + break; + } + if (strcmp (p, "-") == 0) { if (*passwd != '\0') - { - bad_password = 1; - rc = OATH_BAD_PASSWORD; - } + // the user supply a non empty password but + // there is no password ("-") in the current usersfile line + rc = OATH_BAD_PASSWORD; } else if (strcmp (p, "+") == 0) { @@ -132,285 +217,235 @@ parse_usersfile (const char *username, } else if (strcmp (p, passwd) != 0) { - bad_password = 1; + // the password supply by the user doesn't match the one in the + // current usersfile line rc = OATH_BAD_PASSWORD; } + if (rc == OATH_BAD_PASSWORD) { - (*skipped_users)++; + // the user supply a non empty password but there is no password ("-") in the current usersfile line + // or + // the password supply by the user doesn't match the one in the current usersfile line + + // we continue because a user can have multiple password + rc = OATH_OK; continue; } - bad_password = 0; + + // we record the fact we read a line matching the username and the passwd + matching_user_and_passwd_line = 1; } - /* Read key. */ + // read secret p = strtok_r (NULL, whitespace, &saveptr); if (p == NULL) - continue; + { + // there is no secret key in the current line of usersfile + rc = OATH_NO_SECRET; + break; + } + + // convert the secret key in binary format + char secret[32]; + size_t secret_length = sizeof (secret); rc = oath_hex2bin (p, secret, &secret_length); if (rc != OATH_OK) - return rc; + // the secret key can't be converted the binary format + break; + + // record the size of the "log" of the current line + long old_log_size = line_size - strlen (p) - (p - line_buffer); + + // read (optional) moving factor + unsigned long long start_moving_factor = 0; - /* Read (optional) moving factor. */ p = strtok_r (NULL, whitespace, &saveptr); - if (p && *p) + if (p) { + // the current line contain a moving factor + + // convert the string to unsigned long long char *endptr; - unsigned long long int ull = strtoull (p, &endptr, 10); + start_moving_factor = strtoull (p, &endptr, 10); if (endptr && *endptr != '\0') - return OATH_INVALID_COUNTER; - start_moving_factor = ull; + { + // the moving factor is bad formated and + // can't be converted to unsigned long long + rc = OATH_INVALID_COUNTER; + break; + } } - /* Read (optional) last OTP */ - prev_otp = strtok_r (NULL, whitespace, &saveptr); + // read (optional) last OTP + char *last_otp = NULL; + last_otp = strtok_r (NULL, whitespace, &saveptr); - /* Read (optional) last_otp */ + // Read (optional) last OTP timestamp p = strtok_r (NULL, whitespace, &saveptr); - if (p) + if (p && last_otp_timestamp) { + // the current line contain a timestamp + // and the caller off the function want to get it + + // we convert it to tm struct tm tm; char *ts; - ts = strptime (p, TIME_FORMAT_STRING, &tm); if (ts == NULL || *ts != '\0') - return OATH_INVALID_TIMESTAMP; + { + // the timestamp is bad formatted + rc = OATH_INVALID_TIMESTAMP; + break; + } tm.tm_isdst = -1; - if (last_otp) + + // convert the tm into time_t and + // store it in last_otp_timestamp + *last_otp_timestamp = mktime (&tm); + if (*last_otp_timestamp == (time_t) - 1) { - *last_otp = mktime (&tm); - if (*last_otp == (time_t) - 1) - return OATH_INVALID_TIMESTAMP; + // the tm is wrong + rc = OATH_INVALID_TIMESTAMP; + break; } } - if (prev_otp && strcmp (prev_otp, otp) == 0) - return OATH_REPLAYED_OTP; + if (last_otp && strcmp (last_otp, otp) == 0) + { + // the OTP supply by the user is the + // same that is stored in usersfile + rc = OATH_REPLAYED_OTP; + break; + } if (totpstepsize == 0) - rc = oath_hotp_validate (secret, secret_length, - start_moving_factor, window, otp); - else if (prev_otp) { - int prev_otp_pos, this_otp_pos, tmprc; + // token type algorithm is HOTP + + // check if the suppied OTP is valid + rc = oath_hotp_validate (secret, secret_length, + start_moving_factor, window, otp); + } + else if (last_otp) + { + // token type algorithm is TOTP + // and the current line contain a "log" + + // check if the suppied OTP is valid + int totp_position; rc = oath_totp_validate2 (secret, secret_length, time (NULL), totpstepsize, 0, window, - &this_otp_pos, otp); - if (rc == OATH_INVALID_OTP) + &totp_position, otp); + + if (rc == OATH_OK) { - (*skipped_users)++; - continue; + // the supplied OTP is valide + // but it may have been already by played + // since it's valide for a periode of time in which a new + // OTP could have been played : + // + // OTP1 → OTP2 → OTP1(replay) + // OTP1 validity |--------------------------| + // + // in that case OTP1(replay) should be rejected + + // get the time validity of the last recorded OTP + int last_totp_position, tmprc; + tmprc = oath_totp_validate2 (secret, secret_length, + time (NULL), totpstepsize, 0, + window, &last_totp_position, + last_otp); + + if (tmprc >= 0 && last_totp_position >= totp_position) + { + // last recorded otp is newer than the one supplied by the user + rc = OATH_REPLAYED_OTP; + break; + } } - if (rc < 0) - return rc; - tmprc = oath_totp_validate2 (secret, secret_length, - time (NULL), totpstepsize, 0, window, - &prev_otp_pos, prev_otp); - if (tmprc >= 0 && prev_otp_pos >= this_otp_pos) - return OATH_REPLAYED_OTP; } else - rc = oath_totp_validate (secret, secret_length, - time (NULL), totpstepsize, 0, window, otp); - if (rc == OATH_INVALID_OTP) { - (*skipped_users)++; - continue; - } - if (rc < 0) - return rc; - *new_moving_factor = start_moving_factor + rc; - return OATH_OK; - } - - if (*skipped_users) - { - if (bad_password) - return OATH_BAD_PASSWORD; - else - return OATH_INVALID_OTP; - } - - return OATH_UNKNOWN_USER; -} - -static int -update_usersfile2 (const char *username, - const char *otp, - FILE * infh, - FILE * outfh, - char **lineptr, - size_t * n, char *timestamp, uint64_t new_moving_factor, - size_t skipped_users) -{ - size_t got_users = 0; - - while (getline (lineptr, n, infh) != -1) - { - char *saveptr; - char *origline; - const char *user, *type, *passwd, *secret; - int r; - unsigned digits, totpstepsize; - - origline = strdup (*lineptr); - - type = strtok_r (*lineptr, whitespace, &saveptr); - if (type == NULL) - goto skip_line; - - /* Read token type */ - if (parse_type (type, &digits, &totpstepsize) != 0) - goto skip_line; - - /* Read username */ - user = strtok_r (NULL, whitespace, &saveptr); - if (user == NULL || strcmp (user, username) != 0 - || got_users++ != skipped_users) - goto skip_line; - - passwd = strtok_r (NULL, whitespace, &saveptr); - if (passwd == NULL) - passwd = "-"; - - secret = strtok_r (NULL, whitespace, &saveptr); - if (secret == NULL) - secret = "-"; - - r = fprintf (outfh, "%s\t%s\t%s\t%s\t%llu\t%s\t%s\n", - type, username, passwd, secret, - (unsigned long long) new_moving_factor, otp, timestamp); - free (origline); - if (r <= 0) - return OATH_PRINTF_ERROR; - continue; - - skip_line: - r = fprintf (outfh, "%s", origline); - free (origline); - if (r <= 0) - return OATH_PRINTF_ERROR; - continue; - } + // token type algorithm is TOTP + // but the current line doesn't contain a "log" + // it's the first OTP the user supply - return OATH_OK; -} - -static int -update_usersfile (const char *usersfile, - const char *username, - const char *otp, - FILE * infh, - char **lineptr, - size_t * n, char *timestamp, uint64_t new_moving_factor, - size_t skipped_users) -{ - FILE *outfh, *lockfh; - int rc; - char *newfilename, *lockfile; - - /* Rewind input file. */ - { - int pos; - - pos = fseeko (infh, 0L, SEEK_SET); - if (pos == -1) - return OATH_FILE_SEEK_ERROR; - clearerr (infh); - } - - /* Open lockfile. */ - { - int l; + // check if the suppied OTP is valid + rc = oath_totp_validate (secret, secret_length, + time (NULL), totpstepsize, 0, window, otp); + } - l = asprintf (&lockfile, "%s.lock", usersfile); - if (lockfile == NULL || ((size_t) l) != strlen (usersfile) + 5) - return OATH_PRINTF_ERROR; + if (rc == OATH_INVALID_OTP) + { + // the supplied otp doesn't match the current line - lockfh = fopen (lockfile, "w"); - if (!lockfh) - { - free (lockfile); - return OATH_FILE_CREATE_ERROR; - } - } + // we continue because we can have multiple secret key for the + // same pair + rc = OATH_OK; + continue; + } - /* Lock the lockfile. */ - { - struct flock l; + if (rc < 0) + // there were an error other than OATH_INVALID_OTP + break; - memset (&l, 0, sizeof (l)); - l.l_whence = SEEK_SET; - l.l_start = 0; - l.l_len = 0; - l.l_type = F_WRLCK; + // OTP is valide - while ((rc = fcntl (fileno (lockfh), F_SETLKW, &l)) < 0 && errno == EINTR) - continue; - if (rc == -1) - { - fclose (lockfh); - free (lockfile); - return OATH_FILE_LOCK_ERROR; - } - } + // compute the current timestamp + char timestamp_buffer[TIME_BUFFER_SIZE]; + rc = compute_timestamp (timestamp_buffer); - /* Open the "new" file. */ - { - int l; + if (rc != OATH_OK) + // timestamp can't be generated + break; - l = asprintf (&newfilename, "%s.new", usersfile); - if (newfilename == NULL || ((size_t) l) != strlen (usersfile) + 4) - { - fclose (lockfh); - free (lockfile); - return OATH_PRINTF_ERROR; - } + // compute the new moving factor + unsigned long long new_moving_factor = start_moving_factor + rc; - outfh = fopen (newfilename, "w"); - if (!outfh) - { - free (newfilename); - fclose (lockfh); - free (lockfile); - return OATH_FILE_CREATE_ERROR; - } - } + // record the new "log" in new_log_buffer + rc = + snprintf (new_log_buffer, BUFFER_SIZE, "\t%llu\t%s\t%s\n", + new_moving_factor, otp, timestamp_buffer); - /* Create the new usersfile content. */ - rc = update_usersfile2 (username, otp, infh, outfh, lineptr, n, - timestamp, new_moving_factor, skipped_users); + if (rc < 0) + { + rc = OATH_PRINTF_ERROR; + break; + } - /* On success, flush the buffers. */ - if (rc == OATH_OK && fflush (outfh) != 0) - rc = OATH_FILE_FLUSH_ERROR; + // save the file position of the start and end + // of the old "log" + *old_log_end = ftello (usersfile_fd); + *old_log_start = *old_log_end - old_log_size; - /* On success, sync the disks. */ - if (rc == OATH_OK && fsync (fileno (outfh)) != 0) - rc = OATH_FILE_SYNC_ERROR; + if (*old_log_end == -1) + { + rc = OATH_FILE_TELL_ERROR; + break; + } - /* Close the file regardless of success. */ - if (fclose (outfh) != 0) - rc = OATH_FILE_CLOSE_ERROR; + free (line_buffer); + return OATH_OK; + } - /* On success, overwrite the usersfile with the new copy. */ - if (rc == OATH_OK && rename (newfilename, usersfile) != 0) - rc = OATH_FILE_RENAME_ERROR; + free (line_buffer); - /* Something has failed, don't leave garbage lying around. */ if (rc != OATH_OK) - unlink (newfilename); + return rc; - free (newfilename); + // the usersfile was parse entirely without error + // but no matching OTP have been found - /* Complete, close the lockfile */ - if (fclose (lockfh) != 0) - rc = OATH_FILE_CLOSE_ERROR; - if (unlink (lockfile) != 0) - rc = OATH_FILE_UNLINK_ERROR; - free (lockfile); + if (matching_user_and_passwd_line) + // there were line(s) matchine username and password + return OATH_INVALID_OTP; + else if (matching_user_line) + // there were line(s) matchine username (but not password) + return OATH_BAD_PASSWORD; - return rc; + // there were no line matchine the username + return OATH_UNKNOWN_USER; } /** @@ -420,7 +455,7 @@ update_usersfile (const char *usersfile, * @otp: string with one-time password to authenticate * @window: how many past/future OTPs to search * @passwd: string with password, or NULL to disable password checking - * @last_otp: output variable holding last successful authentication + * @last_otp_timestamp: output variable holding last successful authentication timestamp, or NULL to not record this value * * Authenticate user named @username with the one-time password @otp * and (optional) password @passwd. Credentials are read (and @@ -434,7 +469,7 @@ update_usersfile (const char *usersfile, * Returns: On successful validation, %OATH_OK is returned. If the * supplied @otp is the same as the last successfully authenticated * one-time password, %OATH_REPLAYED_OTP is returned and the - * timestamp of the last authentication is returned in @last_otp. + * timestamp of the last authentication is returned in @last_otp_timestamp. * If the one-time password is not found in the indicated search * window, %OATH_INVALID_OTP is returned. Otherwise, an error code * is returned. @@ -444,52 +479,128 @@ oath_authenticate_usersfile (const char *usersfile, const char *username, const char *otp, size_t window, - const char *passwd, time_t * last_otp) + const char *passwd, time_t * last_otp_timestamp) { - FILE *infh; - char *line = NULL; - size_t n = 0; - uint64_t new_moving_factor; - int rc; - size_t skipped_users; - - infh = fopen (usersfile, "r"); - if (!infh) - return OATH_NO_SUCH_FILE; - - rc = parse_usersfile (username, otp, window, passwd, last_otp, - infh, &line, &n, &new_moving_factor, &skipped_users); - - if (rc == OATH_OK) - { - char timestamp[30]; - size_t max = sizeof (timestamp); - struct tm now; - time_t t; - size_t l; - mode_t old_umask; - - if (time (&t) == (time_t) - 1) - return OATH_TIME_ERROR; - - if (localtime_r (&t, &now) == NULL) - return OATH_TIME_ERROR; - - l = strftime (timestamp, max, TIME_FORMAT_STRING, &now); - if (l != 20) - return OATH_TIME_ERROR; - - old_umask = umask (~(S_IRUSR | S_IWUSR)); - - rc = update_usersfile (usersfile, username, otp, infh, - &line, &n, timestamp, new_moving_factor, - skipped_users); - - umask (old_umask); - } - - free (line); - fclose (infh); + int rc = OATH_OK; + // temporary variable to check various syscall output for error + int syscall_output = 0; + + // open usersfile + FILE *usersfile_fd; + usersfile_fd = fopen (usersfile, "r+"); + IF_ERROR_GOTO (usersfile_fd == NULL, OATH_FILE_OPEN_ERROR, end); + + { // this context was created to avoid goto crosses variable declaration + + // put a read lock on usersfile + struct flock lock; + memset (&lock, 0, sizeof (lock)); + lock.l_whence = SEEK_SET; + lock.l_start = 0; + lock.l_len = 0; + lock.l_type = F_RDLCK; + syscall_output = fcntl (fileno (usersfile_fd), F_SETLKW, &lock); + IF_ERROR_GOTO (syscall_output == -1, OATH_FILE_LOCK_ERROR, close_end); + + // parse usersfile and check if the supplied otp is valide + off_t old_log_start, old_log_end; + char new_log_buffer[BUFFER_SIZE]; + rc = + parse_usersfile (username, otp, window, passwd, usersfile_fd, + last_otp_timestamp, &old_log_start, &old_log_end, + new_log_buffer); + if (rc != OATH_OK) + // supplied otp is not valide or + // the were error during usersfile the parsing + goto close_end; + + // the otp is valide + // now we have to record the new "log" in usersfile + + // put a write lock on usersfile + lock.l_type = F_WRLCK; + syscall_output = fcntl (fileno (usersfile_fd), F_SETLKW, &lock); + IF_ERROR_GOTO (syscall_output == -1, OATH_FILE_LOCK_ERROR, close_end); + + size_t new_log_len = strlen (new_log_buffer); + if ((size_t) (old_log_end - old_log_start) == new_log_len) + { + // the new and old "log" have the same size + // we write directly the new log in the usersfile inplace + // of the old "log" + + // go the the start possition of the old "log" + syscall_output = fseeko (usersfile_fd, old_log_start, SEEK_SET); + IF_ERROR_GOTO (syscall_output == -1, OATH_FILE_SEEK_ERROR, close_end); + + // write the new "log" + fwrite (new_log_buffer, sizeof (char), strlen (new_log_buffer), + usersfile_fd); + IF_ERROR_GOTO (ferror (usersfile_fd), OATH_FILE_WRITE_ERROR, + close_end); + } + else + { + // the new and old "log" doesn't have the same size + // we will load in memory the end of the usersfile from the end of the old "log" to the end of the file + // write the new "log" from the starting point of the old "log" + // and append the end of the usersfile previously loaded in memory + + // compute the amont of memory we will need + syscall_output = fseeko (usersfile_fd, 0, SEEK_END); + IF_ERROR_GOTO (syscall_output == -1, OATH_FILE_SEEK_ERROR, close_end); + + off_t usersfile_buffer_size = ftello (usersfile_fd) - old_log_end; + IF_ERROR_GOTO (usersfile_buffer_size < 0, OATH_FILE_TELL_ERROR, + close_end); + + // allocate the dynamic memory + char *usersfile_buffer = + malloc (usersfile_buffer_size * sizeof (char)); + IF_ERROR_GOTO (usersfile_buffer == NULL, OATH_MALLOC_ERROR, + close_end); + + // load usersfile from the end of the old "log" to the end of the file + syscall_output = fseeko (usersfile_fd, old_log_end, SEEK_SET); + IF_ERROR_GOTO (syscall_output == -1, OATH_FILE_SEEK_ERROR, + free_close_end); + + syscall_output = + fread (usersfile_buffer, sizeof (char), usersfile_buffer_size, + usersfile_fd); + IF_ERROR_GOTO (syscall_output != usersfile_buffer_size, + OATH_FILE_READ_ERROR, free_close_end); + + // write the new log in the usersfile from the start of the old "log" + syscall_output = fseeko (usersfile_fd, old_log_start, SEEK_SET); + IF_ERROR_GOTO (syscall_output == -1, OATH_FILE_SEEK_ERROR, + free_close_end); + + fwrite (new_log_buffer, sizeof (char), strlen (new_log_buffer), + usersfile_fd); + IF_ERROR_GOTO (ferror (usersfile_fd), OATH_FILE_WRITE_ERROR, + free_close_end); + + // write the end of the usersfile previously loaded in memory + fwrite (usersfile_buffer, sizeof (char), usersfile_buffer_size, + usersfile_fd); + IF_ERROR_GOTO (ferror (usersfile_fd), OATH_FILE_WRITE_ERROR, + free_close_end); + + // in case the old log was longer than the new one we truncate the end of the file + syscall_output = + ftruncate (fileno (usersfile_fd), ftello (usersfile_fd)); + IF_ERROR_GOTO (syscall_output == -1, OATH_FILE_TRUNCATE_ERROR, + free_close_end); + + free_close_end: + free (usersfile_buffer); + } + } +close_end: + // close the usersfile and remove the lock + fclose (usersfile_fd); +end: return rc; } diff --git a/pam_oath/README b/pam_oath/README index 5fc534b..639cd73 100644 --- a/pam_oath/README +++ b/pam_oath/README @@ -34,7 +34,7 @@ file: --------- # cat>/etc/users.oath HOTP root - 00 -# chmod go-rw /etc/users.oath +# chmod 600 /etc/users.oath # --------- -- 2.2.1