diff --git a/Hashes/MD5.js b/Hashes/MD5.js new file mode 100644 index 0000000000..42bef45cc5 --- /dev/null +++ b/Hashes/MD5.js @@ -0,0 +1,205 @@ +// Module that replicates the MD5 Cryptographic Hash +// function in Javascript. + +// main variables +const S = [ + 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 5, 9, 14, 20, 5, + 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, + 16, 23, 4, 11, 16, 23, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, + 21 +] + +const K = [ + 0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, 0xf57c0faf, 0x4787c62a, + 0xa8304613, 0xfd469501, 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, 0xf61e2562, 0xc040b340, + 0x265e5a51, 0xe9b6c7aa, 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, 0xa9e3e905, 0xfcefa3f8, + 0x676f02d9, 0x8d2a4c8a, 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, 0x289b7ec6, 0xeaa127fa, + 0xd4ef3085, 0x04881d05, 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, 0x655b59c3, 0x8f0ccc92, + 0xffeff47d, 0x85845dd1, 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391 +] + +/** + * Separates an array into equal sized chunks + * + * @param {Array|string} array - array or string to separate into chunks + * @param {number} size - number of elements wanted in each chunk + * @return {Array} - array of original array split into chunks + * + * @example + * chunkify("this is a test", 2) + */ +function chunkify(array, size) { + const chunks = [] + for (let i = 0; i < array.length; i += size) { + chunks.push(array.slice(i, i + size)) + } + return chunks +} + +/** + * Rotates the bits to the left + * + * @param {number} bits - 32 bit number + * @param {number} turns - number of rotations to make + * @return {number} - number after bits rotation + * + * @example + * rotateLeft(0b1011, 3); // 0b1011000 + */ +function rotateLeft(bits, turns) { + return (bits << turns) | (bits >>> (32 - turns)) +} + +/** + * Converts Uint8Array to Uint32Array + * + * @param {Uint8Array} u8Array Uint8Array to convert + * @returns {Uint32Array} - Required Uint32Array + */ +function u8ToU32(u8Array) { + const uint32Array = new Uint32Array(u8Array.length / 4) + + for (let i = 0; i < u8Array.length; i += 4) { + uint32Array[i / 4] = + (u8Array[i] | + (u8Array[i + 1] << 8) | + (u8Array[i + 2] << 16) | + (u8Array[i + 3] << 24)) >>> + 0 + } + + return uint32Array +} + +/** + * Converts Uint32Array to Uint8Array + * + * @param {Uint32Array} u32Array Uint32Array to convert + * @returns {Uint8Array} - Required Uint8Array + */ +function u32ToU8(u32Array) { + const uint8Array = new Uint8Array(u32Array.length * 4) + + for (let i = 0; i < u32Array.length; i++) { + uint8Array[i * 4] = u32Array[i] & 0xff + uint8Array[i * 4 + 1] = (u32Array[i] >> 8) & 0xff + uint8Array[i * 4 + 2] = (u32Array[i] >> 16) & 0xff + uint8Array[i * 4 + 3] = (u32Array[i] >> 24) & 0xff + } + + return uint8Array +} + +/** + * Adds padding to the end of the given array + * + * @param {Uint8Array} u8Array Array to pad + * @param {number} size Resulting size of the array + */ +function padEnd(u8Array, size) { + const result = new Uint8Array(size) + result.set(u8Array) + result.fill(0, u8Array.length) + + return result +} + +/** + * Pre-processes message to feed the algorithm loop + * + * @param {Uint8Array} message - message to pre-process + * @return {Uint32Array} - processed message + */ +function preProcess(message) { + // Extend message by adding '0' + // + // message.length + 1 is for adding '1' bit + // 56 - (length % 64) is for padding with '0's + // 8 is for appending 64 bit message length + let m = padEnd( + message, + message.length + 1 + (56 - ((message.length + 1) % 64)) + 8 + ) + + // Add '1' bit at the end of the message + m[message.length] = 1 << 7 + + // convert message to 32 bit uint array + m = u8ToU32(m) + + // Append the length of the message to the end + // (ml / 0x100000000) | 0 is equivalent to (ml >> 32) & 0xffffffff) in other languages + let ml = message.length * 8 + m[m.length - 2] = ml & 0xffffffff + m[m.length - 1] = (ml / 0x100000000) | 0 + + return m +} + +/** + * Hashes message using MD5 Cryptographic Hash Function + * + * @see + * For more info: https://en.wikipedia.org/wiki/MD5 + * + * @param {Uint8Array} message - message to hash + * @return {Uint8Array} - message digest (hash value) + */ +function MD5(message) { + // Initialize variables: + let [a0, b0, c0, d0] = [ + 0x67452301 >>> 0, + 0xefcdab89 >>> 0, + 0x98badcfe >>> 0, + 0x10325476 >>> 0 + ] + + // pre-process message and split into 512 bit chunks + const words = Array.from(preProcess(message)) + const chunks = chunkify(words, 16) + + chunks.forEach(function (chunk, _) { + // initialize variables for this chunk + let [A, B, C, D] = [a0, b0, c0, d0] + + for (let i = 0; i < 64; i++) { + let [F, g] = [0, 0] + + if (i <= 15) { + F = (B & C) | (~B & D) + g = i + } else if (i <= 31) { + F = (D & B) | (~D & C) + g = (5 * i + 1) % 16 + } else if (i <= 47) { + F = B ^ C ^ D + g = (3 * i + 5) % 16 + } else { + F = C ^ (B | ~D) + g = (7 * i) % 16 + } + + F = (F + A + K[i] + chunk[g]) >>> 0 + A = D + D = C + C = B + B = ((B + rotateLeft(F, S[i])) & 0xffffffff) >>> 0 + } + + // add values for this chunk to main hash variables (unsigned) + a0 = (a0 + A) >>> 0 + b0 = (b0 + B) >>> 0 + c0 = (c0 + C) >>> 0 + d0 = (d0 + D) >>> 0 + }) + + return u32ToU8([a0, b0, c0, d0]) +} + +// export MD5 function +export { MD5 } diff --git a/Hashes/tests/MD5.test.js b/Hashes/tests/MD5.test.js new file mode 100644 index 0000000000..5c44c7a57c --- /dev/null +++ b/Hashes/tests/MD5.test.js @@ -0,0 +1,38 @@ +import { MD5 } from '../MD5' + +/** + * Returns the MD5 hash of the given message as a hexadecimal string + * + * @param {Uint8Array} message - message to hash + * @return {string} - hash as a hexadecimal string + */ +function hexMD5(message) { + return Array.from(MD5(message), (byte) => + byte.toString(16).padStart(2, '0') + ).join('') +} + +describe('Testing MD5 function', () => { + it('should return the correct hash for "The quick brown fox jumps over the lazy dog"', () => { + const input = new TextEncoder().encode( + 'The quick brown fox jumps over the lazy dog' + ) + const hash = hexMD5(input) + + expect(hash).toBe('9e107d9d372bb6826bd81d3542a419d6') + }) + + it('should return the correct hash for "JavaScript!"', () => { + const input = new TextEncoder().encode('JavaScript!') + const hash = hexMD5(input) + + expect(hash).toBe('209eddd6b61af0643907a8e069a08fb8') + }) + + it('should correctly hash an empty string', () => { + const input = new TextEncoder().encode('') + const hash = hexMD5(input) + + expect(hash).toBe('d41d8cd98f00b204e9800998ecf8427e') + }) +})