/*
 * keydb_keyring.c - Routines to fetch keys from a PGP keyring file.
 *
 * Copyright 2019 Jonathan McDowell <noodles@earth.li>
 *
 * This program is free software: you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; version 2 of the License.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "build-config.h"
#include "charfuncs.h"
#include "keyarray.h"
#include "keydb.h"
#include "keyid.h"
#include "keystructs.h"
#include "log.h"
#include "mem.h"
#include "onak.h"
#include "onak-conf.h"
#include "parsekey.h"

struct onak_keyring_dbctx {
	uint8_t *file;
	size_t   length;
	unsigned int space;
	unsigned int count;
	struct {
		struct openpgp_fingerprint fp;
		uint8_t *start;
		size_t len;
	} *keys;
};

/**
 *	starttrans - Start a transaction.
 *
 *	This is just a no-op for keyring file access.
 */
static bool keyring_starttrans(__unused struct onak_dbctx *dbctx)
{
	return true;
}

/**
 *	endtrans - End a transaction.
 *
 *	This is just a no-op for keyring file access.
 */
static void keyring_endtrans(__unused struct onak_dbctx *dbctx)
{
	return;
}

/**
 * keyring_fetch_key - fetch a key given its index
 */
static int keyring_fetch_key_idx(struct onak_keyring_dbctx *privctx,
		unsigned int index,
		struct openpgp_publickey **publickey)
{
	struct openpgp_packet_list *packets = NULL;
	struct buffer_ctx buf;

	if (index > privctx->count)
		return 0;

	buf.buffer = (char *) privctx->keys[index].start;
	buf.size = privctx->keys[index].len;
	buf.offset = 0;

	read_openpgp_stream(buffer_fetchchar, &buf, &packets, 0);
	parse_keys(packets, publickey);
	free_packet_list(packets);
	packets = NULL;

	return 1;
}

/*
 * We only index the primary fingerprint of the key, so will only return one
 * key at most from this function.
 */
static int keyring_fetch_key(struct onak_dbctx *dbctx,
			struct openpgp_fingerprint *fingerprint,
			struct openpgp_publickey **publickey,
			__unused bool intrans)
{
	struct onak_keyring_dbctx *privctx =
		(struct onak_keyring_dbctx *) dbctx->priv;
	int i;

	for (i = 0; i < privctx->count; i++) {
		if (fingerprint_cmp(fingerprint, &privctx->keys[i].fp) == 0)
			break;
	}

	if (i < privctx->count) {
		return keyring_fetch_key_idx(privctx, i, publickey);
	}

	return 0;
}

/**
 *	fetch_key_id - Given a keyid fetch the key from storage.
 *	@keyid: The keyid to fetch.
 *	@publickey: A pointer to a structure to return the key in.
 *	@intrans: If we're already in a transaction.
 */
static int keyring_fetch_key_id(struct onak_dbctx *dbctx,
		uint64_t keyid,
		struct openpgp_publickey **publickey,
		__unused bool intrans)
{
	struct onak_keyring_dbctx *privctx =
		(struct onak_keyring_dbctx *) dbctx->priv;
	int count, i;

	count = 0;
	for (i = 0; i < privctx->count; i++) {
		if (fingerprint2keyid(&privctx->keys[i].fp) == keyid) {
			if (keyring_fetch_key_idx(privctx, i, publickey))
				count++;
		}
	}

	return count;
}

/**
 *	store_key - Takes a key and stores it.
 *	@publickey: A pointer to the public key to store.
 *	@intrans: If we're already in a transaction.
 *	@update: If true the key exists and should be updated.
 *
 *	We don't support storing keys into a keyring file.
 */
static int keyring_store_key(__unused struct onak_dbctx *dbctx,
		__unused struct openpgp_publickey *publickey,
		__unused bool intrans,
		__unused bool update)
{
	return 0;
}

/**
 *	delete_key - Given a keyid delete the key from storage.
 *	@fp: The fingerprint of the key to delete.
 *	@intrans: If we're already in a transaction.
 *
 *	We don't support removing keys from a keyring file.
 */
static int keyring_delete_key(__unused struct onak_dbctx *dbctx,
		__unused struct openpgp_fingerprint *fp, __unused bool intrans)
{
	return 1;
}

