GCC Code Coverage Report
Directory: ./ Exec Total Coverage
File: lib/libskey/skeylogin.c Lines: 0 213 0.0 %
Date: 2017-11-07 Branches: 0 175 0.0 %

Line Branch Exec Source
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.61 2017/04/17 21:55:20 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
	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
}