#include "system.h"

#include <string>

#include <fcntl.h>
#include <unistd.h>
#include <sys/file.h>
#include <filesystem>

#include <rpm/header.h>
#include <rpm/rpmbase64.h>
#include <rpm/rpmdb.h>
#include <rpm/rpmds.h>
#include <rpm/rpmfileutil.h>
#include <rpm/rpmlib.h>
#include <rpm/rpmlog.h>
#include <rpm/rpmmacro.h>
#include <rpm/rpmts.h>
#include <rpm/rpmtypes.h>

#include "rpmts_internal.hh"
#include "rpmmacro_internal.hh"

#include "debug.h"

using std::string;
using namespace rpm;
namespace fs = std::filesystem;

keystore *rpm::getKeystore(const char * krtype)
{
    keystore * keystore = NULL;
    if (rstreq(krtype, "fs")) {
	keystore = new keystore_fs();
    } else if (rstreq(krtype, "rpmdb")) {
	keystore = new keystore_rpmdb();
    } else if (rstreq(krtype, "openpgp")) {
	keystore = new keystore_openpgp_cert_d();
    }
    return keystore;
}


static int makePubkeyHeader(rpmts ts, rpmPubkey key, Header * hdrp);

static rpmRC load_keys_from_glob(rpmtxn txn, rpmKeyring keyring, string glob)
{
    ARGV_t files = NULL;
    /* XXX TODO: deal with chroot path issues */
    char *pkpath = rpmGetPath(rpmtxnRootDir(txn), glob.c_str(), NULL);

    rpmlog(RPMLOG_DEBUG, "loading keyring from pubkeys in %s\n", pkpath);
    if (rpmGlob(pkpath, NULL, &files)) {
	rpmlog(RPMLOG_DEBUG, "couldn't find any keys in %s\n", pkpath);
	goto exit;
    }

    for (char **f = files; *f; f++) {
	rpmPubkey key = rpmPubkeyRead(*f);

	if (!key) {
	    rpmlog(RPMLOG_WARNING, _("Could not read key %s\n"), *f);
	    continue;
	}

	if (rpmKeyringAddKey(keyring, key) == 0) {
	    rpmlog(RPMLOG_DEBUG, "Loaded key %s\n", *f);
	} else {
	    rpmlog(RPMLOG_WARNING, _("Could not load key %s\n"), *f);
	}
	rpmPubkeyFree(key);
    }
exit:
    free(pkpath);
    argvFree(files);
    return RPMRC_OK;
}

static rpmRC write_key_to_disk(rpmPubkey key, string & dir, string & filename, int replace, rpmFlags flags)
{
    rpmRC rc = RPMRC_FAIL;
    char *keyval = rpmPubkeyArmorWrap(key);
    string tmppath = "";
    string path = dir + "/" + filename;

    if (replace) {
	tmppath =  dir + "/" + filename + ".new";
	unlink(tmppath.c_str());
    }

    if (rpmMkdirs(NULL, dir.c_str())) {
	rpmlog(RPMLOG_ERR, _("failed to create keyring directory %s: %s\n"),
	       path.c_str(), strerror(errno));
	goto exit;
    }

    if (FD_t fd = Fopen(replace ? tmppath.c_str() : path.c_str(), "wx")) {
	size_t keylen = strlen(keyval);
	if (Fwrite(keyval, 1, keylen, fd) == keylen)
	    rc = RPMRC_OK;
	Fclose(fd);
    }
    if (!rc && replace && rename(tmppath.c_str(), path.c_str()) != 0)
	rc = RPMRC_FAIL;

    if (rc) {
	rpmlog(RPMLOG_ERR, _("failed to import key: %s: %s\n"),
	       replace ? tmppath.c_str() : path.c_str(), strerror(errno));
	if (replace)
	    unlink(tmppath.c_str());
    }

exit:
    free(keyval);
    return rc;
}

static rpmRC delete_file_store(std::string path)
{
    try {
	fs::remove_all(path);
    } catch (std::filesystem::filesystem_error& e) {
	return RPMRC_FAIL;
    }
    return RPMRC_OK;
}

