skeylogin.c revision 1.14 1 /* $NetBSD: skeylogin.c,v 1.14 2000/07/06 22:30:19 mjl Exp $ */
2
3 /* S/KEY v1.1b (skeylogin.c)
4 *
5 * Authors:
6 * Neil M. Haller <nmh (at) thumper.bellcore.com>
7 * Philip R. Karn <karn (at) chicago.qualcomm.com>
8 * John S. Walden <jsw (at) thumper.bellcore.com>
9 * Scott Chasin <chasin (at) crimelab.com>
10 *
11 * Modifications:
12 * Todd C. Miller <Todd.Miller (at) courtesan.com>
13 * Angelos D. Keromytis <adk (at) 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(*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(*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(*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 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(*p); p++)
457 if (isalpha(*p) && isupper(*p))
458 *p = tolower(*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