javascripthashendianness

Writing a pure JS function for md4 hashing


I am trying to write a JavaScript script for md4 hashing. I am not sure why the code does not work as I have coded the appropriate endian conversion functions.

The full code is shown as below:

class Md4Context {
    constructor() {
        this.h = new Uint32Array([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476]);
        this.digest = new Uint8Array(16); // uint8_t
        this.x = new Uint32Array(16); // uint32_t
        this.buffer = new Uint8Array(64); // uint8_t
        this.size = 0;
        this.totalSize = BigInt(0); // uint64_t;
    }
}

// Helper functions for JS endian conversion
const LETOH32 = (value) => {
    return (
        ((value & 0x000000FF) << 24) |
        ((value & 0x0000FF00) << 8) |
        ((value & 0x00FF0000) >> 8) |
        ((value & 0xFF000000) >>> 24)
    );
}

const letoh32 = (buffer, offset) => {
    return (
        (buffer[offset] |
            (buffer[offset + 1] << 8) |
            (buffer[offset + 2] << 16) |
            (buffer[offset + 3] << 24)) >>>
        0
    );
};

const HTOLE32 = (value) => {
    return Uint8Array.from([
        value & 0xFF,
        (value >> 8) & 0xFF,
        (value >> 16) & 0xFF,
        (value >> 24) & 0xFF,
    ]);
};

const HTOLE32_BI = (value) => {
    if (typeof value !== 'bigint')
        throw new TypeError('Value must be a BigInt');

    let byteLength = value.toString(2).length;

    if (byteLength <= 0 || !Number.isInteger(byteLength))
        throw new RangeError('byteLength must be a positive integer');

    const byteArray = new Uint8Array(byteLength);
    let tempValue = value;

    for (let i = 0; i < byteLength; i++) {
        byteArray[i] = Number(tempValue & 0xFFn); // Extract the least significant byte
        tempValue >>= 8n; // Shift right by 8 bits to process the next byte
    }

    if (tempValue !== 0n)
        throw new RangeError('Value exceeds the specified byte length');

    return byteArray;
}

/* Start of Essential Process Functinos */
// Rotate 32-bit integer left
function ROL32(inputInt, rotationAmount) {
    return (inputInt << rotationAmount) | (inputInt >>> (32 - rotationAmount));
}

const F = (x, y, z) => ((x & y) | (~x & z)); // Round 1
const G = (x, y, z) => ((x & y) | (x & z) | (y & z)); // Round 2
const H = (x, y, z) => (x ^ y ^ z); // Round 3

// FF fn for round 1
const FF = (a, b, c, d, x, s) => {
    a += F(b, c, d) + x;
    a = ROL32(a, s);
    return a;
}

// GG fn for round 2
const GG = (a, b, c, d, x, s) => {
    a += G(b, c, d) + x + 0x5A827999;
    a = ROL32(a, s);
    return a;
}

// HH fn for round 3
const HH = (a, b, c, d, x, s) => {
    a += H(b, c, d) + x + 0x6ED9EBA1;
    a = ROL32(a, s);
    return a;
}
/* End of Essential Process Functinos */

// Initialize padding
const padding = new Uint8Array(64);
padding[0] = 0x80;

function md4Compute(data, length, digest) {
    // Check parameters
    if (data === null && length !== 0)
        throw new Error('ERROR_INVALID_PARAMETER');

    if (digest === null)
        throw new Error('ERROR_INVALID_PARAMETER');

    try {
        const context = new Md4Context(); // Allocate a memory buffer to hold the MD4 context
        md4Update(context, data, length); // Digest the message

        // Finalize the MD4 message digest
        let output = md4Final(context, digest);
        return output;
    } catch (error) {
        throw new Error('Error during MD4 computation: ' + error);
    } finally {
        // Clean up context if necessary (JavaScript handles memory management)
        context = null;
    }
}

