xref: /openbsd/lib/libskey/skeylogin.c (revision aea60bee)
1 /* OpenBSD S/Key (skeylogin.c)
2  *
3  * Authors:
4  *          Neil M. Haller <nmh@thumper.bellcore.com>
5  *          Philip R. Karn <karn@chicago.qualcomm.com>
6  *          John S. Walden <jsw@thumper.bellcore.com>
7  *          Scott Chasin <chasin@crimelab.com>
8  *          Todd C. Miller <Todd.Miller@courtesan.com>
9  *	    Angelos D. Keromytis <adk@adk.gr>
10  *
11  * S/Key verification check, lookups, and authentication.
12  *
13  * $OpenBSD: skeylogin.c,v 1.56 2015/01/16 16:48:52 deraadt Exp $
14  */
15 
16 #ifdef	QUOTA
17 #include <sys/quota.h>
18 #endif
19 #include <sys/stat.h>
20 #include <sys/time.h>
21 #include <sys/resource.h>
22 
23 #include <ctype.h>
24 #include <err.h>
25 #include <errno.h>
26 #include <fcntl.h>
27 #include <paths.h>
28 #include <poll.h>
29 #include <stdio.h>
30 #include <stdlib.h>
31 #include <string.h>
32 #include <time.h>
33 #include <unistd.h>
34 #include <limits.h>
35 #include <sha1.h>
36 
37 #include "skey.h"
38 
39 static void skey_fakeprompt(char *, char *);
40 static char *tgetline(int, char *, size_t, int);
41 static int skeygetent(int, struct skey *, const char *);
42 
43 /*
44  * Return an skey challenge string for user 'name'. If successful,
45  * fill in the caller's skey structure and return (0). If unsuccessful
46  * (e.g., if name is unknown) return (-1).
47  *
48  * The file read/write pointer is left at the start of the record.
49  */
50 int
51 skeychallenge2(int fd, struct skey *mp, char *name, char *ss)
52 {
53 	int rval;
54 
55 	memset(mp, 0, sizeof(*mp));
56 	rval = skeygetent(fd, mp, name);
57 
58 	switch (rval) {
59 	case 0:		/* Lookup succeeded, return challenge */
60 		(void)snprintf(ss, SKEY_MAX_CHALLENGE,
61 		    "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
62 		    skey_get_algorithm(), mp->n - 1,
63 		    SKEY_MAX_SEED_LEN, mp->seed);
64 		return (0);
65 
66 	case 1:		/* User not found */
67 		if (mp->keyfile) {
68 			(void)fclose(mp->keyfile);
69 			mp->keyfile = NULL;
70 		}
71 		/* FALLTHROUGH */
72 
73 	default:	/* File error */
74 		skey_fakeprompt(name, ss);
75 		return (-1);
76 	}
77 }
78 
79 int
80 skeychallenge(struct skey *mp, char *name, char *ss)
81 {
82 	return (skeychallenge2(-1, mp, name, ss));
83 }
84 
85 /*
86  * Get an entry in the One-time Password database and lock it.
87  *
88  * Return codes:
89  * -1: error in opening database or unable to lock entry
90  *  0: entry found, file R/W pointer positioned at beginning of record
91  *  1: entry not found
92  */
93 static int
94 skeygetent(int fd, struct skey *mp, const char *name)
95 {
96 	char *cp, filename[PATH_MAX], *last;
97 	struct stat statbuf;
98 	size_t nread;
99 	FILE *keyfile;
100 
101 	/* Check to see that /etc/skey has not been disabled. */
102 	if (stat(_PATH_SKEYDIR, &statbuf) != 0)
103 		return (-1);
104 	if ((statbuf.st_mode & ALLPERMS) == 0) {
105 		errno = EPERM;
106 		return (-1);
107 	}
108 
109 	if (fd == -1) {
110 		/* Open the user's databse entry, creating it as needed. */
111 		if (snprintf(filename, sizeof(filename), "%s/%s", _PATH_SKEYDIR,
112 		    name) >= sizeof(filename)) {
113 			errno = ENAMETOOLONG;
114 			return (-1);
115 		}
116 		if ((fd = open(filename, O_RDWR | O_NOFOLLOW | O_NONBLOCK,
117 		    S_IRUSR | S_IWUSR)) == -1) {
118 			if (errno == ENOENT)
119 				goto not_found;
120 			return (-1);
121 		}
122 	}
123 
124 	/* Lock and stat the user's skey file. */
125 	if (flock(fd, LOCK_EX) != 0 || fstat(fd, &statbuf) != 0) {
126 		close(fd);
127 		return (-1);
128 	}
129 	if (statbuf.st_size == 0)
130 		goto not_found;
131 
132 	/* Sanity checks. */
133 	if ((statbuf.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR) ||
134 	    !S_ISREG(statbuf.st_mode) || statbuf.st_nlink != 1 ||
135 	    (keyfile = fdopen(fd, "r+")) == NULL) {
136 		close(fd);
137 		return (-1);
138 	}
139 
140 	/* At this point, we are committed. */
141 	mp->keyfile = keyfile;
142 
143 	if ((nread = fread(mp->buf, 1, sizeof(mp->buf), keyfile)) == 0 ||
144 	    !isspace((unsigned char)mp->buf[nread - 1]))
145 		goto bad_keyfile;
146 	mp->buf[nread - 1] = '\0';
147 
148 	if ((mp->logname = strtok_r(mp->buf, " \t\n\r", &last)) == NULL ||
149 	    strcmp(mp->logname, name) != 0)
150 		goto bad_keyfile;
151 	if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL)
152 		goto bad_keyfile;
153 	if (skey_set_algorithm(cp) == NULL)
154 		goto bad_keyfile;
155 	if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL)
156 		goto bad_keyfile;
157 	mp->n = atoi(cp);	/* XXX - use strtol() */
158 	if ((mp->seed = strtok_r(NULL, " \t\n\r", &last)) == NULL)
159 		goto bad_keyfile;
160 	if ((mp->val = strtok_r(NULL, " \t\n\r", &last)) == NULL)
161 		goto bad_keyfile;
162 
163 	(void)fseek(keyfile, 0L, SEEK_SET);
164 	return (0);
165 
166     bad_keyfile:
167 	fclose(keyfile);
168 	return (-1);
169 
170     not_found:
171 	/* No existing entry, fill in what we can and return */
172 	memset(mp, 0, sizeof(*mp));
173 	strlcpy(mp->buf, name, sizeof(mp->buf));
174 	mp->logname = mp->buf;
175 	if (fd != -1)
176 		close(fd);
177 	return (1);
178 }
179 
180 /*
181  * Look up an entry in the One-time Password database and lock it.
182  * Zeroes out the passed in struct skey before using it.
183  *
184  * Return codes:
185  * -1: error in opening database or unable to lock entry
186  *  0: entry found, file R/W pointer positioned at beginning of record
187  *  1: entry not found
188  */
189 int
190 skeylookup(struct skey *mp, char *name)
191 {
192 	memset(mp, 0, sizeof(*mp));
193 	return (skeygetent(-1, mp, name));
194 }
195 
196 /*
197  * Get the next entry in the One-time Password database.
198  *
199  * Return codes:
200  * -1: error in opening database
201  *  0: next entry found and stored in mp
202  *  1: no more entries, keydir is closed.
203  */
204 int
205 skeygetnext(struct skey *mp)
206 {
207 	struct dirent entry, *dp;
208 	int rval;
209 
210 	if (mp->keyfile != NULL) {
211 		fclose(mp->keyfile);
212 		mp->keyfile = NULL;
213 	}
214 
215 	/* Open _PATH_SKEYDIR if it exists, else return an error */
216 	if (mp->keydir == NULL && (mp->keydir = opendir(_PATH_SKEYDIR)) == NULL)
217 		return (-1);
218 
219 	rval = 1;
220 	while ((readdir_r(mp->keydir, &entry, &dp)) == 0 && dp == &entry) {
221 		/* Skip dot files and zero-length files. */
222 		if (entry.d_name[0] != '.' &&
223 		    (rval = skeygetent(-1, mp, entry.d_name)) != 1)
224 			break;
225 	}
226 
227 	if (dp == NULL) {
228 		closedir(mp->keydir);
229 		mp->keydir = NULL;
230 	}
231 
232 	return (rval);
233 }
234 
235 /*
236  * Verify response to a S/Key challenge.
237  *
238  * Return codes:
239  * -1: Error of some sort; database unchanged
240  *  0:  Verify successful, database updated
241  *  1:  Verify failed, database unchanged
242  *
243  * The database file is always closed by this call.
244  */
245 int
246 skeyverify(struct skey *mp, char *response)
247 {
248 	char key[SKEY_BINKEY_SIZE], fkey[SKEY_BINKEY_SIZE];
249 	char filekey[SKEY_BINKEY_SIZE], *cp, *last;
250 	size_t nread;
251 
252 	if (response == NULL)
253 		goto verify_failure;
254 
255 	/*
256 	 * The record should already be locked but lock it again
257 	 * just to be safe.  We don't wait for the lock to become
258 	 * available since we should already have it...
259 	 */
260 	if (flock(fileno(mp->keyfile), LOCK_EX | LOCK_NB) != 0)
261 		goto verify_failure;
262 
263 	/* Convert response to binary */
264 	rip(response);
265 	if (etob(key, response) != 1 && atob8(key, response) != 0)
266 		goto verify_failure; /* Neither english words nor ascii hex */
267 
268 	/* Compute fkey = f(key) */
269 	(void)memcpy(fkey, key, sizeof(key));
270 	f(fkey);
271 
272 	/*
273 	 * Reread the file record NOW in case it has been modified.
274 	 * The only field we really need to worry about is mp->val.
275 	 */
276 	(void)fseek(mp->keyfile, 0L, SEEK_SET);
277 	if ((nread = fread(mp->buf, 1, sizeof(mp->buf), mp->keyfile)) == 0 ||
278 	    !isspace((unsigned char)mp->buf[nread - 1]))
279 		goto verify_failure;
280 	if ((mp->logname = strtok_r(mp->buf, " \t\r\n", &last)) == NULL)
281 		goto verify_failure;
282 	if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL)
283 		goto verify_failure;
284 	if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL)
285 		goto verify_failure;
286 	if ((mp->seed = strtok_r(NULL, " \t\r\n", &last)) == NULL)
287 		goto verify_failure;
288 	if ((mp->val = strtok_r(NULL, " \t\r\n", &last)) == NULL)
289 		goto verify_failure;
290 
291 	/* Convert file value to hex and compare. */
292 	atob8(filekey, mp->val);
293 	if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0)
294 		goto verify_failure;	/* Wrong response */
295 
296 	/*
297 	 * Update key in database.
298 	 * XXX - check return values of things that write to disk.
299 	 */
300 	btoa8(mp->val,key);
301 	mp->n--;
302 	(void)fseek(mp->keyfile, 0L, SEEK_SET);
303 	(void)fprintf(mp->keyfile, "%s\n%s\n%d\n%s\n%s\n", mp->logname,
304 	    skey_get_algorithm(), mp->n, mp->seed, mp->val);
305 	(void)fflush(mp->keyfile);
306 	(void)ftruncate(fileno(mp->keyfile), ftello(mp->keyfile));
307 	(void)fclose(mp->keyfile);
308 	mp->keyfile = NULL;
309 	return (0);
310 
311     verify_failure:
312 	(void)fclose(mp->keyfile);
313 	mp->keyfile = NULL;
314 	return (-1);
315 }
316 
317 /*
318  * skey_haskey()
319  *
320  * Returns: 1 user doesn't exist, -1 file error, 0 user exists.
321  *
322  */
323 int
324 skey_haskey(char *username)
325 {
326 	struct skey skey;
327 	int i;
328 
329 	i = skeylookup(&skey, username);
330 	if (skey.keyfile != NULL) {
331 		fclose(skey.keyfile);
332 		skey.keyfile = NULL;
333 	}
334 	return (i);
335 }
336 
337 /*
338  * skey_keyinfo()
339  *
340  * Returns the current sequence number and
341  * seed for the passed user.
342  *
343  */
344 char *
345 skey_keyinfo(char *username)
346 {
347 	static char str[SKEY_MAX_CHALLENGE];
348 	struct skey skey;
349 	int i;
350 
351 	i = skeychallenge(&skey, username, str);
352 	if (i == -1)
353 		return (0);
354 
355 	if (skey.keyfile != NULL) {
356 		fclose(skey.keyfile);
357 		skey.keyfile = NULL;
358 	}
359 	return (str);
360 }
361 
362 /*
363  * skey_passcheck()
364  *
365  * Check to see if answer is the correct one to the current
366  * challenge.
367  *
368  * Returns: 0 success, -1 failure
369  *
370  */
371 int
372 skey_passcheck(char *username, char *passwd)
373 {
374 	struct skey skey;
375 	int i;
376 
377 	i = skeylookup(&skey, username);
378 	if (i == -1 || i == 1)
379 		return (-1);
380 
381 	if (skeyverify(&skey, passwd) == 0)
382 		return (skey.n);
383 
384 	return (-1);
385 }
386 
387 #define ROUND(x)   (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \
388 		    ((x)[3]))
389 
390 /*
391  * hash_collapse()
392  */
393 static u_int32_t
394 hash_collapse(u_char *s)
395 {
396 	int len, target;
397 	u_int32_t i;
398 
399 	if ((strlen(s) % sizeof(u_int32_t)) == 0)
400 		target = strlen(s);    /* Multiple of 4 */
401 	else
402 		target = strlen(s) - (strlen(s) % sizeof(u_int32_t));
403 
404 	for (i = 0, len = 0; len < target; len += 4)
405 		i ^= ROUND(s + len);
406 
407 	return i;
408 }
409 
410 /*
411  * skey_fakeprompt()
412  *
413  * Generate a fake prompt for the specified user.
414  *
415  */
416 static void
417 skey_fakeprompt(char *username, char *skeyprompt)
418 {
419 	char hseed[SKEY_MAX_SEED_LEN], *secret, pbuf[SKEY_MAX_PW_LEN+1], *p, *u;
420 	u_char flg = 1, *up;
421 	size_t secretlen;
422 	SHA1_CTX ctx;
423 	u_int ptr;
424 	int i;
425 
426 	/*
427 	 * Base first 4 chars of seed on hostname.
428 	 * Add some filler for short hostnames if necessary.
429 	 */
430 	if (gethostname(pbuf, sizeof(pbuf)) == -1)
431 		*(p = pbuf) = '.';
432 	else
433 		for (p = pbuf; isalnum((unsigned char)*p); p++)
434 			if (isalpha((unsigned char)*p) &&
435 			    isupper((unsigned char)*p))
436 				*p = (char)tolower((unsigned char)*p);
437 	if (*p && pbuf - p < 4)
438 		(void)strncpy(p, "asjd", 4 - (pbuf - p));
439 	pbuf[4] = '\0';
440 
441 	/* Hash the username if possible */
442 	if ((up = SHA1Data(username, strlen(username), NULL)) != NULL) {
443 		struct stat sb;
444 		time_t t;
445 		int fd;
446 
447 		/* Collapse the hash */
448 		ptr = hash_collapse(up);
449 		memset(up, 0, strlen(up));
450 
451 		/* See if the random file's there, else use ctime */
452 		if ((fd = open(_SKEY_RAND_FILE_PATH_, O_RDONLY)) != -1 &&
453 		    fstat(fd, &sb) == 0 &&
454 		    sb.st_size > (off_t)SKEY_MAX_SEED_LEN &&
455 		    lseek(fd, ptr % (sb.st_size - SKEY_MAX_SEED_LEN),
456 		    SEEK_SET) != -1 && read(fd, hseed,
457 		    SKEY_MAX_SEED_LEN) == SKEY_MAX_SEED_LEN) {
458 			close(fd);
459 			fd = -1;
460 			secret = hseed;
461 			secretlen = SKEY_MAX_SEED_LEN;
462 			flg = 0;
463 		} else if (!stat(_PATH_MEM, &sb) || !stat("/", &sb)) {
464 			t = sb.st_ctime;
465 			secret = ctime(&t);
466 			secretlen = strlen(secret);
467 			flg = 0;
468 		}
469 		if (fd != -1)
470 			close(fd);
471 	}
472 
473 	/* Put that in your pipe and smoke it */
474 	if (flg == 0) {
475 		/* Hash secret value with username */
476 		SHA1Init(&ctx);
477 		SHA1Update(&ctx, secret, secretlen);
478 		SHA1Update(&ctx, username, strlen(username));
479 		SHA1End(&ctx, up);
480 
481 		/* Zero out */
482 		memset(secret, 0, secretlen);
483 
484 		/* Now hash the hash */
485 		SHA1Init(&ctx);
486 		SHA1Update(&ctx, up, strlen(up));
487 		SHA1End(&ctx, up);
488 
489 		ptr = hash_collapse(up + 4);
490 
491 		for (i = 4; i < 9; i++) {
492 			pbuf[i] = (ptr % 10) + '0';
493 			ptr /= 10;
494 		}
495 		pbuf[i] = '\0';
496 
497 		/* Sequence number */
498 		ptr = ((up[2] + up[3]) % 99) + 1;
499 
500 		memset(up, 0, 20); /* SHA1 specific */
501 		free(up);
502 
503 		(void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE,
504 		    "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
505 		    skey_get_algorithm(), ptr, SKEY_MAX_SEED_LEN, pbuf);
506 	} else {
507 		/* Base last 8 chars of seed on username */
508 		u = username;
509 		i = 8;
510 		p = &pbuf[4];
511 		do {
512 			if (*u == 0) {
513 				/* Pad remainder with zeros */
514 				while (--i >= 0)
515 					*p++ = '0';
516 				break;
517 			}
518 
519 			*p++ = (*u++ % 10) + '0';
520 		} while (--i != 0);
521 		pbuf[12] = '\0';
522 
523 		(void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE,
524 		    "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
525 		    skey_get_algorithm(), 99, SKEY_MAX_SEED_LEN, pbuf);
526 	}
527 }
528 
529 /*
530  * skey_authenticate()
531  *
532  * Used when calling program will allow input of the user's
533  * response to the challenge.
534  *
535  * Returns: 0 success, -1 failure
536  *
537  */
538 int
539 skey_authenticate(char *username)
540 {
541 	char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1];
542 	struct skey skey;
543 	int i;
544 
545 	/* Get the S/Key challenge (may be fake) */
546 	i = skeychallenge(&skey, username, skeyprompt);
547 	(void)fprintf(stderr, "%s\nResponse: ", skeyprompt);
548 	(void)fflush(stderr);
549 
550 	/* Time out on user input after 2 minutes */
551 	tgetline(fileno(stdin), pbuf, sizeof(pbuf), 120);
552 	sevenbit(pbuf);
553 	(void)rewind(stdin);
554 
555 	/* Is it a valid response? */
556 	if (i == 0 && skeyverify(&skey, pbuf) == 0) {
557 		if (skey.n < 5) {
558 			(void)fprintf(stderr,
559 			    "\nWarning! Key initialization needed soon.  (%d logins left)\n",
560 			    skey.n);
561 		}
562 		return (0);
563 	}
564 	return (-1);
565 }
566 
567 /*
568  * Unlock current entry in the One-time Password database.
569  *
570  * Return codes:
571  * -1: unable to lock the record
572  *  0: record was successfully unlocked
573  */
574 int
575 skey_unlock(struct skey *mp)
576 {
577 	if (mp->logname == NULL || mp->keyfile == NULL)
578 		return (-1);
579 
580 	return (flock(fileno(mp->keyfile), LOCK_UN));
581 }
582 
583 /*
584  * Get a line of input (optionally timing out) and place it in buf.
585  */
586 static char *
587 tgetline(int fd, char *buf, size_t bufsiz, int timeout)
588 {
589 	struct pollfd pfd[1];
590 	size_t left;
591 	char c, *cp;
592 	ssize_t ss;
593 	int n;
594 
595 	if (bufsiz == 0)
596 		return (NULL);			/* sanity */
597 
598 	cp = buf;
599 	left = bufsiz;
600 
601 	/*
602 	 * Timeout of <= 0 means no timeout.
603 	 */
604 	if (timeout > 0) {
605 		timeout *= 1000;		/* convert to miliseconds */
606 
607 		pfd[0].fd = fd;
608 		pfd[0].events = POLLIN;
609 		while (--left) {
610 			/* Poll until we are ready or we time out */
611 			while ((n = poll(pfd, 1, timeout)) == -1 &&
612 			    (errno == EINTR || errno == EAGAIN))
613 				;
614 			if (n <= 0 ||
615 			    (pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)))
616 				break;		/* timeout or error */
617 
618 			/* Read a character, exit loop on error, EOF or EOL */
619 			ss = read(fd, &c, 1);
620 			if (ss != 1 || c == '\n' || c == '\r')
621 				break;
622 			*cp++ = c;
623 		}
624 	} else {
625 		/* Keep reading until out of space, EOF, error, or newline */
626 		while (--left && read(fd, &c, 1) == 1 && c != '\n' && c != '\r')
627 			*cp++ = c;
628 	}
629 	*cp = '\0';
630 
631 	return (cp == buf ? NULL : buf);
632 }
633