/*
 * This file is part of the Amalthea library.
 * Copyright (C) 2018-2021, 2024-2026 Eugene 'Vindex' Stulin
 * Distributed under the BSL 1.0 or the GNU LGPL 3.0 or later.
 */

/**
 * The module contains the Streebog cryptographic hash function
 * and several simple cryptographic functions.
 */
module amalthea.crypto;
public import amalthea.libcore;

import std.array : array;
import std.conv : to;
import std.digest : toHexString;
import std.digest.md : MD5Digest;
import std.digest.sha : SHA256;
import std.exception : enforce;
static import std.file;
import std.file : getSize;
import std.range : empty, iota;
import std.stdio : File, SEEK_SET;
import std.string : toLower, toUpper;
import std.traits : isSomeChar;


/*******************************************************************************
 * The function computes MD5 digest for the file.
 */
string getFileMD5Sum(string filepath) {
    auto file = File(filepath, "r");
    auto md5 = new MD5Digest();
    enum chunkSize = 65_536; // 64 KiB
    foreach(buffer; file.byChunk(chunkSize)) {
        md5.put(buffer);
    }
    auto hash = md5.finish();
    return hash.toHexString.idup.toLower;
}


/*******************************************************************************
 * The function computes SHA-2556 digest for the file.
 */
string getFileSha256Sum(string filepath) {
    auto file = File(filepath, "rb");
    SHA256 hash;
    hash.start();
    enum chunkSize = 65_536; // 64 KiB
    foreach(buffer; file.byChunk(chunkSize)) {
        hash.put(buffer);
    }
    auto result = hash.finish();
    return result.toHexString.idup.toLower;
}


/*******************************************************************************
 * The function computes Streebog hash sum for the file.
 */
string getFileStreebogSum(alias bits)(string filepath)
if (bits == 256 || bits == 512) {
    int chunkSize = 64*16;
    auto streebogObj = new Streebog(bits);
    auto file = File(filepath, "r");
    long filesize = getSize(filepath);
    while(filesize > 0) {
        size_t portionSize;
        if (filesize < chunkSize) {
            portionSize = cast(size_t)filesize;
            filesize = 0;
        } else {
            portionSize = chunkSize;
            filesize -= chunkSize;
        }
        file.seek(filesize, SEEK_SET);
        ubyte[] buffer = file.rawRead(new ubyte[portionSize]);
        streebogObj.pushFront(buffer);
    }
    auto hash = streebogObj.finish();
    return hash.toHexString.toLower;
}


/*******************************************************************************
 * 256-bit version of file Streebog hash sum.
 */
string getFileStreebog256Sum(string filepath) {
    return getFileStreebogSum!256(filepath);
}


/*******************************************************************************
 * 512-bit version of file Streebog hash sum.
 */
string getFileStreebog512Sum(string filepath) {
    return getFileStreebogSum!512(filepath);
}


unittest {
    ubyte[68] bytes = 0x25;
    bytes[0 .. 4] = [0xA1, 0x77, 0x19, 0xFC];
    string experimentalFile = "build/test.bin";
    std.file.write(experimentalFile, bytes);
    string expected = Streebog.calcHash!512(bytes).toHexString.toLower;
    assert(getFileStreebogSum!512(experimentalFile) == expected);
    expected = Streebog.calcHash!256(bytes).toHexString.toLower;
    assert(getFileStreebogSum!256(experimentalFile) == expected);
    std.file.remove(experimentalFile);
}


/*******************************************************************************
 * Auxiliary function of the remainder of the division
 * for the Streebog algorithm.
 */
ubyte[64] modulo512(in ubyte[64] a, in ubyte[64] b) {
    auto t = 0;
    ubyte[64] c;
    for(int i = 63; i >= 0; --i) {
        t = a[i] + b[i] + (t >> 8);
        c[i] = t & 0xFF;
    }
    return c;
}


/*******************************************************************************
 * Auxiliary XOR-function for the Streebog algorithm.
 */
ubyte[64] xor512(in ubyte[64] a, in ubyte[64] b) {
    ubyte[64] c;
    foreach(i; 0 .. 64) {
        c[i] = a[i] ^ b[i];
    }
    return c;
}


/*******************************************************************************
 * The Streebog class implements GOST R 34.11-2012
 * for calculating a hash value.
 */
class Streebog {
    size_t bitMode;
    this() {
        bitMode = 512;
    }
    this(size_t bitMode) {
        enforce(bitMode == 256 || bitMode == 512);
        this.bitMode = bitMode;
    }