/**
 *	fetch_key_text - Trys to find the keys that contain the supplied text.
 *	@search: The text to search for.
 *	@publickey: A pointer to a structure to return the key in.
 *
 *	This function searches for the supplied text and returns the keys that
 *	contain it.
 *
 *	TODO: Write for flat file access. Some sort of grep?
 */
static int keyring_fetch_key_text(__unused struct onak_dbctx *dbctx,
		__unused const char *search,
		__unused struct openpgp_publickey **publickey)
{
	return 0;
}

/**
 *	iterate_keys - call a function once for each key in the db.
 *	@iterfunc: The function to call.
 *	@ctx: A context pointer
 *
 *	Calls iterfunc once for each key in the database. ctx is passed
 *	unaltered to iterfunc. This function is intended to aid database dumps
 *	and statistic calculations.
 *
 *	Returns the number of keys we iterated over.
 */
static int keyring_iterate_keys(struct onak_dbctx *dbctx,
		void (*iterfunc)(void *ctx, struct openpgp_publickey *key),
		void *ctx)
{
	struct onak_keyring_dbctx *privctx =
		(struct onak_keyring_dbctx *) dbctx->priv;
	struct openpgp_publickey  *key = NULL;
	int count, i;

	count = 0;
	for (i = 0; i < privctx->count; i++) {
		if (keyring_fetch_key_idx(privctx, i, &key)) {
			iterfunc(ctx, key);
			free_publickey(key);
			key = NULL;
		}
	}

	return count;
}

static int keyring_update_keys(__unused struct onak_dbctx *dbctx,
		__unused struct openpgp_publickey **keys,
		__unused struct keyarray *blacklist,
		__unused bool updateonly,
		__unused bool sendsync)
{
	return 0;
}

/*
 * Include the basic keydb routines.
 */
#define NEED_KEYID2UID 1
#define NEED_GETKEYSIGS 1
#include "keydb.c"

static int keyring_parse_keys(struct onak_keyring_dbctx *privctx)
{
	size_t len, pos, start, totlen;
	struct openpgp_publickey *key;
	uint8_t tag;

	if (privctx == NULL) {
		return 0;
	}

	if (privctx->file == NULL) {
		return 0;
	}

	/*
	 * Walk the keyring file, noting the start of each public key and the
	 * total length of packets associated with it.
	 */
	len = pos = start = totlen = 0;
	while (((privctx->length - pos) > 5) && (privctx->file[pos] & 0x80)) {
		if (privctx->file[pos] & 0x40) {
			tag = privctx->file[pos] & 0x3F;
			len = privctx->file[pos + 1];
			if (len > 191 && len < 224) {
				len -= 192;
				len <<= 8;
				len += privctx->file[pos + 2];
				len += 192;
				len += 1; /* Header */
			} else if (len > 223 && len < 255) {
				// Unsupported
			} else if (len == 255) {
				len = privctx->file[pos + 2];
				len <<= 8;
				len += privctx->file[pos + 3];
				len <<= 8;
				len += privctx->file[pos + 4];
				len <<= 8;
				len += privctx->file[pos + 5];
				len += 4; /* Header */
			}
			len += 2; /* Header */
		} else {
			tag = (privctx->file[pos] & 0x3C) >> 2;
			switch (privctx->file[pos] & 3) {
			case 0:
				len = privctx->file[pos + 1];
				len += 2; /* Header */
				break;
			case 1:
				len = privctx->file[pos + 1];
				len <<= 8;
				len += privctx->file[pos + 2];
				len += 3; /* Header */
				break;
			case 2:
				len = privctx->file[pos + 1];
				len <<= 8;
				len += privctx->file[pos + 2];
				len <<= 8;
				len += privctx->file[pos + 3];
				len <<= 8;
				len += privctx->file[pos + 4];
				len += 5; /* Header */
				break;
			case 3:
				// Unsupported
				break;
			}
		}
		if (tag == OPENPGP_PACKET_PUBLICKEY) {
			if (totlen > 0) {
				/* Expand the array of keys if necessary */
				if (privctx->count == privctx->space) {
					privctx->space *= 2;
					privctx->keys = realloc(privctx->keys,
						privctx->space *
						sizeof(*privctx->keys));
				}

				/* TODO: Sort by fingerprint? */
				privctx->keys[privctx->count].start =
					&privctx->file[start];
				privctx->keys[privctx->count].len = totlen;
				privctx->count++;

				/*
				 * We need to fetch the key to calculate the
				 * fingerprint.
				 */
				keyring_fetch_key_idx(privctx,
						privctx->count - 1,
						&key);
				get_fingerprint(key->publickey,
					&privctx->keys[privctx->count - 1].fp);
				free_publickey(key);
				key = NULL;
			}
			start = pos;
			totlen = 0;
		}
		totlen += len;
		pos += len;
	}

	return privctx->count;
}

