/*
 * Copyright (c) 2017-2021 [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 OWNER 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 <fstream>
#include <vector>
#include <string>

#include <cstring>
#include <rnp/rnp.h>
#include "rnp_tests.h"
#include "support.h"
#include "utils.h"
#include <vector>
#include <string>
#include <crypto/cipher.hpp>
#include <crypto/mem.h>

static std::vector<uint8_t>
decode_hex(const char *hex)
{
    if (!hex) {
        return {};
    }
    std::vector<uint8_t> data(strlen(hex) / 2);
    if (!rnp::hex_decode(hex, data.data(), data.size())) {
        throw std::invalid_argument("hex");
    }
    return data;
}

static void
test_cipher(pgp_symm_alg_t    alg,
            pgp_cipher_mode_t mode,
            size_t            tag_size,
            bool              disable_padding,
            const char *      key_hex,
            const char *      iv_hex,
            const char *      ad_hex,
            const char *      pt_hex,
            const char *      expected_ct_hex)
{
    const std::vector<uint8_t> key(decode_hex(key_hex));
    const std::vector<uint8_t> iv(decode_hex(iv_hex));
    const std::vector<uint8_t> ad(decode_hex(ad_hex));
    const std::vector<uint8_t> pt(decode_hex(pt_hex));
    const std::vector<uint8_t> expected_ct(decode_hex(expected_ct_hex));

    auto enc = Cipher::encryption(alg, mode, tag_size, disable_padding);
    assert_non_null(enc);
    const size_t         block_size = enc->block_size();
    const size_t         ud = enc->update_granularity();
    std::vector<uint8_t> ct;
    // make room for padding
    ct.resize(((pt.size() + tag_size) / block_size + 1) * block_size);
    // set key & iv
    assert_true(enc->set_key(key.data(), key.size()));
    assert_true(enc->set_iv(iv.data(), iv.size()));
    if (!ad.empty()) {
        assert_true(enc->set_ad(ad.data(), ad.size()));
    }

    // encrypt all in one go
    size_t output_written, input_consumed;
    assert_true(enc->finish(
      ct.data(), ct.size(), &output_written, pt.data(), pt.size(), &input_consumed));
    ct.resize(output_written);
    assert_memory_equal(ct.data(), expected_ct.data(), expected_ct.size());

    // start over
    enc.reset(Cipher::encryption(alg, mode, tag_size, disable_padding).release());
    assert_true(enc->set_key(key.data(), key.size()));
    assert_true(enc->set_iv(iv.data(), iv.size()));
    if (!ad.empty()) {
        assert_true(enc->set_ad(ad.data(), ad.size()));
    }
    ct.clear();
    ct.resize(((pt.size() + tag_size) / block_size + 1) * block_size);
    // encrypt in pieces
    assert_memory_not_equal(ct.data(), expected_ct.data(), expected_ct.size());
    // all except the last block
    size_t nonfinal_bytes = rnp_round_up(pt.size(), ud) - ud;
    output_written = 0;
    input_consumed = 0;
    size_t written, consumed;
    while (input_consumed != nonfinal_bytes) {
        assert_true(enc->update(ct.data() + output_written,
                                ct.size() - output_written,
                                &written,
                                pt.data() + input_consumed,
                                ud,
                                &consumed));
        output_written += written;
        input_consumed += consumed;
    }
    assert_true(enc->finish(ct.data() + output_written,
                            ct.size() - output_written,
                            &written,
                            pt.data() + input_consumed,
                            pt.size() - input_consumed,
                            &consumed));
    output_written += written;
    ct.resize(output_written);
    assert_int_equal(ct.size(), expected_ct.size());
    assert_memory_equal(ct.data(), expected_ct.data(), expected_ct.size());
    enc.reset();

    // decrypt
    auto dec = Cipher::decryption(alg, mode, tag_size, disable_padding);
    assert_true(dec->set_key(key.data(), key.size()));
    assert_true(dec->set_iv(iv.data(), iv.size()));
    if (!ad.empty()) {
        assert_true(dec->set_ad(ad.data(), ad.size()));
    }
    // decrypt in pieces
    std::vector<uint8_t> decrypted(ct.size());
    // all except the last block but see below for openssl
    nonfinal_bytes = rnp_round_up(ct.size(), ud) - ud;
#if defined(CRYPTO_BACKEND_OPENSSL) || defined(CRYPTO_BACKEND_BOTAN3)
    /* Since ossl backend sets tag explicitly tag bytes cannot be
       split between two blocks.
       The issue may easily occur is (for example)
          us = 16
          ct.size() = 24
          tag_size=16
    */
    /* Botan 3 also requires to include whole tag in the finish() call. */
    if (ct.size() - nonfinal_bytes < tag_size) {
        nonfinal_bytes = ct.size() - tag_size;
    }
