1 /* OpenBSD S/Key (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 * Todd C. Miller <Todd.Miller@courtesan.com> 9 * Angelos D. Keromytis <adk@adk.gr> 10 * 11 * S/Key verification check, lookups, and authentication. 12 * 13 * $OpenBSD: skeylogin.c,v 1.56 2015/01/16 16:48:52 deraadt Exp $ 14 */ 15 16 #ifdef QUOTA 17 #include <sys/quota.h> 18 #endif 19 #include <sys/stat.h> 20 #include <sys/time.h> 21 #include <sys/resource.h> 22 23 #include <ctype.h> 24 #include <err.h> 25 #include <errno.h> 26 #include <fcntl.h> 27 #include <paths.h> 28 #include <poll.h> 29 #include <stdio.h> 30 #include <stdlib.h> 31 #include <string.h> 32 #include <time.h> 33 #include <unistd.h> 34 #include <limits.h> 35 #include <sha1.h> 36 37 #include "skey.h" 38 39 static void skey_fakeprompt(char *, char *); 40 static char *tgetline(int, char *, size_t, int); 41 static int skeygetent(int, struct skey *, const char *); 42 43 /* 44 * Return an skey challenge string for user 'name'. If successful, 45 * fill in the caller's skey structure and return (0). If unsuccessful 46 * (e.g., if name is unknown) return (-1). 47 * 48 * The file read/write pointer is left at the start of the record. 49 */ 50 int 51 skeychallenge2(int fd, struct skey *mp, char *name, char *ss) 52 { 53 int rval; 54 55 memset(mp, 0, sizeof(*mp)); 56 rval = skeygetent(fd, mp, name); 57 58 switch (rval) { 59 case 0: /* Lookup succeeded, return challenge */ 60 (void)snprintf(ss, SKEY_MAX_CHALLENGE, 61 "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN, 62 skey_get_algorithm(), mp->n - 1, 63 SKEY_MAX_SEED_LEN, mp->seed); 64 return (0); 65 66 case 1: /* User not found */ 67 if (mp->keyfile) { 68 (void)fclose(mp->keyfile); 69 mp->keyfile = NULL; 70 } 71 /* FALLTHROUGH */ 72 73 default: /* File error */ 74 skey_fakeprompt(name, ss); 75 return (-1); 76 } 77 } 78 79 int 80 skeychallenge(struct skey *mp, char *name, char *ss) 81 { 82 return (skeychallenge2(-1, mp, name, ss)); 83 } 84 85 /* 86 * Get an entry in the One-time Password database and lock it. 87 * 88 * Return codes: 89 * -1: error in opening database or unable to lock entry 90 * 0: entry found, file R/W pointer positioned at beginning of record 91 * 1: entry not found 92 */ 93 static int 94 skeygetent(int fd, struct skey *mp, const char *name) 95 { 96 char *cp, filename[PATH_MAX], *last; 97 struct stat statbuf; 98 size_t nread; 99 FILE *keyfile; 100 101 /* Check to see that /etc/skey has not been disabled. */ 102 if (stat(_PATH_SKEYDIR, &statbuf) != 0) 103 return (-1); 104 if ((statbuf.st_mode & ALLPERMS) == 0) { 105 errno = EPERM; 106 return (-1); 107 } 108 109 if (fd == -1) { 110 /* Open the user's databse entry, creating it as needed. */ 111 if (snprintf(filename, sizeof(filename), "%s/%s", _PATH_SKEYDIR, 112 name) >= sizeof(filename)) { 113 errno = ENAMETOOLONG; 114 return (-1); 115 } 116 if ((fd = open(filename, O_RDWR | O_NOFOLLOW | O_NONBLOCK, 117 S_IRUSR | S_IWUSR)) == -1) { 118 if (errno == ENOENT) 119 goto not_found; 120 return (-1); 121 } 122 } 123 124 /* Lock and stat the user's skey file. */ 125 if (flock(fd, LOCK_EX) != 0 || fstat(fd, &statbuf) != 0) { 126 close(fd); 127 return (-1); 128 } 129 if (statbuf.st_size == 0) 130 goto not_found; 131 132 /* Sanity checks. */ 133 if ((statbuf.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR) || 134 !S_ISREG(statbuf.st_mode) || statbuf.st_nlink != 1 || 135 (keyfile = fdopen(fd, "r+")) == NULL) { 136 close(fd); 137 return (-1); 138 } 139 140 /* At this point, we are committed. */ 141 mp->keyfile = keyfile; 142 143 if ((nread = fread(mp->buf, 1, sizeof(mp->buf), keyfile)) == 0 || 144 !isspace((unsigned char)mp->buf[nread - 1])) 145 goto bad_keyfile; 146 mp->buf[nread - 1] = '\0'; 147 148 if ((mp->logname = strtok_r(mp->buf, " \t\n\r", &last)) == NULL || 149 strcmp(mp->logname, name) != 0) 150 goto bad_keyfile; 151 if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL) 152 goto bad_keyfile; 153 if (skey_set_algorithm(cp) == NULL) 154 goto bad_keyfile; 155 if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL) 156 goto bad_keyfile; 157 mp->n = atoi(cp); /* XXX - use strtol() */ 158 if ((mp->seed = strtok_r(NULL, " \t\n\r", &last)) == NULL) 159 goto bad_keyfile; 160 if ((mp->val = strtok_r(NULL, " \t\n\r", &last)) == NULL) 161 goto bad_keyfile; 162 163 (void)fseek(keyfile, 0L, SEEK_SET); 164 return (0); 165 166 bad_keyfile: 167 fclose(keyfile); 168 return (-1); 169 170 not_found: 171 /* No existing entry, fill in what we can and return */ 172 memset(mp, 0, sizeof(*mp)); 173 strlcpy(mp->buf, name, sizeof(mp->buf)); 174 mp->logname = mp->buf; 175 if (fd != -1) 176 close(fd); 177 return (1); 178 } 179 180 /* 181 * Look up an entry in the One-time Password database and lock it. 182 * Zeroes out the passed in struct skey before using it. 183 * 184 * Return codes: 185 * -1: error in opening database or unable to lock entry 186 * 0: entry found, file R/W pointer positioned at beginning of record 187 * 1: entry not found 188 */ 189 int 190 skeylookup(struct skey *mp, char *name) 191 { 192 memset(mp, 0, sizeof(*mp)); 193 return (skeygetent(-1, mp, name)); 194 } 195 196 /* 197 * Get the next entry in the One-time Password database. 198 * 199 * Return codes: 200 * -1: error in opening database 201 * 0: next entry found and stored in mp 202 * 1: no more entries, keydir is closed. 203 */ 204 int 205 skeygetnext(struct skey *mp) 206 { 207 struct dirent entry, *dp; 208 int rval; 209 210 if (mp->keyfile != NULL) { 211 fclose(mp->keyfile); 212 mp->keyfile = NULL; 213 } 214 215 /* Open _PATH_SKEYDIR if it exists, else return an error */ 216 if (mp->keydir == NULL && (mp->keydir = opendir(_PATH_SKEYDIR)) == NULL) 217 return (-1); 218 219 rval = 1; 220 while ((readdir_r(mp->keydir, &entry, &dp)) == 0 && dp == &entry) { 221 /* Skip dot files and zero-length files. */ 222 if (entry.d_name[0] != '.' && 223 (rval = skeygetent(-1, mp, entry.d_name)) != 1) 224 break; 225 } 226 227 if (dp == NULL) { 228 closedir(mp->keydir); 229 mp->keydir = NULL; 230 } 231 232 return (rval); 233 } 234 235 /* 236 * Verify response to a S/Key challenge. 237 * 238 * Return codes: 239 * -1: Error of some sort; database unchanged 240 * 0: Verify successful, database updated 241 * 1: Verify failed, database unchanged 242 * 243 * The database file is always closed by this call. 244 */ 245 int 246 skeyverify(struct skey *mp, char *response) 247 { 248 char key[SKEY_BINKEY_SIZE], fkey[SKEY_BINKEY_SIZE]; 249 char filekey[SKEY_BINKEY_SIZE], *cp, *last; 250 size_t nread; 251 252 if (response == NULL) 253 goto verify_failure; 254 255 /* 256 * The record should already be locked but lock it again 257 * just to be safe. We don't wait for the lock to become 258 * available since we should already have it... 259 */ 260 if (flock(fileno(mp->keyfile), LOCK_EX | LOCK_NB) != 0) 261 goto verify_failure; 262 263 /* Convert response to binary */ 264 rip(response); 265 if (etob(key, response) != 1 && atob8(key, response) != 0) 266 goto verify_failure; /* Neither english words nor ascii hex */ 267 268 /* Compute fkey = f(key) */ 269 (void)memcpy(fkey, key, sizeof(key)); 270 f(fkey); 271 272 /* 273 * Reread the file record NOW in case it has been modified. 274 * The only field we really need to worry about is mp->val. 275 */ 276 (void)fseek(mp->keyfile, 0L, SEEK_SET); 277 if ((nread = fread(mp->buf, 1, sizeof(mp->buf), mp->keyfile)) == 0 || 278 !isspace((unsigned char)mp->buf[nread - 1])) 279 goto verify_failure; 280 if ((mp->logname = strtok_r(mp->buf, " \t\r\n", &last)) == NULL) 281 goto verify_failure; 282 if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL) 283 goto verify_failure; 284 if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL) 285 goto verify_failure; 286 if ((mp->seed = strtok_r(NULL, " \t\r\n", &last)) == NULL) 287 goto verify_failure; 288 if ((mp->val = strtok_r(NULL, " \t\r\n", &last)) == NULL) 289 goto verify_failure; 290 291 /* Convert file value to hex and compare. */ 292 atob8(filekey, mp->val); 293 if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0) 294 goto verify_failure; /* Wrong response */ 295 296 /* 297 * Update key in database. 298 * XXX - check return values of things that write to disk. 299 */ 300 btoa8(mp->val,key); 301 mp->n--; 302 (void)fseek(mp->keyfile, 0L, SEEK_SET); 303 (void)fprintf(mp->keyfile, "%s\n%s\n%d\n%s\n%s\n", mp->logname, 304 skey_get_algorithm(), mp->n, mp->seed, mp->val); 305 (void)fflush(mp->keyfile); 306 (void)ftruncate(fileno(mp->keyfile), ftello(mp->keyfile)); 307 (void)fclose(mp->keyfile); 308 mp->keyfile = NULL; 309 return (0); 310 311 verify_failure: 312 (void)fclose(mp->keyfile); 313 mp->keyfile = NULL; 314 return (-1); 315 } 316 317 /* 318 * skey_haskey() 319 * 320 * Returns: 1 user doesn't exist, -1 file error, 0 user exists. 321 * 322 */ 323 int 324 skey_haskey(char *username) 325 { 326 struct skey skey; 327 int i; 328 329 i = skeylookup(&skey, username); 330 if (skey.keyfile != NULL) { 331 fclose(skey.keyfile); 332 skey.keyfile = NULL; 333 } 334 return (i); 335 } 336 337 /* 338 * skey_keyinfo() 339 * 340 * Returns the current sequence number and 341 * seed for the passed user. 342 * 343 */ 344 char * 345 skey_keyinfo(char *username) 346 { 347 static char str[SKEY_MAX_CHALLENGE]; 348 struct skey skey; 349 int i; 350 351 i = skeychallenge(&skey, username, str); 352 if (i == -1) 353 return (0); 354 355 if (skey.keyfile != NULL) { 356 fclose(skey.keyfile); 357 skey.keyfile = NULL; 358 } 359 return (str); 360 } 361 362 /* 363 * skey_passcheck() 364 * 365 * Check to see if answer is the correct one to the current 366 * challenge. 367 * 368 * Returns: 0 success, -1 failure 369 * 370 */ 371 int 372 skey_passcheck(char *username, char *passwd) 373 { 374 struct skey skey; 375 int i; 376 377 i = skeylookup(&skey, username); 378 if (i == -1 || i == 1) 379 return (-1); 380 381 if (skeyverify(&skey, passwd) == 0) 382 return (skey.n); 383 384 return (-1); 385 } 386 387 #define ROUND(x) (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \ 388 ((x)[3])) 389 390 /* 391 * hash_collapse() 392 */ 393 static u_int32_t 394 hash_collapse(u_char *s) 395 { 396 int len, target; 397 u_int32_t i; 398 399 if ((strlen(s) % sizeof(u_int32_t)) == 0) 400 target = strlen(s); /* Multiple of 4 */ 401 else 402 target = strlen(s) - (strlen(s) % sizeof(u_int32_t)); 403 404 for (i = 0, len = 0; len < target; len += 4) 405 i ^= ROUND(s + len); 406 407 return i; 408 } 409 410 /* 411 * skey_fakeprompt() 412 * 413 * Generate a fake prompt for the specified user. 414 * 415 */ 416 static void 417 skey_fakeprompt(char *username, char *skeyprompt) 418 { 419 char hseed[SKEY_MAX_SEED_LEN], *secret, pbuf[SKEY_MAX_PW_LEN+1], *p, *u; 420 u_char flg = 1, *up; 421 size_t secretlen; 422 SHA1_CTX ctx; 423 u_int ptr; 424 int i; 425 426 /* 427 * Base first 4 chars of seed on hostname. 428 * Add some filler for short hostnames if necessary. 429 */ 430 if (gethostname(pbuf, sizeof(pbuf)) == -1) 431 *(p = pbuf) = '.'; 432 else 433 for (p = pbuf; isalnum((unsigned char)*p); p++) 434 if (isalpha((unsigned char)*p) && 435 isupper((unsigned char)*p)) 436 *p = (char)tolower((unsigned char)*p); 437 if (*p && pbuf - p < 4) 438 (void)strncpy(p, "asjd", 4 - (pbuf - p)); 439 pbuf[4] = '\0'; 440 441 /* Hash the username if possible */ 442 if ((up = SHA1Data(username, strlen(username), NULL)) != NULL) { 443 struct stat sb; 444 time_t t; 445 int fd; 446 447 /* Collapse the hash */ 448 ptr = hash_collapse(up); 449 memset(up, 0, strlen(up)); 450 451 /* See if the random file's there, else use ctime */ 452 if ((fd = open(_SKEY_RAND_FILE_PATH_, O_RDONLY)) != -1 && 453 fstat(fd, &sb) == 0 && 454 sb.st_size > (off_t)SKEY_MAX_SEED_LEN && 455 lseek(fd, ptr % (sb.st_size - SKEY_MAX_SEED_LEN), 456 SEEK_SET) != -1 && read(fd, hseed, 457 SKEY_MAX_SEED_LEN) == SKEY_MAX_SEED_LEN) { 458 close(fd); 459 fd = -1; 460 secret = hseed; 461 secretlen = SKEY_MAX_SEED_LEN; 462 flg = 0; 463 } else if (!stat(_PATH_MEM, &sb) || !stat("/", &sb)) { 464 t = sb.st_ctime; 465 secret = ctime(&t); 466 secretlen = strlen(secret); 467 flg = 0; 468 } 469 if (fd != -1) 470 close(fd); 471 } 472 473 /* Put that in your pipe and smoke it */ 474 if (flg == 0) { 475 /* Hash secret value with username */ 476 SHA1Init(&ctx); 477 SHA1Update(&ctx, secret, secretlen); 478 SHA1Update(&ctx, username, strlen(username)); 479 SHA1End(&ctx, up); 480 481 /* Zero out */ 482 memset(secret, 0, secretlen); 483 484 /* Now hash the hash */ 485 SHA1Init(&ctx); 486 SHA1Update(&ctx, up, strlen(up)); 487 SHA1End(&ctx, up); 488 489 ptr = hash_collapse(up + 4); 490 491 for (i = 4; i < 9; i++) { 492 pbuf[i] = (ptr % 10) + '0'; 493 ptr /= 10; 494 } 495 pbuf[i] = '\0'; 496 497 /* Sequence number */ 498 ptr = ((up[2] + up[3]) % 99) + 1; 499 500 memset(up, 0, 20); /* SHA1 specific */ 501 free(up); 502 503 (void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE, 504 "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN, 505 skey_get_algorithm(), ptr, SKEY_MAX_SEED_LEN, pbuf); 506 } else { 507 /* Base last 8 chars of seed on username */ 508 u = username; 509 i = 8; 510 p = &pbuf[4]; 511 do { 512 if (*u == 0) { 513 /* Pad remainder with zeros */ 514 while (--i >= 0) 515 *p++ = '0'; 516 break; 517 } 518 519 *p++ = (*u++ % 10) + '0'; 520 } while (--i != 0); 521 pbuf[12] = '\0'; 522 523 (void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE, 524 "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN, 525 skey_get_algorithm(), 99, SKEY_MAX_SEED_LEN, pbuf); 526 } 527 } 528 529 /* 530 * skey_authenticate() 531 * 532 * Used when calling program will allow input of the user's 533 * response to the challenge. 534 * 535 * Returns: 0 success, -1 failure 536 * 537 */ 538 int 539 skey_authenticate(char *username) 540 { 541 char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1]; 542 struct skey skey; 543 int i; 544 545 /* Get the S/Key challenge (may be fake) */ 546 i = skeychallenge(&skey, username, skeyprompt); 547 (void)fprintf(stderr, "%s\nResponse: ", skeyprompt); 548 (void)fflush(stderr); 549 550 /* Time out on user input after 2 minutes */ 551 tgetline(fileno(stdin), pbuf, sizeof(pbuf), 120); 552 sevenbit(pbuf); 553 (void)rewind(stdin); 554 555 /* Is it a valid response? */ 556 if (i == 0 && skeyverify(&skey, pbuf) == 0) { 557 if (skey.n < 5) { 558 (void)fprintf(stderr, 559 "\nWarning! Key initialization needed soon. (%d logins left)\n", 560 skey.n); 561 } 562 return (0); 563 } 564 return (-1); 565 } 566 567 /* 568 * Unlock current entry in the One-time Password database. 569 * 570 * Return codes: 571 * -1: unable to lock the record 572 * 0: record was successfully unlocked 573 */ 574 int 575 skey_unlock(struct skey *mp) 576 { 577 if (mp->logname == NULL || mp->keyfile == NULL) 578 return (-1); 579 580 return (flock(fileno(mp->keyfile), LOCK_UN)); 581 } 582 583 /* 584 * Get a line of input (optionally timing out) and place it in buf. 585 */ 586 static char * 587 tgetline(int fd, char *buf, size_t bufsiz, int timeout) 588 { 589 struct pollfd pfd[1]; 590 size_t left; 591 char c, *cp; 592 ssize_t ss; 593 int n; 594 595 if (bufsiz == 0) 596 return (NULL); /* sanity */ 597 598 cp = buf; 599 left = bufsiz; 600 601 /* 602 * Timeout of <= 0 means no timeout. 603 */ 604 if (timeout > 0) { 605 timeout *= 1000; /* convert to miliseconds */ 606 607 pfd[0].fd = fd; 608 pfd[0].events = POLLIN; 609 while (--left) { 610 /* Poll until we are ready or we time out */ 611 while ((n = poll(pfd, 1, timeout)) == -1 && 612 (errno == EINTR || errno == EAGAIN)) 613 ; 614 if (n <= 0 || 615 (pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL))) 616 break; /* timeout or error */ 617 618 /* Read a character, exit loop on error, EOF or EOL */ 619 ss = read(fd, &c, 1); 620 if (ss != 1 || c == '\n' || c == '\r') 621 break; 622 *cp++ = c; 623 } 624 } else { 625 /* Keep reading until out of space, EOF, error, or newline */ 626 while (--left && read(fd, &c, 1) == 1 && c != '\n' && c != '\r') 627 *cp++ = c; 628 } 629 *cp = '\0'; 630 631 return (cp == buf ? NULL : buf); 632 } 633