// SPDX-License-Identifier: GPL-2.0-only
/*
 * Copyright (C) 2015, Trustees of Indiana University
 *
 * Copyright (c) 2016, Intel Corporation.
 *
 * Author: Jeremy Filizetti <jfilizet@iu.edu>
 */

#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <linux/lustre/lustre_user.h>

#include "sk_utils.h"
#include "err_util.h"

#ifndef _GNU_SOURCE
#define _GNU_SOURCE
#endif

/* One week default expiration */
#define SK_DEFAULT_EXPIRE 604800
/* But only one day in FIPS mode */
#define SK_DEFAULT_EXPIRE_FIPS 86400
#define SK_DEFAULT_SK_KEYLEN 256
#define SK_DEFAULT_PRIME_BITS 2048
#define SK_DEFAULT_NODEMAP "default"

static void usage(FILE *fp, char *program)
{
	int i;

	fprintf(fp, "Usage %s [OPTIONS] {-l|-m|-r|-w} <keyfile>\n", program);
	fprintf(fp, "-l|--load       <keyfile>	Load key from file into user's "
		"session keyring\n");
	fprintf(fp, "-m|--modify     <keyfile>	Modify keyfile's attributes\n");
	fprintf(fp, "-r|--read       <keyfile>	Show keyfile's attributes\n");
	fprintf(fp, "-w|--write      <keyfile>	Generate keyfile\n\n");
	fprintf(fp, "Modify/Write Options:\n");
	fprintf(fp, "-a|--ascii      <keyfile>	Output key in ASCII-encoded format\n");
	fprintf(fp, "-c|--crypt      <num>	Cipher for encryption "
		"(Default: AES Counter mode)\n");
	for (i = 1; i < ARRAY_SIZE(sk_crypt_algs); i++)
		fprintf(fp, "                        %s\n",
			sk_crypt_algs[i].sct_name);
	fprintf(fp, "-i|--hmac       <num>	Hash algorithm for integrity "
		"(Default: SHA256)\n");
	for (i = 1; i < ARRAY_SIZE(sk_hmac_algs); i++)
		fprintf(fp, "                        %s\n",
			sk_hmac_algs[i].sht_name);
	fprintf(fp, "-e|--expire     <num>	Seconds before contexts from "
		"key expire (Default: %d seconds (%.3g days))\n",
		SK_DEFAULT_EXPIRE, (double)SK_DEFAULT_EXPIRE / 3600 / 24);
	fprintf(fp, "-f|--fsname     <name>	File system name for key\n");
	fprintf(fp, "-g|--mgsnids    <nids>	Comma seperated list of MGS "
		"NIDs.  Only required when mgssec is used (Default: \"\")\n");
	fprintf(fp, "-n|--nodemap    <name>	Nodemap name for key "
		"(Default: \"%s\")\n", SK_DEFAULT_NODEMAP);
	fprintf(fp, "-p|--prime-bits <len>	Prime length (p) for DHKE in "
		"bits (Default: %d)\n", SK_DEFAULT_PRIME_BITS);
	fprintf(fp, "-t|--type       <type>	Key type (mgs, server, "
		"client)\n");
	fprintf(fp, "-k|--key-bits   <len>	Shared key length in bits "
		"(Default: %d)\n", SK_DEFAULT_SK_KEYLEN);
	fprintf(fp, "-d|--data       <file>	Key data source for new keys "
		"(Default: /dev/random)\n");
	fprintf(fp, "                        Not a seed value. "
		"This is the actual key value.\n\n");
	fprintf(fp, "Other Options:\n");
	fprintf(fp, "-v|--verbose           Increase verbosity for errors\n");
	exit(EXIT_FAILURE);
}

