/*
 * Copyright (c) 2017-2022, [Ribose Inc](https://www.ribose.com).
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/types.h>
#ifdef HAVE_SYS_PARAM_H
#include <sys/param.h>
#else
#include "uniwin.h"
#endif
#include <string.h>
#include <stdint.h>
#include <time.h>
#include <inttypes.h>
#include <cassert>

#include "key_store_pgp.h"
#include "key_store_kbx.h"
#include "pgp-key.h"
#include <librepgp/stream-sig.h>

/* same limit with GnuPG 2.1 */
#define BLOB_SIZE_LIMIT (5 * 1024 * 1024)
/* limit the number of keys/sigs/uids in the blob */
#define BLOB_OBJ_LIMIT 0x8000

#define BLOB_HEADER_SIZE 0x5
#define BLOB_FIRST_SIZE 0x20
#define BLOB_KEY_SIZE 0x1C
#define BLOB_UID_SIZE 0x0C
#define BLOB_SIG_SIZE 0x04
#define BLOB_VALIDITY_SIZE 0x10

uint8_t
kbx_blob_t::ru8(size_t idx)
{
    return image_[idx];
}

uint16_t
kbx_blob_t::ru16(size_t idx)
{
    return read_uint16(image_.data() + idx);
}

uint32_t
kbx_blob_t::ru32(size_t idx)
{
    return read_uint32(image_.data() + idx);
}

kbx_blob_t::kbx_blob_t(std::vector<uint8_t> &data)
{
    if (data.size() < BLOB_HEADER_SIZE) {
        RNP_LOG("Too small KBX blob.");
        throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS);
    }
    uint32_t len = read_uint32(data.data());
    if (len > BLOB_SIZE_LIMIT) {
        RNP_LOG("Too large KBX blob.");
        throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS);
    }
    if (len != data.size()) {
        RNP_LOG("KBX blob size mismatch.");
        throw rnp::rnp_exception(RNP_ERROR_BAD_PARAMETERS);
    }
    image_ = data;
    type_ = (kbx_blob_type_t) ru8(4);
}

bool
kbx_header_blob_t::parse()
{
    if (length() != BLOB_FIRST_SIZE) {
        RNP_LOG("The first blob has wrong length: %" PRIu32 " but expected %d",
                length(),
                (int) BLOB_FIRST_SIZE);
        return false;
    }

    size_t idx = BLOB_HEADER_SIZE;
    version_ = ru8(idx++);
    if (version_ != 1) {
        RNP_LOG("Wrong version, expect 1 but has %" PRIu8, version_);
        return false;
    }

    flags_ = ru16(idx);
    idx += 2;

    // blob should contains a magic KBXf
    if (memcmp(image_.data() + idx, "KBXf", 4)) {
        RNP_LOG("The first blob hasn't got a KBXf magic string");
        return false;
    }
    idx += 4;
    // RFU
    idx += 4;
    // File creation time
    file_created_at_ = ru32(idx);
    idx += 4;
    // Duplicated?
    file_created_at_ = ru32(idx);
    // RFU +4 bytes
    // RFU +4 bytes
    return true;
}

