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