static ssize_t get_key_data(char *src, void *buffer, size_t bits)
{
	char *ptr = buffer;
	size_t remain;
	ssize_t rc;
	int fd;

	/* convert bits to minimum number of bytes */
	remain = (bits + 7) / 8;

	printf("Reading random data for shared key from '%s'\n", src);
	fd = open(src, O_RDONLY);
	if (fd < 0) {
		fprintf(stderr, "error: opening '%s': %s\n", src,
			strerror(errno));
		return -errno;
	}

	while (remain > 0) {
		rc = read(fd, ptr, remain);
		if (rc < 0) {
			if (errno == EINTR)
				continue;
			fprintf(stderr, "error: reading from '%s': %s\n", src,
				strerror(errno));
			rc = -errno;
			goto out;

		} else if (rc == 0) {
			fprintf(stderr,
				"error: key source too short for %zd-bit key\n",
				bits);
			rc = -ENODATA;
			goto out;
		}
		ptr += rc;
		remain -= rc;
	}
	rc = 0;

out:
	close(fd);
	return rc;
}

static int print_config(char *filename)
{
	struct sk_keyfile_config *config;
	int i;

	config = sk_read_file(filename);
	if (!config)
		return EXIT_FAILURE;

	if (sk_validate_config(config)) {
		fprintf(stderr, "error: key configuration failed validation\n");
		free(config);
		return EXIT_FAILURE;
	}

	printf("Version:        %u\n", config->skc_version);
	printf("Type:          ");
	if (config->skc_type & SK_TYPE_MGS)
		printf(" mgs");
	if (config->skc_type & SK_TYPE_SERVER)
		printf(" server");
	if (config->skc_type & SK_TYPE_CLIENT)
		printf(" client");
	printf("\n");
	printf("HMAC alg:       %s\n", sk_hmac2name(config->skc_hmac_alg));
	printf("Crypto alg:     %s\n", sk_crypt2name(config->skc_crypt_alg));
	printf("Ctx Expiration: %u seconds\n", config->skc_expire);
	printf("Shared keylen:  %u bits\n", config->skc_shared_keylen);
	printf("Prime length:   %u bits\n", config->skc_prime_bits);
	printf("File system:    %s\n", config->skc_fsname);
	printf("MGS NIDs:      ");
	for (i = 0; i < MAX_MGSNIDS; i++) {
		if (config->skc_mgsnids[i] == LNET_NID_ANY)
			continue;
		printf(" %s", libcfs_nid2str(config->skc_mgsnids[i]));
	}
	printf("\n");
	printf("Nodemap name:   %s\n", config->skc_nodemap);
	printf("Shared key:\n");
	print_hex(0, config->skc_shared_key, config->skc_shared_keylen / 8);

	/* Don't print empty keys */
	for (i = 0; i < SK_MAX_P_BYTES; i++)
		if (config->skc_p[i] != 0)
			break;

	if (i != SK_MAX_P_BYTES) {
		printf("Prime (p):\n");
		print_hex(0, config->skc_p, config->skc_prime_bits / 8);
	}

	free(config);
	return EXIT_SUCCESS;
}

static int parse_mgsnids(char *mgsnids, struct sk_keyfile_config *config)
{
	lnet_nid_t nid;
	char *ptr;
	char *sep;
	char *end;
	int rc = 0;
	int i;

	/* replace all old values */
	for (i = 0; i < MAX_MGSNIDS; i++)
		config->skc_mgsnids[i] = LNET_NID_ANY;

	i = 0;
	end = mgsnids + strlen(mgsnids);
	ptr = mgsnids;
	while (ptr < end && i < MAX_MGSNIDS) {
		sep = strstr(ptr, ",");
		if (sep != NULL)
			*sep = '\0';

		nid = libcfs_str2nid(ptr);
		if (nid == LNET_NID_ANY) {
			fprintf(stderr, "error: invalid MGS NID: %s\n", ptr);
			rc = -EINVAL;
			break;
		}

		config->skc_mgsnids[i++] = nid;
		ptr += strlen(ptr) + 1;
	}

	if (i == MAX_MGSNIDS) {
		fprintf(stderr, "error: more than %u MGS NIDs provided\n", i);
		rc = -E2BIG;
	}

	return rc;
}