    /***************************************************************************
     * The static template function takes an array of bytes
     * and returns a hash sum.
     */
    static ubyte[] calcHash(alias bits)(in ubyte[] msg)
    if (bits == 512 || bits == 256) {
        ubyte[] message = msg.dup;
        ubyte[64] v512;
        v512[$-2] = 0x02;
        ubyte[64] v0;
        ubyte[64] sigma;
        ubyte[64] N;
        ubyte[64] m;
        ubyte[64] hash = bits == 512 ? IV512 : IV256;
        size_t len = message.length*8;
        while (len >= 512) {
            m = message[$-64 .. $];
            hash = g(N, hash, m);
            N = modulo512(N, v512);
            sigma = modulo512(sigma, m);
            len -= 512;
            message = message[0 .. $-64];
        }
        m[] = 0;
        size_t l = len/8 + 1 - ((len & 0x7) == 0);
        size_t mshift = 63 - len/8 + ((len & 0x7) == 0);
        m[mshift .. mshift + l] = message[0 .. l];

        m[63 - len/8] |= (1 << (len & 0x7));

        hash = g(N, hash, m);
        v512[63] = len & 0xFF;
        v512[62] = cast(ubyte)(len >> 8);
        N = modulo512(N, v512);
        sigma = modulo512(sigma, m);

        hash = g(v0, hash, N);
        hash = g(v0, hash, sigma);
        return bits == 256 ? hash[0 .. $/2].dup : hash.dup;
    }


    /***************************************************************************
     * Accepts a sequence of data.
     * Each new sequence is perceived as preceding relative to the previous one.
     */
    void pushFront(in ubyte[] portion) {
        ubyte[] message = portion.dup ~ currState.residue;
        if (currState == ObjectState.init) {
            currState.hash = bitMode == 512 ? IV512 : IV256;
            currState.v512[$-2] = 0x02;
        }

        ulong bitlen = message.length * 8;
        while (bitlen >= 512) {
            ubyte[64] m = message[$-64 .. $];
            message = message[0 .. $-64];
            currState.hash = g(currState.N, currState.hash, m);
            currState.N = modulo512(currState.N, currState.v512);
            currState.sigma = modulo512(currState.sigma, m);
            bitlen -= 512;
        }
        currState.residue = message.dup;
    }

    /***************************************************************************
     * Finishes the calculation of the hash sum and returns it.
     */
    ubyte[] finish() {
        ubyte[64] v0;
        ubyte[64] m;
        size_t len = currState.residue.length * 8;
        size_t l = len/8 + 1 - ((len & 0x7) == 0);
        size_t mshift = 63 - len/8 + ((len & 0x7) == 0);
        m[mshift .. mshift + l] = currState.residue[0 .. l];
        m[63 - len/8] |= (1 << (len & 0x7));

        currState.hash = g(currState.N, currState.hash, m);
        currState.v512[63] = len & 0xFF;
        currState.v512[62] = cast(ubyte)(len >> 8);
        currState.N = modulo512(currState.N, currState.v512);
        currState.sigma = modulo512(currState.sigma, m);

        currState.hash = g(v0, currState.hash, currState.N);
        currState.hash = g(v0, currState.hash, currState.sigma);
        return bitMode == 256 ? currState.hash[0 .. $/2].dup
                              : currState.hash.dup;
    }

protected:

    struct ObjectState {
        ubyte[64] hash;
        ubyte[64] N;
        ubyte[64] sigma;
        ubyte[] residue;
        ubyte[64] v512;
    }
    ObjectState currentObjectState;
    alias currState = currentObjectState;


    static immutable ubyte[64] IV512 = 0x00;
    static immutable ubyte[64] IV256 = 0x01;

    static ubyte[64] S(in ubyte[64] state) {
        ubyte[64] newState = state;
        foreach(i; 0 .. 64) {
            newState[i] = piTable[state[i]];
        }
        return newState;
    }

    static ubyte[64] P(ubyte[64] state) {
        ubyte[64] t;
        foreach(i; 0 .. 64) {
            t[i] = state[tauTable[i]];
        }
        return t;
    }

    static ubyte[64] L(ubyte[64] state) {
        ubyte[64] newState = state;
        ulong v = 0;
        foreach(i; 0 .. 8) {
            v = 0;
            foreach(k; 0 .. 8) {
                foreach(j; 0 .. 8) {
                    if ((newState[i*8+k] & (1 << (7-j))) != 0) {
                        v ^= A[k*8+j];
                    }
                }
            }
            foreach(k; 0 .. 8) {
                auto value = (v & (0xFFL << (7-k)*8)) >> (7-k)*8;
                newState[i*8+k] = cast(ubyte)value;
            }
        }
        return newState;
    }

