/*
 * Copyright (c) Atmosphère-NX
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope 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 <http://www.gnu.org/licenses/>.
 */
#include <exosphere.hpp>
#include "../secmon_error.hpp"
#include "../secmon_key_storage.hpp"
#include "secmon_boot.hpp"

namespace ams::secmon::boot {

    void CalculatePackage2Hash(se::Sha256Hash *dst, const pkg2::Package2Meta &meta, uintptr_t package2_start) {
        /* Determine the region to hash. */
        const void *data  = reinterpret_cast<const void *>(package2_start);
        const size_t size = meta.GetSize();

        /* Flush to ensure the SE sees the correct data. */
        hw::FlushDataCache(data, size);
        hw::DataSynchronizationBarrierInnerShareable();

        /* Calculate the hash. */
        se::CalculateSha256(dst, data, size);
    }

    bool VerifyPackage2Signature(pkg2::Package2Header &header, const void *mod, size_t mod_size) {
        return VerifySignature(header.signature, sizeof(header.signature), mod, mod_size, std::addressof(header.meta), sizeof(header.meta));
    }

    int PrepareMasterKey(int key_generation) {
        if (key_generation == GetKeyGeneration()) {
            return pkg1::AesKeySlot_Master;
        }

        constexpr int Slot = pkg1::AesKeySlot_Temporary;
        LoadMasterKey(Slot, key_generation);

        return Slot;
    }

    void PreparePackage2Key(int pkg2_slot, int key_generation, const void *key, size_t key_size) {
        /* Get keyslot for the desired master key. */
        const int master_slot = PrepareMasterKey(key_generation);

        /* Load the package2 key into the desired keyslot. */
        se::SetEncryptedAesKey128(pkg2_slot, master_slot, key, key_size);
    }

    void DecryptPackage2(void *dst, size_t dst_size, const void *src, size_t src_size, const void *key, size_t key_size, const void *iv, size_t iv_size, u8 key_generation) {
        /* Ensure that the SE sees consistent data. */
        hw::FlushDataCache(key, key_size);
        hw::FlushDataCache(src, src_size);
        hw::FlushDataCache(dst, dst_size);
        hw::DataSynchronizationBarrierInnerShareable();

        /* Load the package2 key into the temporary keyslot. */
        PreparePackage2Key(pkg1::AesKeySlot_Temporary, key_generation, key, key_size);

        /* Decrypt the data. */
        se::ComputeAes128Ctr(dst, dst_size, pkg1::AesKeySlot_Temporary, src, src_size, iv, iv_size);

        /* Clear the keyslot we just used. */
        se::ClearAesKeySlot(pkg1::AesKeySlot_Temporary);

        /* Ensure that the cpu sees consistent data. */
        hw::DataSynchronizationBarrierInnerShareable();
        hw::FlushDataCache(dst, dst_size);
        hw::DataSynchronizationBarrierInnerShareable();
    }

    bool VerifyPackage2Meta(const pkg2::Package2Meta &meta) {
        /* Get the obfuscated metadata. */
        const size_t size       = meta.GetSize();
        const u8 key_generation = meta.GetKeyGeneration();

        /* Check that size is big enough for the header. */
        if (size <= sizeof(pkg2::Package2Header)) {
            return false;
        }

        /* Check that the size isn't larger than what we allow. */
        if (size > pkg2::Package2SizeMax) {
            return false;
        }

        /* Check that the key generation is one that we can use. */
        static_assert(pkg1::KeyGeneration_Count == 18);
        if (key_generation >= pkg1::KeyGeneration_Count) {
            return false;
        }

        /* Check the magic number. */
        if (!crypto::IsSameBytes(meta.magic, pkg2::Package2Meta::Magic::String, sizeof(meta.magic))) {
            return false;
        }

        /* Check the payload alignments. */
        if ((meta.entrypoint % pkg2::PayloadAlignment) != 0) {
            return false;
        }

        for (int i = 0; i < pkg2::PayloadCount; ++i) {
            if ((meta.payload_sizes[i] % pkg2::PayloadAlignment) != 0) {
                return false;
            }
        }

        /* Check that the sizes sum to the total. */
        if (size != sizeof(pkg2::Package2Header) + meta.payload_sizes[0] + meta.payload_sizes[1] + meta.payload_sizes[2]) {
            return false;
        }

        /* Check that the payloads do not overflow. */
        for (int i = 0; i < pkg2::PayloadCount; ++i) {
            if (meta.payload_offsets[i] > meta.payload_offsets[i] + meta.payload_sizes[i]) {
                return false;
            }
        }

        /* Verify that no payloads overlap. */
        for (int i = 0; i < pkg2::PayloadCount - 1; ++i) {
            for (int j = i + 1; j < pkg2::PayloadCount; ++j) {
                if (util::HasOverlap(meta.payload_offsets[i], meta.payload_sizes[i], meta.payload_offsets[j], meta.payload_sizes[j])) {
                    return false;
                }
            }
        }

        /* Check whether any payload contains the entrypoint. */
        for (int i = 0; i < pkg2::PayloadCount; ++i) {
            if (util::Contains(meta.payload_offsets[i], meta.payload_sizes[i], meta.entrypoint)) {
                return true;
            }
        }

        /* No payload contains the entrypoint, so we're not valid. */
        return false;
    }

    bool VerifyPackage2Version(const pkg2::Package2Meta &meta) {
        return meta.bootloader_version <= pkg2::CurrentBootloaderVersion && meta.package2_version >= pkg2::MinimumValidDataVersion;
    }

    bool VerifyPackage2Payloads(const pkg2::Package2Meta &meta, uintptr_t payload_address) {
        /* Verify hashes match for all payloads. */
        for (int i = 0; i < pkg2::PayloadCount; ++i) {
            /* Allow all-zero bytes to match any payload. */
            if (!(meta.payload_hashes[i][0] == 0 && std::memcmp(meta.payload_hashes[i] + 0, meta.payload_hashes[i] + 1, sizeof(meta.payload_hashes[i]) - 1) == 0)) {
                if (!VerifyHash(meta.payload_hashes[i], payload_address, meta.payload_sizes[i])) {
                    return false;
                }
            }

            payload_address += meta.payload_sizes[i];
        }

        return true;
    }

}
