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