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