//#
//# Copyright (C) 2018-2019 QuasarApp.
//# Distributed under the lgplv3 software license, see the accompanying
//# Everyone is permitted to copy and distribute verbatim copies
//# of this license document, but changing it is not allowed.
//#

#include "qrsaencryption.h"

#include <QString>
#include <iostream>
#include <ctime>
#include <chrono>

#define KEY_GEN_LIMIT 10

const QString SIGN_MARKER = "-SIGN-";
const int signMarkerLength = SIGN_MARKER.length();

QRSAEncryption::INT eulerFunc(const QRSAEncryption::INT &p, const QRSAEncryption::INT &q) {
    return (p - 1) * (q - 1);
}

bool QRSAEncryption::isMutuallyPrime(const INT &a, const INT &b) {
    if (        (!(a % 2) && !(b % 2))
                || (!(a % 3) && !(b % 3))
                || (!(a % 5) && !(b % 5))
                || (!(a % 7) && !(b % 7))
                ) return false;

    return INT().gcd(a, b) == 1;
}

QRSAEncryption::Rsa QRSAEncryption::getBitsSize(const INT &i) const {
    int rsaBits = RSA_64;
    int intBits = i.sizeBits();

    while (rsaBits < intBits) {
        rsaBits *= 2;
    }

    return static_cast<QRSAEncryption::Rsa>(rsaBits);

}

QRSAEncryption::Rsa QRSAEncryption::getBitsSize(const QByteArray &key) const {
    if (isValidRsaKey(key)) {
        return static_cast<QRSAEncryption::Rsa>(key.size() * 4);
    }

    return QRSAEncryption::Rsa::Invalid;
}

QRSAEncryption::INT QRSAEncryption::fromArray(const QByteArray &array) const {
    INT res = 0;
    res.fromHex(array.toHex().toStdString());
    return res;
}

QByteArray QRSAEncryption::toArray(const INT &i, short sizeBlok) {
    QByteArray res;
    res = QByteArray::fromHex(QByteArray::fromStdString(i.getString(16)));

    if (sizeBlok < 0) {
        return res;
    }

    while (res.size() < sizeBlok) {
        res.push_front(char(0));
    }

    return res.left(sizeBlok);
}

QRSAEncryption::INT QRSAEncryption::randomNumber(bool fullFill) const {

    srand(std::chrono::duration_cast<std::chrono::nanoseconds>
          (std::chrono::system_clock::now().time_since_epoch()).count()
          % std::numeric_limits<int>::max());

    INT res{1};

    if(fullFill) {

        while(res.longBits() < _rsa) {
            res *= (rand() % (std::numeric_limits<int>::max() - 1)) + 1;
        }
    } else {

        int longDiff = _rsa / (sizeof (int) * 8);

        while (longDiff > 0) {
            longDiff--;
            res *= (rand() % (std::numeric_limits<int>::max() - 1)) + 1;
        }
    }

    return res;
}

QRSAEncryption::INT QRSAEncryption::toPrime(INT n) const {
    if (!(n % 2)) {
        ++n;
    }

    INT LN = n;
    INT RN = n;

    while (true) {

        if (LN.isPrime(false)) return LN;

        RN+=2;

        if (RN.isPrime(false)) return RN;
        LN-=2;
    }
}

QRSAEncryption::INT QRSAEncryption::randomPrimeNumber(INT no) const {
    srand(static_cast<unsigned int>(time(nullptr)));

    // max INT
    INT max('1', _rsa / 2, 2);

    auto p = toPrime(randomNumber() % max);
    while(p == no) p = toPrime(randomNumber() % max);

    return p;
}

QRSAEncryption::INT QRSAEncryption::extEuclid(INT a, INT b) const {
    INT x = 0, y = 1, u = 1, v = 0, gcd = b, m, n, q, r;
    while (a != 0) {
        q = gcd / a;
        r = gcd % a;
        m = x - u * q;
        n = y - v * q;
        gcd = a;
        a = r;
        x = u;
        y = v;
        u = m;
        v = n;
    }
    return y;
}

short QRSAEncryption::getBlockSize(INT i) const {
    return static_cast<short>(i.longBytes()) - 1;
}

QByteArray QRSAEncryption::encodeBlok(const INT &block, const INT &e, const INT &m, short blockSize) {

    return toArray(INT::powm(block, e, m), blockSize);
}

QByteArray QRSAEncryption::decodeBlok(const INT &block, const INT &d, const INT &m, short blockSize) {

    return toArray(INT::powm(block, d, m), blockSize);
}