bool
kbx_pgp_blob_t::parse()
{
    if (image_.size() < 15 + BLOB_HEADER_SIZE) {
        RNP_LOG("Too few data in the blob.");
        return false;
    }

    size_t idx = BLOB_HEADER_SIZE;
    /* version */
    version_ = ru8(idx++);
    if (version_ != 1) {
        RNP_LOG("Wrong version: %" PRIu8, version_);
        return false;
    }
    /* flags */
    flags_ = ru16(idx);
    idx += 2;
    /* keyblock offset */
    keyblock_offset_ = ru32(idx);
    idx += 4;
    /* keyblock length */
    keyblock_length_ = ru32(idx);
    idx += 4;

    if ((keyblock_offset_ > image_.size()) ||
        (keyblock_offset_ > (UINT32_MAX - keyblock_length_)) ||
        (image_.size() < (keyblock_offset_ + keyblock_length_))) {
        RNP_LOG("Wrong keyblock offset/length, blob size: %zu"
                ", keyblock offset: %" PRIu32 ", length: %" PRIu32,
                image_.size(),
                keyblock_offset_,
                keyblock_length_);
        return false;
    }
    /* number of key blocks */
    size_t nkeys = ru16(idx);
    idx += 2;
    if (nkeys < 1) {
        RNP_LOG("PGP blob should contains at least 1 key");
        return false;
    }
    if (nkeys > BLOB_OBJ_LIMIT) {
        RNP_LOG("Too many keys in the PGP blob");
        return false;
    }

    /* Size of the single key record */
    size_t keys_len = ru16(idx);
    idx += 2;
    if (keys_len < BLOB_KEY_SIZE) {
        RNP_LOG(
          "PGP blob needs %d bytes, but contains: %zu bytes", (int) BLOB_KEY_SIZE, keys_len);
        return false;
    }

    for (size_t i = 0; i < nkeys; i++) {
        if (image_.size() - idx < keys_len) {
            RNP_LOG("Too few bytes left for key blob");
            return false;
        }

        kbx_pgp_key_t nkey = {};
        /* copy fingerprint */
        memcpy(nkey.fp, &image_[idx], 20);
        idx += 20;
        /* keyid offset */
        nkey.keyid_offset = ru32(idx);
        idx += 4;
        /* flags */
        nkey.flags = ru16(idx);
        idx += 2;
        /* RFU */
        idx += 2;
        /* skip padding bytes if it existed */
        idx += keys_len - BLOB_KEY_SIZE;
        keys_.push_back(std::move(nkey));
    }

    if (image_.size() - idx < 2) {
        RNP_LOG("No data for sn_size");
        return false;
    }
    size_t sn_size = ru16(idx);
    idx += 2;

    if (image_.size() - idx < sn_size) {
        RNP_LOG("SN is %zu, while bytes left are %zu", sn_size, image_.size() - idx);
        return false;
    }

    if (sn_size) {
        sn_ = {image_.begin() + idx, image_.begin() + idx + sn_size};
        idx += sn_size;
    }

    if (image_.size() - idx < 4) {
        RNP_LOG("Too few data for uids");
        return false;
    }
    size_t nuids = ru16(idx);
    if (nuids > BLOB_OBJ_LIMIT) {
        RNP_LOG("Too many uids in the PGP blob");
        return false;
    }

    size_t uids_len = ru16(idx + 2);
    idx += 4;

    if (uids_len < BLOB_UID_SIZE) {
        RNP_LOG("Too few bytes for uid struct: %zu", uids_len);
        return false;
    }

    for (size_t i = 0; i < nuids; i++) {
        if (image_.size() - idx < uids_len) {
            RNP_LOG("Too few bytes to read uid struct.");
            return false;
        }
        kbx_pgp_uid_t nuid = {};
        /* offset */
        nuid.offset = ru32(idx);
        idx += 4;
        /* length */
        nuid.length = ru32(idx);
        idx += 4;
        /* flags */
        nuid.flags = ru16(idx);
        idx += 2;
        /* validity */
        nuid.validity = ru8(idx);
        idx++;
        /* RFU */
        idx++;
        // skip padding bytes if it existed
        idx += uids_len - BLOB_UID_SIZE;

        uids_.push_back(std::move(nuid));
    }

    if (image_.size() - idx < 4) {
        RNP_LOG("No data left for sigs");
        return false;
    }

    size_t nsigs = ru16(idx);
    if (nsigs > BLOB_OBJ_LIMIT) {
        RNP_LOG("Too many sigs in the PGP blob");
        return false;
    }

    size_t sigs_len = ru16(idx + 2);
    idx += 4;

    if (sigs_len < BLOB_SIG_SIZE) {
        RNP_LOG("Too small SIGN structure: %zu", uids_len);
        return false;
    }

    for (size_t i = 0; i < nsigs; i++) {
        if (image_.size() - idx < sigs_len) {
            RNP_LOG("Too few data for sig");
            return false;
        }

        kbx_pgp_sig_t nsig = {};
        nsig.expired = ru32(idx);
        idx += 4;

        // skip padding bytes if it existed
        idx += (sigs_len - BLOB_SIG_SIZE);

        sigs_.push_back(nsig);
    }

    if (image_.size() - idx < BLOB_VALIDITY_SIZE) {
        RNP_LOG("Too few data for trust/validities");
        return false;
    }

    ownertrust_ = ru8(idx);
    idx++;
    all_validity_ = ru8(idx);
    idx++;
    // RFU
    idx += 2;
    recheck_after_ = ru32(idx);
    idx += 4;
    latest_timestamp_ = ru32(idx);
    idx += 4;
    blob_created_at_ = ru32(idx);
    // do not forget to idx += 4 on further expansion

    // here starts keyblock, UID and reserved space for future usage

    // Maybe we should add checksum verify but GnuPG never checked it
    // Checksum is last 20 bytes of blob and it is SHA-1, if it invalid MD5 and starts from 4
    // zero it is MD5.

    return true;
}