    static ubyte[64] g(in ubyte[64] N, in ubyte[64] h, in ubyte[64] m) {
        ubyte[64] K = xor512(h, N);
        K = L(P(S(K)));
        ubyte[64] t = E(K, m);
        t = xor512(h, t);
        ubyte[64] G = xor512(t, m);
        return G;
    }

    static ubyte[64] E(ubyte[64] K, ubyte[64] m) {
        void keySchedule(size_t i) {
            K = xor512(K, C[i]);
            K = L(P(S(K)));
        }
        ubyte[64] state = xor512(K, m);
        foreach(i; 0 .. 12) {
            state = L(P(S(state)));
            keySchedule(i);
            state = xor512(state, K);
        }
        return state;
    }

    static immutable ulong[64] A = [
        0x8e20faa72ba0b470, 0x47107ddd9b505a38, 0xad08b0e0c3282d1c,
        0xd8045870ef14980e, 0x6c022c38f90a4c07, 0x3601161cf205268d,
        0x1b8e0b0e798c13c8, 0x83478b07b2468764, 0xa011d380818e8f40,
        0x5086e740ce47c920, 0x2843fd2067adea10, 0x14aff010bdd87508,
        0x0ad97808d06cb404, 0x05e23c0468365a02, 0x8c711e02341b2d01,
        0x46b60f011a83988e, 0x90dab52a387ae76f, 0x486dd4151c3dfdb9,
        0x24b86a840e90f0d2, 0x125c354207487869, 0x092e94218d243cba,
        0x8a174a9ec8121e5d, 0x4585254f64090fa0, 0xaccc9ca9328a8950,
        0x9d4df05d5f661451, 0xc0a878a0a1330aa6, 0x60543c50de970553,
        0x302a1e286fc58ca7, 0x18150f14b9ec46dd, 0x0c84890ad27623e0,
        0x0642ca05693b9f70, 0x0321658cba93c138, 0x86275df09ce8aaa8,
        0x439da0784e745554, 0xafc0503c273aa42a, 0xd960281e9d1d5215,
        0xe230140fc0802984, 0x71180a8960409a42, 0xb60c05ca30204d21,
        0x5b068c651810a89e, 0x456c34887a3805b9, 0xac361a443d1c8cd2,
        0x561b0d22900e4669, 0x2b838811480723ba, 0x9bcf4486248d9f5d,
        0xc3e9224312c8c1a0, 0xeffa11af0964ee50, 0xf97d86d98a327728,
        0xe4fa2054a80b329c, 0x727d102a548b194e, 0x39b008152acb8227,
        0x9258048415eb419d, 0x492c024284fbaec0, 0xaa16012142f35760,
        0x550b8e9e21f7a530, 0xa48b474f9ef5dc18, 0x70a6a56e2440598e,
        0x3853dc371220a247, 0x1ca76e95091051ad, 0x0edd37c48a08a6d8,
        0x07e095624504536c, 0x8d70c431ac02a736, 0xc83862965601dd1b,
        0x641c314b2b8ee083
    ];

