/* vi: set sw=4 ts=4: */
/*
 * Licensed under GPLv2 or later, see file LICENSE in this tarball for details.
 */

#include <fcntl.h>
#include <stdio.h>
#include <string.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <utime.h>
#include <syslog.h>
#include <time.h>
#include <sys/resource.h>
#include <errno.h>

#include "busybox.h"

static char crypt_passwd[128];

static int create_backup(const char *backup, FILE * fp);
static int new_password(const struct passwd *pw, int amroot, int algo);
static void set_filesize_limit(int blocks);


static int get_algo(char *a)
{
	int x = 1;					/* standard: MD5 */

	if (strcasecmp(a, "des") == 0)
		x = 0;
	return x;
}


static int update_passwd(const struct passwd *pw, const char *crypt_pw)
{
	char filename[1024];
	char buf[1025];
	char buffer[80];
	char username[32];
	char *pw_rest;
	int mask;
	int continued;
	FILE *fp;
	FILE *out_fp;
	struct stat sb;
	struct flock lock;

#if ENABLE_FEATURE_SHADOWPASSWDS
	if (access(bb_path_shadow_file, F_OK) == 0) {
		snprintf(filename, sizeof filename, "%s", bb_path_shadow_file);
	} else
#endif
	{
		snprintf(filename, sizeof filename, "%s", bb_path_passwd_file);
	}

	if (((fp = fopen(filename, "r+")) == 0) || (fstat(fileno(fp), &sb))) {
		/* return 0; */
		return 1;
	}

	/* Lock the password file before updating */
	lock.l_type = F_WRLCK;
	lock.l_whence = SEEK_SET;
	lock.l_start = 0;
	lock.l_len = 0;
	if (fcntl(fileno(fp), F_SETLK, &lock) < 0) {
		fprintf(stderr, "%s: %s\n", filename, strerror(errno));
		return 1;
	}
	lock.l_type = F_UNLCK;

	snprintf(buf, sizeof buf, "%s-", filename);
	if (create_backup(buf, fp)) {
		fcntl(fileno(fp), F_SETLK, &lock);
		fclose(fp);
		return 1;
	}
	snprintf(buf, sizeof buf, "%s+", filename);
	mask = umask(0777);
	out_fp = fopen(buf, "w");
	umask(mask);
	if ((!out_fp) || (fchmod(fileno(out_fp), sb.st_mode & 0777))
		|| (fchown(fileno(out_fp), sb.st_uid, sb.st_gid))) {
		fcntl(fileno(fp), F_SETLK, &lock);
		fclose(fp);
		fclose(out_fp);
		return 1;
	}

	continued = 0;
	snprintf(username, sizeof username, "%s:", pw->pw_name);
	rewind(fp);
	while (!feof(fp)) {
		fgets(buffer, sizeof buffer, fp);
		if (!continued) { /* Check to see if we're updating this line.  */
			if (strncmp(username, buffer, strlen(username)) == 0) {
				/* we have a match. */
				pw_rest = strchr(buffer, ':');
				*pw_rest++ = '\0';
				pw_rest = strchr(pw_rest, ':');
				fprintf(out_fp, "%s:%s%s", buffer, crypt_pw, pw_rest);
			} else {
				fputs(buffer, out_fp);
			}
		} else {
			fputs(buffer, out_fp);
		}
		if (buffer[strlen(buffer) - 1] == '\n') {
			continued = 0;
		} else {
			continued = 1;
		}
		memset(buffer, 0, sizeof buffer);
	}

	if (fflush(out_fp) || fsync(fileno(out_fp)) || fclose(out_fp)) {
		unlink(buf);
		fcntl(fileno(fp), F_SETLK, &lock);
		fclose(fp);
		return 1;
	}
	if (rename(buf, filename) < 0) {
		fcntl(fileno(fp), F_SETLK, &lock);
		fclose(fp);
		return 1;
	} else {
		fcntl(fileno(fp), F_SETLK, &lock);
		fclose(fp);
		return 0;
	}
}


