/* See LICENSE file for copyright and license details. */
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/syscall.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <pwd.h>
#include <shadow.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "ubase/config.h"
#include "ubase/passwd.h"
#include "ubase/text.h"
#include "ubase/util.h"

static FILE *
spw_get_file(const char *user)
{
	FILE *fp = NULL;
	char file[PATH_MAX];
	int r;

	r = snprintf(file, sizeof(file), "/etc/tcb/%s/shadow", user);
	if (r < 0 || (size_t)r >= sizeof(file))
		eprintf("snprintf:");
	fp = fopen(file, "r+");
	if (!fp)
		fp = fopen("/etc/shadow", "r+");
	return fp;
}

static int
spw_write_file(FILE *fp, const struct spwd *spw, char *pwhash)
{
	struct spwd *spwent;
	int r = -1, w = 0;
	FILE *tfp = NULL;

	/* write to temporary file. */
	tfp = tmpfile();
	if (!tfp) {
		weprintf("tmpfile:");
		goto cleanup;
	}
	while ((spwent = fgetspent(fp))) {
		/* update entry on name match */
		if (strcmp(spwent->sp_namp, spw->sp_namp) == 0) {
			spwent->sp_pwdp = pwhash;
			w++;
		}
		errno = 0;
		if (putspent(spwent, tfp) == -1) {
			weprintf("putspent:");
			goto cleanup;
		}
	}
	if (!w) {
		weprintf("shadow: no matching entry to write to\n");
		goto cleanup;
	}
	fflush(tfp);

	if (fseek(fp, 0, SEEK_SET) == -1 || fseek(tfp, 0, SEEK_SET) == -1) {
		weprintf("fseek:");
		goto cleanup;
	}

	/* write temporary file to (tcb) shadow file */
	concat(tfp, "tmpfile", fp, "shadow");
	ftruncate(fileno(fp), ftell(tfp));

	r = 0; /* success */
cleanup:
	if (tfp)
		fclose(tfp);
	return r;
}

static int
pw_write_file(FILE *fp, const struct passwd *pw, char *pwhash) {
	struct passwd *pwent;
	int r = -1, w = 0;
	FILE *tfp = NULL;

	/* write to temporary file. */
	tfp = tmpfile();
	if (!tfp) {
		weprintf("tmpfile:");
		goto cleanup;
	}
	while ((pwent = fgetpwent(fp))) {
		/* update entry on name match */
		if (strcmp(pwent->pw_name, pw->pw_name) == 0) {
			pwent->pw_passwd = pwhash;
			w++;
		}
		errno = 0;
		if (putpwent(pwent, tfp) == -1) {
			weprintf("putpwent:");
			goto cleanup;
		}
	}
	if (!w) {
		weprintf("passwd: no matching entry to write to\n");
		goto cleanup;
	}
	fflush(tfp);

	if (fseek(fp, 0, SEEK_SET) == -1 || fseek(tfp, 0, SEEK_SET) == -1) {
		weprintf("fseek:");
		goto cleanup;
	}

	/* write to passwd file. */
	concat(tfp, "tmpfile", fp, "passwd");
	ftruncate(fileno(fp), ftell(tfp));

	r = 0; /* success */
cleanup:
	if (tfp)
		fclose(tfp);
	return r;
}

/* generates a random base64-encoded salt string of length 16 */
static void
gensalt(char *s)
{
	static const char b64[] = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
	uint8_t buf[12];
	uint32_t n;
	int i;

	if (syscall(SYS_getrandom, buf, sizeof(buf), 0) < 0)
		eprintf("getrandom:");
	for (i = 0; i < 12; i += 3) {
		n = buf[i] << 16 | buf[i+1] << 8 | buf[i+2];
		*s++ = b64[n%64]; n /= 64;
		*s++ = b64[n%64]; n /= 64;
		*s++ = b64[n%64]; n /= 64;
		*s++ = b64[n];
	}
	*s++ = '\0';
}