    static immutable ubyte[256] piTable = [
        0xFC, 0xEE, 0xDD, 0x11, 0xCF, 0x6E, 0x31, 0x16,
        0xFB, 0xC4, 0xFA, 0xDA, 0x23, 0xC5, 0x04, 0x4D,
        0xE9, 0x77, 0xF0, 0xDB, 0x93, 0x2E, 0x99, 0xBA,
        0x17, 0x36, 0xF1, 0xBB, 0x14, 0xCD, 0x5F, 0xC1,
        0xF9, 0x18, 0x65, 0x5A, 0xE2, 0x5C, 0xEF, 0x21,
        0x81, 0x1C, 0x3C, 0x42, 0x8B, 0x01, 0x8E, 0x4F,
        0x05, 0x84, 0x02, 0xAE, 0xE3, 0x6A, 0x8F, 0xA0,
        0x06, 0x0B, 0xED, 0x98, 0x7F, 0xD4, 0xD3, 0x1F,

        0xEB, 0x34, 0x2C, 0x51, 0xEA, 0xC8, 0x48, 0xAB,
        0xF2, 0x2A, 0x68, 0xA2, 0xFD, 0x3A, 0xCE, 0xCC,
        0xB5, 0x70, 0x0E, 0x56, 0x08, 0x0C, 0x76, 0x12,
        0xBF, 0x72, 0x13, 0x47, 0x9C, 0xB7, 0x5D, 0x87,
        0x15, 0xA1, 0x96, 0x29, 0x10, 0x7B, 0x9A, 0xC7,
        0xF3, 0x91, 0x78, 0x6F, 0x9D, 0x9E, 0xB2, 0xB1,
        0x32, 0x75, 0x19, 0x3D, 0xFF, 0x35, 0x8A, 0x7E,
        0x6D, 0x54, 0xC6, 0x80, 0xC3, 0xBD, 0x0D, 0x57,

        0xDF, 0xF5, 0x24, 0xA9, 0x3E, 0xA8, 0x43, 0xC9,
        0xD7, 0x79, 0xD6, 0xF6, 0x7C, 0x22, 0xB9, 0x03,
        0xE0, 0x0F, 0xEC, 0xDE, 0x7A, 0x94, 0xB0, 0xBC,
        0xDC, 0xE8, 0x28, 0x50, 0x4E, 0x33, 0x0A, 0x4A,
        0xA7, 0x97, 0x60, 0x73, 0x1E, 0x00, 0x62, 0x44,
        0x1A, 0xB8, 0x38, 0x82, 0x64, 0x9F, 0x26, 0x41,
        0xAD, 0x45, 0x46, 0x92, 0x27, 0x5E, 0x55, 0x2F,
        0x8C, 0xA3, 0xA5, 0x7D, 0x69, 0xD5, 0x95, 0x3B,

        0x07, 0x58, 0xB3, 0x40, 0x86, 0xAC, 0x1D, 0xF7,
        0x30, 0x37, 0x6B, 0xE4, 0x88, 0xD9, 0xE7, 0x89,
        0xE1, 0x1B, 0x83, 0x49, 0x4C, 0x3F, 0xF8, 0xFE,
        0x8D, 0x53, 0xAA, 0x90, 0xCA, 0xD8, 0x85, 0x61,
        0x20, 0x71, 0x67, 0xA4, 0x2D, 0x2B, 0x09, 0x5B,
        0xCB, 0x9B, 0x25, 0xD0, 0xBE, 0xE5, 0x6C, 0x52,
        0x59, 0xA6, 0x74, 0xD2, 0xE6, 0xF4, 0xB4, 0xC0,
        0xD1, 0x66, 0xAF, 0xC2, 0x39, 0x4B, 0x63, 0xB6
    ];

    static immutable ubyte[64] tauTable = [
        0,  8, 16, 24, 32, 40, 48, 56,
        1,  9, 17, 25, 33, 41, 49, 57,
        2, 10, 18, 26, 34, 42, 50, 58,
        3, 11, 19, 27, 35, 43, 51, 59,
        4, 12, 20, 28, 36, 44, 52, 60,
        5, 13, 21, 29, 37, 45, 53, 61,
        6, 14, 22, 30, 38, 46, 54, 62,
        7, 15, 23, 31, 39, 47, 55, 63
    ];

