Source: kupyna/kupyna.js

/*jslint node: true, esversion:6 */
'use strict';

var KupynaTransShort = require('./kupyna_trans_short');
var KupynaTransLong = require('./kupyna_trans_long');
var LongLongBuffer = require('./longlong_buffer');
//var KupynaTables = require('./kupyna_tables');

/**
 * Kupyna class allows for calculating Kupyna type digest as defined in 
 * Ukrainian DSTU 7564:2014 standard.
 * @private
 */
class Kupyna {
    /**
     * Creates kupyna hashing object
     * @param {Number} hashBits - number of kupyna hash output bits
     * @public
     */
    constructor(hashBits) {
        if (hashBits !== 256 && hashBits !== 384 && hashBits !== 512) {
            throw new Error("expected hash len: 512|384|256");
        }
        this.memStatePos = 0;
        this.total = 0;
        this.initialized = false;

        switch (hashBits) {
            case 256:
                this.memState = new LongLongBuffer(8);
                this.hashState = new LongLongBuffer(8);
                this.kupynaTrans = new KupynaTransShort();
                this.numRounds = 10;
                this.stateLen = 8;
                this.stateLenBytes = 64;
                this.hashBits = 256;
                break;
            case 384:
            case 512:
                this.memState = new LongLongBuffer(16);
                this.hashState = new LongLongBuffer(16);
                this.kupynaTrans = new KupynaTransLong();
                this.numRounds = 14;
                this.stateLen = 16;
                this.stateLenBytes = 128;
                this.hashBits = hashBits;
                break;      
        }
    }

    /**
     * Calculates value of P transformation
     * @param {Number} x - first argument to transform
     * @param {Number} y - second argument to transform
     * @param {Number} round - number of transformation round
     * @private
     */
    _P(x, y, round) {
        for (var index = 0; index < this.stateLen; ++index) {
            x.lo[index] ^= (index << 4) ^ round;
        }
        this.kupynaTrans.G1(x, y, round + 1);
        this.kupynaTrans.G(y, x);
    }

    /**
     * Calculates value of Q transformation
     * @param {Number} x - first argument to transform
     * @param {Number} y - second argument to transform
     * @param {Number} round - number of transformation round
     * @private
     */
    _Q(x, y, round) {
        for (var index = 0; index < this.stateLen; index++) {
            var addHi = 0x00f0f0f0 ^ ((((this.stateLen - 1 - index) * 16) ^ round) << 24);
            var result = this.kupynaTrans.addLongLong(x.hi[index], x.lo[index], addHi, 0xf0f0f0f3);
            x.hi[index] = result.hi;
            x.lo[index] = result.lo;
        }
        this.kupynaTrans.G2(x, y, round + 1);
        this.kupynaTrans.G(y, x);
    }

    /**
     * Applies internal state transformations
     * @private
     */
    _transform() {
        var AQ1 = {
            hi: [],
            lo: []
        };
        var AP1 = {
            hi: [],
            lo: []
        };
        var tmp = {
            hi: [],
            lo: []
        };

        for (var column = 0; column < this.stateLen; column++) {
            AP1.hi[column] = this.hashState.longArr.hi[column] ^ this.memState.longArr.hi[column];
            AP1.lo[column] = this.hashState.longArr.lo[column] ^ this.memState.longArr.lo[column];
            AQ1.hi[column] = this.memState.longArr.hi[column];
            AQ1.lo[column] = this.memState.longArr.lo[column];
        }

        for (var r = 0; r < this.numRounds; r += 2) {
            this._P(AP1, tmp, r);
            this._Q(AQ1, tmp, r);
        }

        for (column = 0; column < this.stateLen; column++) {
            this.hashState.longArr.hi[column] ^= AP1.hi[column] ^ AQ1.hi[column];
            this.hashState.longArr.lo[column] ^= AP1.lo[column] ^ AQ1.lo[column];
        }
        this.hashState.notifyLongUpdated();
    }

    /**
     * Applies final state transformation.
     * @private
     */
    _outputTransform() {
        var t1 = {
                lo: [],
                hi: []
            },
            t2 = {
                lo: [],
                hi: []
            };
        for (var index = 0; index < this.stateLen; index++) {
            t1.hi[index] = this.hashState.longArr.hi[index];
            t1.lo[index] = this.hashState.longArr.lo[index];
        }
        for (var round = 0; round < this.numRounds; round += 2) {
            this._P(t1, t2, round);
        }
        for (var column = 0; column < this.stateLen; ++column) {
            this.hashState.longArr.hi[column] ^= t1.hi[column];
            this.hashState.longArr.lo[column] ^= t1.lo[column];
        }
        this.hashState.notifyLongUpdated();
    }

    /**
     * Extracts hash from internal state table.
     * @return {Array} extracted hash
     * @private
     */
    _extractHash() {
        var hash = [];
        this.hashState.copyBytesFrom(this.stateLenBytes - this.hashBits / 8, hash, 0, this.hashBits / 8);
        return hash;
    }

    /**
     * Reinitialize hashing object to its clear state.
     * Allows to digest new data.
     * @public
     */
    init() {
        this.total = 0;
        this.memStatePos = 0;
        this.hashState.zeroAll();
        this.memState.zeroAll();
        this.hashState.setByte(0, this.stateLenBytes);
        this.initialized = true;
    }

    /**
     * Updates the digest with new piece of dat.
     * @param {Array} data - bunch of data to update hash with
     * @public
     */
    update(data) {
        if (!this.initialized) {
            throw new Error("need to call Kupyna.init() before");
        }
        if (data.constructor !== Array) {
            throw new Error("update expects array of bytes");
        }
        for (var index = 0; index < data.length; index++) {
            var temp = data[index];
            if (temp < 0 || temp > 255) {
                throw new Error("data must be byte array of utf8 elements");
            }
        }
        var len = data.length;
        var dataPos = 0;

        if (this.memStatePos > 0 && this.memStatePos + len >= this.stateLenBytes) {
            this.memState.copyBytesTo(data, dataPos, this.memStatePos, this.stateLenBytes - this.memStatePos);
            this._transform();
            len -= this.stateLenBytes - this.memStatePos;
            dataPos += this.stateLenBytes - this.memStatePos;
            this.memStatePos = 0;
        }
        while (len >= this.stateLenBytes) {
            this.memState.copyBytesTo(data, dataPos, 0, this.stateLenBytes);
            this._transform();
            len -= this.stateLenBytes;
            dataPos += this.stateLenBytes;
        }
        if (len > 0) {
            this.memState.copyBytesTo(data, dataPos, this.memStatePos, len);
            this.memStatePos += len;
        }
        this.total += data.length * 8;
    }

    /**
     * Calculates final result of complete data provided to update method.
     * @return {Array} calculated final hash 
     * @public
     */
    digest() {
        this.memState.setByte(this.memStatePos, 0x80);
        this.memStatePos++;
        if (this.memStatePos > this.stateLenBytes - 12) {
            this.memState.zeroBytes(this.memStatePos, this.stateLenBytes - this.memStatePos);
            this._transform();
            this.memStatePos = 0;
        }
        this.memState.zeroBytes(this.memStatePos, this.stateLenBytes - this.memStatePos);
        this.memState.setLongAsBytes(this.stateLenBytes - 12, this.total);
        this._transform();
        this._outputTransform();
        this.initialized = false;
        return this._extractHash();
    }
}

module.exports = Kupyna;