function md4Update(context, data) {
    let n;

    while (data.length > 0) {
        // The buffer can hold at most 64 bytes
        n = Math.min(data.length, 64 - context.size);

        // Copy the data to the buffer
        for (let i = 0; i < n; i++) {
            context.buffer[context.size + i] = data[i];
        }

        // Update the MD4 context
        context.size += n;
        context.totalSize += BigInt(n);

        // Advance the data pointer
        data = data.slice(n);

        // Process message in 16-word blocks
        if (context.size === 64) {
            // Transform the 16-word block
            md4ProcessBlock(context);

            // Empty the buffer
            context.size = 0;
        }
    }
}

function md4Final(context, digest) {
    let paddingSize;
    const totalSize = context.totalSize * 8n;

    // Pad the message so that its length is congruent to 56 modulo 64
    if (context.size < 56) {
        paddingSize = 56 - context.size;
    } else {
        paddingSize = 64 + 56 - context.size;
    }

    // Append padding
    md4Update(context, padding, paddingSize);

    // Append the length of the original message in little-endian
    context.x[14] = HTOLE32_BI(totalSize & 0xFFFFFFFFn)[0];
    context.x[15] = HTOLE32_BI(totalSize >> 32n)[0];

    // Process the final block
    md4ProcessBlock(context);

    // Convert hash state to little-endian and store it in the digest
    for (let i = 0; i < 4; i++) {
        context.h[i] = HTOLE32(context.h[i]);
    }

    if (digest !== null && digest !== undefined) {
        digest.set(context.digest.slice(0, 16));
    }

    return context.digest;
}

function md4ProcessBlock(context) {
    let a = context.h[0];
    let b = context.h[1];
    let c = context.h[2];
    let d = context.h[3];

    let x = context.x;

    // Convert from little-endian byte order to host byte order (little-endian)
    for (let i = 0; i < 16; i++) {
        x[i] = LETOH32(x[i]);
    }

    // Round 1
    a = FF(a, b, c, d, x[0], 3);
    d = FF(d, a, b, c, x[1], 7);
    c = FF(c, d, a, b, x[2], 11);
    b = FF(b, c, d, a, x[3], 19);
    a = FF(a, b, c, d, x[4], 3);
    d = FF(d, a, b, c, x[5], 7);
    c = FF(c, d, a, b, x[6], 11);
    b = FF(b, c, d, a, x[7], 19);
    a = FF(a, b, c, d, x[8], 3);
    d = FF(d, a, b, c, x[9], 7);
    c = FF(c, d, a, b, x[10], 11);
    b = FF(b, c, d, a, x[11], 19);
    a = FF(a, b, c, d, x[12], 3);
    d = FF(d, a, b, c, x[13], 7);
    c = FF(c, d, a, b, x[14], 11);
    b = FF(b, c, d, a, x[15], 19);

    // Round 2
    a = GG(a, b, c, d, x[0], 3);
    d = GG(d, a, b, c, x[4], 5);
    c = GG(c, d, a, b, x[8], 9);
    b = GG(b, c, d, a, x[12], 13);
    a = GG(a, b, c, d, x[1], 3);
    d = GG(d, a, b, c, x[5], 5);
    c = GG(c, d, a, b, x[9], 9);
    b = GG(b, c, d, a, x[13], 13);
    a = GG(a, b, c, d, x[2], 3);
    d = GG(d, a, b, c, x[6], 5);
    c = GG(c, d, a, b, x[10], 9);
    b = GG(b, c, d, a, x[14], 13);
    a = GG(a, b, c, d, x[3], 3);
    d = GG(d, a, b, c, x[7], 5);
    c = GG(c, d, a, b, x[11], 9);
    b = GG(b, c, d, a, x[15], 13);

    // Round 3
    a = HH(a, b, c, d, x[0], 3);
    d = HH(d, a, b, c, x[8], 9);
    c = HH(c, d, a, b, x[4], 11);
    b = HH(b, c, d, a, x[12], 15);
    a = HH(a, b, c, d, x[2], 3);
    d = HH(d, a, b, c, x[10], 9);
    c = HH(c, d, a, b, x[6], 11);
    b = HH(b, c, d, a, x[14], 15);
    a = HH(a, b, c, d, x[1], 3);
    d = HH(d, a, b, c, x[9], 9);
    c = HH(c, d, a, b, x[5], 11);
    b = HH(b, c, d, a, x[13], 15);
    a = HH(a, b, c, d, x[3], 3);
    d = HH(d, a, b, c, x[11], 9);
    c = HH(c, d, a, b, x[7], 11);
    b = HH(b, c, d, a, x[15], 15);

    // Update the hash value
    context.h[0] = (context.h[0] + a) >>> 0;
    context.h[1] = (context.h[1] + b) >>> 0;
    context.h[2] = (context.h[2] + c) >>> 0;
    context.h[3] = (context.h[3] + d) >>> 0;
}