/* Wrapper for private delete_key() methods that falls back to short keyid */
rpmRC rpm::delete_key_compat(auto keystore, rpmtxn txn, rpmPubkey key, auto skip)
{
    rpmRC rc = RPMRC_NOTFOUND;
    string fp = rpmPubkeyFingerprintAsHex(key);

    if (keystore->delete_key(txn, fp, skip) == RPMRC_NOTFOUND) {
	/* make sure an old, short keyid version gets removed */
	keystore->delete_key(txn, fp.substr(32), skip);
    }

    return rc;
}

/*****************************************************************************/

rpmRC keystore_fs::load_keys(rpmtxn txn, rpmKeyring keyring)
{
    return load_keys_from_glob(txn, keyring, "%{_keyringpath}/*.key");
}

rpmRC keystore_fs::delete_key(rpmtxn txn, const string & keyid, const string & newname)
{
    rpmRC rc = RPMRC_NOTFOUND;
    string keyglob = "gpg-pubkey-" + keyid + "*.key";
    ARGV_t files = NULL;
    char *pkpath = rpmGenPath(rpmtxnRootDir(txn), "%{_keyringpath}/", keyglob.c_str());
    if (rpmGlob(pkpath, NULL, &files) == 0) {
	char **f;
	for (f = files; *f; f++) {
	    char *bf = strrchr(*f, '/');
	    if (newname.empty() || (bf && strcmp(bf + 1, newname.c_str()) != 0))
		rc = unlink(*f) ? RPMRC_FAIL : RPMRC_OK;
	}
	argvFree(files);
    }
    free(pkpath);
    return rc;
}

rpmRC keystore_fs::delete_key(rpmtxn txn, rpmPubkey key)
{
    return delete_key_compat(this, txn, key, "");
}

rpmRC keystore_fs::import_key(rpmtxn txn, rpmPubkey key, int replace, rpmFlags flags)
{
    rpmRC rc = RPMRC_FAIL;
    string fp = rpmPubkeyFingerprintAsHex(key);
    string keyfmt = "gpg-pubkey-" + fp + ".key";
    char *dir = rpmGenPath(rpmtxnRootDir(txn), "%{_keyringpath}/", NULL);
    string dirstr = dir;

    rc = write_key_to_disk(key, dirstr, keyfmt, replace, flags);

    if (!rc && replace) {
	/* find and delete the old pubkey entry */
	delete_key_compat(this, txn, key, keyfmt);
    }

    free(dir);
    return rc;
}

rpmRC keystore_fs::delete_store(rpmtxn txn)
{
    return delete_file_store(expand_path({rpmtxnRootDir(txn), "%{_keyringpath}/"}));
}

/*****************************************************************************/

static int acquire_write_lock(rpmtxn txn)
{
    char * keyringpath = rpmGenPath(rpmtxnRootDir(txn), "%{_keyringpath}/", NULL);
    char * lockpath = rpmGenPath(rpmtxnRootDir(txn), "%{_keyringpath}/", "writelock");
    int fd = -1;

    if (rpmMkdirs(NULL, keyringpath)) {
	rpmlog(RPMLOG_ERR, _("failed to create keyring directory %s: %s\n"),
	       keyringpath, strerror(errno));
	goto exit;
    }

    if ((fd = open(lockpath, O_WRONLY|O_CREAT, 644)) == -1) {
	rpmlog(RPMLOG_ERR, _("Can't create writelock for keyring at %s: %s\n"), keyringpath, strerror(errno));
    } else if (flock(fd, LOCK_EX|LOCK_NB)) {
	rpmlog(RPMLOG_ERR, _("Can't acquire writelock for keyring at %s\n"), keyringpath);
	close(fd);
	fd = -1;
    }

 exit:
    free(keyringpath);
    free(lockpath);
    return fd;
}

static void free_write_lock(int fd)
{
    flock(fd, LOCK_UN);
}

rpmRC keystore_openpgp_cert_d::load_keys(rpmtxn txn, rpmKeyring keyring)
{
    return load_keys_from_glob(txn, keyring, "%{_keyringpath}/*/*");
}

