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.65 2024/03/23 16:30:01 guenther 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
39static void skey_fakeprompt(char *, char *);
40static char *tgetline(int, char *, size_t, int);
41static 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 */
50int
51skeychallenge2(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
79int
80skeychallenge(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 */
93static int
94skeygetent(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 database 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 */
192int
193skeylookup(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 */
207int
208skeygetnext(struct skey *mp)
209{
210	struct dirent *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 ((dp = readdir(mp->keydir)) != NULL) {
224		/* Skip dot files and zero-length files. */
225		if (dp->d_name[0] != '.' &&
226		    (rval = skeygetent(-1, mp, dp->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 */
248int
249skeyverify(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 */
326int
327skey_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 */
347char *
348skey_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 */
374int
375skey_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 */
396static u_int32_t
397hash_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 */
419static void
420skey_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 && p - pbuf < 4)
440		(void)strncpy(p, "asjd", 4 - (p - pbuf));
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 */
514int
515skey_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 */
550int
551skey_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 */
562static char *
563tgetline(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