The function is called as below and should return an Uint8Array which can then be converted to hex:

let main_string = new TextEncoder().encode('hello world');
let digest = new TextEncoder().encode('digest123');

console.log(md4Compute(main_string, main_string.length, digest));

However, it gives an error output of:

RangeError: offset is out of bounds

Any ideas on how this can be fixed?


Solution

  • UPDATE 2: I've successfully fixed the code the perform as expected. This is the final and completed code.

    The key changes are:

    1. Removed unnecessary class methods and variables
    2. Adopted the native DataView API for endianess conversions
    3. Rewrote the chunking logic using inspiration from Python code

    class Md4Context {
        constructor() {
            this.h = new Array(4);
            this.digest = new Uint8Array(16);
            this.buffer = new Array();
            this.totalSize = 0;
        }
    }
    
    /* Start of Round Functions */
    const F = (x, y, z) => ((x & y) | (~x & z)); // Round 1
    const G = (x, y, z) => ((x & y) | (x & z) | (y & z)); // Round 2
    const H = (x, y, z) => (x ^ y ^ z); // Round 3
    
    // FF fn for round 1
    const FF = (a, b, c, d, x, s) => {
        a += F(b, c, d) + x;
        a = ROL32(a, s);
        return a;
    }
    
    // GG fn for round 2
    const GG = (a, b, c, d, x, s) => {
        a += G(b, c, d) + x + 0x5A827999;
        a = ROL32(a, s);
        return a;
    }
    
    // HH fn for round 3
    const HH = (a, b, c, d, x, s) => {
        a += H(b, c, d) + x + 0x6ED9EBA1;
        a = ROL32(a, s);
        return a;
    }
    
    // Rotate 32-bit integer left
    function ROL32(inputInt, rotationAmount) {
        return (inputInt << rotationAmount) | (inputInt >>> (32 - rotationAmount));
    }
    /* End of Round Functions */
    
    function md4Compute(data, length) {
        // Check parameters
        if (data === null && length !== 0)
            throw new Error('ERROR_INVALID_PARAMETER');
    
        try {
            const context = new Md4Context(); // Allocate a memory buffer to hold the MD4 context
            md4Init(context, length)
            md4Update(context, data); // Digest the message
            md4Final(context); // Finalize the MD4 message digest
    
            return context.digest;
        } catch (error) {
            throw new Error('Error during MD4 computation: ' + error);
        } finally {
            // Clean up context if necessary (JavaScript handles memory management)
            context = null;
        }
    }
    
    // Set initial hash value
    function md4Init(context, length) {
        context.h[0] = 0x67452301;
        context.h[1] = 0xEFCDAB89;
        context.h[2] = 0x98BADCFE;
        context.h[3] = 0x10325476;
    
        // Store message length in bits
        context.totalSize = length * 8;
    }
    
    function md4Update(context, data) {
        // Append the `0x80` byte to data
        data = new Uint8Array([...data, 0x80]);
    
        // Add `0x00` bytes to align to 64-byte blocks (minus 8 bytes for the length)
        const padLen = (64 - ((data.length + 8) % 64)) % 64;
        const padding = new Uint8Array(padLen);
        data = new Uint8Array([...data, ...padding]);
    
        // Append the original message length as a 64-bit little-endian integer
        const lengthBytes = new Uint8Array(8);
        const tempView = new DataView(lengthBytes.buffer);
        tempView.setUint32(0, context.totalSize, true); // Lower 32 bits
        data = new Uint8Array([...data, ...lengthBytes]);
    
        // Split data into 64-byte chunks and push to buffer
        for (let i = 0; i < data.length; i += 64) {
            context.buffer.push(data.slice(i, i + 64));
        }
    }
    
    function md4Final(context) {
        md4ProcessBlock(context);
    
        // Finalise and copy digest as LE
        new DataView(context.digest.buffer).setUint32(0, context.h[0], true);
        new DataView(context.digest.buffer).setUint32(4, context.h[1], true);
        new DataView(context.digest.buffer).setUint32(8, context.h[2], true);
        new DataView(context.digest.buffer).setUint32(12, context.h[3], true);
    }
    
    function md4ProcessBlock(context) {
        for (const chunk of context.buffer) {
            let a = context.h[0],
                b = context.h[1],
                c = context.h[2],
                d = context.h[3];
    
            const x = new Uint32Array(new Uint8Array(chunk).buffer);
    
            // Round 1
            a = FF(a, b, c, d, x[0], 3);
            d = FF(d, a, b, c, x[1], 7);
            c = FF(c, d, a, b, x[2], 11);
            b = FF(b, c, d, a, x[3], 19);
            a = FF(a, b, c, d, x[4], 3);
            d = FF(d, a, b, c, x[5], 7);
            c = FF(c, d, a, b, x[6], 11);
            b = FF(b, c, d, a, x[7], 19);
            a = FF(a, b, c, d, x[8], 3);
            d = FF(d, a, b, c, x[9], 7);
            c = FF(c, d, a, b, x[10], 11);
            b = FF(b, c, d, a, x[11], 19);
            a = FF(a, b, c, d, x[12], 3);
            d = FF(d, a, b, c, x[13], 7);
            c = FF(c, d, a, b, x[14], 11);
            b = FF(b, c, d, a, x[15], 19);
    
            // Round 2
            a = GG(a, b, c, d, x[0], 3);
            d = GG(d, a, b, c, x[4], 5);
            c = GG(c, d, a, b, x[8], 9);
            b = GG(b, c, d, a, x[12], 13);
            a = GG(a, b, c, d, x[1], 3);
            d = GG(d, a, b, c, x[5], 5);
            c = GG(c, d, a, b, x[9], 9);
            b = GG(b, c, d, a, x[13], 13);
            a = GG(a, b, c, d, x[2], 3);
            d = GG(d, a, b, c, x[6], 5);
            c = GG(c, d, a, b, x[10], 9);
            b = GG(b, c, d, a, x[14], 13);
            a = GG(a, b, c, d, x[3], 3);
            d = GG(d, a, b, c, x[7], 5);
            c = GG(c, d, a, b, x[11], 9);
            b = GG(b, c, d, a, x[15], 13);
    
            // Round 3
            a = HH(a, b, c, d, x[0], 3);
            d = HH(d, a, b, c, x[8], 9);
            c = HH(c, d, a, b, x[4], 11);
            b = HH(b, c, d, a, x[12], 15);
            a = HH(a, b, c, d, x[2], 3);
            d = HH(d, a, b, c, x[10], 9);
            c = HH(c, d, a, b, x[6], 11);
            b = HH(b, c, d, a, x[14], 15);
            a = HH(a, b, c, d, x[1], 3);
            d = HH(d, a, b, c, x[9], 9);
            c = HH(c, d, a, b, x[5], 11);
            b = HH(b, c, d, a, x[13], 15);
            a = HH(a, b, c, d, x[3], 3);
            d = HH(d, a, b, c, x[11], 9);
            c = HH(c, d, a, b, x[7], 11);
            b = HH(b, c, d, a, x[15], 15);
    
            context.h[0] += a;
            context.h[1] += b;
            context.h[2] += c;
            context.h[3] += d;
        }
    }

    Here's how to call the function:

    let input_string = new TextEncoder().encode('hello world');
    let digest = md4Compute(input_string, input_string.length);
    

    Note that this will return a Uint8Array so if you want to return a hex value, you can do the following:

    Array.from(digest, i => i.toString(16).padStart(2, '0')).join('')