rpmRC keystore_openpgp_cert_d::delete_key(rpmtxn txn, rpmPubkey key)
{
    rpmRC rc = RPMRC_NOTFOUND;
    int lock_fd = -1;

    if ((lock_fd = acquire_write_lock(txn)) == -1)
	return RPMRC_FAIL;

    string fp = rpmPubkeyFingerprintAsHex(key);
    string dir = fp.substr(0, 2);
    string filename = fp.substr(2);
    char * filepath = rpmGetPath(rpmtxnRootDir(txn), "%{_keyringpath}/", dir.c_str(), "/", filename.c_str(), NULL);
    char * dirpath = rpmGetPath(rpmtxnRootDir(txn), "%{_keyringpath}/", dir.c_str(), NULL);

    if (!access(filepath, F_OK))
	rc = unlink(filepath) ? RPMRC_FAIL : RPMRC_OK;
    /* delete directory if empty */
    rmdir(dirpath);

    free(filepath);
    free(dirpath);
    free_write_lock(lock_fd);
    return rc;
}

rpmRC keystore_openpgp_cert_d::delete_store(rpmtxn txn)
{
    return delete_file_store(expand_path({rpmtxnRootDir(txn), "%{_keyringpath}/"}));
}

rpmRC keystore_openpgp_cert_d::import_key(rpmtxn txn, rpmPubkey key, int replace, rpmFlags flags)
{
    rpmRC rc = RPMRC_NOTFOUND;
    int lock_fd = -1;

    if ((lock_fd = acquire_write_lock(txn)) == -1)
	return RPMRC_FAIL;

    string fp = rpmPubkeyFingerprintAsHex(key);
    string dir = fp.substr(0, 2);
    string filename = fp.substr(2);
    char *dirpath = rpmGetPath(rpmtxnRootDir(txn), "%{_keyringpath}/", dir.c_str(), NULL);
    string dirstr = dirpath;

    rc = write_key_to_disk(key, dirstr, filename, replace, flags);

    free_write_lock(lock_fd);
    free(dirpath);

    return rc;
}

/*****************************************************************************/

rpmRC keystore_rpmdb::load_keys(rpmtxn txn, rpmKeyring keyring)
{
    Header h;
    rpmdbMatchIterator mi;

    rpmlog(RPMLOG_DEBUG, "loading keyring from rpmdb\n");
    rpmts ts = rpmtxnTs(txn);
    if (rpmtsGetDBMode(ts) == -1 && rpmtsOpenDB(ts, O_RDONLY))
        return RPMRC_FAIL;

    mi = rpmtsInitIterator(rpmtxnTs(txn), RPMDBI_NAME, "gpg-pubkey", 0);
    while ((h = rpmdbNextIterator(mi)) != NULL) {
	struct rpmtd_s pubkeys;
	const char *key;
	char *nevr = headerGetAsString(h, RPMTAG_NEVR);

	/* don't allow normal packages named gpg-pubkey */
	if (headerIsEntry(h, RPMTAG_ARCH) || headerIsEntry(h, RPMTAG_OS) ||
	    !headerGet(h, RPMTAG_PUBKEYS, &pubkeys, HEADERGET_MINMEM))
	{
	    rpmlog(RPMLOG_WARNING, _("%s is not a valid public key\n"), nevr);
	    free(nevr);
	    continue;
	}

	while ((key = rpmtdNextString(&pubkeys))) {
	    int rc = 1;
	    uint8_t *pkt;
	    size_t pktlen;

	    if ((rc = rpmBase64Decode(key, (void **) &pkt, &pktlen)) == 0) {
		rpmPubkey key = rpmPubkeyNew(pkt, pktlen);

		if (key) {
		    if ((rc = rpmKeyringAddKey(keyring, key)) == 0) {
			rpmlog(RPMLOG_DEBUG, "Loaded key %s\n", nevr);
		    }
		    rpmPubkeyFree(key);
		}
		free(pkt);
	    }

	    if (rc)
		rpmlog(RPMLOG_WARNING, _("Could not load key %s\n"), nevr);
	}
	free(nevr);
	rpmtdFreeData(&pubkeys);
    }
    rpmdbFreeIterator(mi);

    return RPMRC_OK;
}

rpmRC keystore_rpmdb::delete_key(rpmtxn txn, const string & keyid, unsigned int newinstance)
{
    rpmts ts = rpmtxnTs(txn);
    if (rpmtsOpenDB(ts, (O_RDWR|O_CREAT)))
	return RPMRC_FAIL;

    rpmRC rc = RPMRC_NOTFOUND;
    unsigned int otherinstance = 0;
    Header oh;
    string label = "gpg-pubkey-" + keyid;
    rpmdbMatchIterator mi = rpmtsInitIterator(ts, RPMDBI_LABEL, label.c_str(), 0);

    while (otherinstance == 0 && (oh = rpmdbNextIterator(mi)) != NULL)
	if (headerGetInstance(oh) != newinstance)
	    otherinstance = headerGetInstance(oh);
    rpmdbFreeIterator(mi);
    if (otherinstance) {
	rc = rpmdbRemove(rpmtsGetRdb(ts), otherinstance) ?
		RPMRC_FAIL : RPMRC_OK;
    }

    return rc;
}