    static immutable ubyte[64][12] C = [
        [
            0xb1, 0x08, 0x5b, 0xda, 0x1e, 0xca, 0xda, 0xe9,
            0xeb, 0xcb, 0x2f, 0x81, 0xc0, 0x65, 0x7c, 0x1f,
            0x2f, 0x6a, 0x76, 0x43, 0x2e, 0x45, 0xd0, 0x16,
            0x71, 0x4e, 0xb8, 0x8d, 0x75, 0x85, 0xc4, 0xfc,
            0x4b, 0x7c, 0xe0, 0x91, 0x92, 0x67, 0x69, 0x01,
            0xa2, 0x42, 0x2a, 0x08, 0xa4, 0x60, 0xd3, 0x15,
            0x05, 0x76, 0x74, 0x36, 0xcc, 0x74, 0x4d, 0x23,
            0xdd, 0x80, 0x65, 0x59, 0xf2, 0xa6, 0x45, 0x07
        ],[
            0x6f, 0xa3, 0xb5, 0x8a, 0xa9, 0x9d, 0x2f, 0x1a,
            0x4f, 0xe3, 0x9d, 0x46, 0x0f, 0x70, 0xb5, 0xd7,
            0xf3, 0xfe, 0xea, 0x72, 0x0a, 0x23, 0x2b, 0x98,
            0x61, 0xd5, 0x5e, 0x0f, 0x16, 0xb5, 0x01, 0x31,
            0x9a, 0xb5, 0x17, 0x6b, 0x12, 0xd6, 0x99, 0x58,
            0x5c, 0xb5, 0x61, 0xc2, 0xdb, 0x0a, 0xa7, 0xca,
            0x55, 0xdd, 0xa2, 0x1b, 0xd7, 0xcb, 0xcd, 0x56,
            0xe6, 0x79, 0x04, 0x70, 0x21, 0xb1, 0x9b, 0xb7
        ],[
            0xf5, 0x74, 0xdc, 0xac, 0x2b, 0xce, 0x2f, 0xc7,
            0x0a, 0x39, 0xfc, 0x28, 0x6a, 0x3d, 0x84, 0x35,
            0x06, 0xf1, 0x5e, 0x5f, 0x52, 0x9c, 0x1f, 0x8b,
            0xf2, 0xea, 0x75, 0x14, 0xb1, 0x29, 0x7b, 0x7b,
            0xd3, 0xe2, 0x0f, 0xe4, 0x90, 0x35, 0x9e, 0xb1,
            0xc1, 0xc9, 0x3a, 0x37, 0x60, 0x62, 0xdb, 0x09,
            0xc2, 0xb6, 0xf4, 0x43, 0x86, 0x7a, 0xdb, 0x31,
            0x99, 0x1e, 0x96, 0xf5, 0x0a, 0xba, 0x0a, 0xb2
        ],[
            0xef, 0x1f, 0xdf, 0xb3, 0xe8, 0x15, 0x66, 0xd2,
            0xf9, 0x48, 0xe1, 0xa0, 0x5d, 0x71, 0xe4, 0xdd,
            0x48, 0x8e, 0x85, 0x7e, 0x33, 0x5c, 0x3c, 0x7d,
            0x9d, 0x72, 0x1c, 0xad, 0x68, 0x5e, 0x35, 0x3f,
            0xa9, 0xd7, 0x2c, 0x82, 0xed, 0x03, 0xd6, 0x75,
            0xd8, 0xb7, 0x13, 0x33, 0x93, 0x52, 0x03, 0xbe,
            0x34, 0x53, 0xea, 0xa1, 0x93, 0xe8, 0x37, 0xf1,
            0x22, 0x0c, 0xbe, 0xbc, 0x84, 0xe3, 0xd1, 0x2e
        ],[
            0x4b, 0xea, 0x6b, 0xac, 0xad, 0x47, 0x47, 0x99,
            0x9a, 0x3f, 0x41, 0x0c, 0x6c, 0xa9, 0x23, 0x63,
            0x7f, 0x15, 0x1c, 0x1f, 0x16, 0x86, 0x10, 0x4a,
            0x35, 0x9e, 0x35, 0xd7, 0x80, 0x0f, 0xff, 0xbd,
            0xbf, 0xcd, 0x17, 0x47, 0x25, 0x3a, 0xf5, 0xa3,
            0xdf, 0xff, 0x00, 0xb7, 0x23, 0x27, 0x1a, 0x16,
            0x7a, 0x56, 0xa2, 0x7e, 0xa9, 0xea, 0x63, 0xf5,
            0x60, 0x17, 0x58, 0xfd, 0x7c, 0x6c, 0xfe, 0x57
        ],[
            0xae, 0x4f, 0xae, 0xae, 0x1d, 0x3a, 0xd3, 0xd9,
            0x6f, 0xa4, 0xc3, 0x3b, 0x7a, 0x30, 0x39, 0xc0,
            0x2d, 0x66, 0xc4, 0xf9, 0x51, 0x42, 0xa4, 0x6c,
            0x18, 0x7f, 0x9a, 0xb4, 0x9a, 0xf0, 0x8e, 0xc6,
            0xcf, 0xfa, 0xa6, 0xb7, 0x1c, 0x9a, 0xb7, 0xb4,
            0x0a, 0xf2, 0x1f, 0x66, 0xc2, 0xbe, 0xc6, 0xb6,
            0xbf, 0x71, 0xc5, 0x72, 0x36, 0x90, 0x4f, 0x35,
            0xfa, 0x68, 0x40, 0x7a, 0x46, 0x64, 0x7d, 0x6e
        ],[
            0xf4, 0xc7, 0x0e, 0x16, 0xee, 0xaa, 0xc5, 0xec,
            0x51, 0xac, 0x86, 0xfe, 0xbf, 0x24, 0x09, 0x54,
            0x39, 0x9e, 0xc6, 0xc7, 0xe6, 0xbf, 0x87, 0xc9,
            0xd3, 0x47, 0x3e, 0x33, 0x19, 0x7a, 0x93, 0xc9,
            0x09, 0x92, 0xab, 0xc5, 0x2d, 0x82, 0x2c, 0x37,
            0x06, 0x47, 0x69, 0x83, 0x28, 0x4a, 0x05, 0x04,
            0x35, 0x17, 0x45, 0x4c, 0xa2, 0x3c, 0x4a, 0xf3,
            0x88, 0x86, 0x56, 0x4d, 0x3a, 0x14, 0xd4, 0x93
        ],[
            0x9b, 0x1f, 0x5b, 0x42, 0x4d, 0x93, 0xc9, 0xa7,
            0x03, 0xe7, 0xaa, 0x02, 0x0c, 0x6e, 0x41, 0x41,
            0x4e, 0xb7, 0xf8, 0x71, 0x9c, 0x36, 0xde, 0x1e,
            0x89, 0xb4, 0x44, 0x3b, 0x4d, 0xdb, 0xc4, 0x9a,
            0xf4, 0x89, 0x2b, 0xcb, 0x92, 0x9b, 0x06, 0x90,
            0x69, 0xd1, 0x8d, 0x2b, 0xd1, 0xa5, 0xc4, 0x2f,
            0x36, 0xac, 0xc2, 0x35, 0x59, 0x51, 0xa8, 0xd9,
            0xa4, 0x7f, 0x0d, 0xd4, 0xbf, 0x02, 0xe7, 0x1e
        ],[
            0x37, 0x8f, 0x5a, 0x54, 0x16, 0x31, 0x22, 0x9b,
            0x94, 0x4c, 0x9a, 0xd8, 0xec, 0x16, 0x5f, 0xde,
            0x3a, 0x7d, 0x3a, 0x1b, 0x25, 0x89, 0x42, 0x24,
            0x3c, 0xd9, 0x55, 0xb7, 0xe0, 0x0d, 0x09, 0x84,
            0x80, 0x0a, 0x44, 0x0b, 0xdb, 0xb2, 0xce, 0xb1,
            0x7b, 0x2b, 0x8a, 0x9a, 0xa6, 0x07, 0x9c, 0x54,
            0x0e, 0x38, 0xdc, 0x92, 0xcb, 0x1f, 0x2a, 0x60,
            0x72, 0x61, 0x44, 0x51, 0x83, 0x23, 0x5a, 0xdb
        ],[
            0xab, 0xbe, 0xde, 0xa6, 0x80, 0x05, 0x6f, 0x52,
            0x38, 0x2a, 0xe5, 0x48, 0xb2, 0xe4, 0xf3, 0xf3,
            0x89, 0x41, 0xe7, 0x1c, 0xff, 0x8a, 0x78, 0xdb,
            0x1f, 0xff, 0xe1, 0x8a, 0x1b, 0x33, 0x61, 0x03,
            0x9f, 0xe7, 0x67, 0x02, 0xaf, 0x69, 0x33, 0x4b,
            0x7a, 0x1e, 0x6c, 0x30, 0x3b, 0x76, 0x52, 0xf4,
            0x36, 0x98, 0xfa, 0xd1, 0x15, 0x3b, 0xb6, 0xc3,
            0x74, 0xb4, 0xc7, 0xfb, 0x98, 0x45, 0x9c, 0xed
        ],[
            0x7b, 0xcd, 0x9e, 0xd0, 0xef, 0xc8, 0x89, 0xfb,
            0x30, 0x02, 0xc6, 0xcd, 0x63, 0x5a, 0xfe, 0x94,
            0xd8, 0xfa, 0x6b, 0xbb, 0xeb, 0xab, 0x07, 0x61,
            0x20, 0x01, 0x80, 0x21, 0x14, 0x84, 0x66, 0x79,
            0x8a, 0x1d, 0x71, 0xef, 0xea, 0x48, 0xb9, 0xca,
            0xef, 0xba, 0xcd, 0x1d, 0x7d, 0x47, 0x6e, 0x98,
            0xde, 0xa2, 0x59, 0x4a, 0xc0, 0x6f, 0xd8, 0x5d,
            0x6b, 0xca, 0xa4, 0xcd, 0x81, 0xf3, 0x2d, 0x1b
        ],[
            0x37, 0x8e, 0xe7, 0x67, 0xf1, 0x16, 0x31, 0xba,
            0xd2, 0x13, 0x80, 0xb0, 0x04, 0x49, 0xb1, 0x7a,
            0xcd, 0xa4, 0x3c, 0x32, 0xbc, 0xdf, 0x1d, 0x77,
            0xf8, 0x20, 0x12, 0xd4, 0x30, 0x21, 0x9f, 0x9b,
            0x5d, 0x80, 0xef, 0x9d, 0x18, 0x91, 0xcc, 0x86,
            0xe7, 0x1d, 0xa4, 0xaa, 0x88, 0xe1, 0x28, 0x52,
            0xfa, 0xf4, 0x17, 0xd5, 0xd9, 0xb2, 0x1b, 0x99,
            0x48, 0xbc, 0x92, 0x4a, 0xf1, 0x1b, 0xd7, 0x20
        ]
    ];
}


