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?
UPDATE 2: I've successfully fixed the code the perform as expected. This is the final and completed code.
The key changes are:
DataView
API for endianess conversionsclass 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('')