QRSAEncryption::QRSAEncryption(Rsa rsa) {
    _rsa = rsa;
}

bool QRSAEncryption::generatePairKeyS(QByteArray &pubKey, QByteArray &privKey, QRSAEncryption::Rsa rsa) {
    qWarning() << "method " << Q_FUNC_INFO <<
                  " will be deleted in newxt version. please use generatePairKey method";
    return generatePairKey(pubKey, privKey, rsa);
}

QByteArray QRSAEncryption::encodeS(const QByteArray &rawData, const QByteArray &pubKey, QRSAEncryption::Rsa rsa, QRSAEncryption::BlockSize blockSizeMode) {
    qWarning() << "method " << Q_FUNC_INFO <<
                  " will be deleted in newxt version. please use encode method";
    return encode(rawData, pubKey, rsa, blockSizeMode);

}

QByteArray QRSAEncryption::decodeS(const QByteArray &rawData, const QByteArray &privKey, QRSAEncryption::Rsa rsa, QRSAEncryption::BlockSize blockSizeMode) {
    qWarning() << "method " << Q_FUNC_INFO <<
                  " will be deleted in newxt version. please use decode method";
    return decode(rawData, privKey, rsa, blockSizeMode);

}

QByteArray QRSAEncryption::signMessageS(QByteArray rawData, const QByteArray &privKey, QRSAEncryption::Rsa rsa) {
    qWarning() << "method " << Q_FUNC_INFO <<
                  " will be deleted in newxt version. please use signMessage method";
    return signMessage(rawData, privKey, rsa);

}

bool QRSAEncryption::checkSignMessageS(const QByteArray &rawData, const QByteArray &pubKey, QRSAEncryption::Rsa rsa) {
    qWarning() << "method " << Q_FUNC_INFO <<
                  " will be deleted in newxt version. please use signMessage method";
    return checkSignMessage(rawData, pubKey, rsa);
}

unsigned int QRSAEncryption::getKeyBytesSize(QRSAEncryption::Rsa rsa) {

    return rsa / 4;
}

// --- static methods ---
bool QRSAEncryption::generatePairKey(QByteArray &pubKey, QByteArray &privKey,
                                      QRSAEncryption::Rsa rsa) {

    return QRSAEncryption(rsa).generatePairKey(pubKey, privKey);
}

QByteArray QRSAEncryption::encode(const QByteArray &rawData, const QByteArray &pubKey,
                                   Rsa rsa, BlockSize blockSizeMode) {

    return QRSAEncryption(rsa).encode(rawData, pubKey, blockSizeMode);
}

QByteArray QRSAEncryption::decode(const QByteArray &rawData, const QByteArray &privKey,
                                   Rsa rsa, BlockSize blockSizeMode) {

    return QRSAEncryption(rsa).decode(rawData, privKey, blockSizeMode);
}

QByteArray QRSAEncryption::signMessage(QByteArray rawData, const QByteArray &privKey, Rsa rsa) {

    return QRSAEncryption(rsa).signMessage(rawData, privKey);
}

bool QRSAEncryption::checkSignMessage(const QByteArray &rawData, const QByteArray &pubKey, Rsa rsa) {

    return QRSAEncryption(rsa).checkSignMessage(rawData, pubKey);
}

// --- end of static methods ---

bool QRSAEncryption::generatePairKey(QByteArray &pubKey, QByteArray &privKey) {

    int cnt{0};
    bool keyGenRes{false};
    INT p, q, modul, eilor, e, d;
    do {

        pubKey.clear();
        privKey.clear();

        p = randomPrimeNumber();
        q = randomPrimeNumber(p);

        modul = 0;
        while ((modul = p * q) < 0) {
            p = toPrime((p - 1) / 2);
        }

        eilor = eulerFunc(p, q);
        e = randomNumber() % eilor;

        if (!(e % 2)) --e;

        do {
            e -= 2;
        } while((!isMutuallyPrime(eilor, e)));

        d = extEuclid(eilor , e);

        while(d < 0 ) {
            d += eilor;
        }

        pubKey.append(toArray(e, _rsa / 8));
        pubKey.append(toArray(modul, _rsa / 8));
        privKey.append(toArray(d, _rsa / 8));
        privKey.append(toArray(modul, _rsa / 8));

    } while (!(keyGenRes = testKeyPair(pubKey, privKey)) && (++cnt < KEY_GEN_LIMIT));

    if(cnt >= KEY_GEN_LIMIT) qWarning() << QString("(Warning): Exceeded limit of key generation (%0)!").arg(KEY_GEN_LIMIT);

    return (keyGenRes && cnt < KEY_GEN_LIMIT);
}

