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
UPDATE 1: This is an update to the question after taking in suggestions from the answers.
This update includes the following bug fixes:
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:Instead, it produces:
In
md4ProcessBlock
you dobut
letoh32
expects two arguments, the second beingoffset
So, it will be
undefined