/*
 * Copyright 2005-2018 Gentoo Foundation
 * Distributed under the terms of the GNU General Public License v2
 *
 * Copyright 2005-2010 Ned Ludd        - <solar@gentoo.org>
 * Copyright 2005-2014 Mike Frysinger  - <vapier@gentoo.org>
 * Copyright 2017-2018 Sam Besselink
 */

#ifdef APPLET_qtegrity

#define QTEGRITY_FLAGS "a:is" COMMON_FLAGS
static struct option const qtegrity_long_opts[] = {
	{"add",			a_argument, NULL, 'a'},
	{"ignore-non-existent", no_argument, NULL, 'i'},
	{"show-matches", no_argument, NULL, 's'},
/* TODO, add this functionality
	{"convert", a_argument, NULL, 'c'}
*/
	COMMON_LONG_OPTS
};
static const char * const qtegrity_opts_help[] = {
	"Add file to store of known-good digests",
	"Be silent if recorded file no longer exists",
	"Show recorded digests that match with known-good digests",
/* TODO
	"Convert known good digests to different hash function",
*/
	COMMON_OPTS_HELP
};
#define qtegrity_usage(ret) usage(ret, QTEGRITY_FLAGS, qtegrity_long_opts, qtegrity_opts_help, NULL, lookup_applet_idx("qtegrity"))

struct qtegrity_opt_state {
	bool ima;
	bool add;
	char* add_file;
	bool ignore_non_exist;
	bool show_matches;
/* TODO
	bool convert;
*/
};

#define FILE_SUCCESS 1
#define FILE_EMPTY 2
#define FILE_RELATIVE 3

#define SHA1_DIGEST_LENGTH 40
#define SHA256_PREFIX_LENGTH 8
#define SHA256_DIGEST_LENGTH 64
#define SHA256_LENGTH (SHA256_PREFIX_LENGTH + SHA256_DIGEST_LENGTH)
#define SHA512_DIGEST_LENGTH 128

static void external_check_sha(char * ret_digest, char * filepath, char * algo) {
	size_t size_digest = 1;

	if (strcmp(algo, "sha256") == 0) {
		size_digest = 64;
	} else if (strcmp(algo, "sha512") == 0) {
		size_digest = 128;
	}

	if ((strcmp(algo, "sha256") != 0) && (strcmp(algo, "sha512") != 0)) {
		return;
	}

	char cmd[11];
	snprintf(cmd, 10, "%ssum", algo);

	int pipefd[2];
	pid_t pid;

	if (pipe(pipefd) == -1) {
		perror("Couldn't create pipe to shasum\n");
		exit(1);
	}
	if ((pid = fork()) == -1) {
		perror("Couldn't fork to shasum\n");
		exit(1);
	}
	if (pid == 0)
	{
		/* Child. Redirect stdout and stderr to pipe, replace execution
		 * environment */
		close(pipefd[0]);
		dup2(pipefd[1], STDOUT_FILENO);
		dup2(pipefd[1], STDERR_FILENO);
		execlp(cmd, cmd, filepath, NULL);
		perror("Executing shasum failed\n");
		exit(1);
	}

	/* Only parent gets here. Listen to pipe */
	close(pipefd[1]);
	FILE* output = fdopen(pipefd[0], "r");
	if (output == NULL) {
		printf("Failed to run command '%s'\n", cmd);
		exit(1);
	}

	/* Read pipe line for line */
	while (fgets(ret_digest, size_digest+1, output))
	{
		if (strlen(ret_digest) == 64) /* Found what we need, can stop */
		{
			kill(pid, SIGKILL);
			break;
		}
	}

	pclose(output);
	return;
}