int passwd_main(int argc, char **argv)
{
	int amroot;
	char *cp;
	char *np;
	char *name;
	char *myname;
	int flag;
	int algo = 1;				/* -a - password algorithm */
	int lflg = 0;				/* -l - lock account */
	int uflg = 0;				/* -u - unlock account */
	int dflg = 0;				/* -d - delete password */
	const struct passwd *pw;

	amroot = (getuid() == 0);
	openlog("passwd", LOG_PID | LOG_CONS | LOG_NOWAIT, LOG_AUTH);
	while ((flag = getopt(argc, argv, "a:dlu")) != EOF) {
		switch (flag) {
		case 'a':
			algo = get_algo(optarg);
			break;
		case 'd':
			dflg++;
			break;
		case 'l':
			lflg++;
			break;
		case 'u':
			uflg++;
			break;
		default:
			bb_show_usage();
		}
	}
	myname = (char *) bb_xstrdup(bb_getpwuid(NULL, getuid(), -1));
	/* exits on error */
	if (optind < argc) {
		name = argv[optind];
	} else {
		name = myname;
	}
	if ((lflg || uflg || dflg) && (optind >= argc || !amroot)) {
		bb_show_usage();
	}
	pw = getpwnam(name);
	if (!pw) {
		bb_error_msg_and_die("Unknown user %s\n", name);
	}
	if (!amroot && pw->pw_uid != getuid()) {
		syslog(LOG_WARNING, "can't change pwd for `%s'", name);
		bb_error_msg_and_die("Permission denied.\n");
	}
	if (ENABLE_FEATURE_SHADOWPASSWDS) {
		struct spwd *sp = getspnam(name);
		if (!sp) bb_error_msg_and_die("Unknown user %s", name);
		cp = sp->sp_pwdp;
	} else cp = pw->pw_passwd;

	np = name;
	safe_strncpy(crypt_passwd, cp, sizeof(crypt_passwd));
	if (!(dflg || lflg || uflg)) {
		if (!amroot) {
			if (cp[0] == '!') {
				syslog(LOG_WARNING, "password locked for `%s'", np);
				bb_error_msg_and_die( "The password for `%s' cannot be changed.\n", np);
			}
		}
		printf("Changing password for %s\n", name);
		if (new_password(pw, amroot, algo)) {
			bb_error_msg_and_die( "The password for %s is unchanged.\n", name);
		}
	} else if (lflg) {
		if (crypt_passwd[0] != '!') {
			memmove(&crypt_passwd[1], crypt_passwd,
					sizeof crypt_passwd - 1);
			crypt_passwd[sizeof crypt_passwd - 1] = '\0';
			crypt_passwd[0] = '!';
		}
	} else if (uflg) {
		if (crypt_passwd[0] == '!') {
			memmove(crypt_passwd, &crypt_passwd[1],
					sizeof crypt_passwd - 1);
		}
	} else if (dflg) {
		crypt_passwd[0] = '\0';
	}
	set_filesize_limit(30000);
	signal(SIGHUP, SIG_IGN);
	signal(SIGINT, SIG_IGN);
	signal(SIGQUIT, SIG_IGN);
	umask(077);
	xsetuid(0);
	if (!update_passwd(pw, crypt_passwd)) {
		syslog(LOG_INFO, "password for `%s' changed by user `%s'", name,
			   myname);
		printf("Password changed.\n");
	} else {
		syslog(LOG_WARNING, "an error occurred updating the password file");
		bb_error_msg_and_die("An error occurred updating the password file.\n");
	}
	if (ENABLE_FEATURE_CLEAN_UP) free(myname);
	return (0);
}