static bool check_orig_ascii_format(const char *filename)
{
	struct stat st;
	char *file_data = NULL;
	bool is_ascii = false;
	ssize_t bytes_read;
	int fd, rc;

	fd = open(filename, O_RDONLY);
	if (fd < 0)
		return false;

	rc = fstat(fd, &st);
	if (rc || st.st_size == 0)
		goto cleanup;

	file_data = malloc(st.st_size + 1);
	if (!file_data)
		goto cleanup;

	bytes_read = read(fd, file_data, st.st_size);
	if (bytes_read > 0) {
		file_data[bytes_read] = '\0';
		is_ascii = sk_is_ascii_encoded(file_data, bytes_read);
	}

cleanup:
	free(file_data);
	close(fd);
	return is_ascii;
}

int main(int argc, char **argv)
{
	struct sk_keyfile_config *config;
	char *datafile = NULL;
	char *input = NULL;
	char *load = NULL;
	char *modify = NULL;
	char *output = NULL;
	char *mgsnids = NULL;
	char *nodemap = NULL;
	char *fsname = NULL;
	char *tmp;
	char *tmp2;
	int crypt = SK_CRYPT_EMPTY;
	int hmac = SK_HMAC_EMPTY;
	int expire = -1;
	int shared_keylen = -1;
	int prime_bits = -1;
	int verbose = 0;
	bool ascii = false;
	int i;
	int opt;
	enum sk_key_type type = SK_TYPE_INVALID;
	bool generate_prime = false;

	static struct option long_opts[] = {
	{ .name = "ascii",	.has_arg = no_argument,	      .val = 'a'},
	{ .name = "crypt",	.has_arg = required_argument, .val = 'c'},
	{ .name = "data",	.has_arg = required_argument, .val = 'd'},
	{ .name = "expire",	.has_arg = required_argument, .val = 'e'},
	{ .name = "fsname",	.has_arg = required_argument, .val = 'f'},
	{ .name = "mgsnids",	.has_arg = required_argument, .val = 'g'},
	{ .name = "help",	.has_arg = no_argument,	      .val = 'h'},
	{ .name = "hmac",	.has_arg = required_argument, .val = 'i'},
	{ .name = "integrity",	.has_arg = required_argument, .val = 'i'},
	{ .name = "key-bits",	.has_arg = required_argument, .val = 'k'},
	{ .name = "shared",	.has_arg = required_argument, .val = 'k'},
	{ .name = "load",	.has_arg = required_argument, .val = 'l'},
	{ .name = "modify",	.has_arg = required_argument, .val = 'm'},
	{ .name = "nodemap",	.has_arg = required_argument, .val = 'n'},
	{ .name = "prime-bits",	.has_arg = required_argument, .val = 'p'},
	{ .name = "read",	.has_arg = required_argument, .val = 'r'},
	{ .name = "type",	.has_arg = required_argument, .val = 't'},
	{ .name = "verbose",	.has_arg = no_argument,	      .val = 'v'},
	{ .name = "write",	.has_arg = required_argument, .val = 'w'},
	{ .name = NULL, } };

	while ((opt = getopt_long(argc, argv,
				  "ac:d:e:f:g:hi:l:m:n:p:r:s:k:t:w:v",
				  long_opts, NULL)) != EOF) {
		switch (opt) {
		case 'a':
			ascii = true;
			break;
		case 'c':
			crypt = sk_name2crypt(optarg);
			break;
		case 'd':
			datafile = optarg;
			break;
		case 'e':
			expire = atoi(optarg);
			if (expire < 60)
				fprintf(stderr, "warning: using a %us key "
					"expiration may cause issues during "
					"key renegotiation\n", expire);
			break;
		case 'f':
			fsname = optarg;
			if (strlen(fsname) > MTI_NAME_MAXLEN) {
				fprintf(stderr,
					"error: file system name longer than "
					"%u characters\n", MTI_NAME_MAXLEN);
				return EXIT_FAILURE;
			}
			break;
		case 'g':
			mgsnids = optarg;
			break;
		case 'h':
			usage(stdout, argv[0]);
			break;
		case 'i':
			hmac = sk_name2hmac(optarg);
			break;
		case 'k':
			shared_keylen = atoi(optarg);
			break;
		case 'l':
			load = optarg;
			break;
		case 'm':
			modify = optarg;
			break;
		case 'n':
			nodemap = optarg;
			if (strlen(nodemap) > LUSTRE_NODEMAP_NAME_LENGTH) {
				fprintf(stderr,
					"error: nodemap name longer than "
					"%u characters\n",
					LUSTRE_NODEMAP_NAME_LENGTH);
				return EXIT_FAILURE;
			}
			break;
		case 'p':
			prime_bits = atoi(optarg);
			if (prime_bits <= 0) {
				fprintf(stderr,
					"error: invalid prime length: '%s'\n",
					optarg);
				return EXIT_FAILURE;
			}
			break;
		case 'r':
			input = optarg;
			break;
		case 't':
			tmp2 = strdup(optarg);
			if (!tmp2) {
				fprintf(stderr,
					"error: failed to allocate type\n");
				return EXIT_FAILURE;
			}
			tmp = strsep(&tmp2, ",");
			while (tmp != NULL) {
				if (strcasecmp(tmp, "server") == 0) {
					type |= SK_TYPE_SERVER;
				} else if (strcasecmp(tmp, "mgs") == 0) {
					type |= SK_TYPE_MGS;
				} else if (strcasecmp(tmp, "client") == 0) {
					type |= SK_TYPE_CLIENT;
				} else {
					fprintf(stderr,
						"error: invalid type '%s', "
						"must be mgs, server, or client"
						"\n", optarg);
					return EXIT_FAILURE;
				}
				tmp = strsep(&tmp2, ",");
			}
			free(tmp2);
			break;
		case 'v':
			verbose++;
			break;
		case 'w':
			output = optarg;
			break;
		default:
			fprintf(stderr, "error: unknown option: '%c'\n", opt);
			return EXIT_FAILURE;
			break;
		}
	}

	if (optind != argc) {
		fprintf(stderr,
			"error: extraneous arguments provided, check usage\n");
		return EXIT_FAILURE;
	}

	if (!input && !output && !load && !modify) {
		usage(stderr, argv[0]);
		return EXIT_FAILURE;
	}

	/* init gss logger for foreground (no syslog) which prints to stderr */
	initerr(NULL, verbose, 1);

	fips_mode = FIPS_mode();

	if (input)
		return print_config(input);

	if (load) {
		int rc = sk_load_keyfile(load, type & SK_TYPE_CLIENT);

		if (rc < 0) {
			fprintf(stderr,
				"error: loading keyfile failed: rc=%d\n", rc);
			return EXIT_FAILURE;
		}
		return EXIT_SUCCESS;
	}

	if (crypt == SK_CRYPT_INVALID) {
		fprintf(stderr, "error: invalid crypt algorithm specified\n");
		return EXIT_FAILURE;
	}
	if (hmac == SK_HMAC_INVALID) {
		fprintf(stderr, "error: invalid HMAC algorithm specified\n");
		return EXIT_FAILURE;
	}

	if (modify && datafile) {
		fprintf(stderr,
			"error: data file option not valid in key modify\n");
		return EXIT_FAILURE;
	}

	if (modify) {
		/* Check if the original file was in ASCII format */
		bool original_ascii = check_orig_ascii_format(modify);

		config = sk_read_file(modify);
		if (!config)
			return EXIT_FAILURE;

		/* Preserve original format unless explicitly overridden */
		if (original_ascii && !ascii)
			ascii = true;

		if (type != SK_TYPE_INVALID) {
			/* generate key when adding client type */
			if (!(config->skc_type & SK_TYPE_CLIENT) &&
			    type & SK_TYPE_CLIENT)
				generate_prime = true;
			else if (!(type & SK_TYPE_CLIENT))
				memset(config->skc_p, 0, SK_MAX_P_BYTES);

			config->skc_type = type;
		}
		if (prime_bits != -1) {
			memset(config->skc_p, 0, SK_MAX_P_BYTES);
			if (config->skc_prime_bits != prime_bits &&
			    config->skc_type & SK_TYPE_CLIENT)
				generate_prime = true;
		}
	} else {
		/* write mode for a new key */
		if (!fsname && !mgsnids) {
			fprintf(stderr,
				"error: missing --fsname or --mgsnids\n");
			return EXIT_FAILURE;
		}

		config = calloc(1, sizeof(*config));
		if (!config)
			return EXIT_FAILURE;

		/* Set the defaults for new key */
		config->skc_version = SK_CONF_VERSION;
		config->skc_expire = fips_mode ?
			SK_DEFAULT_EXPIRE_FIPS : SK_DEFAULT_EXPIRE;
		config->skc_shared_keylen = SK_DEFAULT_SK_KEYLEN;
		config->skc_prime_bits = SK_DEFAULT_PRIME_BITS;
		config->skc_crypt_alg = SK_CRYPT_AES256_CTR;
		config->skc_hmac_alg = SK_HMAC_SHA256;
		for (i = 0; i < MAX_MGSNIDS; i++)
			config->skc_mgsnids[i] = LNET_NID_ANY;

		if (type == SK_TYPE_INVALID) {
			fprintf(stderr, "error: no type specified for key\n");
			goto error;
		}
		config->skc_type = type;
		generate_prime = type & SK_TYPE_CLIENT;

		/* SK_DEFAULT_NODEMAP is made to fit in skc_nodemap */
		strcpy(config->skc_nodemap, SK_DEFAULT_NODEMAP);

		if (!datafile)
			datafile = "/dev/random";
	}

	if (crypt != SK_CRYPT_EMPTY)
		config->skc_crypt_alg = crypt;
	if (hmac != SK_HMAC_EMPTY)
		config->skc_hmac_alg = hmac;
	if (expire != -1)
		config->skc_expire = expire;
	if (fips_mode && config->skc_expire > SK_DEFAULT_EXPIRE_FIPS)
		fprintf(stderr,
			"warning: using a %us key expiration greater than %us is not recommended in FIPS mode\n",
			config->skc_expire, SK_DEFAULT_EXPIRE_FIPS);
	if (shared_keylen != -1)
		config->skc_shared_keylen = shared_keylen;
	if (prime_bits != -1) {
#ifdef HAVE_OPENSSL_EVP_PKEY
		/* #define DH_MIN_MODULUS_BITS 512, not exported by OpenSSL */
		if (prime_bits < 512) {
			fprintf(stderr,
				"error: prime length must be at least 512\n");
			return EXIT_FAILURE;
		}
#endif
		config->skc_prime_bits = prime_bits;
	}
	if (fsname)
		/* fsname string length was checked when parsing
		 * command-line options
		 */
		strcpy(config->skc_fsname, fsname);
	if (nodemap)
		/* nodemap string length was checked when parsing
		 * command-line options
		 */
		strcpy(config->skc_nodemap, nodemap);
	if (mgsnids && parse_mgsnids(mgsnids, config))
		goto error;
	if (sk_validate_config(config)) {
		fprintf(stderr, "error: key configuration failed validation\n");
		goto error;
	}

	if (datafile && get_key_data(datafile, config->skc_shared_key,
				     config->skc_shared_keylen)) {
		fprintf(stderr, "error: failure getting key data from '%s'\n",
			datafile);
		goto error;
	}

	if (generate_prime) {
		printf("Generating DH parameters, this can take a while...\n");
		if (gen_ssk_prime(config))
			goto error;
	}

	if (write_config_file(modify ?: output, config, modify, ascii))
		goto error;

	return EXIT_SUCCESS;

error:
	free(config);
	return EXIT_FAILURE;
}
