xref: /openbsd/lib/libskey/skeylogin.c (revision 133306f0)
1 /* S/KEY v1.1b (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  *
9  * Modifications:
10  *          Todd C. Miller <Todd.Miller@courtesan.com>
11  *	    Angelos D. Keromytis <adk@adk.gr>
12  *
13  * S/KEY verification check, lookups, and authentication.
14  *
15  * $OpenBSD: skeylogin.c,v 1.37 2001/01/04 21:51:52 todd Exp $
16  */
17 
18 #include <sys/param.h>
19 #ifdef	QUOTA
20 #include <sys/quota.h>
21 #endif
22 #include <sys/stat.h>
23 #include <sys/time.h>
24 #include <sys/resource.h>
25 #include <sys/types.h>
26 
27 #include <ctype.h>
28 #include <err.h>
29 #include <errno.h>
30 #include <fcntl.h>
31 #include <paths.h>
32 #include <stdio.h>
33 #include <stdlib.h>
34 #include <string.h>
35 #include <time.h>
36 #include <unistd.h>
37 #include <sha1.h>
38 
39 #include "skey.h"
40 
41 static void skey_fakeprompt __P((char *, char *));
42 
43 /* Issue a skey challenge for user 'name'. If successful,
44  * fill in the caller's skey structure and return(0). If unsuccessful
45  * (e.g., if name is unknown) return(-1).
46  *
47  * The file read/write pointer is left at the start of the
48  * record.
49  */
50 int
51 getskeyprompt(mp, name, prompt)
52 	struct skey *mp;
53 	char *name;
54 	char *prompt;
55 {
56 	int rval;
57 
58 	sevenbit(name);
59 	rval = skeylookup(mp, name);
60 	switch (rval) {
61 	case 0:		/* Lookup succeeded, return challenge */
62 		(void)sprintf(prompt, "otp-%.*s %d %.*s\n",
63 			      SKEY_MAX_HASHNAME_LEN, skey_get_algorithm(),
64 			      mp->n - 1, SKEY_MAX_SEED_LEN, mp->seed);
65 		return(0);
66 
67 	case 1:		/* User not found */
68 		(void)fclose(mp->keyfile);
69 		mp->keyfile = NULL;
70 		/* FALLTHROUGH */
71 
72 	default:	/* File error */
73 		skey_fakeprompt(name, prompt);
74 		strcat(prompt, "\n");
75 		return(-1);
76 	}
77 }
78 
79 /* Return  a skey challenge string for user 'name'. If successful,
80  * fill in the caller's skey structure and return(0). If unsuccessful
81  * (e.g., if name is unknown) return(-1).
82  *
83  * The file read/write pointer is left at the start of the
84  * record.
85  */
86 int
87 skeychallenge(mp, name, ss)
88 	struct skey *mp;
89 	char *name;
90 	char *ss;
91 {
92 	int rval;
93 
94 	rval = skeylookup(mp, name);
95 	switch (rval) {
96 	case 0:		/* Lookup succeeded, return challenge */
97 		(void)sprintf(ss, "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
98 			      skey_get_algorithm(), mp->n - 1,
99 			      SKEY_MAX_SEED_LEN, mp->seed);
100 		return(0);
101 
102 	case 1:		/* User not found */
103 		(void)fclose(mp->keyfile);
104 		mp->keyfile = NULL;
105 		/* FALLTHROUGH */
106 
107 	default:	/* File error */
108 		skey_fakeprompt(name, ss);
109 		return(-1);
110 	}
111 }
112 
113 /* Find an entry in the One-time Password database.
114  * Return codes:
115  * -1: error in opening database
116  *  0: entry found, file R/W pointer positioned at beginning of record
117  *  1: entry not found, file R/W pointer positioned at EOF
118  */
119 int
120 skeylookup(mp, name)
121 	struct skey *mp;
122 	char *name;
123 {
124 	int found = 0;
125 	long recstart = 0;
126 	char *cp, *ht = NULL;
127 	struct stat statbuf;
128 
129 	/* Open _PATH_SKEYKEYS if it exists, else return an error */
130 	if (stat(_PATH_SKEYKEYS, &statbuf) == 0 &&
131 	    (mp->keyfile = fopen(_PATH_SKEYKEYS, "r+")) != NULL) {
132 		if ((statbuf.st_mode & 0007777) != 0600)
133 			fchmod(fileno(mp->keyfile), 0600);
134 	} else {
135 		mp->keyfile = NULL;
136 		return(-1);
137 	}
138 
139 	/* Look up user name in database */
140 	while (!feof(mp->keyfile)) {
141 		recstart = ftell(mp->keyfile);
142 		mp->recstart = recstart;
143 		if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf)
144 			break;
145 		rip(mp->buf);
146 		if (mp->buf[0] == '#')
147 			continue;	/* Comment */
148 		if ((mp->logname = strtok(mp->buf, " \t")) == NULL)
149 			continue;
150 		if ((cp = strtok(NULL, " \t")) == NULL)
151 			continue;
152 		/* Save hash type if specified, else use md4 */
153 		if (isalpha(*cp)) {
154 			ht = cp;
155 			if ((cp = strtok(NULL, " \t")) == NULL)
156 				continue;
157 		} else {
158 			ht = "md4";
159 		}
160 		mp->n = atoi(cp);
161 		if ((mp->seed = strtok(NULL, " \t")) == NULL)
162 			continue;
163 		if ((mp->val = strtok(NULL, " \t")) == NULL)
164 			continue;
165 		if (strcmp(mp->logname, name) == 0) {
166 			found = 1;
167 			break;
168 		}
169 	}
170 	if (found) {
171 		(void)fseek(mp->keyfile, recstart, SEEK_SET);
172 		/* Set hash type */
173 		if (ht && skey_set_algorithm(ht) == NULL) {
174 			warnx("Unknown hash algorithm %s, using %s", ht,
175 			      skey_get_algorithm());
176 		}
177 		return(0);
178 	} else {
179 		return(1);
180 	}
181 }
182 
183 /* Get the next entry in the One-time Password database.
184  * Return codes:
185  * -1: error in opening database
186  *  0: next entry found and stored in mp
187  *  1: no more entries, file R/W pointer positioned at EOF
188  */
189 int
190 skeygetnext(mp)
191 	struct skey *mp;
192 {
193 	long recstart = 0;
194 	char *cp;
195 	struct stat statbuf;
196 
197 	/* Open _PATH_SKEYKEYS if it exists, else return an error */
198 	if (mp->keyfile == NULL) {
199 		if (stat(_PATH_SKEYKEYS, &statbuf) == 0 &&
200 		    (mp->keyfile = fopen(_PATH_SKEYKEYS, "r+")) != NULL) {
201 			if ((statbuf.st_mode & 0007777) != 0600)
202 				fchmod(fileno(mp->keyfile), 0600);
203 		} else {
204 			return(-1);
205 		}
206 	}
207 
208 	/* Look up next user in database */
209 	while (!feof(mp->keyfile)) {
210 		recstart = ftell(mp->keyfile);
211 		mp->recstart = recstart;
212 		if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf)
213 			break;
214 		rip(mp->buf);
215 		if (mp->buf[0] == '#')
216 			continue;	/* Comment */
217 		if ((mp->logname = strtok(mp->buf, " \t")) == NULL)
218 			continue;
219 		if ((cp = strtok(NULL, " \t")) == NULL)
220 			continue;
221 		/* Save hash type if specified, else use md4 */
222 		if (isalpha(*cp)) {
223 			if ((cp = strtok(NULL, " \t")) == NULL)
224 				continue;
225 		}
226 		mp->n = atoi(cp);
227 		if ((mp->seed = strtok(NULL, " \t")) == NULL)
228 			continue;
229 		if ((mp->val = strtok(NULL, " \t")) == NULL)
230 			continue;
231 		/* Got a real entry */
232 		break;
233 	}
234 	return(feof(mp->keyfile));
235 }
236 
237 /* Verify response to a s/key challenge.
238  *
239  * Return codes:
240  * -1: Error of some sort; database unchanged
241  *  0:  Verify successful, database updated
242  *  1:  Verify failed, database unchanged
243  *
244  * The database file is always closed by this call.
245  */
246 int
247 skeyverify(mp, response)
248 	struct skey *mp;
249 	char *response;
250 {
251 	char key[SKEY_BINKEY_SIZE];
252 	char fkey[SKEY_BINKEY_SIZE];
253 	char filekey[SKEY_BINKEY_SIZE];
254 	time_t now;
255 	struct tm *tm;
256 	char tbuf[27];
257 	char *cp;
258 	int i, rval;
259 
260 	time(&now);
261 	tm = localtime(&now);
262 	(void)strftime(tbuf, sizeof(tbuf), " %b %d,%Y %T", tm);
263 
264 	if (response == NULL) {
265 		(void)fclose(mp->keyfile);
266 		mp->keyfile = NULL;
267 		return(-1);
268 	}
269 	rip(response);
270 
271 	/* Convert response to binary */
272 	if (etob(key, response) != 1 && atob8(key, response) != 0) {
273 		/* Neither english words or ascii hex */
274 		(void)fclose(mp->keyfile);
275 		mp->keyfile = NULL;
276 		return(-1);
277 	}
278 
279 	/* Compute fkey = f(key) */
280 	(void)memcpy(fkey, key, sizeof(key));
281         (void)fflush(stdout);
282 	f(fkey);
283 
284 	/*
285 	 * Obtain an exclusive lock on the key file so the same password
286 	 * cannot be used twice to get in to the system.
287 	 */
288 	for (i = 0; i < 300; i++) {
289 		if ((rval = flock(fileno(mp->keyfile), LOCK_EX|LOCK_NB)) == 0 ||
290 		    errno != EWOULDBLOCK)
291 			break;
292 		usleep(100000);			/* Sleep for 0.1 seconds */
293 	}
294 	if (rval == -1) {			/* Can't get exclusive lock */
295 		errno = EAGAIN;
296 		return(-1);
297 	}
298 
299 	/* Reread the file record NOW */
300 	(void)fseek(mp->keyfile, mp->recstart, SEEK_SET);
301 	if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) {
302 		(void)fclose(mp->keyfile);
303 		mp->keyfile = NULL;
304 		return(-1);
305 	}
306 	rip(mp->buf);
307 	mp->logname = strtok(mp->buf, " \t");
308 	cp = strtok(NULL, " \t") ;
309 	if (isalpha(*cp))
310 		cp = strtok(NULL, " \t") ;
311 	mp->seed = strtok(NULL, " \t");
312 	mp->val = strtok(NULL, " \t");
313 	/* And convert file value to hex for comparison */
314 	atob8(filekey, mp->val);
315 
316 	/* Do actual comparison */
317 	if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0){
318 		/* Wrong response */
319 		(void)fclose(mp->keyfile);
320 		mp->keyfile = NULL;
321 		return(1);
322 	}
323 
324 	/*
325 	 * Update key in database by overwriting entire record. Note
326 	 * that we must write exactly the same number of bytes as in
327 	 * the original record (note fixed width field for N)
328 	 */
329 	btoa8(mp->val,key);
330 	mp->n--;
331 	(void)fseek(mp->keyfile, mp->recstart, SEEK_SET);
332 	/* Don't save algorithm type for md4 (keep record length same) */
333 	if (strcmp(skey_get_algorithm(), "md4") == 0)
334 		(void)fprintf(mp->keyfile, "%s %04d %-16s %s %-21s\n",
335 			      mp->logname, mp->n, mp->seed, mp->val, tbuf);
336 	else
337 		(void)fprintf(mp->keyfile, "%s %s %04d %-16s %s %-21s\n",
338 			      mp->logname, skey_get_algorithm(), mp->n,
339 			      mp->seed, mp->val, tbuf);
340 
341 	(void)fclose(mp->keyfile);
342 	mp->keyfile = NULL;
343 	return(0);
344 }
345 
346 /*
347  * skey_haskey()
348  *
349  * Returns: 1 user doesn't exist, -1 file error, 0 user exists.
350  *
351  */
352 int
353 skey_haskey(username)
354 	char *username;
355 {
356 	struct skey skey;
357 	int i;
358 
359 	i = skeylookup(&skey, username);
360 
361 	if (skey.keyfile != NULL) {
362 		fclose(skey.keyfile);
363 		skey.keyfile = NULL;
364 	}
365 	return(i);
366 }
367 
368 /*
369  * skey_keyinfo()
370  *
371  * Returns the current sequence number and
372  * seed for the passed user.
373  *
374  */
375 char *
376 skey_keyinfo(username)
377 	char *username;
378 {
379 	int i;
380 	static char str[SKEY_MAX_CHALLENGE];
381 	struct skey skey;
382 
383 	i = skeychallenge(&skey, username, str);
384 	if (i == -1)
385 		return(0);
386 
387 	if (skey.keyfile != NULL) {
388 		fclose(skey.keyfile);
389 		skey.keyfile = NULL;
390 	}
391 	return(str);
392 }
393 
394 /*
395  * skey_passcheck()
396  *
397  * Check to see if answer is the correct one to the current
398  * challenge.
399  *
400  * Returns: 0 success, -1 failure
401  *
402  */
403 int
404 skey_passcheck(username, passwd)
405 	char *username, *passwd;
406 {
407 	int i;
408 	struct skey skey;
409 
410 	i = skeylookup(&skey, username);
411 	if (i == -1 || i == 1)
412 		return(-1);
413 
414 	if (skeyverify(&skey, passwd) == 0)
415 		return(skey.n);
416 
417 	return(-1);
418 }
419 
420 #define ROUND(x)   (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \
421 		    ((x)[3]))
422 
423 /*
424  * hash_collapse()
425  */
426 static u_int32_t
427 hash_collapse(s)
428         u_char *s;
429 {
430         int len, target;
431 	u_int32_t i;
432 
433 	if ((strlen(s) % sizeof(u_int32_t)) == 0)
434   		target = strlen(s);    /* Multiple of 4 */
435 	else
436 		target = strlen(s) - (strlen(s) % sizeof(u_int32_t));
437 
438 	for (i = 0, len = 0; len < target; len += 4)
439         	i ^= ROUND(s + len);
440 
441 	return i;
442 }
443 
444 /*
445  * skey_fakeprompt()
446  *
447  * Generate a fake prompt for the specified user.
448  *
449  */
450 static void
451 skey_fakeprompt(username, skeyprompt)
452 	char *username;
453 	char *skeyprompt;
454 {
455 	int i;
456 	u_int ptr;
457 	u_char hseed[SKEY_MAX_SEED_LEN], flg = 1, *up;
458 	char *secret, pbuf[SKEY_MAX_PW_LEN+1];
459 	char *p, *u;
460 	size_t secretlen;
461 	SHA1_CTX ctx;
462 
463 	/*
464 	 * Base first 4 chars of seed on hostname.
465 	 * Add some filler for short hostnames if necessary.
466 	 */
467 	if (gethostname(pbuf, sizeof(pbuf)) == -1)
468 		*(p = pbuf) = '.';
469 	else
470 		for (p = pbuf; *p && isalnum(*p); p++)
471 			if (isalpha(*p) && isupper(*p))
472 				*p = tolower(*p);
473 	if (*p && pbuf - p < 4)
474 		(void)strncpy(p, "asjd", 4 - (pbuf - p));
475 	pbuf[4] = '\0';
476 
477 	/* Hash the username if possible */
478 	if ((up = SHA1Data(username, strlen(username), NULL)) != NULL) {
479 		struct stat sb;
480 		time_t t;
481 		int fd;
482 
483 		/* Collapse the hash */
484 		ptr = hash_collapse(up);
485 		memset(up, 0, strlen(up));
486 
487 		/* See if the random file's there, else use ctime */
488 		if ((fd = open(_SKEY_RAND_FILE_PATH_, O_RDONLY)) != -1
489 		    && fstat(fd, &sb) == 0 &&
490 		    sb.st_size > (off_t)SKEY_MAX_SEED_LEN &&
491 		    lseek(fd, ptr % (sb.st_size - SKEY_MAX_SEED_LEN),
492 		    SEEK_SET) != -1 && read(fd, hseed,
493 		    SKEY_MAX_SEED_LEN) == SKEY_MAX_SEED_LEN) {
494 			close(fd);
495 			fd = -1;
496 			secret = hseed;
497 			secretlen = SKEY_MAX_SEED_LEN;
498 			flg = 0;
499 		} else if (!stat(_PATH_MEM, &sb) || !stat("/", &sb)) {
500 			t = sb.st_ctime;
501 			secret = ctime(&t);
502 			secretlen = strlen(secret);
503 			flg = 0;
504 		}
505 		if (fd != -1)
506 			close(fd);
507 	}
508 
509 	/* Put that in your pipe and smoke it */
510 	if (flg == 0) {
511 		/* Hash secret value with username */
512 		SHA1Init(&ctx);
513 		SHA1Update(&ctx, secret, secretlen);
514 		SHA1Update(&ctx, username, strlen(username));
515 		SHA1End(&ctx, up);
516 
517 		/* Zero out */
518 		memset(secret, 0, secretlen);
519 
520 		/* Now hash the hash */
521 		SHA1Init(&ctx);
522 		SHA1Update(&ctx, up, strlen(up));
523 		SHA1End(&ctx, up);
524 
525 		ptr = hash_collapse(up + 4);
526 
527 		for (i = 4; i < 9; i++) {
528 			pbuf[i] = (ptr % 10) + '0';
529 			ptr /= 10;
530 		}
531 		pbuf[i] = '\0';
532 
533 		/* Sequence number */
534 		ptr = ((up[2] + up[3]) % 99) + 1;
535 
536 		memset(up, 0, 20); /* SHA1 specific */
537 		free(up);
538 
539 		(void)sprintf(skeyprompt,
540 			      "otp-%.*s %d %.*s",
541 			      SKEY_MAX_HASHNAME_LEN,
542 			      skey_get_algorithm(),
543 			      ptr, SKEY_MAX_SEED_LEN,
544 			      pbuf);
545 	} else {
546 		/* Base last 8 chars of seed on username */
547 		u = username;
548 		i = 8;
549 		p = &pbuf[4];
550 		do {
551 			if (*u == 0) {
552 				/* Pad remainder with zeros */
553 				while (--i >= 0)
554 					*p++ = '0';
555 				break;
556 			}
557 
558 			*p++ = (*u++ % 10) + '0';
559 		} while (--i != 0);
560 		pbuf[12] = '\0';
561 
562 		(void)sprintf(skeyprompt, "otp-%.*s %d %.*s",
563 			      SKEY_MAX_HASHNAME_LEN,
564 			      skey_get_algorithm(),
565 			      99, SKEY_MAX_SEED_LEN, pbuf);
566 	}
567 }
568 
569 /*
570  * skey_authenticate()
571  *
572  * Used when calling program will allow input of the user's
573  * response to the challenge.
574  *
575  * Returns: 0 success, -1 failure
576  *
577  */
578 int
579 skey_authenticate(username)
580 	char *username;
581 {
582 	int i;
583 	char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1];
584 	struct skey skey;
585 
586 	/* Get the S/Key challenge (may be fake) */
587 	i = skeychallenge(&skey, username, skeyprompt);
588 	(void)fprintf(stderr, "%s\n", skeyprompt);
589 	(void)fflush(stderr);
590 
591 	(void)fputs("Response: ", stderr);
592 	readskey(pbuf, sizeof(pbuf));
593 
594 	/* Is it a valid response? */
595 	if (i == 0 && skeyverify(&skey, pbuf) == 0) {
596 		if (skey.n < 5) {
597 			(void)fprintf(stderr,
598 			    "\nWarning! Key initialization needed soon.  (%d logins left)\n",
599 			    skey.n);
600 		}
601 		return(0);
602 	}
603 	return(-1);
604 }
605 
606 /* Comment out user's entry in the s/key database
607  *
608  * Return codes:
609  * -1: Write error; database unchanged
610  *  0:  Database updated
611  *
612  * The database file is always closed by this call.
613  *
614  */
615 int
616 skeyzero(mp, response)
617 	struct skey *mp;
618 	char *response;
619 {
620 	/*
621 	 * Seek to the right place and write comment character
622 	 * which effectively zero's out the entry.
623 	 */
624 	(void)fseek(mp->keyfile, mp->recstart, SEEK_SET);
625 	if (fputc('#', mp->keyfile) == EOF) {
626 		fclose(mp->keyfile);
627 		mp->keyfile = NULL;
628 		return(-1);
629 	}
630 
631 	(void)fclose(mp->keyfile);
632 	mp->keyfile = NULL;
633 	return(0);
634 }
635