static int create_backup(const char *backup, FILE * fp)
{
	struct stat sb;
	struct utimbuf ub;
	FILE *bkfp;
	int c, mask;

	if (fstat(fileno(fp), &sb))
		/* return -1; */
		return 1;

	mask = umask(077);
	bkfp = fopen(backup, "w");
	umask(mask);
	if (!bkfp)
		/* return -1; */
		return 1;

	/* TODO: faster copy, not one-char-at-a-time.  --marekm */
	rewind(fp);
	while ((c = getc(fp)) != EOF) {
		if (putc(c, bkfp) == EOF)
			break;
	}
	if (c != EOF || fflush(bkfp)) {
		fclose(bkfp);
		/* return -1; */
		return 1;
	}
	if (fclose(bkfp))
		/* return -1; */
		return 1;

	ub.actime = sb.st_atime;
	ub.modtime = sb.st_mtime;
	utime(backup, &ub);
	return 0;
}

static int i64c(int i)
{
	if (i <= 0)
		return ('.');
	if (i == 1)
		return ('/');
	if (i >= 2 && i < 12)
		return ('0' - 2 + i);
	if (i >= 12 && i < 38)
		return ('A' - 12 + i);
	if (i >= 38 && i < 63)
		return ('a' - 38 + i);
	return ('z');
}

static char *crypt_make_salt(void)
{
	time_t now;
	static unsigned long x;
	static char result[3];

	time(&now);
	x += now + getpid() + clock();
	result[0] = i64c(((x >> 18) ^ (x >> 6)) & 077);
	result[1] = i64c(((x >> 12) ^ x) & 077);
	result[2] = '\0';
	return result;
}


static int new_password(const struct passwd *pw, int amroot, int algo)
{
	char *clear;
	char *cipher;
	char *cp;
	char salt[12]; /* "$N$XXXXXXXX" or "XX" */
	char orig[200];
	char pass[200];

	if (!amroot && crypt_passwd[0]) {
		if (!(clear = bb_askpass(0, "Old password:"))) {
			/* return -1; */
			return 1;
		}
		cipher = pw_encrypt(clear, crypt_passwd);
		if (strcmp(cipher, crypt_passwd) != 0) {
			syslog(LOG_WARNING, "incorrect password for `%s'",
				   pw->pw_name);
			bb_do_delay(FAIL_DELAY);
			fprintf(stderr, "Incorrect password.\n");
			/* return -1; */
			return 1;
		}
		safe_strncpy(orig, clear, sizeof(orig));
		memset(clear, 0, strlen(clear));
		memset(cipher, 0, strlen(cipher));
	} else {
		orig[0] = '\0';
	}
	if (! (cp=bb_askpass(0, "Enter the new password (minimum of 5, maximum of 8 characters)\n"
					  "Please use a combination of upper and lower case letters and numbers.\n"
					  "Enter new password: ")))
	{
		memset(orig, 0, sizeof orig);
		/* return -1; */
		return 1;
	}
	safe_strncpy(pass, cp, sizeof(pass));
	memset(cp, 0, strlen(cp));
	/* if (!obscure(orig, pass, pw)) { */
	if (obscure(orig, pass, pw)) {
		if (amroot) {
			printf("\nWarning: weak password (continuing).\n");
		} else {
			/* return -1; */
			return 1;
		}
	}
	if (!(cp = bb_askpass(0, "Re-enter new password: "))) {
		memset(orig, 0, sizeof orig);
		/* return -1; */
		return 1;
	}
	if (strcmp(cp, pass)) {
		fprintf(stderr, "Passwords do not match.\n");
		/* return -1; */
		return 1;
	}
	memset(cp, 0, strlen(cp));
	memset(orig, 0, sizeof(orig));
	memset(salt, 0, sizeof(salt));

	if (algo == 1) {
		strcpy(salt, "$1$");
		strcat(salt, crypt_make_salt());
		strcat(salt, crypt_make_salt());
		strcat(salt, crypt_make_salt());
	}

	strcat(salt, crypt_make_salt());
	cp = pw_encrypt(pass, salt);

	memset(pass, 0, sizeof pass);
	safe_strncpy(crypt_passwd, cp, sizeof(crypt_passwd));
	return 0;
}

static void set_filesize_limit(int blocks)
{
	struct rlimit rlimit_fsize;

	rlimit_fsize.rlim_cur = rlimit_fsize.rlim_max = 512L * blocks;
	setrlimit(RLIMIT_FSIZE, &rlimit_fsize);
}