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