unittest {
    ubyte[] message;
    ubyte[] hash;
    string expected;

    string line =
    "ыверогИ ыкълп яырбарх ан ималертс яром с ътюев ,ицунв ижобиртС ,иртев еС";
    import std.encoding : Windows1251String, transcode;
    Windows1251String cp1251string;
    transcode(line, cp1251string);
    message = cast(ubyte[])cp1251string;

    hash = Streebog.calcHash!512(message);
    expected = "28fbc9bada033b1460642bdcddb90c3f"
             ~ "b3e56c497ccd0f62b8a2ad4935e85f03"
             ~ "7613966de4ee00531ae60f3b5a47f8da"
             ~ "e06915d5f2f194996fcabf2622e6881e";
    assert(expected == hash.toHexString.toLower);

    hash = Streebog.calcHash!256(message);
    expected = "508f7e553c06501d749a66fc28c6cac0"
             ~ "b005746d97537fa85d9e40904efed29d";
    assert(expected == hash.toHexString.toLower);

    message = [
        0x32, 0x31, 0x30, 0x39, 0x38, 0x37, 0x36, 0x35,
        0x34, 0x33, 0x32, 0x31, 0x30, 0x39, 0x38, 0x37,
        0x36, 0x35, 0x34, 0x33, 0x32, 0x31, 0x30, 0x39,
        0x38, 0x37, 0x36, 0x35, 0x34, 0x33, 0x32, 0x31,
        0x30, 0x39, 0x38, 0x37, 0x36, 0x35, 0x34, 0x33,
        0x32, 0x31, 0x30, 0x39, 0x38, 0x37, 0x36, 0x35,
        0x34, 0x33, 0x32, 0x31, 0x30, 0x39, 0x38, 0x37,
        0x36, 0x35, 0x34, 0x33, 0x32, 0x31, 0x30
    ];

    hash = Streebog.calcHash!256(message);
    expected = "00557be5e584fd52a449b16b0251d05d"
             ~ "27f94ab76cbaa6da890b59d8ef1e159d";
    assert(expected == hash.toHexString.toLower);

    hash = Streebog.calcHash!512(message);
    expected = "486f64c1917879417fef082b3381a4e2"
             ~ "11c324f074654c38823a7b76f830ad00"
             ~ "fa1fbae42b1285c0352f227524bc9ab1"
             ~ "6254288dd6863dccd5b9f54a1ad0541b";
    assert(expected == hash.toHexString.toLower);

    auto hashObj = new Streebog(512);
        hashObj.pushFront(message[56 .. $]);
        hashObj.pushFront(message[48 .. 56]);
        hashObj.pushFront(message[32 .. 48]);
        hashObj.pushFront(message[16 .. 32]);
        hashObj.pushFront(message[0 .. 16]);
    assert(expected == hashObj.finish.toHexString.toLower);
}


