@constantiner/cram-md5-digest
Version:
CRAM-MD5 digester implementation in JavaScript
319 lines (316 loc) • 9.96 kB
JavaScript
/**
* @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.
*
*/
;
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