// Copyright (C) 2010-2014 David Sugar, Tycho Softworks.
// Copyroght (C) 2015-2020 Cherokees of Idaho.
//
// This file is part of GNU uCommon C++.
//
// GNU uCommon C++ is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU uCommon C++ 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 Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with GNU uCommon C++.  If not, see <http://www.gnu.org/licenses/>.

#include "local.h"

static const uint8_t *_salt = NULL;
static unsigned _rounds = 1;

namespace ucommon {

void Cipher::Key::assign(const char *text, size_t size)
{
    assign(text, size, _salt, _rounds);
}

void Cipher::Key::options(const uint8_t *salt, unsigned rounds)
{
    _salt = salt;
    _rounds = rounds;
}

void Cipher::Key::assign(const char *text, size_t size, const uint8_t *salt, unsigned rounds)
{
    if(!algotype || !hashtype)
        return;

    if(!size)
        size = strlen((const char *)text);

    if(!rounds)
        rounds = _rounds;

    if(!salt)
        salt = _salt;

    if(EVP_BytesToKey((const EVP_CIPHER*)algotype, (const EVP_MD*)hashtype, salt, (const uint8_t *)text, (int)size, rounds, keybuf, ivbuf) < (int)keysize)
        keysize = 0;
}

void Cipher::Key::set(const char *cipher, const char *digest)
{
    set(cipher);

    // never use sha0...
    if(eq_case(digest, "sha") || eq_case(digest, "sha160"))
        digest = "sha1";

    hashtype = EVP_get_digestbyname(digest);
}

void Cipher::Key::set(const char *cipher)
{
    char algoname[64];

    clear();
    String::set(algoname, sizeof(algoname), cipher);
    char *fpart = strchr(algoname, '-');
    char *lpart = strrchr(algoname, '-');

    if(fpart && fpart == lpart)
        String::set(fpart, sizeof(algoname), fpart + 1);

    algotype = EVP_get_cipherbyname(algoname);

    if(!algotype)
        return;

    keysize = EVP_CIPHER_key_length((const EVP_CIPHER*)algotype);
    blksize = EVP_CIPHER_block_size((const EVP_CIPHER*)algotype);
}


bool Cipher::has(const char *id)
{
    // make sure cipher-bitsize forms without -mode do not fail...
    char algoname[64];
    String::set(algoname, sizeof(algoname), id);
    char *fpart = strchr(algoname, '-');
    char *lpart = strrchr(algoname, '-');

    if(fpart && fpart == lpart)
        String::set(fpart, sizeof(algoname), fpart + 1);

    return (EVP_get_cipherbyname(algoname) != NULL);
}

void Cipher::push(uint8_t *address, size_t size)
{
}

void Cipher::release(void)
{
    keys.clear();
    if(context) {
        EVP_CIPHER_CTX_cleanup((EVP_CIPHER_CTX*)context);
        EVP_CIPHER_CTX_free((EVP_CIPHER_CTX*)context);
        context = NULL;
    }
}

void Cipher::set(const key_t key, mode_t mode, uint8_t *address, size_t size)
{
    release();

    bufsize = size;
    bufmode = mode;
    bufaddr = address;

    memcpy(&keys, key, sizeof(keys));
    if(!keys.keysize)
        return;

    context = EVP_CIPHER_CTX_new();
    EVP_CIPHER_CTX_init((EVP_CIPHER_CTX *)context);
    EVP_CipherInit_ex((EVP_CIPHER_CTX *)context, (EVP_CIPHER *)keys.algotype, NULL, keys.keybuf, keys.ivbuf, (int)mode);
    EVP_CIPHER_CTX_set_padding((EVP_CIPHER_CTX *)context, 0);
}

size_t Cipher::put(const uint8_t *data, size_t size)
{
    int outlen;
    size_t count = 0;

    if(!bufaddr)
        return 0;

    if(size % keys.iosize())
        return 0;

    while(bufsize && size + bufpos > bufsize) {
        size_t diff = bufsize - bufpos;
        count += put(data, diff);
        data += diff;
        size -= diff;
    }

    if(!EVP_CipherUpdate((EVP_CIPHER_CTX *)context, bufaddr + bufpos, &outlen, data, (int)size)) {
        release();
        return count;
    }
    bufpos += outlen;
    count += outlen;
    if(bufsize && bufpos >= bufsize) {
        push(bufaddr, bufsize);
        bufpos = 0;
    }
    return count;
}

size_t Cipher::pad(const uint8_t *data, size_t size)
{
    size_t padsize = 0;
    uint8_t padbuf[64];
    const uint8_t *ep;

    if(!bufaddr)
        return 0;

    switch(bufmode) {
    case DECRYPT:
        if(size % keys.iosize())
            return 0;
        put(data, size);
        ep = data + size - 1;
        bufpos -= *ep;
        size -= *ep;
        break;
    case ENCRYPT:
        padsize = size % keys.iosize();
        put(data, size - padsize);
        if(padsize) {
            memcpy(padbuf, data + size - padsize, padsize);
            memset(padbuf + padsize, (int)(keys.iosize() - padsize), keys.iosize() - padsize);
            size = (size - padsize) + keys.iosize();
        }
        else {
            size += keys.iosize();
            memset(padbuf, (int)keys.iosize(), keys.iosize());
        }

        put((const uint8_t *)padbuf, keys.iosize());
        zerofill(padbuf, sizeof(padbuf));
    }

    flush();
    return size;
}

} // namespace ucommon