/**
 *	cleanupdb - De-initialize the key database.
 *
 *	This is just a no-op for flat file access.
 */
static void keyring_cleanupdb(struct onak_dbctx *dbctx)
{
	struct onak_keyring_dbctx *privctx =
		(struct onak_keyring_dbctx *) dbctx->priv;

	if (dbctx->priv != NULL) {
		if (privctx->file != NULL) {
			munmap(privctx->file, privctx->length);
		}
		free(privctx->keys);
		free(dbctx->priv);
		dbctx->priv = NULL;
	}

	if (dbctx != NULL) {
		free(dbctx);
	}
}

/**
 *	initdb - Initialize the key database.
 *
 *	This is just a no-op for flat file access.
 */
struct onak_dbctx *keydb_keyring_init(struct onak_db_config *dbcfg,
		__unused bool readonly)
{
	struct onak_keyring_dbctx *privctx;
	struct onak_dbctx *dbctx;
	struct stat sb;
	int fd;

	dbctx = malloc(sizeof(struct onak_dbctx));
	if (dbctx == NULL) {
		return NULL;
	}
	dbctx->config = dbcfg;
	dbctx->priv = privctx = calloc(1, sizeof(*privctx));
	if (privctx == NULL) {
		free(dbctx);
		return NULL;
	}
	privctx->space = 16;
	privctx->keys = calloc(privctx->space, sizeof(*privctx->keys));

	fd = open(dbcfg->location, O_RDONLY);
	if (fd < 0) {
		logthing(LOGTHING_CRITICAL,
				"Couldn't open keyring file %s: %s (%d)",
				dbcfg->location,
				strerror(errno),
				errno);
		keyring_cleanupdb(dbctx);
		return NULL;
	}
	if (fstat(fd, &sb) < 0) {
		logthing(LOGTHING_CRITICAL,
				"Couldn't stat keyring file %s: %s (%d)",
				dbcfg->location,
				strerror(errno),
				errno);
		close(fd);
		keyring_cleanupdb(dbctx);
		return NULL;
	}
	privctx->file = mmap(NULL, sb.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
	if (privctx->file == MAP_FAILED) {
		logthing(LOGTHING_CRITICAL,
				"Couldn't mmap keyring file %s: %s (%d)",
				dbcfg->location,
				strerror(errno),
				errno);
		privctx->file = NULL;
		close(fd);
		keyring_cleanupdb(dbctx);
		return NULL;
	}
	privctx->length = sb.st_size;
	close(fd);

	if (keyring_parse_keys(privctx) == 0) {
		logthing(LOGTHING_CRITICAL,
				"Failed to load any keys from keyring file %s",
				dbcfg->location);
		keyring_cleanupdb(dbctx);
		return NULL;
	}

	dbctx->cleanupdb		= keyring_cleanupdb;
	dbctx->starttrans		= keyring_starttrans;
	dbctx->endtrans			= keyring_endtrans;
	dbctx->fetch_key		= keyring_fetch_key;
	/* We don't index by subkey fingerprint, so fallback to fetch_key */
	dbctx->fetch_key_fp		= keyring_fetch_key;
	dbctx->fetch_key_id		= keyring_fetch_key_id;
	dbctx->fetch_key_text		= keyring_fetch_key_text;
	dbctx->store_key		= keyring_store_key;
	dbctx->update_keys		= keyring_update_keys;
	dbctx->delete_key		= keyring_delete_key;
	dbctx->getkeysigs		= generic_getkeysigs;
	dbctx->cached_getkeysigs	= generic_cached_getkeysigs;
	dbctx->keyid2uid		= generic_keyid2uid;
	dbctx->iterate_keys		= keyring_iterate_keys;

	return dbctx;
}