static std::unique_ptr<kbx_blob_t>
rnp_key_store_kbx_parse_blob(const uint8_t *image, size_t image_len)
{
    std::unique_ptr<kbx_blob_t> blob;
    // a blob shouldn't be less of length + type
    if (image_len < BLOB_HEADER_SIZE) {
        RNP_LOG("Blob size is %zu but it shouldn't be less of header", image_len);
        return blob;
    }

    try {
        std::vector<uint8_t> data(image, image + image_len);
        kbx_blob_type_t      type = (kbx_blob_type_t) image[4];

        switch (type) {
        case KBX_EMPTY_BLOB:
            blob = std::unique_ptr<kbx_blob_t>(new kbx_blob_t(data));
            break;
        case KBX_HEADER_BLOB:
            blob = std::unique_ptr<kbx_blob_t>(new kbx_header_blob_t(data));
            break;
        case KBX_PGP_BLOB:
            blob = std::unique_ptr<kbx_blob_t>(new kbx_pgp_blob_t(data));
            break;
        case KBX_X509_BLOB:
            // current we doesn't parse X509 blob, so, keep it as is
            blob = std::unique_ptr<kbx_blob_t>(new kbx_blob_t(data));
            break;
        // unsupported blob type
        default:
            RNP_LOG("Unsupported blob type: %d", (int) type);
            return blob;
        }

        if (!blob->parse()) {
            return NULL;
        }
    } catch (const std::exception &e) {
        RNP_LOG("%s", e.what());
        return NULL;
    }
    return blob;
}

bool
rnp_key_store_kbx_from_src(rnp_key_store_t *         key_store,
                           pgp_source_t *            src,
                           const pgp_key_provider_t *key_provider)
{
    try {
        rnp::MemorySource mem(*src);
        size_t            has_bytes = mem.size();
        uint8_t *         buf = (uint8_t *) mem.memory();

        while (has_bytes > 4) {
            size_t blob_length = read_uint32(buf);
            if (blob_length > BLOB_SIZE_LIMIT) {
                RNP_LOG("Blob size is %zu bytes but limit is %d bytes",
                        blob_length,
                        (int) BLOB_SIZE_LIMIT);
                return false;
            }
            if (blob_length < BLOB_HEADER_SIZE) {
                RNP_LOG("Too small blob header size");
                return false;
            }
            if (has_bytes < blob_length) {
                RNP_LOG("Blob have size %zu bytes but file contains only %zu bytes",
                        blob_length,
                        has_bytes);
                return false;
            }
            auto blob = rnp_key_store_kbx_parse_blob(buf, blob_length);
            if (!blob.get()) {
                RNP_LOG("Failed to parse blob");
                return false;
            }
            kbx_blob_t *pblob = blob.get();
            key_store->blobs.push_back(std::move(blob));

            if (pblob->type() == KBX_PGP_BLOB) {
                // parse keyblock if it existed
                kbx_pgp_blob_t &pgp_blob = dynamic_cast<kbx_pgp_blob_t &>(*pblob);
                if (!pgp_blob.keyblock_length()) {
                    RNP_LOG("PGP blob have zero size");
                    return false;
                }

                rnp::MemorySource blsrc(pgp_blob.image().data() + pgp_blob.keyblock_offset(),
                                        pgp_blob.keyblock_length(),
                                        false);
                if (rnp_key_store_pgp_read_from_src(key_store, &blsrc.src())) {
                    return false;
                }
            }

            has_bytes -= blob_length;
            buf += blob_length;
        }
        return true;
    } catch (const std::exception &e) {
        RNP_LOG("%s", e.what());
        return false;
    }
}

static bool
pbuf(pgp_dest_t *dst, const void *buf, size_t len)
{
    dst_write(dst, buf, len);
    return dst->werr == RNP_SUCCESS;
}

static bool
pu8(pgp_dest_t *dst, uint8_t p)
{
    return pbuf(dst, &p, 1);
}

static bool
pu16(pgp_dest_t *dst, uint16_t f)
{
    uint8_t p[2];
    p[0] = (uint8_t)(f >> 8);
    p[1] = (uint8_t) f;
    return pbuf(dst, p, 2);
}

