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