skip to Main Content

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?

2

Answers


  1. Chosen as BEST ANSWER

    UPDATE 1: This is an update to the question after taking in suggestions from the answers.

    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 Functions */
    
    // 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 Functions */
    
    // Initialize padding
    const padding = new Uint8Array(64);
    padding[0] = 0x80;
    
    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
    
            md4Update(context, data, length); // Digest the message
            md4Final(context); // Finalize the MD4 message digest
    
            // Transfer final hash value to output variable
            let output = context.digest;
    
            // Free context from memory
            for (let key in context) {
                if (context.hasOwnProperty(key))
                    context[key] = null;
            }
    
            return output;
        } catch (error) {
            console.error('Error during MD4 computation:', error);
            return 'ERROR'; // Replace with specific error codes as needed
        } finally {
            // Clean up context if necessary (JavaScript handles memory management)
            context = null;
        }
    }
    
    function md4Update(context, data, length) {
        let n;
    
        // Process the incoming data
        while (length > 0) {
            // Determine how much space is left in the buffer
            n = Math.min(length, 64 - context.size);
    
            // Copy data to the buffer
            context.buffer.set(data.subarray(0, n), context.size);
    
            // Update the context
            context.size += n;
            context.totalSize += BigInt(n);
    
            // Advance the data pointer
            data = data.subarray(n);
            length -= n;
    
            // If the buffer is full, process it
            if (context.size === 64) {
                md4ProcessBlock(context); // Transform the 16-word block
                context.size = 0; // Reset the buffer size
            }
        }
    }
    
    function md4Final(context) {
        let paddingSize, totalSize;
    
        // Length of the original message (before padding)
        totalSize = context.totalSize * BigInt(8);
    
        // 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
        context.x[14] = Number(totalSize >> BigInt(0));
        context.x[15] = Number(totalSize >> BigInt(32));
    
        // Calculate the message digest
        md4ProcessBlock(context);
    
        // Convert from host to little-endian byte order & copy resulting digest into context.digest
        let b = 0; // Create counter to hold base byteset iter
    
        for (let i = 0; i < 4; i++) {
            HTOLE32(context.h[i]).forEach((item, n) => {
                context.digest[b + n] = item;
            });
            b = b + 4; // Iter for next 4 items
        }
    }
    
    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;
    }

    This update includes the following bug fixes:

    1. Changing to appropriate functions
    2. Fixing the input variables
    3. Minor optimizations

    The code now produces an output, but an incorrect output that does not match the md4 specs.

    Example: the string "Hello world" should produce the following output in Uint8array:

    {
        "0": 47,
        "1": 52,
        "2": 231,
        "3": 237,
        "4": 200,
        "5": 24,
        "6": 11,
        "7": 135,
        "8": 87,
        "9": 129,
        "10": 89,
        "11": 255,
        "12": 88,
        "13": 232,
        "14": 124,
        "15": 26
    }
    

    Instead, it produces:

    {
        "0": 239,
        "1": 85,
        "2": 81,
        "3": 1,
        "4": 171,
        "5": 103,
        "6": 95,
        "7": 46,
        "8": 141,
        "9": 201,
        "10": 129,
        "11": 54,
        "12": 21,
        "13": 171,
        "14": 71,
        "15": 116
    }
    

  2. In md4ProcessBlock you do

    for (let i = 0; i < 16; i++) {
        x[i] = letoh32(x[i]);
    }
    

    but letoh32 expects two arguments, the second being offset

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

    So, it will be undefined

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search