rpmRC keystore_rpmdb::delete_key(rpmtxn txn, rpmPubkey key)
{
    return delete_key_compat(this, txn, key, 0);
}

rpmRC keystore_rpmdb::delete_store(rpmtxn txn)
{
    Header h = NULL;
    rpmRC rc = RPMRC_OK;
    rpmts ts = rpmtxnTs(txn);
    if (rpmtsOpenDB(ts, (O_RDWR|O_CREAT)))
	return RPMRC_FAIL;

    rpmdbMatchIterator mi = rpmtsInitIterator(rpmtxnTs(txn), RPMDBI_NAME, "gpg-pubkey", 0);
    while ((h = rpmdbNextIterator(mi)) != NULL) {
	rpmRC rrc = rpmdbRemove(rpmtsGetRdb(rpmtxnTs(txn)), headerGetInstance(h)) ?
	    RPMRC_FAIL : RPMRC_OK;
	if (rrc != RPMRC_OK) {
	    rpmlog(RPMLOG_WARNING, "can't remove key %s", headerGetString(h, RPMTAG_NEVR));
	    rc = rrc;
	}
    }
    rpmdbFreeIterator(mi);
    return rc;
}



rpmRC keystore_rpmdb::import_key(rpmtxn txn, rpmPubkey key, int replace, rpmFlags flags)
{
    Header h = NULL;
    rpmRC rc = RPMRC_FAIL;
    rpmts ts = rpmtxnTs(txn);
    if (rpmtsOpenDB(ts, (O_RDWR|O_CREAT)))
	return RPMRC_FAIL;

    if (makePubkeyHeader(rpmtxnTs(txn), key, &h) != 0)
	return rc;

    rc = rpmtsImportHeader(txn, h, 0);

    if (!rc && replace) {
	/* find and delete the old pubkey entry */
	unsigned int newinstance = headerGetInstance(h);
	delete_key_compat(this, txn, key, newinstance);
    }
    headerFree(h);

    return rc;
}

static void addGpgProvide(Header h, const char *n, const char *v)
{
    rpmsenseFlags pflags = (RPMSENSE_KEYRING|RPMSENSE_EQUAL);
    char * nsn = rstrscat(NULL, "gpg(", n, ")", NULL);

    headerPutString(h, RPMTAG_PROVIDENAME, nsn);
    headerPutString(h, RPMTAG_PROVIDEVERSION, v);
    headerPutUint32(h, RPMTAG_PROVIDEFLAGS, &pflags, 1);

    free(nsn);
}

struct pgpdata_s {
    const char *fingerprint;
    const char *signid;
    char *timestr;
    char *verid;
    const char *userid;
    const char *shortid;
    uint32_t time;
};

static void initPgpData(rpmPubkey key, struct pgpdata_s *pd)
{
    pgpDigParams pubp = rpmPubkeyPgpDigParams(key);
    memset(pd, 0, sizeof(*pd));
    pd->fingerprint = rpmPubkeyFingerprintAsHex(key);
    pd->signid = rpmPubkeyKeyIDAsHex(key);
    pd->shortid = pd->signid + 8;
    pd->userid = pgpDigParamsUserID(pubp);
    if (! pd->userid) {
        pd->userid = "none";
    }
    pd->time = pgpDigParamsCreationTime(pubp);

    rasprintf(&pd->timestr, "%x", pd->time);
    rasprintf(&pd->verid, "%d:%s-%s", pgpDigParamsVersion(pubp), pd->fingerprint, pd->timestr);
}

static void finiPgpData(struct pgpdata_s *pd)
{
    free(pd->timestr);
    free(pd->verid);
    memset(pd, 0, sizeof(*pd));
}