static void
usage(void)
{
	eprintf("usage: %s [username]\n", argv0);
}

int
main(int argc, char *argv[])
{
	char *cryptpass1 = NULL, *cryptpass2 = NULL, *cryptpass3 = NULL;
	char *inpass, *p, *prevhash = NULL, salt[sizeof(PW_CIPHER) + 16] = PW_CIPHER;
	struct passwd *pw;
	struct spwd *spw = NULL;
	FILE *fp = NULL;
	int r = -1, status = 1;

	ARGBEGIN {
	default:
		usage();
	} ARGEND;

	pw_init();
	umask(077);

	errno = 0;
	if (argc == 0)
		pw = getpwuid(getuid());
	else
		pw = getpwnam(argv[0]);
	if (!pw) {
		if (errno)
			eprintf("getpwnam: %s:", argv[0]);
		else
			eprintf("who are you?\n");
	}

	/* is using shadow entry ? */
	if (pw->pw_passwd[0] == 'x' && pw->pw_passwd[1] == '\0') {
		errno = 0;
		spw = getspnam(pw->pw_name);
		if (!spw) {
			if (errno)
				eprintf("getspnam: %s:", pw->pw_name);
			else
				eprintf("who are you?\n");
		}
	}

	/* Flush pending input */
	ioctl(0, TCFLSH, (void *)0);

	if (getuid() == 0) {
		goto newpass;
	} else {
		if (pw->pw_passwd[0] == '!' ||
		    pw->pw_passwd[0] == '*')
			eprintf("denied\n");
		if (pw->pw_passwd[0] == '\0') {
			goto newpass;
		}
		if (pw->pw_passwd[0] == 'x' &&
		    pw->pw_passwd[1] == '\0')
			prevhash = spw->sp_pwdp;
		else
			prevhash = pw->pw_passwd;
	}

	printf("Changing password for %s\n", pw->pw_name);
	inpass = getpass("Old password: ");
	if (!inpass)
		eprintf("getpass:");
	if (inpass[0] == '\0')
		eprintf("no password supplied\n");
	p = crypt(inpass, prevhash);
	if (!p)
		eprintf("crypt:");
	cryptpass1 = estrdup(p);
	if (strcmp(cryptpass1, prevhash) != 0)
		eprintf("incorrect password\n");

newpass:
	inpass = getpass("Enter new password: ");
	if (!inpass)
		eprintf("getpass:");
	if (inpass[0] == '\0')
		eprintf("no password supplied\n");

	if(prevhash) {
		p = crypt(inpass, prevhash);
		if (!p)
			eprintf("crypt:");
		if (cryptpass1 && strcmp(cryptpass1, p) == 0)
			eprintf("password left unchanged\n");
	}
	gensalt(salt + strlen(salt));
	p = crypt(inpass, salt);
	if (!p)
		eprintf("crypt:");
	cryptpass2 = estrdup(p);

	/* Flush pending input */
	ioctl(0, TCFLSH, (void *)0);

	inpass = getpass("Retype new password: ");
	if (!inpass)
		eprintf("getpass:");
	if (inpass[0] == '\0')
		eprintf("no password supplied\n");
	p = crypt(inpass, salt);
	if (!p)
		eprintf("crypt:");
	cryptpass3 = estrdup(p);
	if (strcmp(cryptpass2, cryptpass3) != 0)
		eprintf("passwords don't match\n");

	fp = spw_get_file(pw->pw_name);
	if (fp) {
		r = spw_write_file(fp, spw, cryptpass3);
	} else {
		fp = fopen("/etc/passwd", "r+");
		if (fp)
			r = pw_write_file(fp, pw, cryptpass3);
		else
			weprintf("fopen:");
	}
	if (!r)
		status = 0;

	if (fp)
		fclose(fp);
	free(cryptpass3);
	free(cryptpass2);
	free(cryptpass1);

	return status;
}
