xref: /netbsd-src/lib/libskey/skeylogin.c (revision 8e902846e90071d950c3d8620b9faf391b76e8c7)
1 /*	$NetBSD: skeylogin.c,v 1.19 2005/02/04 16:13:14 perry Exp $	*/
2 
3 /* S/KEY v1.1b (skeylogin.c)
4  *
5  * Authors:
6  *          Neil M. Haller <nmh@thumper.bellcore.com>
7  *          Philip R. Karn <karn@chicago.qualcomm.com>
8  *          John S. Walden <jsw@thumper.bellcore.com>
9  *          Scott Chasin <chasin@crimelab.com>
10  *
11  * Modifications:
12  *          Todd C. Miller <Todd.Miller@courtesan.com>
13  *          Angelos D. Keromytis <adk@adk.gr>
14  *
15  * S/KEY verification check, lookups, and authentication.
16  */
17 
18 #include <sys/cdefs.h>
19 __RCSID("$NetBSD: skeylogin.c,v 1.19 2005/02/04 16:13:14 perry Exp $");
20 
21 #include <sys/param.h>
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 
38 #include "skey.h"
39 
40 #define OTP_FMT "otp-%.*s %d %.*s"
41 
42 /* Issue a skey challenge for user 'name'. If successful,
43  * fill in the caller's skey structure and return 0. If unsuccessful
44  * (e.g., if name is unknown) return -1.
45  *
46  * The file read/write pointer is left at the start of the
47  * record.
48  */
getskeyprompt(struct skey * mp,char * name,char * prompt)49 int getskeyprompt(struct skey *mp, char *name, char *prompt)
50 {
51 	int rval;
52 
53 	sevenbit(name);
54 	rval = skeylookup(mp, name);
55 
56 	*prompt = '\0';
57 	switch (rval) {
58 	case -1:	/* File error */
59 		return -1;
60 	case 0:		/* Lookup succeeded, return challenge */
61 		sprintf(prompt, OTP_FMT "\n",
62 				SKEY_MAX_HASHNAME_LEN, skey_get_algorithm(),
63 				mp->n - 1, SKEY_MAX_SEED_LEN, mp->seed);
64 		return 0;
65 	case 1:		/* User not found */
66 		fclose(mp->keyfile);
67 		mp->keyfile = NULL;
68 		return -1;
69 	}
70 	return -1;	/* Can't happen */
71 }
72 
73 /* Return  a skey challenge string for user 'name'. If successful,
74  * fill in the caller's skey structure and return 0. If unsuccessful
75  * (e.g., if name is unknown) return -1.
76  *
77  * The file read/write pointer is left at the start of the
78  * record.
79  */
skeychallenge(struct skey * mp,const char * name,char * ss,size_t sslen)80 int skeychallenge(struct skey *mp, const char *name, char *ss, size_t sslen)
81 {
82 	int rval;
83 
84 	rval = skeylookup(mp, name);
85 
86 	*ss = '\0';
87 	switch(rval){
88 	case -1:	/* File error */
89 		return -1;
90 	case 0:		/* Lookup succeeded, issue challenge */
91 		snprintf(ss, sslen, OTP_FMT, SKEY_MAX_HASHNAME_LEN,
92 				skey_get_algorithm(), mp->n - 1,
93 				SKEY_MAX_SEED_LEN, mp->seed);
94 		return 0;
95 	case 1:		/* User not found */
96 		fclose(mp->keyfile);
97 		mp->keyfile = NULL;
98 		return -1;
99 	}
100 	return -1;	/* Can't happen */
101 }
102 
openSkey(void)103 static FILE *openSkey(void)
104 {
105 	struct stat statbuf;
106 	FILE *keyfile = NULL;
107 
108 	/* Open _PATH_SKEYKEYS if it exists, else return an error */
109 	if (stat(_PATH_SKEYKEYS, &statbuf) == 0 &&
110 	    (keyfile = fopen(_PATH_SKEYKEYS, "r+"))) {
111 		if ((statbuf.st_mode & 0007777) != 0600)
112 		fchmod(fileno(keyfile), 0600);
113         } else {
114 		keyfile = NULL;
115 	}
116 
117 	return keyfile;
118 }
119 
120 /* Find an entry in the One-time Password database.
121  * Return codes:
122  * -1: error in opening database
123  *  0: entry found, file R/W pointer positioned at beginning of record
124  *  1: entry not found, file R/W pointer positioned at EOF
125  */
skeylookup(struct skey * mp,const char * name)126 int skeylookup(struct skey *mp, const char *name)
127 {
128 	int found = 0;
129 	long recstart = 0;
130 	const char *ht = NULL;
131 	char *last;
132 
133 	if(!(mp->keyfile = openSkey()))
134 		return(-1);
135 
136 	/* Look up user name in database */
137 	while (!feof(mp->keyfile)) {
138 		char *cp;
139 
140 		recstart = ftell(mp->keyfile);
141 		mp->recstart = recstart;
142 		if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf)
143 			break;
144 
145 		rip(mp->buf);
146 		if (mp->buf[0] == '#')
147 			continue;	/* Comment */
148 		if ((mp->logname = strtok_r(mp->buf, " \t", &last)) == NULL)
149 			continue;
150 		if ((cp = strtok_r(NULL, " \t", &last)) == NULL)
151 			continue;
152 		/* Save hash type if specified, else use md4 */
153 		if (isalpha((u_char) *cp)) {
154 			ht = cp;
155 			if ((cp = strtok_r(NULL, " \t", &last)) == NULL)
156 				continue;
157 		} else {
158 			ht = "md4";
159 		}
160 		mp->n = atoi(cp);
161 		if ((mp->seed = strtok_r(NULL, " \t", &last)) == NULL)
162 			continue;
163 		if ((mp->val = strtok_r(NULL, " \t", &last)) == NULL)
164 			continue;
165 		if (strcmp(mp->logname, name) == 0) {
166 			found = 1;
167 			break;
168 		}
169 	}
170 	if (found) {
171 		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  */
skeygetnext(struct skey * mp)189 int skeygetnext(struct skey *mp)
190 {
191 	long recstart = 0;
192 	char *last;
193 
194 	/* Open _PATH_SKEYKEYS if it exists, else return an error */
195 	if (mp->keyfile == NULL) {
196 		if(!(mp->keyfile = openSkey()))
197 			return(-1);
198 	}
199 
200 	/* Look up next user in database */
201 	while (!feof(mp->keyfile)) {
202 		char *cp;
203 
204 		recstart = ftell(mp->keyfile);
205 		mp->recstart = recstart;
206 		if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf)
207 			break;
208 		rip(mp->buf);
209 		if (mp->buf[0] == '#')
210 			continue;	/* Comment */
211 		if ((mp->logname = strtok_r(mp->buf, " \t", &last)) == NULL)
212 			continue;
213 		if ((cp = strtok_r(NULL, " \t", &last)) == NULL)
214 			continue;
215 		/* Save hash type if specified, else use md4 */
216 		if (isalpha((u_char) *cp)) {
217 			if ((cp = strtok_r(NULL, " \t", &last)) == NULL)
218 				continue;
219 		}
220 		mp->n = atoi(cp);
221 		if ((mp->seed = strtok_r(NULL, " \t", &last)) == NULL)
222 			continue;
223 		if ((mp->val = strtok_r(NULL, " \t", &last)) == NULL)
224 			continue;
225 		/* Got a real entry */
226 		break;
227 	}
228 	return(feof(mp->keyfile));
229 }
230 
231 /* Verify response to a s/key challenge.
232  *
233  * Return codes:
234  * -1: Error of some sort; database unchanged
235  *  0:  Verify successful, database updated
236  *  1:  Verify failed, database unchanged
237  *
238  * The database file is always closed by this call.
239  */
240 
skeyverify(struct skey * mp,char * response)241 int skeyverify(struct skey *mp, char *response)
242 {
243 	char key[SKEY_BINKEY_SIZE];
244 	char fkey[SKEY_BINKEY_SIZE];
245 	char filekey[SKEY_BINKEY_SIZE];
246 	time_t now;
247 	struct tm *tm;
248 	char tbuf[27];
249 	char *cp, *last;
250 	int i, rval;
251 
252 	time(&now);
253 	tm = localtime(&now);
254 	strftime(tbuf, sizeof(tbuf), " %b %d,%Y %T", tm);
255 
256 	if (response == NULL) {
257 		fclose(mp->keyfile);
258 		mp->keyfile = NULL;
259 		return -1;
260 	}
261 	rip(response);
262 
263 	/* Convert response to binary */
264 	if (etob(key, response) != 1 && atob8(key, response) != 0) {
265 		/* Neither english words or ascii hex */
266 		fclose(mp->keyfile);
267 		mp->keyfile = NULL;
268 		return -1;
269 	}
270 
271 	/* Compute fkey = f(key) */
272 	memcpy(fkey, key, sizeof(key));
273         fflush(stdout);
274 
275 	f(fkey);
276 
277 	/*
278 	 * Obtain an exclusive lock on the key file so the same password
279 	 * cannot be used twice to get in to the system.
280 	 */
281 	for (i = 0; i < 300; i++) {
282 		if ((rval = flock(fileno(mp->keyfile), LOCK_EX|LOCK_NB)) == 0 ||
283 		    errno != EWOULDBLOCK)
284 			break;
285 		usleep(100000);			/* Sleep for 0.1 seconds */
286 	}
287 	if (rval == -1) {			/* Can't get exclusive lock */
288 		errno = EAGAIN;
289 		return(-1);
290 	}
291 
292 	/* Reread the file record NOW */
293 
294 	fseek(mp->keyfile, mp->recstart, SEEK_SET);
295 	if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) {
296 		fclose(mp->keyfile);
297 		mp->keyfile = NULL;
298 		return -1;
299 	}
300 	rip(mp->buf);
301 	if ((mp->logname = strtok_r(mp->buf, " \t", &last)) == NULL)
302 		goto verify_failure;
303 	if ((cp = strtok_r(NULL, " \t", &last)) == NULL)
304 		goto verify_failure;
305 	if (isalpha((u_char) *cp))
306 		if ((cp = strtok_r(NULL, " \t", &last)) == NULL)
307 			goto verify_failure;
308 	if ((mp->seed = strtok_r(NULL, " \t", &last)) == NULL)
309 		goto verify_failure;
310 	if ((mp->val = strtok_r(NULL, " \t", &last)) == NULL)
311 		goto verify_failure;
312 	/* And convert file value to hex for comparison */
313 	atob8(filekey, mp->val);
314 
315 	/* Do actual comparison */
316 	if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0) {
317 		/* Wrong response */
318 		fclose(mp->keyfile);
319 		mp->keyfile = NULL;
320 		return 1;
321 	}
322 
323 	/*
324 	 * Update key in database by overwriting entire record. Note
325 	 * that we must write exactly the same number of bytes as in
326 	 * the original record (note fixed width field for N)
327 	 */
328 	btoa8(mp->val, key);
329 	mp->n--;
330 	fseek(mp->keyfile, mp->recstart, SEEK_SET);
331 	/* Don't save algorithm type for md4 (keep record length same) */
332 	if (strcmp(skey_get_algorithm(), "md4") == 0)
333 		(void)fprintf(mp->keyfile, "%s %04d %-16s %s %-21s\n",
334 			      mp->logname, mp->n, mp->seed, mp->val, tbuf);
335 	else
336 		(void)fprintf(mp->keyfile, "%s %s %04d %-16s %s %-21s\n",
337 			      mp->logname, skey_get_algorithm(), mp->n,
338 			      mp->seed, mp->val, tbuf);
339 
340 	fclose(mp->keyfile);
341 	mp->keyfile = NULL;
342 	return 0;
343 
344   verify_failure:
345 	fclose(mp->keyfile);
346 	mp->keyfile = NULL;
347 	return -1;
348 }
349 
350 
351 /* Returns: 1 user doesnt exist, -1 file error, 0 user exists. */
352 
skey_haskey(const char * username)353 int skey_haskey(const char *username)
354 {
355 	struct skey skey;
356 	int i;
357 
358 	i = skeylookup(&skey, username);
359 
360 	if (skey.keyfile != NULL) {
361 		fclose(skey.keyfile);
362 		skey.keyfile = NULL;
363 	}
364 	return(i);
365 }
366 
367 /*
368  * Returns the current sequence number and
369  * seed for the passed user.
370  */
skey_keyinfo(const char * username)371 const char *skey_keyinfo(const char *username)
372 {
373 	int i;
374 	static char str[SKEY_MAX_CHALLENGE];
375 	struct skey skey;
376 
377 	i = skeychallenge(&skey, username, str, sizeof str);
378 	if (i == -1)
379 		return 0;
380 
381 	if (skey.keyfile != NULL) {
382 		fclose(skey.keyfile);
383 		skey.keyfile = NULL;
384 	}
385 	return str;
386 }
387 
388 /*
389  * Check to see if answer is the correct one to the current
390  * challenge.
391  *
392  * Returns: 0 success, -1 failure
393  */
394 
skey_passcheck(const char * username,char * passwd)395 int skey_passcheck(const char *username, char *passwd)
396 {
397 	int i;
398 	struct skey skey;
399 
400 	i = skeylookup (&skey, username);
401 	if (i == -1 || i == 1)
402 		return -1;
403 
404 	if (skeyverify (&skey, passwd) == 0)
405 		return skey.n;
406 
407 	return -1;
408 }
409 
410 #if DO_FAKE_CHALLENGE
411 #define ROUND(x)   (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \
412 		    ((x)[3]))
413 
414 /*
415  * hash_collapse()
416  */
hash_collapse(u_char * s)417 static u_int32_t hash_collapse(u_char *s)
418 {
419         int len, target;
420 	u_int32_t i;
421 	int slen;
422 
423 	slen = strlen((char *)s);
424 	if ((slen % sizeof(u_int32_t)) == 0)
425   		target = slen;    /* Multiple of 4 */
426 	else
427 		target = slen - slen % sizeof(u_int32_t);
428 
429 	for (i = 0, len = 0; len < target; len += 4)
430         	i ^= ROUND(s + len);
431 
432 	return i;
433 }
434 #endif
435 
436 /*
437  * Used when calling program will allow input of the user's
438  * response to the challenge.
439  *
440  * Returns: 0 success, -1 failure
441  */
442 
skey_authenticate(const char * username)443 int skey_authenticate(const char *username)
444 {
445 	int i;
446 	char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1];
447 	struct skey skey;
448 #if DO_FAKE_CHALLENGE
449 	u_int ptr;
450 	u_char hseed[SKEY_MAX_SEED_LEN], flg = 1, *up;
451 	size_t secretlen;
452 	struct skey skey;
453 	SHA1_CTX ctx;
454 #endif
455 	/* Attempt a S/Key challenge */
456 	i = skeychallenge(&skey, username, skeyprompt, sizeof skeyprompt);
457 
458 #if DO_FAKE_CHALLENGE
459 	/* Cons up a fake prompt if no entry in keys file */
460 	if (i != 0) {
461 		char *p, *u;
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((u_char) *p); p++)
471 				if (isalpha((u_char)*p) && isupper((u_char)*p))
472 					*p = tolower((u_char)*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 #endif
569 
570 	fprintf(stderr, "[%s]\n", skeyprompt);
571 	fflush(stderr);
572 
573 	fputs("Response: ", stderr);
574 	readskey(pbuf, sizeof(pbuf));
575 
576 	/* Is it a valid response? */
577 	if (i == 0 && skeyverify(&skey, pbuf) == 0) {
578 		if (skey.n < 5) {
579 			fprintf(stderr,
580 			"\nWarning! Key initialization needed soon.  (%d logins left)\n",
581 			skey.n);
582 		}
583 		return 0;
584 	}
585 	return -1;
586 }
587 
588 /* Comment out user's entry in the s/key database
589  *
590  * Return codes:
591  * -1: Write error; database unchanged
592  *  0:  Database updated
593  *
594  * The database file is always closed by this call.
595  */
596 
597 /* ARGSUSED */
skeyzero(struct skey * mp,char * response)598 int skeyzero(struct skey *mp, char *response)
599 {
600 	/*
601 	 * Seek to the right place and write comment character
602 	 * which effectively zero's out the entry.
603 	 */
604 	fseek(mp->keyfile, mp->recstart, SEEK_SET);
605 	if (fputc('#', mp->keyfile) == EOF) {
606 		fclose(mp->keyfile);
607 		mp->keyfile = NULL;
608 		return(-1);
609 	}
610 
611 	fclose(mp->keyfile);
612 	mp->keyfile = NULL;
613 	return(0);
614 }
615