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.37 2001/01/04 21:51:52 todd Exp $ 16 */ 17 18 #include <sys/param.h> 19 #ifdef QUOTA 20 #include <sys/quota.h> 21 #endif 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 #include <sha1.h> 38 39 #include "skey.h" 40 41 static void skey_fakeprompt __P((char *, char *)); 42 43 /* Issue a skey challenge for user 'name'. If successful, 44 * fill in the caller's skey structure and return(0). If unsuccessful 45 * (e.g., if name is unknown) return(-1). 46 * 47 * The file read/write pointer is left at the start of the 48 * record. 49 */ 50 int 51 getskeyprompt(mp, name, prompt) 52 struct skey *mp; 53 char *name; 54 char *prompt; 55 { 56 int rval; 57 58 sevenbit(name); 59 rval = skeylookup(mp, name); 60 switch (rval) { 61 case 0: /* Lookup succeeded, return challenge */ 62 (void)sprintf(prompt, "otp-%.*s %d %.*s\n", 63 SKEY_MAX_HASHNAME_LEN, skey_get_algorithm(), 64 mp->n - 1, SKEY_MAX_SEED_LEN, mp->seed); 65 return(0); 66 67 case 1: /* User not found */ 68 (void)fclose(mp->keyfile); 69 mp->keyfile = NULL; 70 /* FALLTHROUGH */ 71 72 default: /* File error */ 73 skey_fakeprompt(name, prompt); 74 strcat(prompt, "\n"); 75 return(-1); 76 } 77 } 78 79 /* Return a skey challenge string for user 'name'. If successful, 80 * fill in the caller's skey structure and return(0). If unsuccessful 81 * (e.g., if name is unknown) return(-1). 82 * 83 * The file read/write pointer is left at the start of the 84 * record. 85 */ 86 int 87 skeychallenge(mp, name, ss) 88 struct skey *mp; 89 char *name; 90 char *ss; 91 { 92 int rval; 93 94 rval = skeylookup(mp, name); 95 switch (rval) { 96 case 0: /* Lookup succeeded, return challenge */ 97 (void)sprintf(ss, "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN, 98 skey_get_algorithm(), mp->n - 1, 99 SKEY_MAX_SEED_LEN, mp->seed); 100 return(0); 101 102 case 1: /* User not found */ 103 (void)fclose(mp->keyfile); 104 mp->keyfile = NULL; 105 /* FALLTHROUGH */ 106 107 default: /* File error */ 108 skey_fakeprompt(name, ss); 109 return(-1); 110 } 111 } 112 113 /* Find an entry in the One-time Password database. 114 * Return codes: 115 * -1: error in opening database 116 * 0: entry found, file R/W pointer positioned at beginning of record 117 * 1: entry not found, file R/W pointer positioned at EOF 118 */ 119 int 120 skeylookup(mp, name) 121 struct skey *mp; 122 char *name; 123 { 124 int found = 0; 125 long recstart = 0; 126 char *cp, *ht = NULL; 127 struct stat statbuf; 128 129 /* Open _PATH_SKEYKEYS if it exists, else return an error */ 130 if (stat(_PATH_SKEYKEYS, &statbuf) == 0 && 131 (mp->keyfile = fopen(_PATH_SKEYKEYS, "r+")) != NULL) { 132 if ((statbuf.st_mode & 0007777) != 0600) 133 fchmod(fileno(mp->keyfile), 0600); 134 } else { 135 mp->keyfile = NULL; 136 return(-1); 137 } 138 139 /* Look up user name in database */ 140 while (!feof(mp->keyfile)) { 141 recstart = ftell(mp->keyfile); 142 mp->recstart = recstart; 143 if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) 144 break; 145 rip(mp->buf); 146 if (mp->buf[0] == '#') 147 continue; /* Comment */ 148 if ((mp->logname = strtok(mp->buf, " \t")) == NULL) 149 continue; 150 if ((cp = strtok(NULL, " \t")) == NULL) 151 continue; 152 /* Save hash type if specified, else use md4 */ 153 if (isalpha(*cp)) { 154 ht = cp; 155 if ((cp = strtok(NULL, " \t")) == NULL) 156 continue; 157 } else { 158 ht = "md4"; 159 } 160 mp->n = atoi(cp); 161 if ((mp->seed = strtok(NULL, " \t")) == NULL) 162 continue; 163 if ((mp->val = strtok(NULL, " \t")) == NULL) 164 continue; 165 if (strcmp(mp->logname, name) == 0) { 166 found = 1; 167 break; 168 } 169 } 170 if (found) { 171 (void)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 190 skeygetnext(mp) 191 struct skey *mp; 192 { 193 long recstart = 0; 194 char *cp; 195 struct stat statbuf; 196 197 /* Open _PATH_SKEYKEYS if it exists, else return an error */ 198 if (mp->keyfile == NULL) { 199 if (stat(_PATH_SKEYKEYS, &statbuf) == 0 && 200 (mp->keyfile = fopen(_PATH_SKEYKEYS, "r+")) != NULL) { 201 if ((statbuf.st_mode & 0007777) != 0600) 202 fchmod(fileno(mp->keyfile), 0600); 203 } else { 204 return(-1); 205 } 206 } 207 208 /* Look up next user in database */ 209 while (!feof(mp->keyfile)) { 210 recstart = ftell(mp->keyfile); 211 mp->recstart = recstart; 212 if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) 213 break; 214 rip(mp->buf); 215 if (mp->buf[0] == '#') 216 continue; /* Comment */ 217 if ((mp->logname = strtok(mp->buf, " \t")) == NULL) 218 continue; 219 if ((cp = strtok(NULL, " \t")) == NULL) 220 continue; 221 /* Save hash type if specified, else use md4 */ 222 if (isalpha(*cp)) { 223 if ((cp = strtok(NULL, " \t")) == NULL) 224 continue; 225 } 226 mp->n = atoi(cp); 227 if ((mp->seed = strtok(NULL, " \t")) == NULL) 228 continue; 229 if ((mp->val = strtok(NULL, " \t")) == NULL) 230 continue; 231 /* Got a real entry */ 232 break; 233 } 234 return(feof(mp->keyfile)); 235 } 236 237 /* Verify response to a s/key challenge. 238 * 239 * Return codes: 240 * -1: Error of some sort; database unchanged 241 * 0: Verify successful, database updated 242 * 1: Verify failed, database unchanged 243 * 244 * The database file is always closed by this call. 245 */ 246 int 247 skeyverify(mp, response) 248 struct skey *mp; 249 char *response; 250 { 251 char key[SKEY_BINKEY_SIZE]; 252 char fkey[SKEY_BINKEY_SIZE]; 253 char filekey[SKEY_BINKEY_SIZE]; 254 time_t now; 255 struct tm *tm; 256 char tbuf[27]; 257 char *cp; 258 int i, rval; 259 260 time(&now); 261 tm = localtime(&now); 262 (void)strftime(tbuf, sizeof(tbuf), " %b %d,%Y %T", tm); 263 264 if (response == NULL) { 265 (void)fclose(mp->keyfile); 266 mp->keyfile = NULL; 267 return(-1); 268 } 269 rip(response); 270 271 /* Convert response to binary */ 272 if (etob(key, response) != 1 && atob8(key, response) != 0) { 273 /* Neither english words or ascii hex */ 274 (void)fclose(mp->keyfile); 275 mp->keyfile = NULL; 276 return(-1); 277 } 278 279 /* Compute fkey = f(key) */ 280 (void)memcpy(fkey, key, sizeof(key)); 281 (void)fflush(stdout); 282 f(fkey); 283 284 /* 285 * Obtain an exclusive lock on the key file so the same password 286 * cannot be used twice to get in to the system. 287 */ 288 for (i = 0; i < 300; i++) { 289 if ((rval = flock(fileno(mp->keyfile), LOCK_EX|LOCK_NB)) == 0 || 290 errno != EWOULDBLOCK) 291 break; 292 usleep(100000); /* Sleep for 0.1 seconds */ 293 } 294 if (rval == -1) { /* Can't get exclusive lock */ 295 errno = EAGAIN; 296 return(-1); 297 } 298 299 /* Reread the file record NOW */ 300 (void)fseek(mp->keyfile, mp->recstart, SEEK_SET); 301 if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) { 302 (void)fclose(mp->keyfile); 303 mp->keyfile = NULL; 304 return(-1); 305 } 306 rip(mp->buf); 307 mp->logname = strtok(mp->buf, " \t"); 308 cp = strtok(NULL, " \t") ; 309 if (isalpha(*cp)) 310 cp = strtok(NULL, " \t") ; 311 mp->seed = strtok(NULL, " \t"); 312 mp->val = strtok(NULL, " \t"); 313 /* And convert file value to hex for comparison */ 314 atob8(filekey, mp->val); 315 316 /* Do actual comparison */ 317 if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0){ 318 /* Wrong response */ 319 (void)fclose(mp->keyfile); 320 mp->keyfile = NULL; 321 return(1); 322 } 323 324 /* 325 * Update key in database by overwriting entire record. Note 326 * that we must write exactly the same number of bytes as in 327 * the original record (note fixed width field for N) 328 */ 329 btoa8(mp->val,key); 330 mp->n--; 331 (void)fseek(mp->keyfile, mp->recstart, SEEK_SET); 332 /* Don't save algorithm type for md4 (keep record length same) */ 333 if (strcmp(skey_get_algorithm(), "md4") == 0) 334 (void)fprintf(mp->keyfile, "%s %04d %-16s %s %-21s\n", 335 mp->logname, mp->n, mp->seed, mp->val, tbuf); 336 else 337 (void)fprintf(mp->keyfile, "%s %s %04d %-16s %s %-21s\n", 338 mp->logname, skey_get_algorithm(), mp->n, 339 mp->seed, mp->val, tbuf); 340 341 (void)fclose(mp->keyfile); 342 mp->keyfile = NULL; 343 return(0); 344 } 345 346 /* 347 * skey_haskey() 348 * 349 * Returns: 1 user doesn't exist, -1 file error, 0 user exists. 350 * 351 */ 352 int 353 skey_haskey(username) 354 char *username; 355 { 356 struct skey skey; 357 int i; 358 359 i = skeylookup(&skey, username); 360 361 if (skey.keyfile != NULL) { 362 fclose(skey.keyfile); 363 skey.keyfile = NULL; 364 } 365 return(i); 366 } 367 368 /* 369 * skey_keyinfo() 370 * 371 * Returns the current sequence number and 372 * seed for the passed user. 373 * 374 */ 375 char * 376 skey_keyinfo(username) 377 char *username; 378 { 379 int i; 380 static char str[SKEY_MAX_CHALLENGE]; 381 struct skey skey; 382 383 i = skeychallenge(&skey, username, str); 384 if (i == -1) 385 return(0); 386 387 if (skey.keyfile != NULL) { 388 fclose(skey.keyfile); 389 skey.keyfile = NULL; 390 } 391 return(str); 392 } 393 394 /* 395 * skey_passcheck() 396 * 397 * Check to see if answer is the correct one to the current 398 * challenge. 399 * 400 * Returns: 0 success, -1 failure 401 * 402 */ 403 int 404 skey_passcheck(username, passwd) 405 char *username, *passwd; 406 { 407 int i; 408 struct skey skey; 409 410 i = skeylookup(&skey, username); 411 if (i == -1 || i == 1) 412 return(-1); 413 414 if (skeyverify(&skey, passwd) == 0) 415 return(skey.n); 416 417 return(-1); 418 } 419 420 #define ROUND(x) (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \ 421 ((x)[3])) 422 423 /* 424 * hash_collapse() 425 */ 426 static u_int32_t 427 hash_collapse(s) 428 u_char *s; 429 { 430 int len, target; 431 u_int32_t i; 432 433 if ((strlen(s) % sizeof(u_int32_t)) == 0) 434 target = strlen(s); /* Multiple of 4 */ 435 else 436 target = strlen(s) - (strlen(s) % sizeof(u_int32_t)); 437 438 for (i = 0, len = 0; len < target; len += 4) 439 i ^= ROUND(s + len); 440 441 return i; 442 } 443 444 /* 445 * skey_fakeprompt() 446 * 447 * Generate a fake prompt for the specified user. 448 * 449 */ 450 static void 451 skey_fakeprompt(username, skeyprompt) 452 char *username; 453 char *skeyprompt; 454 { 455 int i; 456 u_int ptr; 457 u_char hseed[SKEY_MAX_SEED_LEN], flg = 1, *up; 458 char *secret, pbuf[SKEY_MAX_PW_LEN+1]; 459 char *p, *u; 460 size_t secretlen; 461 SHA1_CTX ctx; 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(*p); p++) 471 if (isalpha(*p) && isupper(*p)) 472 *p = tolower(*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 569 /* 570 * skey_authenticate() 571 * 572 * Used when calling program will allow input of the user's 573 * response to the challenge. 574 * 575 * Returns: 0 success, -1 failure 576 * 577 */ 578 int 579 skey_authenticate(username) 580 char *username; 581 { 582 int i; 583 char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1]; 584 struct skey skey; 585 586 /* Get the S/Key challenge (may be fake) */ 587 i = skeychallenge(&skey, username, skeyprompt); 588 (void)fprintf(stderr, "%s\n", skeyprompt); 589 (void)fflush(stderr); 590 591 (void)fputs("Response: ", stderr); 592 readskey(pbuf, sizeof(pbuf)); 593 594 /* Is it a valid response? */ 595 if (i == 0 && skeyverify(&skey, pbuf) == 0) { 596 if (skey.n < 5) { 597 (void)fprintf(stderr, 598 "\nWarning! Key initialization needed soon. (%d logins left)\n", 599 skey.n); 600 } 601 return(0); 602 } 603 return(-1); 604 } 605 606 /* Comment out user's entry in the s/key database 607 * 608 * Return codes: 609 * -1: Write error; database unchanged 610 * 0: Database updated 611 * 612 * The database file is always closed by this call. 613 * 614 */ 615 int 616 skeyzero(mp, response) 617 struct skey *mp; 618 char *response; 619 { 620 /* 621 * Seek to the right place and write comment character 622 * which effectively zero's out the entry. 623 */ 624 (void)fseek(mp->keyfile, mp->recstart, SEEK_SET); 625 if (fputc('#', mp->keyfile) == EOF) { 626 fclose(mp->keyfile); 627 mp->keyfile = NULL; 628 return(-1); 629 } 630 631 (void)fclose(mp->keyfile); 632 mp->keyfile = NULL; 633 return(0); 634 } 635