UNPKG

@constantiner/cram-md5-digest

Version:
319 lines (316 loc) 9.96 kB
/** * @constantiner/cram-md5-digest * CRAM-MD5 digester implementation in JavaScript * * @author Konstantin Kovalev <constantiner@gmail.com> * @version v0.9.8 * @link https://github.com/Constantiner/cram-md5-digest-js.git * @date 08 May 2020 * * MIT License * * Copyright (c) 2018-2020 Konstantin Kovalev * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. * */ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const range = (from, to) => Array.from( { length: to - from + 1 }, (_, index) => index + from ), range64 = range(0, 63), T = range64.map(i => (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0), // eslint-disable-next-line @typescript-eslint/no-explicit-any pipe = (...fns) => fns.reduceRight((f, g) => value => f(g(value))), getEmptyArray = length => Array.from({ length }), copyState = (source, overridesObject = {}) => Object.assign( {}, source, { block: [...source.block], padding: [...source.padding] }, overridesObject ), init = initObject => copyState(initObject, { byteCount: 0, state0: 0x67452301, state1: 0xefcdab89, state2: 0x98badcfe, state3: 0x10325476 }), initialInit = () => init({ padding: getEmptyArray(64).map((_, index) => (index === 0 ? 0x80 : 0)), block: getEmptyArray(64) }), nBitsGenerator = function* (nBits) { while (nBits >= 6) { nBits = (Math.floor(nBits / 6) - 1) * 6; yield nBits; } }, utfTextValue = code => nBits => 0x80 + ((code >>> nBits) & 0x3f), nBitsMatches = [ [0x00000800, 11], [0x00010000, 16], [0x00200000, 21], [0x04000000, 26], [0x80000000, 31], [Infinity, 0] ], initialNBits = code => nBitsMatches.find(([hexValue]) => code < hexValue)[1], initialUtfTextForCodeMoreThan0x80 = (code, nBits) => ((0xfe << nBits % 6) & 0xff) | (code >>> (Math.floor(nBits / 6) * 6)), utfTextForCodeMoreThan0x80 = code => (nBits => [ initialUtfTextForCodeMoreThan0x80(code, nBits), ...[...nBitsGenerator(nBits)].map(utfTextValue(code)) ])(initialNBits(code)), toUTFArray = stringToEncode => [...stringToEncode] .map(x => x.charCodeAt(0)) .reduce((utfText, code) => [...utfText, ...(code < 0x80 ? [code] : utfTextForCodeMoreThan0x80(code))], []), toUnsigned = x => (x + 0x100000000) % 0x100000000, lShift = (x, s) => (x << s) | (x >>> (32 - s)), // F, G, H and I are basic MD5 functions. F = (X, Y, Z) => toUnsigned((X & Y) | (~X & Z)), G = (X, Y, Z) => toUnsigned((X & Z) | (Y & ~Z)), H = (X, Y, Z) => toUnsigned(X ^ Y ^ Z), I = (X, Y, Z) => toUnsigned(Y ^ (X | ~Z)), // FF, GG, HH, and II transformations for rounds 1-4. XX = func => (xi, s) => ([a, b, c, d, i, x]) => [ d, toUnsigned(lShift(a + func(b, c, d) + x[xi] + T[i], s) + b), b, c, i + 1, x ], FF = XX(F), GG = XX(G), HH = XX(H), II = XX(I), modifyStates = stateObject => ([a, b, c, d]) => copyState(stateObject, { state0: toUnsigned(stateObject.state0 + a), state1: toUnsigned(stateObject.state1 + b), state2: toUnsigned(stateObject.state2 + c), state3: toUnsigned(stateObject.state3 + d) }), x = stateObject => getEmptyArray(16).map( (_, j) => ((stateObject.block[j * 4 + 3] * 256 + stateObject.block[j * 4 + 2]) * 256 + stateObject.block[j * 4 + 1]) * 256 + stateObject.block[j * 4] ), // // MD5 basic transformation. Transforms state based on block. // transformBlock = stateObject => pipe( FF(0, 7), FF(1, 12), FF(2, 17), FF(3, 22), FF(4, 7), FF(5, 12), FF(6, 17), FF(7, 22), FF(8, 7), FF(9, 12), FF(10, 17), FF(11, 22), FF(12, 7), FF(13, 12), FF(14, 17), FF(15, 22), GG(1, 5), GG(6, 9), GG(11, 14), GG(0, 20), GG(5, 5), GG(10, 9), GG(15, 14), GG(4, 20), GG(9, 5), GG(14, 9), GG(3, 14), GG(8, 20), GG(13, 5), GG(2, 9), GG(7, 14), GG(12, 20), HH(5, 4), HH(8, 11), HH(11, 16), HH(14, 23), HH(1, 4), HH(4, 11), HH(7, 16), HH(10, 23), HH(13, 4), HH(0, 11), HH(3, 16), HH(6, 23), HH(9, 4), HH(12, 11), HH(15, 16), HH(2, 23), II(0, 6), II(7, 10), II(14, 15), II(5, 21), II(12, 6), II(3, 10), II(10, 15), II(1, 21), II(8, 6), II(15, 10), II(6, 15), II(13, 21), II(4, 6), II(11, 10), II(2, 15), II(9, 21), modifyStates(stateObject) )([stateObject.state0, stateObject.state1, stateObject.state2, stateObject.state3, 0, x(stateObject)]), update = (inputArray, inputLength = inputArray.length) => stateObject => inputArray.slice(0, inputLength).reduce((stateObject_, item) => { stateObject_.block[stateObject_.byteCount % 64] = item; if (++stateObject_.byteCount % 64 === 0) { stateObject_ = transformBlock(stateObject_); } return stateObject_; }, copyState(stateObject)), set32Little = (value, index) => array => range(0, 3).reduce((array_, i) => ((array_[index + i] = (value >>> (i * 8)) & 0xff), array_), [...array]), getBits = stateObject => pipe( set32Little(stateObject.byteCount * 8, 0), set32Little(Math.floor((stateObject.byteCount * 8) / 0x100000000), 4) )(getEmptyArray(8)), alignIndex = index => [ [56, ind => 120 - ind], [-Infinity, ind => 56 - ind] ].find(range => index >= range[0])[1](index), getIndex = stateObject => alignIndex(stateObject.byteCount % 64), updateWithIndex = stateObject => update(stateObject.padding, getIndex(stateObject))(stateObject), getFinalDigestObject = stateObject => ({ digest: pipe( set32Little(stateObject.state0, 0), set32Little(stateObject.state1, 4), set32Little(stateObject.state2, 8), set32Little(stateObject.state3, 12) )(getEmptyArray(16)), padding: [...stateObject.padding], block: [...stateObject.block] }), finalDigest = stateObject => pipe(updateWithIndex, update(getBits(stateObject)), getFinalDigestObject)(stateObject), hexByte = x => (x < 16 ? "0" : "") + x.toString(16), toHexString = byteArray => byteArray.reduce((accumulator, x) => accumulator + hexByte(x), "").toLowerCase(), finalHexDigest = stateObject => toHexString(finalDigest(stateObject).digest), expandArrayTo64Length = original => [ ...original, ...Array.from({ length: 64 - original.length }).fill(0) ], updateWithDigest = stateObject => update(stateObject.digest)(stateObject), performCram = (passwordAsUTFArray, cramKey, stateObject) => { const paddedKey = expandArrayTo64Length( passwordAsUTFArray.length > 64 ? ((stateObject = pipe(update(passwordAsUTFArray), finalDigest, init)(stateObject)), stateObject.digest) : passwordAsUTFArray ).map(sym => sym ^ 0x36); // H(K XOR ipad, text) -> digest return pipe( update(paddedKey), update(toUTFArray(cramKey)), finalDigest, init, update(paddedKey.map(sym => sym ^ 0x36 ^ 0x5c)), updateWithDigest, finalHexDigest )(stateObject); }, /** * Generates MD5 hash from passwordString and cramKey * * @param {string} passwordString Is a secret (password) to hash. * @param {string} cramKey Is a key (digest) to hash password with. * @returns {string} Is resulting hash. */ cramMd5Digest = (passwordString, cramKey) => performCram(toUTFArray(passwordString), cramKey, initialInit()), encodeTable = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/", getPaddingSymbol = padding => (padding ? "=" : ""), postBase64Actions = (base64Sequence, encodeTable, md5Binary, padding) => base64Sequence + encodeTable.charAt(md5Binary[30] >>> 2) + encodeTable.charAt(((md5Binary[30] << 4) & 0x30) | (md5Binary[30 + 1] >>> 4)) + encodeTable.charAt((md5Binary[31] << 2) & 0x3c) + getPaddingSymbol(padding), base64EncodeIndexGenerator = () => Array.from( { length: 10 }, (_, i) => i * 3 ), getBase64Sequence = md5Binary => base64EncodeIndexGenerator().reduce( (result, index) => result + encodeTable.charAt(md5Binary[index] >>> 2) + encodeTable.charAt(((md5Binary[index] << 4) & 0x30) | (md5Binary[index + 1] >>> 4)) + encodeTable.charAt(((md5Binary[index + 1] << 2) & 0x3c) | (md5Binary[index + 2] >>> 6)) + encodeTable.charAt(md5Binary[index + 2] & 0x3f), "" ), base64Encode = (md5Binary, padding) => postBase64Actions(getBase64Sequence(md5Binary), encodeTable, md5Binary, padding), /** * Generates MD5 hash from passwordString and cramKey and encodes it in Base64. * * @param {string} passwordString Is a password to hash. * @param {string} cramKey Is a key to hash password with. * @param {boolean} [padding=false] If true there is Base64 padding (=) added. * @returns {string} Is resulting hash. */ cramMd5DigestBase64 = (passwordString, cramKey, padding = false) => base64Encode( [...cramMd5Digest(passwordString, cramKey)].map(char => char.charCodeAt(0)), padding ); exports.cramMd5Digest = cramMd5Digest; exports.cramMd5DigestBase64 = cramMd5DigestBase64; //# sourceMappingURL=cramMd5Digest.js.map