static void get_fname_from_line(char * line, char **ret, int digest_size, int offset)
{
	size_t dlenstr = strlen(line);
	char *p;
	/* Skip first 123 chars to get to file depends on digest_func in IMA */
	size_t skip = ((digest_size == SHA256_DIGEST_LENGTH) ||
			(digest_size == SHA512_DIGEST_LENGTH)) ?
		digest_size+offset+8 : digest_size+offset+6;

	if (dlenstr > skip) { /* assume file is at least two chars long */
		int segment_size = dlenstr - skip - 1;
		p = xmalloc(segment_size+1);
		memcpy(p, line + skip, segment_size);
		p[segment_size] = '\0';
	} else {
		/* E.g. digest used wrong hash algo, or malformed input */
		p = NULL;
	}

	*ret = p;
}

static void get_digest_from_line(char * line, char * ret, int digest_size, int offset)
{
	size_t dlenstr = strlen(line);
	/* Skip first chars to get to digest depends on digest_func in IMA */
	size_t skip = ((digest_size == SHA256_DIGEST_LENGTH) ||
			(digest_size == SHA512_DIGEST_LENGTH)) ?
		offset+8 : offset+6;

	if (dlenstr > (digest_size+skip+1)) {
		memcpy(ret, line+skip, digest_size);
		ret[digest_size] = '\0';
	}
}

static void get_known_good_digest(const char * fn_store, char * recorded_fname, char * ret, int recorded_digest_size)
{
	/* Open file with known good hashes */
	int fd_store;
	FILE *fp_store;

	fd_store = open(fn_store, O_RDONLY|O_CLOEXEC, 0);
	if (fd_store == -1) {
		warnp("unable to open(%s)", fn_store);
		exit(0);
	}
	if ((fp_store = fdopen(fd_store, "r")) == NULL) {
		warnp("unable to fopen(%s, r)", fn_store);
		close(fd_store);
		exit(0);
	}

	char *buffered_line, *line, *fname;
	size_t linelen;

	/* Iterate over lines in known-good-hashes-file; per line: if fname
	 * matches, grab hash. */
	buffered_line = line = fname = NULL;
	while (getline(&line, &linelen, fp_store) != -1) {
		free(buffered_line);
		buffered_line = xstrdup(line);

		get_fname_from_line(line, &fname, recorded_digest_size, 15);

		if (fname == NULL) {
			/* probably line without digest (e.g. symlink) */
			continue;
		}

		if (strcmp(recorded_fname, fname) == 0) {
			get_digest_from_line(line, ret, recorded_digest_size, 9);

			free(fname);
			break;
		}

		free(fname);
	}

	free(line);
	free(buffered_line);

	close(fd_store);
	fclose(fp_store);
}

static int get_size_digest(char * line)
{
	int ret = 0;

	char *pfound;
	/* find colon; it is boundary between end of hash func & begin of
	 * digest */
	pfound = strchr(line, ':');
	if (pfound != NULL) {
		int dpfound = pfound - line;
		int cutoff_prefix = 0;

		if (dpfound == 55 || dpfound == 6) {
			ret = SHA1_DIGEST_LENGTH;
		} else if (dpfound == 57) {
			cutoff_prefix = 51;
		} else if (dpfound == 8) {
			cutoff_prefix = 0;
		}

		int dsegment = dpfound - cutoff_prefix;

		char *line_segment;
		line_segment = xmalloc(dsegment + 1);
		/* chop off the first chars to get to the hash func */
		memcpy(line_segment, line + cutoff_prefix, dsegment);
		line_segment[dsegment] = '\0';

		/* If line segment equals name of hash func, then return
		 * relevant const. */
		if (strcmp(line_segment, "sha512") == 0) {
			ret = SHA512_DIGEST_LENGTH;
		} else if (strcmp(line_segment, "sha256") == 0) {
			ret = SHA256_DIGEST_LENGTH;
		} else {
			printf("Expected sha algo, got %s", line_segment);
		}

		free(line_segment);
	}

	return ret;
}

static int check_file(char * filename)
{
	/* TODO, this is 4096 too low, because this variable also holds
	 * path; for linux path is max 4096 chars */
	if (strlen(filename) > 255)
		err("Filename too long");

	if (filename[0] != '/') {
		return FILE_RELATIVE;
	}

	return FILE_SUCCESS;
}