#endif // CRYPTO_BACKEND_OPENSSL
    output_written = 0;
    input_consumed = 0;
    while (input_consumed != nonfinal_bytes) {
        size_t consume = std::min(ud, nonfinal_bytes - input_consumed);
        if (consume < ud) {
            break;
        }
        assert_true(dec->update(decrypted.data() + output_written,
                                decrypted.size() - output_written,
                                &written,
                                (const uint8_t *) ct.data() + input_consumed,
                                // ++++                                    ud,
                                consume,
                                &consumed));
        output_written += written;
        input_consumed += consumed;
    }
    assert_true(dec->finish(decrypted.data() + output_written,
                            decrypted.size() - output_written,
                            &written,
                            (const uint8_t *) ct.data() + input_consumed,
                            ct.size() - input_consumed,
                            &consumed));
    output_written += written;
    decrypted.resize(output_written);
    assert_int_equal(decrypted.size(), pt.size());
    assert_memory_equal(decrypted.data(), pt.data(), pt.size());

    // decrypt with a bad tag
    if (tag_size != 0) {
        dec.reset(Cipher::decryption(alg, mode, tag_size, disable_padding).release());
        assert_true(dec->set_key(key.data(), key.size()));
        assert_true(dec->set_iv(iv.data(), iv.size()));
        if (!ad.empty()) {
            assert_true(dec->set_ad(ad.data(), ad.size()));
        }
        // decrypt in pieces
        std::vector<uint8_t> decrypted(ct.size());
        // all except the last block but see above for openssl
        nonfinal_bytes = rnp_round_up(ct.size(), ud) - ud;
#ifdef CRYPTO_BACKEND_OPENSSL
        if (ct.size() - nonfinal_bytes < tag_size) {
            nonfinal_bytes = ct.size() - tag_size;
        }
#endif // CRYPTO_BACKEND_OPENSSL

        output_written = 0;
        input_consumed = 0;
        while (input_consumed != nonfinal_bytes) {
            assert_true(dec->update(decrypted.data() + output_written,
                                    decrypted.size() - output_written,
                                    &written,
                                    (const uint8_t *) ct.data() + input_consumed,
                                    // ++++                                    ud,
                                    std::min(ud, nonfinal_bytes - input_consumed),
                                    &consumed));
            output_written += written;
            input_consumed += consumed;
        }
        // tamper with the tag
        ct.back() ^= 0xff;
        assert_false(dec->finish(decrypted.data() + output_written,
                                 decrypted.size() - output_written,
                                 &written,
                                 (const uint8_t *) ct.data() + input_consumed,
                                 ct.size() - input_consumed,
                                 &consumed));
    }
}

TEST_F(rnp_tests, test_cipher_idea)
{
#if defined(ENABLE_IDEA)
    assert_true(idea_enabled());
    // OpenSSL do_crypt man page example
    test_cipher(PGP_SA_IDEA,
                PGP_CIPHER_MODE_CBC,
                0,
                false,
                "000102030405060708090a0b0c0d0e0f",
                "0102030405060708",
                NULL,
                "536f6d652043727970746f2054657874",
                "8974b718d0cb68b44e27c480546dfcc7a33895f461733219");
#else
    assert_false(idea_enabled());
    assert_null(Cipher::encryption(PGP_SA_IDEA, PGP_CIPHER_MODE_CBC, 0, false));
#endif
}