//Atbash, Caeser and Vernam functions inspired
//by https://habr.com/ru/post/444176/

static immutable dchar[] enAlphabetL;
static immutable dchar[] enAlphabetU;
static immutable dchar[] ruAlphabetL;
static immutable dchar[] ruAlphabetU;
shared static this() {
    enAlphabetL  = iota(cast(dchar)'a', cast(dchar)('z'+1)).array;
    enAlphabetU = enAlphabetL.toUpper;
    ruAlphabetL = "абвгдеёжзийклмнопрстуфхцчшщъыьэюя"d.dup;
    ruAlphabetU = ruAlphabetL.toUpper;
}


/*******************************************************************************
 * Atbash encryption.
 * Params:
 *     message = Text for encryption.
 *     alphabets = Array of alphabets to use when flipping.
 * Returns: Encrypted message.
 */
immutable(Char)[] atbash(Char)(immutable(Char)[] message,
                               immutable(Char)[][] alphabets)
if (isSomeChar!Char) {
    dchar[] result = message.dup.to!dstring.dup;
    foreach(ref c; result) {
        foreach(ABC; alphabets) {
            foreach(i; 0 .. ABC.length) {
                if (ABC[i] == c) {
                    c = ABC[ABC.length-i-1];
                    break;
                }
            }
        }
    }
    return result.to!(immutable(Char)[]);
}