// --- non-static methods ---
QByteArray QRSAEncryption::encode(const QByteArray &rawData, const QByteArray &pubKey, BlockSize blockSizeMode) {

    if (getBitsSize(pubKey) != _rsa) {
        return QByteArray();
    }

    int index = 0;

    QByteArray block;

    INT e = fromArray(pubKey.mid(0, pubKey.size() / 2));
    INT m = fromArray(pubKey.mid(pubKey.size() / 2));

    short blockSizeOut = getBlockSize(m) + 1; // BlockSize::OneByte
    short blockSizeIn = 1; // BlockSize::OneByte

    if (blockSizeMode == BlockSize::Auto) {
        blockSizeIn = getBlockSize(m);
    }

    if (!blockSizeIn) {
        qDebug() << "module of key small! size = 1 byte, 2 byte is minimum";
        return QByteArray();
    }

    QByteArray res;

    while ((block = rawData.mid(index, blockSizeIn)).size()) {

        if (index + blockSizeIn > rawData.size() && block.size() && !block[0]) {
            qWarning() << "When trying to encrypt data, problems arose, the last block contains non-significant zeros."
                          " These zeros will be deleted during the decryption process."
                          " For encode and decode data with non-significant zeros use BlockSize::OneByte";
        }

        res.append(encodeBlok(fromArray(block), e, m, blockSizeOut));
        index += blockSizeIn;
    }

    return res;

}
QByteArray QRSAEncryption::decode(const QByteArray &rawData, const QByteArray &privKey, BlockSize blockSizeMode) {

    if (getBitsSize(privKey) != _rsa) {
        return QByteArray();
    }

    int index = 0;

    QByteArray block;

    INT d = fromArray(privKey.mid(0, privKey.size() / 2));
    INT m = fromArray(privKey.mid(privKey.size() / 2));

    short blockSizeIn = getBlockSize(m) + 1;

    short blockSizeOut = 1; // BlockSize::OneByte
    if (blockSizeMode == BlockSize::Auto) {
        blockSizeOut = getBlockSize(m);
    }

    QByteArray res;
    while ((block = rawData.mid(index, blockSizeIn)).size()) {
        bool isLastBlock = (index + blockSizeIn) >= rawData.size();

        res.append(decodeBlok(fromArray(block), d, m,
                              (isLastBlock && blockSizeMode == BlockSize::Auto)? -1 : blockSizeOut));
        index += blockSizeIn;
    }
    return res;

}
QByteArray QRSAEncryption::signMessage(QByteArray rawData, const QByteArray &privKey) {

    QByteArray hash = QCryptographicHash::hash(rawData, HashAlgorithm::Sha256);

    QByteArray signature = encode(hash, privKey, BlockSize::OneByte);

    rawData.append(SIGN_MARKER + signature.toHex() + SIGN_MARKER);

    return rawData;
}
bool QRSAEncryption::checkSignMessage(const QByteArray &rawData, const QByteArray &pubKey) {

    // start position of SIGN_MARKER in rawData
    auto signStartPos = rawData.lastIndexOf(SIGN_MARKER, rawData.length() - signMarkerLength - 1);

    // length of signature in rawData
    auto signLength   = rawData.length() - signStartPos - signMarkerLength * 2;

    // message, that was recieved from channel
    QByteArray message = rawData.left(signStartPos);

    // hash, that was decrypt from recieved signature
    QByteArray recievedHash = decode(QByteArray::fromHex(rawData.mid(signStartPos + signMarkerLength, signLength)),
                                     pubKey, BlockSize::OneByte);

    // if recievedHash == hashAlgorithm(recived message), then signed message is valid
    return recievedHash == QCryptographicHash::hash(message, HashAlgorithm::Sha256);
}

QRSAEncryption::Rsa QRSAEncryption::getRsa() const {
    return _rsa;
}

bool QRSAEncryption::testKeyPair(const QByteArray &pubKey, const QByteArray &privKey) {

    QByteArray tesVal = "Test message of encrypkey";

    bool result = tesVal == decode(encode(tesVal, pubKey), privKey);

    if (!result) qWarning() << "(Warning): Testkey Fail, try generate new key pair!";

    return result;
}

// --- end of non-static methods ---

bool QRSAEncryption::isValidRsaKey(const QByteArray &key) {
    return key.size() && ((static_cast<unsigned int>(key.size()) % getKeyBytesSize(RSA_64)) == 0);
}