static Header makeImmutable(Header h)
{
    h = headerReload(h, RPMTAG_HEADERIMMUTABLE);
    if (h != NULL) {
	char *sha1 = NULL;
	char *sha256 = NULL;
	char *sha3_256 = NULL;
	unsigned int blen = 0;
	void *blob = headerExport(h, &blen);

	/* XXX FIXME: bah, this code is repeated in way too many places */
	rpmDigestBundle bundle = rpmDigestBundleNew();
	rpmDigestBundleAdd(bundle, RPM_HASH_SHA1, RPMDIGEST_NONE);
	rpmDigestBundleAdd(bundle, RPM_HASH_SHA256, RPMDIGEST_NONE);
	rpmDigestBundleAdd(bundle, RPM_HASH_SHA3_256, RPMDIGEST_NONE);

	rpmDigestBundleUpdate(bundle, rpm_header_magic, sizeof(rpm_header_magic));
	rpmDigestBundleUpdate(bundle, blob, blen);

	rpmDigestBundleFinal(bundle, RPM_HASH_SHA1, (void **)&sha1, NULL, 1);
	rpmDigestBundleFinal(bundle, RPM_HASH_SHA256, (void **)&sha256, NULL, 1);
	rpmDigestBundleFinal(bundle, RPM_HASH_SHA3_256, (void **)&sha3_256, NULL, 1);

	if (sha1 && sha256 && sha3_256) {
	    headerPutString(h, RPMTAG_SHA1HEADER, sha1);
	    headerPutString(h, RPMTAG_SHA256HEADER, sha256);
	    headerPutString(h, RPMTAG_SHA3_256HEADER, sha3_256);
	} else {
	    h = headerFree(h);
	}
	free(sha1);
	free(sha256);
	free(sha3_256);
	free(blob);
	rpmDigestBundleFree(bundle);
    }
    return h;
}

/* Build pubkey header. */
static int makePubkeyHeader(rpmts ts, rpmPubkey key, Header * hdrp)
{
    Header h = headerNew();
    const char * afmt = "%{pubkeys:armor}";
    const char * group = "Public Keys";
    const char * license = "pubkey";
    const char * buildhost = "localhost";
    uint32_t zero = 0;
    struct pgpdata_s kd;
    char * d = NULL;
    char * enc = NULL;
    char * s = NULL;
    int rc = -1;

    memset(&kd, 0, sizeof(kd));

    if ((enc = rpmPubkeyBase64(key)) == NULL)
	goto exit;

    /* Build header elements. */
    initPgpData(key, &kd);

    rasprintf(&s, "%s public key", kd.userid);
    headerPutString(h, RPMTAG_PUBKEYS, enc);

    if ((d = headerFormat(h, afmt, NULL)) == NULL)
	goto exit;

    headerPutString(h, RPMTAG_NAME, "gpg-pubkey");
    headerPutString(h, RPMTAG_VERSION, kd.fingerprint);
    headerPutString(h, RPMTAG_RELEASE, kd.timestr);
    headerPutString(h, RPMTAG_DESCRIPTION, d);
    headerPutString(h, RPMTAG_GROUP, group);
    headerPutString(h, RPMTAG_LICENSE, license);
    headerPutString(h, RPMTAG_SUMMARY, s);
    headerPutString(h, RPMTAG_PACKAGER, kd.userid);
    headerPutUint32(h, RPMTAG_SIZE, &zero, 1);
    headerPutString(h, RPMTAG_RPMVERSION, RPMVERSION);
    headerPutString(h, RPMTAG_BUILDHOST, buildhost);
    headerPutUint32(h, RPMTAG_BUILDTIME, &kd.time, 1);
    headerPutString(h, RPMTAG_SOURCERPM, "(none)");

    addGpgProvide(h, kd.userid, kd.verid);
    addGpgProvide(h, kd.shortid, kd.verid);
    addGpgProvide(h, kd.signid, kd.verid);

    /* Reload it into immutable region and stomp standard digests on it */
    h = makeImmutable(h);
    if (h != NULL) {
	*hdrp = headerLink(h);
	rc = 0;

	/* Add install time/tid as icing on the cake */
	rpm_tid_t tid = rpmtsGetTid(ts);
	headerPutUint32(h, RPMTAG_INSTALLTIME, &tid, 1);
	headerPutUint32(h, RPMTAG_INSTALLTID, &tid, 1);
    }

exit:
    headerFree(h);
    finiPgpData(&kd);
    free(enc);
    free(d);
    free(s);

    return rc;
}