/*******************************************************************************
 * A special case of Atbash encryption — for English and Russian alphabets.
 * Params:
 *     message = Text for encryption.
 * Returns: Encrypted message.
 */
immutable(Char)[] atbashEnRu(Char)(immutable(Char)[] message)
if (isSomeChar!Char) {
    alias genstring = immutable(Char)[];
    auto alphabets = [enAlphabetL.to!genstring, enAlphabetU.to!genstring,
                      ruAlphabetL.to!genstring, ruAlphabetU.to!genstring];
    return atbash(message, alphabets);
}
unittest {
    assert(atbashEnRu("Hello, World!") == "Svool, Dliow!");
    assert(atbashEnRu("I love habr") == "R olev szyi");
}


/*******************************************************************************
 * Caesar encryption.
 * Params:
 *     message = Text for encryption.
 *     alphabets = Array of alphabets for internal shifting.
 *     shift = Numerical value of the shift.
 * Returns: Encrypted message.
 */
immutable(Char)[] caesarCypher(Char)(immutable(Char)[] message,
                                     immutable(Char)[][] alphabets,
                                     ssize_t shift)
if (isSomeChar!Char) {
    dstring[] dAlphabets;
    foreach(ABC; alphabets) {
        dAlphabets ~= ABC.to!dstring;
    }
    dchar[][] cyphers;
    cyphers.length = dAlphabets.length;
    foreach(i, ABC; dAlphabets) {
        if (shift == 0) {
            cyphers[i] = ABC.dup;
        } else if (shift > 0) {
            cyphers[i] = ABC[$-shift .. $].dup ~ ABC[0 .. $-shift].dup;
        } else {
            cyphers[i] = ABC[-shift .. $].dup ~ ABC[0 .. -shift].dup;
        }
    }
    dchar[] result = message.to!dstring.dup;
    foreach(i, c; message) {
        foreach(x; 0 .. dAlphabets.length) {
            auto ABC = dAlphabets[x];
            auto cypher = cyphers[x];
            foreach(n; 0 .. ABC.length) {
                if (ABC[n] == c) {
                    result[i] = cypher[n];
                }
            }
        }
    }
    return result.to!dstring.to!(immutable(Char)[]);
}
unittest {
    assert(caesarCypher("hello world"d, [enAlphabetL], 4) == "dahhk sknhz");
    dstring message, expected;
    message = "Широкая электрификация южных губерний даст "d
            ~ "мощный толчок подъёму сельского хозяйства."d;
    expected = "Удлйёыъ шжаёнлдпдёысдъ щвицр юоьалиде яымн "d
             ~ "зйфице нйжтйё кйяхбзо мажчмёйюй рйгъемнэы."d;
    assert(expected == caesarCypher(message, [ruAlphabetU, ruAlphabetL], 5));
}


/*******************************************************************************
 * Vernam encryption.
 * Params:
 *     message = Byte array for encryption.
 *     key = Encryption key.
 *           Its length must be equal to the length of the message.
 * Returns: Encrypted message.
 */
ubyte[] vernamByteCipher(in ubyte[] message, in ubyte[] key) {
    assert(message.length == key.length);
    auto len = message.length;
    ubyte[] result = new ubyte[](len);
    foreach(i; 0 .. len) {
        result[i] = message[i] ^ key[i];
    }
    return result;
}
inout(T)[] vernamCipher(T)(inout(T)[] message, inout(T)[] key) {
    ubyte[] byteMessage = cast(ubyte[])message.dup;
    ubyte[] byteKey = cast(ubyte[])key.dup;
    return cast(inout(T)[])vernamByteCipher(byteMessage, byteKey);
}
unittest {
    auto encryptedMessage = vernamCipher("LONDON", "SYSTEM");
    assert("LONDON" != encryptedMessage);
    auto originalMessage = vernamCipher(encryptedMessage, "SYSTEM");
    assert("LONDON" == originalMessage);
}

