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 (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/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 */ 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 */ 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 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 */ 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 */ 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 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 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 */ 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 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 */ 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 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 */ 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