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