static bool
pu32(pgp_dest_t *dst, uint32_t f)
{
    uint8_t p[4];
    STORE32BE(p, f);
    return pbuf(dst, p, 4);
}

static bool
rnp_key_store_kbx_write_header(rnp_key_store_t *key_store, pgp_dest_t *dst)
{
    uint16_t flags = 0;
    uint32_t file_created_at = key_store->secctx.time();

    if (!key_store->blobs.empty() && (key_store->blobs[0]->type() == KBX_HEADER_BLOB)) {
        kbx_header_blob_t &blob = dynamic_cast<kbx_header_blob_t &>(*key_store->blobs[0]);
        file_created_at = blob.file_created_at();
    }

    return !(!pu32(dst, BLOB_FIRST_SIZE) || !pu8(dst, KBX_HEADER_BLOB) ||
             !pu8(dst, 1)                                                   // version
             || !pu16(dst, flags) || !pbuf(dst, "KBXf", 4) || !pu32(dst, 0) // RFU
             || !pu32(dst, 0)                                               // RFU
             || !pu32(dst, file_created_at) || !pu32(dst, key_store->secctx.time()) ||
             !pu32(dst, 0)); // RFU
}

static bool
rnp_key_store_kbx_write_pgp(rnp_key_store_t *key_store, pgp_key_t *key, pgp_dest_t *dst)
{
    rnp::MemoryDest mem(NULL, BLOB_SIZE_LIMIT);

    if (!pu32(&mem.dst(), 0)) { // length, we don't know length of blob yet, so it's 0
        return false;
    }

    if (!pu8(&mem.dst(), KBX_PGP_BLOB) || !pu8(&mem.dst(), 1)) { // type, version
        return false;
    }

    if (!pu16(&mem.dst(), 0)) { // flags, not used by GnuPG
        return false;
    }

    if (!pu32(&mem.dst(), 0) ||
        !pu32(&mem.dst(), 0)) { // offset and length of keyblock, update later
        return false;
    }

    if (!pu16(&mem.dst(), 1 + key->subkey_count())) { // number of keys in keyblock
        return false;
    }
    if (!pu16(&mem.dst(), 28)) { // size of key info structure)
        return false;
    }

    if (!pbuf(&mem.dst(), key->fp().fingerprint, PGP_FINGERPRINT_SIZE) ||
        !pu32(&mem.dst(), mem.writeb() - 8) || // offset to keyid (part of fpr for V4)
        !pu16(&mem.dst(), 0) ||                // flags, not used by GnuPG
        !pu16(&mem.dst(), 0)) {                // RFU
        return false;
    }

    // same as above, for each subkey
    std::vector<uint32_t> subkey_sig_expirations;
    for (auto &sfp : key->subkey_fps()) {
        pgp_key_t *subkey = rnp_key_store_get_key_by_fpr(key_store, sfp);
        if (!subkey || !pbuf(&mem.dst(), subkey->fp().fingerprint, PGP_FINGERPRINT_SIZE) ||
            !pu32(&mem.dst(), mem.writeb() - 8) || // offset to keyid (part of fpr for V4)
            !pu16(&mem.dst(), 0) ||                // flags, not used by GnuPG
            !pu16(&mem.dst(), 0)) {                // RFU
            return false;
        }
        // load signature expirations while we're at it
        for (size_t i = 0; i < subkey->sig_count(); i++) {
            uint32_t expiration = subkey->get_sig(i).sig.key_expiration();
            subkey_sig_expirations.push_back(expiration);
        }
    }

    if (!pu16(&mem.dst(), 0)) { // Zero size of serial number
        return false;
    }

    // skip serial number
    if (!pu16(&mem.dst(), key->uid_count()) || !pu16(&mem.dst(), 12)) {
        return false;
    }

    size_t uid_start = mem.writeb();
    for (size_t i = 0; i < key->uid_count(); i++) {
        if (!pu32(&mem.dst(), 0) ||
            !pu32(&mem.dst(), 0)) { // UID offset and length, update when blob has done
            return false;
        }

        if (!pu16(&mem.dst(), 0)) { // flags, (not yet used)
            return false;
        }

        if (!pu8(&mem.dst(), 0) || !pu8(&mem.dst(), 0)) { // Validity & RFU
            return false;
        }
    }

    if (!pu16(&mem.dst(), key->sig_count() + subkey_sig_expirations.size()) ||
        !pu16(&mem.dst(), 4)) {
        return false;
    }

    for (size_t i = 0; i < key->sig_count(); i++) {
        if (!pu32(&mem.dst(), key->get_sig(i).sig.key_expiration())) {
            return false;
        }
    }
    for (auto &expiration : subkey_sig_expirations) {
        if (!pu32(&mem.dst(), expiration)) {
            return false;
        }
    }

    if (!pu8(&mem.dst(), 0) ||
        !pu8(&mem.dst(), 0)) { // Assigned ownertrust & All_Validity (not yet used)
        return false;
    }

    if (!pu16(&mem.dst(), 0) || !pu32(&mem.dst(), 0)) { // RFU & Recheck_after
        return false;
    }

    if (!pu32(&mem.dst(), key_store->secctx.time()) ||
        !pu32(&mem.dst(), key_store->secctx.time())) { // Latest timestamp && created
        return false;
    }

    if (!pu32(&mem.dst(), 0)) { // Size of reserved space
        return false;
    }

    // wrtite UID, we might redesign PGP write and use this information from keyblob
    for (size_t i = 0; i < key->uid_count(); i++) {
        const pgp_userid_t &uid = key->get_uid(i);
        uint8_t *           p = (uint8_t *) mem.memory() + uid_start + (12 * i);
        /* store absolute uid offset in the output stream */
        uint32_t pt = mem.writeb() + dst->writeb;
        STORE32BE(p, pt);
        /* and uid length */
        pt = uid.str.size();
        STORE32BE(p + 4, pt);
        /* uid data itself */
        if (!pbuf(&mem.dst(), uid.str.c_str(), pt)) {
            return false;
        }
    }

    /* write keyblock and fix the offset/length */
    size_t   key_start = mem.writeb();
    uint32_t pt = key_start;
    uint8_t *p = (uint8_t *) mem.memory() + 8;
    STORE32BE(p, pt);

    key->write(mem.dst());
    if (mem.werr()) {
        return false;
    }

    for (auto &sfp : key->subkey_fps()) {
        const pgp_key_t *subkey = rnp_key_store_get_key_by_fpr(key_store, sfp);
        if (!subkey) {
            return false;
        }
        subkey->write(mem.dst());
        if (mem.werr()) {
            return false;
        }
    }

    /* key blob length */
    pt = mem.writeb() - key_start;
    p = (uint8_t *) mem.memory() + 12;
    STORE32BE(p, pt);

    // fix the length of blob
    pt = mem.writeb() + 20;
    p = (uint8_t *) mem.memory();
    STORE32BE(p, pt);

    // checksum
    auto hash = rnp::Hash::create(PGP_HASH_SHA1);
    hash->add(mem.memory(), mem.writeb());
    uint8_t checksum[PGP_SHA1_HASH_SIZE];
    assert(hash->size() == sizeof(checksum));
    hash->finish(checksum);

    if (!(pbuf(&mem.dst(), checksum, PGP_SHA1_HASH_SIZE))) {
        return false;
    }

    /* finally write to the output */
    dst_write(dst, mem.memory(), mem.writeb());
    return !dst->werr;
}

static bool
rnp_key_store_kbx_write_x509(rnp_key_store_t *key_store, pgp_dest_t *dst)
{
    for (auto &blob : key_store->blobs) {
        if (blob->type() != KBX_X509_BLOB) {
            continue;
        }
        if (!pbuf(dst, blob->image().data(), blob->length())) {
            return false;
        }
    }
    return true;
}

bool
rnp_key_store_kbx_to_dst(rnp_key_store_t *key_store, pgp_dest_t *dst)
{
    try {
        if (!rnp_key_store_kbx_write_header(key_store, dst)) {
            RNP_LOG("Can't write KBX header");
            return false;
        }

        for (auto &key : key_store->keys) {
            if (!key.is_primary()) {
                continue;
            }
            if (!rnp_key_store_kbx_write_pgp(key_store, &key, dst)) {
                RNP_LOG("Can't write PGP blobs for key %p", &key);
                return false;
            }
        }

        if (!rnp_key_store_kbx_write_x509(key_store, dst)) {
            RNP_LOG("Can't write X509 blobs");
            return false;
        }
        return true;
    } catch (const std::exception &e) {
        RNP_LOG("Failed to write KBX store: %s", e.what());
        return false;
    }
}