int qtegrity_main(int argc, char **argv)
{
	int i;

	struct qtegrity_opt_state state = {
		.ima = true,
		.add = false,
		.ignore_non_exist = false,
		.show_matches = false,
/* TODO
		.convert = false;
*/
	};

	while ((i = GETOPT_LONG(QTEGRITY, qtegrity, "")) != -1) {
		switch (i) {
			COMMON_GETOPTS_CASES(qtegrity)
			case 'a':
				state.ima = false;
				state.add = true;
				if (check_file(optarg) == FILE_SUCCESS) {
					state.add_file = xstrdup(optarg);
				} else {
					err("Expected absolute file as argument, got '%s'", optarg);
				}
				break;
			case 'i': state.ignore_non_exist = true; break;
			case 's': state.show_matches = true; break;
		}
	}

	if (state.ima) {
		const char *fn_ima =
			"/sys/kernel/security/ima/ascii_runtime_measurements";
		int fd_ima;
		FILE *fp_ima;
		struct stat st;

		fd_ima = open(fn_ima, O_RDONLY|O_CLOEXEC, 0);
		if (fd_ima == -1) {
			/* TODO, shouldn't we explicitly remind user IMA/securityfs
			 * is needed? */
			warnp("Unable to open(%s)", fn_ima);
			exit(0);
		}
		if ((fp_ima = fdopen(fd_ima, "r")) == NULL) {
			warnp("Unable to fopen(%s, r)", fn_ima);
			close(fd_ima);
			exit(0);
		}

		char *buffered_line, *line, *recorded_fname;
		int recorded_digest_size = 0;
		size_t linelen;

		/* Iterate over IMA file, grab fname and digest, get known good
		 * digest for fname and compare */
		buffered_line = line = recorded_fname = NULL;
		while (getline(&line, &linelen, fp_ima) != -1) {
			char *recorded_digest;
			char *digest;

			free(buffered_line);
			buffered_line = xstrdup(line);

			if (buffered_line[0] != '1' || buffered_line[1] != '0')
				continue;

			recorded_digest_size = get_size_digest(buffered_line);
			recorded_digest = xmalloc(recorded_digest_size+1);
			recorded_digest[0] = '\0';

			/* grab fname from IMA file line */
			get_fname_from_line(buffered_line, &recorded_fname,
					recorded_digest_size, 51);
			/* grab digest from IMA file line, @TODO, check whether
			 * digest == 000etc */
			get_digest_from_line(buffered_line, recorded_digest,
					recorded_digest_size, 50);

			if (recorded_fname == NULL || recorded_digest == NULL) {
				printf("Empty recorded filename: %s\n", line);

				if (recorded_fname != NULL)
					free(recorded_fname);

				if (recorded_digest != NULL)
					free(recorded_digest);

				continue;
			}

			if (check_file(recorded_fname) == FILE_RELATIVE) {
				printf("Seems like a kernel process: %s\n", recorded_fname);

				free(recorded_fname);
				free(recorded_digest);
				continue;
			}

			if (stat(recorded_fname, &st) < 0) {
				if (!state.ignore_non_exist)
					printf("Couldn't access recorded file '%s'\n",
							recorded_fname);

				free(recorded_fname);
				free(recorded_digest);
				continue;
			}

			if (!(st.st_mode & S_IXUSR ||
						st.st_mode & S_IXGRP ||
						st.st_mode & S_IXOTH))
			{
				free(recorded_fname);
				free(recorded_digest);
				continue;
			}

			digest = xmalloc(recorded_digest_size+1);
			digest[0] = '\0';

			/* first try custom known good digests for fname */
			get_known_good_digest("/var/db/QTEGRITY_custom",
					recorded_fname, digest, recorded_digest_size);

			if (digest[0] == '\0') {
				digest[0] = '\0';
				/* then try from OS source */
				get_known_good_digest("/var/db/QTEGRITY",
						recorded_fname, digest, recorded_digest_size);

				if (digest[0] == '\0') {
					printf("No digest found for: %s\n", line);

					free(recorded_fname);
					free(recorded_digest);
					free(digest);
					continue;
				}
			}

			if (strcmp(recorded_digest, digest) != 0) {
				printf("Digest didn't match for %s\n", recorded_fname);
				printf("Known-good: '%s'...\nRecorded: '%s'\n\n",
						digest, recorded_digest);
			} else if (state.show_matches) {
				printf("Success! Digest matched for %s\n", recorded_fname);
			}

			free(recorded_fname);
			free(recorded_digest);
			free(digest);
		}

		free(line);
		free(buffered_line);

		close(fd_ima);
		fclose(fp_ima);
	} else if (state.add) {
		/* Add a single executable file+digest to the custom digest store */
		const char *fn_qtegrity_custom = "/var/db/QTEGRITY_custom";
		int fd_qtegrity_custom;
		FILE *fp_qtegrity_custom;
		struct stat st;
		int flush_status;

		fd_qtegrity_custom =
			open(fn_qtegrity_custom, O_RDWR|O_CREAT|O_CLOEXEC, 0);
		if (fd_qtegrity_custom == -1) {
			warnp("Unable to open(%s)", fn_qtegrity_custom);
			exit(0);
		}
		if ((fp_qtegrity_custom = fdopen(fd_qtegrity_custom, "w+")) == NULL) {
			warnp("Unable to fopen(%s, r)", fn_qtegrity_custom);
			close(fd_qtegrity_custom);
			exit(0);
		}

		printf("Adding %s to %s\n", state.add_file, fn_qtegrity_custom);

		if (stat(state.add_file, &st) < 0)
			err("Couldn't access file '%s'\n", state.add_file);

		if (!(st.st_mode & S_IXUSR ||
					st.st_mode & S_IXGRP ||
					st.st_mode & S_IXOTH))
			err("File '%s' is not executable\n", state.add_file);

		/* add digest */
		char *hash_algo = (char *)"sha256";
		char *file_digest;
		file_digest = xmalloc(SHA256_DIGEST_LENGTH+1);
		file_digest[0] = '\0';
		external_check_sha(file_digest, state.add_file, hash_algo);

		/* Iterate over lines; if fname matches, exit-loop */
		char *line, *fname;
		size_t linelen;
		int recorded_digest_size = 0;
		int skip = 0;
		line = fname = NULL;
		while (getline(&line, &linelen, fp_qtegrity_custom) != -1) {
			recorded_digest_size = get_size_digest(line);
			get_fname_from_line(line, &fname, recorded_digest_size, 5);

			/* probably line without digest (e.g. symlink) */
			if (fname == NULL)
				continue;

			if (strcmp(state.add_file, fname) == 0) {
				printf("Executable already recorded, "
						"replacing digest with %s\n", file_digest);
				skip = ((recorded_digest_size == SHA256_DIGEST_LENGTH) ||
						(recorded_digest_size == SHA512_DIGEST_LENGTH)) ?
					recorded_digest_size+6+8 : recorded_digest_size+6+6;
				fseek(fp_qtegrity_custom, -skip-strlen(fname), SEEK_CUR);
				free(fname);
				break;
			}

			free(fname);
		}

		free(line);

		fputs(hash_algo, fp_qtegrity_custom);
		fputs(":", fp_qtegrity_custom);
		fputs(file_digest, fp_qtegrity_custom);
		fputs(" file:", fp_qtegrity_custom);
		fputs(state.add_file, fp_qtegrity_custom);
		fputs("\n", fp_qtegrity_custom);

		flush_status = fflush(fp_qtegrity_custom);
		if (flush_status != 0)
			puts("Error flushing stream!");

		free(file_digest);
	}

	if (state.add)
		free(state.add_file);

	return EXIT_SUCCESS;
}

#else
DEFINE_APPLET_STUB(qtegrity)
#endif