TEST_F(rnp_tests, test_cipher_aes_128_ocb)
{
    // RFC 7253 -- Appendix A -- Sample Results
    // ( The first ten test sets )
    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F", // key
                "BBAA99887766554433221100",         // nounce
                nullptr,                            // ad
                nullptr,                            // data
                "785407BFFFC8AD9EDCC5520AC9111EE6");

    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F",
                "BBAA99887766554433221101",
                "0001020304050607",
                "0001020304050607",
                "6820B3657B6F615A5725BDA0D3B4EB3A257C9AF1F8F03009");

    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F",
                "BBAA99887766554433221102",
                "0001020304050607",
                nullptr,
                "81017F8203F081277152FADE694A0A00");

    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F",
                "BBAA99887766554433221103",
                nullptr,
                "0001020304050607",
                "45DD69F8F5AAE72414054CD1F35D82760B2CD00D2F99BFA9");

    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F",
                "BBAA99887766554433221104",
                "000102030405060708090A0B0C0D0E0F",
                "000102030405060708090A0B0C0D0E0F",
                "571D535B60B277188BE5147170A9A22C3AD7A4FF3835B8C5701C1CCEC8FC3358");

    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F",
                "BBAA99887766554433221105",
                "000102030405060708090A0B0C0D0E0F",
                nullptr,
                "8CF761B6902EF764462AD86498CA6B97");

    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F",
                "BBAA99887766554433221106",
                nullptr,
                "000102030405060708090A0B0C0D0E0F",
                "5CE88EC2E0692706A915C00AEB8B2396F40E1C743F52436BDF06D8FA1ECA343D");

    test_cipher(
      PGP_SA_AES_128,
      PGP_CIPHER_MODE_OCB,
      16,
      false,
      "000102030405060708090A0B0C0D0E0F",
      "BBAA99887766554433221107",
      "000102030405060708090A0B0C0D0E0F1011121314151617",
      "000102030405060708090A0B0C0D0E0F1011121314151617",
      "1CA2207308C87C010756104D8840CE1952F09673A448A122C92C62241051F57356D7F3C90BB0E07F");

    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_OCB,
                16,
                false,
                "000102030405060708090A0B0C0D0E0F",
                "BBAA99887766554433221108",
                "000102030405060708090A0B0C0D0E0F1011121314151617",
                nullptr,
                "6DC225A071FC1B9F7C69F93B0F1E10DE");

    test_cipher(
      PGP_SA_AES_128,
      PGP_CIPHER_MODE_OCB,
      16,
      false,
      "000102030405060708090A0B0C0D0E0F",
      "BBAA99887766554433221109",
      nullptr,
      "000102030405060708090A0B0C0D0E0F1011121314151617",
      "221BD0DE7FA6FE993ECCD769460A0AF2D6CDED0C395B1C3CE725F32494B9F914D85C0B1EB38357FF");
}

TEST_F(rnp_tests, test_cipher_aes_128_cbc)
{
    // botan test vectors
    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_CBC,
                0,
                false,
                "10d6f8e78c0ccf8736e4307aaf5b07ef",
                "3eb182d95bbd5a609aecfb59a0ca898b",
                NULL,
                "3a",
                "a7d290687ae325054a8691014d6821d7");
    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_CBC,
                0,
                false,
                "10d6f8e78c0ccf8736e4307aaf5b07ef",
                "3eb182d95bbd5a609aecfb59a0ca898b",
                NULL,
                "3a513eb569a503b4413b31fa883ddc88",
                "0cbaf4fa94df265fe264633a994bc25fc7654f19c282a3e2db81499c941ca2b3");
}

TEST_F(rnp_tests, test_cipher_aes_128_cbc_nopadding)
{
    // botan test vectors
    test_cipher(PGP_SA_AES_128,
                PGP_CIPHER_MODE_CBC,
                0,
                true,
                "1f8e4973953f3fb0bd6b16662e9a3c17",
                "2fe2b333ceda8f98f4a99b40d2cd34a8",
                NULL,
                "45cf12964fc824ab76616ae2f4bf0822",
                "0f61c4d44c5147c03c195ad7e2cc12b2");
}
