/* $NetBSD: skeylogin.c,v 1.19 2005/02/04 16:13:14 perry Exp $ */ /* S/KEY v1.1b (skeylogin.c) * * Authors: * Neil M. Haller * Philip R. Karn * John S. Walden * Scott Chasin * * Modifications: * Todd C. Miller * Angelos D. Keromytis * * S/KEY verification check, lookups, and authentication. */ #include __RCSID("$NetBSD: skeylogin.c,v 1.19 2005/02/04 16:13:14 perry Exp $"); #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "skey.h" #define OTP_FMT "otp-%.*s %d %.*s" /* Issue a skey challenge for user 'name'. If successful, * fill in the caller's skey structure and return 0. If unsuccessful * (e.g., if name is unknown) return -1. * * The file read/write pointer is left at the start of the * record. */ int getskeyprompt(struct skey *mp, char *name, char *prompt) { int rval; sevenbit(name); rval = skeylookup(mp, name); *prompt = '\0'; switch (rval) { case -1: /* File error */ return -1; case 0: /* Lookup succeeded, return challenge */ sprintf(prompt, OTP_FMT "\n", SKEY_MAX_HASHNAME_LEN, skey_get_algorithm(), mp->n - 1, SKEY_MAX_SEED_LEN, mp->seed); return 0; case 1: /* User not found */ fclose(mp->keyfile); mp->keyfile = NULL; return -1; } return -1; /* Can't happen */ } /* Return a skey challenge string for user 'name'. If successful, * fill in the caller's skey structure and return 0. If unsuccessful * (e.g., if name is unknown) return -1. * * The file read/write pointer is left at the start of the * record. */ int skeychallenge(struct skey *mp, const char *name, char *ss, size_t sslen) { int rval; rval = skeylookup(mp, name); *ss = '\0'; switch(rval){ case -1: /* File error */ return -1; case 0: /* Lookup succeeded, issue challenge */ snprintf(ss, sslen, OTP_FMT, SKEY_MAX_HASHNAME_LEN, skey_get_algorithm(), mp->n - 1, SKEY_MAX_SEED_LEN, mp->seed); return 0; case 1: /* User not found */ fclose(mp->keyfile); mp->keyfile = NULL; return -1; } return -1; /* Can't happen */ } static FILE *openSkey(void) { struct stat statbuf; FILE *keyfile = NULL; /* Open _PATH_SKEYKEYS if it exists, else return an error */ if (stat(_PATH_SKEYKEYS, &statbuf) == 0 && (keyfile = fopen(_PATH_SKEYKEYS, "r+"))) { if ((statbuf.st_mode & 0007777) != 0600) fchmod(fileno(keyfile), 0600); } else { keyfile = NULL; } return keyfile; } /* Find an entry in the One-time Password database. * Return codes: * -1: error in opening database * 0: entry found, file R/W pointer positioned at beginning of record * 1: entry not found, file R/W pointer positioned at EOF */ int skeylookup(struct skey *mp, const char *name) { int found = 0; long recstart = 0; const char *ht = NULL; char *last; if(!(mp->keyfile = openSkey())) return(-1); /* Look up user name in database */ while (!feof(mp->keyfile)) { char *cp; recstart = ftell(mp->keyfile); mp->recstart = recstart; if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) break; rip(mp->buf); if (mp->buf[0] == '#') continue; /* Comment */ if ((mp->logname = strtok_r(mp->buf, " \t", &last)) == NULL) continue; if ((cp = strtok_r(NULL, " \t", &last)) == NULL) continue; /* Save hash type if specified, else use md4 */ if (isalpha((u_char) *cp)) { ht = cp; if ((cp = strtok_r(NULL, " \t", &last)) == NULL) continue; } else { ht = "md4"; } mp->n = atoi(cp); if ((mp->seed = strtok_r(NULL, " \t", &last)) == NULL) continue; if ((mp->val = strtok_r(NULL, " \t", &last)) == NULL) continue; if (strcmp(mp->logname, name) == 0) { found = 1; break; } } if (found) { fseek(mp->keyfile, recstart, SEEK_SET); /* Set hash type */ if (ht && skey_set_algorithm(ht) == NULL) { warnx("Unknown hash algorithm %s, using %s", ht, skey_get_algorithm()); } return(0); } else { return(1); } } /* Get the next entry in the One-time Password database. * Return codes: * -1: error in opening database * 0: next entry found and stored in mp * 1: no more entries, file R/W pointer positioned at EOF */ int skeygetnext(struct skey *mp) { long recstart = 0; char *last; /* Open _PATH_SKEYKEYS if it exists, else return an error */ if (mp->keyfile == NULL) { if(!(mp->keyfile = openSkey())) return(-1); } /* Look up next user in database */ while (!feof(mp->keyfile)) { char *cp; recstart = ftell(mp->keyfile); mp->recstart = recstart; if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) break; rip(mp->buf); if (mp->buf[0] == '#') continue; /* Comment */ if ((mp->logname = strtok_r(mp->buf, " \t", &last)) == NULL) continue; if ((cp = strtok_r(NULL, " \t", &last)) == NULL) continue; /* Save hash type if specified, else use md4 */ if (isalpha((u_char) *cp)) { if ((cp = strtok_r(NULL, " \t", &last)) == NULL) continue; } mp->n = atoi(cp); if ((mp->seed = strtok_r(NULL, " \t", &last)) == NULL) continue; if ((mp->val = strtok_r(NULL, " \t", &last)) == NULL) continue; /* Got a real entry */ break; } return(feof(mp->keyfile)); } /* Verify response to a s/key challenge. * * Return codes: * -1: Error of some sort; database unchanged * 0: Verify successful, database updated * 1: Verify failed, database unchanged * * The database file is always closed by this call. */ int skeyverify(struct skey *mp, char *response) { char key[SKEY_BINKEY_SIZE]; char fkey[SKEY_BINKEY_SIZE]; char filekey[SKEY_BINKEY_SIZE]; time_t now; struct tm *tm; char tbuf[27]; char *cp, *last; int i, rval; time(&now); tm = localtime(&now); strftime(tbuf, sizeof(tbuf), " %b %d,%Y %T", tm); if (response == NULL) { fclose(mp->keyfile); mp->keyfile = NULL; return -1; } rip(response); /* Convert response to binary */ if (etob(key, response) != 1 && atob8(key, response) != 0) { /* Neither english words or ascii hex */ fclose(mp->keyfile); mp->keyfile = NULL; return -1; } /* Compute fkey = f(key) */ memcpy(fkey, key, sizeof(key)); fflush(stdout); f(fkey); /* * Obtain an exclusive lock on the key file so the same password * cannot be used twice to get in to the system. */ for (i = 0; i < 300; i++) { if ((rval = flock(fileno(mp->keyfile), LOCK_EX|LOCK_NB)) == 0 || errno != EWOULDBLOCK) break; usleep(100000); /* Sleep for 0.1 seconds */ } if (rval == -1) { /* Can't get exclusive lock */ errno = EAGAIN; return(-1); } /* Reread the file record NOW */ fseek(mp->keyfile, mp->recstart, SEEK_SET); if (fgets(mp->buf, sizeof(mp->buf), mp->keyfile) != mp->buf) { fclose(mp->keyfile); mp->keyfile = NULL; return -1; } rip(mp->buf); if ((mp->logname = strtok_r(mp->buf, " \t", &last)) == NULL) goto verify_failure; if ((cp = strtok_r(NULL, " \t", &last)) == NULL) goto verify_failure; if (isalpha((u_char) *cp)) if ((cp = strtok_r(NULL, " \t", &last)) == NULL) goto verify_failure; if ((mp->seed = strtok_r(NULL, " \t", &last)) == NULL) goto verify_failure; if ((mp->val = strtok_r(NULL, " \t", &last)) == NULL) goto verify_failure; /* And convert file value to hex for comparison */ atob8(filekey, mp->val); /* Do actual comparison */ if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0) { /* Wrong response */ fclose(mp->keyfile); mp->keyfile = NULL; return 1; } /* * Update key in database by overwriting entire record. Note * that we must write exactly the same number of bytes as in * the original record (note fixed width field for N) */ btoa8(mp->val, key); mp->n--; fseek(mp->keyfile, mp->recstart, SEEK_SET); /* Don't save algorithm type for md4 (keep record length same) */ if (strcmp(skey_get_algorithm(), "md4") == 0) (void)fprintf(mp->keyfile, "%s %04d %-16s %s %-21s\n", mp->logname, mp->n, mp->seed, mp->val, tbuf); else (void)fprintf(mp->keyfile, "%s %s %04d %-16s %s %-21s\n", mp->logname, skey_get_algorithm(), mp->n, mp->seed, mp->val, tbuf); fclose(mp->keyfile); mp->keyfile = NULL; return 0; verify_failure: fclose(mp->keyfile); mp->keyfile = NULL; return -1; } /* Returns: 1 user doesnt exist, -1 file error, 0 user exists. */ int skey_haskey(const char *username) { struct skey skey; int i; i = skeylookup(&skey, username); if (skey.keyfile != NULL) { fclose(skey.keyfile); skey.keyfile = NULL; } return(i); } /* * Returns the current sequence number and * seed for the passed user. */ const char *skey_keyinfo(const char *username) { int i; static char str[SKEY_MAX_CHALLENGE]; struct skey skey; i = skeychallenge(&skey, username, str, sizeof str); if (i == -1) return 0; if (skey.keyfile != NULL) { fclose(skey.keyfile); skey.keyfile = NULL; } return str; } /* * Check to see if answer is the correct one to the current * challenge. * * Returns: 0 success, -1 failure */ int skey_passcheck(const char *username, char *passwd) { int i; struct skey skey; i = skeylookup (&skey, username); if (i == -1 || i == 1) return -1; if (skeyverify (&skey, passwd) == 0) return skey.n; return -1; } #if DO_FAKE_CHALLENGE #define ROUND(x) (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \ ((x)[3])) /* * hash_collapse() */ static u_int32_t hash_collapse(u_char *s) { int len, target; u_int32_t i; int slen; slen = strlen((char *)s); if ((slen % sizeof(u_int32_t)) == 0) target = slen; /* Multiple of 4 */ else target = slen - slen % sizeof(u_int32_t); for (i = 0, len = 0; len < target; len += 4) i ^= ROUND(s + len); return i; } #endif /* * Used when calling program will allow input of the user's * response to the challenge. * * Returns: 0 success, -1 failure */ int skey_authenticate(const char *username) { int i; char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1]; struct skey skey; #if DO_FAKE_CHALLENGE u_int ptr; u_char hseed[SKEY_MAX_SEED_LEN], flg = 1, *up; size_t secretlen; struct skey skey; SHA1_CTX ctx; #endif /* Attempt a S/Key challenge */ i = skeychallenge(&skey, username, skeyprompt, sizeof skeyprompt); #if DO_FAKE_CHALLENGE /* Cons up a fake prompt if no entry in keys file */ if (i != 0) { char *p, *u; /* * Base first 4 chars of seed on hostname. * Add some filler for short hostnames if necessary. */ if (gethostname(pbuf, sizeof(pbuf)) == -1) *(p = pbuf) = '.'; else for (p = pbuf; *p && isalnum((u_char) *p); p++) if (isalpha((u_char)*p) && isupper((u_char)*p)) *p = tolower((u_char)*p); if (*p && pbuf - p < 4) (void)strncpy(p, "asjd", 4 - (pbuf - p)); pbuf[4] = '\0'; /* Hash the username if possible */ if ((up = SHA1Data(username, strlen(username), NULL)) != NULL) { struct stat sb; time_t t; int fd; /* Collapse the hash */ ptr = hash_collapse(up); memset(up, 0, strlen(up)); /* See if the random file's there, else use ctime */ if ((fd = open(_SKEY_RAND_FILE_PATH_, O_RDONLY)) != -1 && fstat(fd, &sb) == 0 && sb.st_size > (off_t)SKEY_MAX_SEED_LEN && lseek(fd, ptr % (sb.st_size - SKEY_MAX_SEED_LEN), SEEK_SET) != -1 && read(fd, hseed, SKEY_MAX_SEED_LEN) == SKEY_MAX_SEED_LEN) { close(fd); fd = -1; secret = hseed; secretlen = SKEY_MAX_SEED_LEN; flg = 0; } else if (!stat(_PATH_MEM, &sb) || !stat("/", &sb)) { t = sb.st_ctime; secret = ctime(&t); secretlen = strlen(secret); flg = 0; } if (fd != -1) close(fd); } /* Put that in your pipe and smoke it */ if (flg == 0) { /* Hash secret value with username */ SHA1Init(&ctx); SHA1Update(&ctx, secret, secretlen); SHA1Update(&ctx, username, strlen(username)); SHA1End(&ctx, up); /* Zero out */ memset(secret, 0, secretlen); /* Now hash the hash */ SHA1Init(&ctx); SHA1Update(&ctx, up, strlen(up)); SHA1End(&ctx, up); ptr = hash_collapse(up + 4); for (i = 4; i < 9; i++) { pbuf[i] = (ptr % 10) + '0'; ptr /= 10; } pbuf[i] = '\0'; /* Sequence number */ ptr = ((up[2] + up[3]) % 99) + 1; memset(up, 0, 20); /* SHA1 specific */ free(up); (void)sprintf(skeyprompt, "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN, skey_get_algorithm(), ptr, SKEY_MAX_SEED_LEN, pbuf); } else { /* Base last 8 chars of seed on username */ u = username; i = 8; p = &pbuf[4]; do { if (*u == 0) { /* Pad remainder with zeros */ while (--i >= 0) *p++ = '0'; break; } *p++ = (*u++ % 10) + '0'; } while (--i != 0); pbuf[12] = '\0'; (void)sprintf(skeyprompt, "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN, skey_get_algorithm(), 99, SKEY_MAX_SEED_LEN, pbuf); } } #endif fprintf(stderr, "[%s]\n", skeyprompt); fflush(stderr); fputs("Response: ", stderr); readskey(pbuf, sizeof(pbuf)); /* Is it a valid response? */ if (i == 0 && skeyverify(&skey, pbuf) == 0) { if (skey.n < 5) { fprintf(stderr, "\nWarning! Key initialization needed soon. (%d logins left)\n", skey.n); } return 0; } return -1; } /* Comment out user's entry in the s/key database * * Return codes: * -1: Write error; database unchanged * 0: Database updated * * The database file is always closed by this call. */ /* ARGSUSED */ int skeyzero(struct skey *mp, char *response) { /* * Seek to the right place and write comment character * which effectively zero's out the entry. */ fseek(mp->keyfile, mp->recstart, SEEK_SET); if (fputc('#', mp->keyfile) == EOF) { fclose(mp->keyfile); mp->keyfile = NULL; return(-1); } fclose(mp->keyfile); mp->keyfile = NULL; return(0); }