File: [local] / src / lib / libskey / skeylogin.c (download)
Revision 1.54, Tue Mar 20 03:40:06 2007 UTC (17 years, 2 months ago) by tedu
Branch: MAIN
CVS Tags: OPENBSD_5_4_BASE, OPENBSD_5_4, OPENBSD_5_3_BASE, OPENBSD_5_3, OPENBSD_5_2_BASE, OPENBSD_5_2, OPENBSD_5_1_BASE, OPENBSD_5_1, OPENBSD_5_0_BASE, OPENBSD_5_0, OPENBSD_4_9_BASE, OPENBSD_4_9, OPENBSD_4_8_BASE, OPENBSD_4_8, OPENBSD_4_7_BASE, OPENBSD_4_7, OPENBSD_4_6_BASE, OPENBSD_4_6, OPENBSD_4_5_BASE, OPENBSD_4_5, OPENBSD_4_4_BASE, OPENBSD_4_4, OPENBSD_4_3_BASE, OPENBSD_4_3, OPENBSD_4_2_BASE, OPENBSD_4_2 Changes since 1.53: +2 -2 lines
remove some bogus *p tests from charles longeau
ok deraadt millert
|
/* OpenBSD S/Key (skeylogin.c)
*
* Authors:
* Neil M. Haller <nmh@thumper.bellcore.com>
* Philip R. Karn <karn@chicago.qualcomm.com>
* John S. Walden <jsw@thumper.bellcore.com>
* Scott Chasin <chasin@crimelab.com>
* Todd C. Miller <Todd.Miller@courtesan.com>
* Angelos D. Keromytis <adk@adk.gr>
*
* S/Key verification check, lookups, and authentication.
*
* $OpenBSD: skeylogin.c,v 1.54 2007/03/20 03:40:06 tedu Exp $
*/
#include <sys/param.h>
#ifdef QUOTA
#include <sys/quota.h>
#endif
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <ctype.h>
#include <err.h>
#include <errno.h>
#include <fcntl.h>
#include <paths.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include <sha1.h>
#include "skey.h"
static void skey_fakeprompt(char *, char *);
static char *tgetline(int, char *, size_t, int);
static int skeygetent(int, struct skey *, const char *);
/*
* Return an skey challenge string for user 'name'. If successful,
* fill in the caller's skey structure and return (0). If unsuccessful
* (e.g., if name is unknown) return (-1).
*
* The file read/write pointer is left at the start of the record.
*/
int
skeychallenge2(int fd, struct skey *mp, char *name, char *ss)
{
int rval;
memset(mp, 0, sizeof(*mp));
rval = skeygetent(fd, mp, name);
switch (rval) {
case 0: /* Lookup succeeded, return challenge */
(void)snprintf(ss, SKEY_MAX_CHALLENGE,
"otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
skey_get_algorithm(), mp->n - 1,
SKEY_MAX_SEED_LEN, mp->seed);
return (0);
case 1: /* User not found */
if (mp->keyfile) {
(void)fclose(mp->keyfile);
mp->keyfile = NULL;
}
/* FALLTHROUGH */
default: /* File error */
skey_fakeprompt(name, ss);
return (-1);
}
}
int
skeychallenge(struct skey *mp, char *name, char *ss)
{
return (skeychallenge2(-1, mp, name, ss));
}
/*
* Get an entry in the One-time Password database and lock it.
*
* Return codes:
* -1: error in opening database or unable to lock entry
* 0: entry found, file R/W pointer positioned at beginning of record
* 1: entry not found
*/
static int
skeygetent(int fd, struct skey *mp, const char *name)
{
char *cp, filename[PATH_MAX], *last;
struct stat statbuf;
size_t nread;
FILE *keyfile;
/* Check to see that /etc/skey has not been disabled. */
if (stat(_PATH_SKEYDIR, &statbuf) != 0)
return (-1);
if ((statbuf.st_mode & ALLPERMS) == 0) {
errno = EPERM;
return (-1);
}
if (fd == -1) {
/* Open the user's databse entry, creating it as needed. */
if (snprintf(filename, sizeof(filename), "%s/%s", _PATH_SKEYDIR,
name) >= sizeof(filename)) {
errno = ENAMETOOLONG;
return (-1);
}
if ((fd = open(filename, O_RDWR | O_NOFOLLOW | O_NONBLOCK,
S_IRUSR | S_IWUSR)) == -1) {
if (errno == ENOENT)
goto not_found;
return (-1);
}
}
/* Lock and stat the user's skey file. */
if (flock(fd, LOCK_EX) != 0 || fstat(fd, &statbuf) != 0) {
close(fd);
return (-1);
}
if (statbuf.st_size == 0)
goto not_found;
/* Sanity checks. */
if ((statbuf.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR) ||
!S_ISREG(statbuf.st_mode) || statbuf.st_nlink != 1 ||
(keyfile = fdopen(fd, "r+")) == NULL) {
close(fd);
return (-1);
}
/* At this point, we are committed. */
mp->keyfile = keyfile;
if ((nread = fread(mp->buf, 1, sizeof(mp->buf), keyfile)) == 0 ||
!isspace(mp->buf[nread - 1]))
goto bad_keyfile;
mp->buf[nread - 1] = '\0';
if ((mp->logname = strtok_r(mp->buf, " \t\n\r", &last)) == NULL ||
strcmp(mp->logname, name) != 0)
goto bad_keyfile;
if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL)
goto bad_keyfile;
if (skey_set_algorithm(cp) == NULL)
goto bad_keyfile;
if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL)
goto bad_keyfile;
mp->n = atoi(cp); /* XXX - use strtol() */
if ((mp->seed = strtok_r(NULL, " \t\n\r", &last)) == NULL)
goto bad_keyfile;
if ((mp->val = strtok_r(NULL, " \t\n\r", &last)) == NULL)
goto bad_keyfile;
(void)fseek(keyfile, 0L, SEEK_SET);
return (0);
bad_keyfile:
fclose(keyfile);
return (-1);
not_found:
/* No existing entry, fill in what we can and return */
memset(mp, 0, sizeof(*mp));
strlcpy(mp->buf, name, sizeof(mp->buf));
mp->logname = mp->buf;
if (fd != -1)
close(fd);
return (1);
}
/*
* Look up an entry in the One-time Password database and lock it.
* Zeroes out the passed in struct skey before using it.
*
* Return codes:
* -1: error in opening database or unable to lock entry
* 0: entry found, file R/W pointer positioned at beginning of record
* 1: entry not found
*/
int
skeylookup(struct skey *mp, char *name)
{
memset(mp, 0, sizeof(*mp));
return (skeygetent(-1, mp, name));
}
/*
* Get the next entry in the One-time Password database.
*
* Return codes:
* -1: error in opening database
* 0: next entry found and stored in mp
* 1: no more entries, keydir is closed.
*/
int
skeygetnext(struct skey *mp)
{
struct dirent entry, *dp;
int rval;
if (mp->keyfile != NULL) {
fclose(mp->keyfile);
mp->keyfile = NULL;
}
/* Open _PATH_SKEYDIR if it exists, else return an error */
if (mp->keydir == NULL && (mp->keydir = opendir(_PATH_SKEYDIR)) == NULL)
return (-1);
rval = 1;
while ((readdir_r(mp->keydir, &entry, &dp)) == 0 && dp == &entry) {
/* Skip dot files and zero-length files. */
if (entry.d_name[0] != '.' &&
(rval = skeygetent(-1, mp, entry.d_name)) != 1)
break;
}
if (dp == NULL) {
closedir(mp->keydir);
mp->keydir = NULL;
}
return (rval);
}
/*
* Verify response to a S/Key challenge.
*
* Return codes:
* -1: Error of some sort; database unchanged
* 0: Verify successful, database updated
* 1: Verify failed, database unchanged
*
* The database file is always closed by this call.
*/
int
skeyverify(struct skey *mp, char *response)
{
char key[SKEY_BINKEY_SIZE], fkey[SKEY_BINKEY_SIZE];
char filekey[SKEY_BINKEY_SIZE], *cp, *last;
size_t nread;
if (response == NULL)
goto verify_failure;
/*
* The record should already be locked but lock it again
* just to be safe. We don't wait for the lock to become
* available since we should already have it...
*/
if (flock(fileno(mp->keyfile), LOCK_EX | LOCK_NB) != 0)
goto verify_failure;
/* Convert response to binary */
rip(response);
if (etob(key, response) != 1 && atob8(key, response) != 0)
goto verify_failure; /* Neither english words nor ascii hex */
/* Compute fkey = f(key) */
(void)memcpy(fkey, key, sizeof(key));
f(fkey);
/*
* Reread the file record NOW in case it has been modified.
* The only field we really need to worry about is mp->val.
*/
(void)fseek(mp->keyfile, 0L, SEEK_SET);
if ((nread = fread(mp->buf, 1, sizeof(mp->buf), mp->keyfile)) == 0 ||
!isspace(mp->buf[nread - 1]))
goto verify_failure;
if ((mp->logname = strtok_r(mp->buf, " \t\r\n", &last)) == NULL)
goto verify_failure;
if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL)
goto verify_failure;
if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL)
goto verify_failure;
if ((mp->seed = strtok_r(NULL, " \t\r\n", &last)) == NULL)
goto verify_failure;
if ((mp->val = strtok_r(NULL, " \t\r\n", &last)) == NULL)
goto verify_failure;
/* Convert file value to hex and compare. */
atob8(filekey, mp->val);
if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0)
goto verify_failure; /* Wrong response */
/*
* Update key in database.
* XXX - check return values of things that write to disk.
*/
btoa8(mp->val,key);
mp->n--;
(void)fseek(mp->keyfile, 0L, SEEK_SET);
(void)fprintf(mp->keyfile, "%s\n%s\n%d\n%s\n%s\n", mp->logname,
skey_get_algorithm(), mp->n, mp->seed, mp->val);
(void)fflush(mp->keyfile);
(void)ftruncate(fileno(mp->keyfile), ftello(mp->keyfile));
(void)fclose(mp->keyfile);
mp->keyfile = NULL;
return (0);
verify_failure:
(void)fclose(mp->keyfile);
mp->keyfile = NULL;
return (-1);
}
/*
* skey_haskey()
*
* Returns: 1 user doesn't exist, -1 file error, 0 user exists.
*
*/
int
skey_haskey(char *username)
{
struct skey skey;
int i;
i = skeylookup(&skey, username);
if (skey.keyfile != NULL) {
fclose(skey.keyfile);
skey.keyfile = NULL;
}
return (i);
}
/*
* skey_keyinfo()
*
* Returns the current sequence number and
* seed for the passed user.
*
*/
char *
skey_keyinfo(char *username)
{
static char str[SKEY_MAX_CHALLENGE];
struct skey skey;
int i;
i = skeychallenge(&skey, username, str);
if (i == -1)
return (0);
if (skey.keyfile != NULL) {
fclose(skey.keyfile);
skey.keyfile = NULL;
}
return (str);
}
/*
* skey_passcheck()
*
* Check to see if answer is the correct one to the current
* challenge.
*
* Returns: 0 success, -1 failure
*
*/
int
skey_passcheck(char *username, char *passwd)
{
struct skey skey;
int i;
i = skeylookup(&skey, username);
if (i == -1 || i == 1)
return (-1);
if (skeyverify(&skey, passwd) == 0)
return (skey.n);
return (-1);
}
#define ROUND(x) (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \
((x)[3]))
/*
* hash_collapse()
*/
static u_int32_t
hash_collapse(u_char *s)
{
int len, target;
u_int32_t i;
if ((strlen(s) % sizeof(u_int32_t)) == 0)
target = strlen(s); /* Multiple of 4 */
else
target = strlen(s) - (strlen(s) % sizeof(u_int32_t));
for (i = 0, len = 0; len < target; len += 4)
i ^= ROUND(s + len);
return i;
}
/*
* skey_fakeprompt()
*
* Generate a fake prompt for the specified user.
*
*/
static void
skey_fakeprompt(char *username, char *skeyprompt)
{
char hseed[SKEY_MAX_SEED_LEN], *secret, pbuf[SKEY_MAX_PW_LEN+1], *p, *u;
u_char flg = 1, *up;
size_t secretlen;
SHA1_CTX ctx;
u_int ptr;
int i;
/*
* Base first 4 chars of seed on hostname.
* Add some filler for short hostnames if necessary.
*/
if (gethostname(pbuf, sizeof(pbuf)) == -1)
*(p = pbuf) = '.';
else
for (p = pbuf; isalnum(*p); p++)
if (isalpha(*p) && isupper(*p))
*p = (char)tolower(*p);
if (*p && pbuf - p < 4)
(void)strncpy(p, "asjd", 4 - (pbuf - p));
pbuf[4] = '\0';
/* Hash the username if possible */
if ((up = SHA1Data(username, strlen(username), NULL)) != NULL) {
struct stat sb;
time_t t;
int fd;
/* Collapse the hash */
ptr = hash_collapse(up);
memset(up, 0, strlen(up));
/* See if the random file's there, else use ctime */
if ((fd = open(_SKEY_RAND_FILE_PATH_, O_RDONLY)) != -1 &&
fstat(fd, &sb) == 0 &&
sb.st_size > (off_t)SKEY_MAX_SEED_LEN &&
lseek(fd, ptr % (sb.st_size - SKEY_MAX_SEED_LEN),
SEEK_SET) != -1 && read(fd, hseed,
SKEY_MAX_SEED_LEN) == SKEY_MAX_SEED_LEN) {
close(fd);
fd = -1;
secret = hseed;
secretlen = SKEY_MAX_SEED_LEN;
flg = 0;
} else if (!stat(_PATH_MEM, &sb) || !stat("/", &sb)) {
t = sb.st_ctime;
secret = ctime(&t);
secretlen = strlen(secret);
flg = 0;
}
if (fd != -1)
close(fd);
}
/* Put that in your pipe and smoke it */
if (flg == 0) {
/* Hash secret value with username */
SHA1Init(&ctx);
SHA1Update(&ctx, secret, secretlen);
SHA1Update(&ctx, username, strlen(username));
SHA1End(&ctx, up);
/* Zero out */
memset(secret, 0, secretlen);
/* Now hash the hash */
SHA1Init(&ctx);
SHA1Update(&ctx, up, strlen(up));
SHA1End(&ctx, up);
ptr = hash_collapse(up + 4);
for (i = 4; i < 9; i++) {
pbuf[i] = (ptr % 10) + '0';
ptr /= 10;
}
pbuf[i] = '\0';
/* Sequence number */
ptr = ((up[2] + up[3]) % 99) + 1;
memset(up, 0, 20); /* SHA1 specific */
free(up);
(void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE,
"otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
skey_get_algorithm(), ptr, SKEY_MAX_SEED_LEN, pbuf);
} else {
/* Base last 8 chars of seed on username */
u = username;
i = 8;
p = &pbuf[4];
do {
if (*u == 0) {
/* Pad remainder with zeros */
while (--i >= 0)
*p++ = '0';
break;
}
*p++ = (*u++ % 10) + '0';
} while (--i != 0);
pbuf[12] = '\0';
(void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE,
"otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
skey_get_algorithm(), 99, SKEY_MAX_SEED_LEN, pbuf);
}
}
/*
* skey_authenticate()
*
* Used when calling program will allow input of the user's
* response to the challenge.
*
* Returns: 0 success, -1 failure
*
*/
int
skey_authenticate(char *username)
{
char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1];
struct skey skey;
int i;
/* Get the S/Key challenge (may be fake) */
i = skeychallenge(&skey, username, skeyprompt);
(void)fprintf(stderr, "%s\nResponse: ", skeyprompt);
(void)fflush(stderr);
/* Time out on user input after 2 minutes */
tgetline(fileno(stdin), pbuf, sizeof(pbuf), 120);
sevenbit(pbuf);
(void)rewind(stdin);
/* Is it a valid response? */
if (i == 0 && skeyverify(&skey, pbuf) == 0) {
if (skey.n < 5) {
(void)fprintf(stderr,
"\nWarning! Key initialization needed soon. (%d logins left)\n",
skey.n);
}
return (0);
}
return (-1);
}
/*
* Unlock current entry in the One-time Password database.
*
* Return codes:
* -1: unable to lock the record
* 0: record was successfully unlocked
*/
int
skey_unlock(struct skey *mp)
{
if (mp->logname == NULL || mp->keyfile == NULL)
return (-1);
return (flock(fileno(mp->keyfile), LOCK_UN));
}
/*
* Get a line of input (optionally timing out) and place it in buf.
*/
static char *
tgetline(int fd, char *buf, size_t bufsiz, int timeout)
{
struct pollfd pfd[1];
size_t left;
char c, *cp;
ssize_t ss;
int n;
if (bufsiz == 0)
return (NULL); /* sanity */
cp = buf;
left = bufsiz;
/*
* Timeout of <= 0 means no timeout.
*/
if (timeout > 0) {
timeout *= 1000; /* convert to miliseconds */
pfd[0].fd = fd;
pfd[0].events = POLLIN;
while (--left) {
/* Poll until we are ready or we time out */
while ((n = poll(pfd, 1, timeout)) == -1 &&
(errno == EINTR || errno == EAGAIN))
;
if (n <= 0 ||
(pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)))
break; /* timeout or error */
/* Read a character, exit loop on error, EOF or EOL */
ss = read(fd, &c, 1);
if (ss != 1 || c == '\n' || c == '\r')
break;
*cp++ = c;
}
} else {
/* Keep reading until out of space, EOF, error, or newline */
while (--left && read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
*cp++ = c;
}
*cp = '\0';
return (cp == buf ? NULL : buf);
}