I implemented a sha256 hash function for javascript just for a practice and I want to know if my code needs more improvement. Please tell me which to simplify if there's any. Thanks.
Here is my code:
var sha256TOOLS = {};
//sha256 constant
sha256TOOLS.k = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
//sha256 intial hashes
sha256TOOLS.h = [0x6a09e667,
0xbb67ae85,
0x3c6ef372,
0xa54ff53a,
0x510e527f,
0x9b05688c,
0x1f83d9ab,
0x5be0cd19];
sha256TOOLS.addFrontZero = function addFrontZero(str, fixedlength){
while(str.length < fixedlength){
str = 0 + str;
}
return str;
};
sha256TOOLS.rotr = function rotr(n,x){
return (((x >>> n) | (x << 32 - n)) >>> 0) % Math.pow(2,32);
};
sha256TOOLS.ch = function ch(x,y,z){
return (x & y) ^ (~x & z);
};
sha256TOOLS.maj = function maj(x,y,z){
return (x & y) ^ (x & z) ^ (y & z);
};
sha256TOOLS.sigma0 = function sigma0(x){
return this.rotr(2,x) ^ this.rotr(13,x) ^ this.rotr(22,x);
};
sha256TOOLS.sigma1 = function sigma1(x){
return this.rotr(6,x) ^ this.rotr(11,x) ^ this.rotr(25,x);
};
sha256TOOLS.omega0 = function omega0(x){
return this.rotr(7,x) ^ this.rotr(18,x) ^ (x >>> 3);
};
sha256TOOLS.omega1 = function omega1(x){
return this.rotr(17,x) ^ this.rotr(19,x) ^ (x >>> 10);
};
sha256TOOLS.mod = function mod(a,b){
var temp = a % b;
while(temp < 0){
temp += b;
}
return temp;
};
var sha256 = (sha256 || function sha256(string){
//copy constant values and initial hashes
var H = sha256TOOLS.h.slice();
var K = sha256TOOLS.k.slice();
//sha256 pre processing
//convert string to ascii code first then to binary
//////////////////////////////////////////////////////
var str =[];
for(var ia=0; ia<string.length; ia++){
var ta = (string.charCodeAt(ia)).toString(2);
ta = (ta.length < 8) ? sha256TOOLS.addFrontZero(ta,8) : ta;
str.push(ta);
}
//turn str into string
str = str.join("");
//////////////////////////////////////////////////////
//Padding the message
var zeroBits = sha256TOOLS.addFrontZero("",sha256TOOLS.mod(448-(str.length+1), 512));
var lengthBits = sha256TOOLS.addFrontZero((str.length).toString(2), 64);
str = str + "1" + zeroBits + lengthBits;
//////////////////////////////////////////////////////
//checking length off message
if(str.length > Math.pow(2,64)){
throw "message length greater than 2 ** 64";
}
//////////////////////////////////////////////////////
//parsing the message M into N 512 bit block
//////////////////////////////////////////////////////
var M = [];
for(var ib=0; ib<str.length; ib+=512){
var tempa = [];
for(var j=0; j < 512; j+=32){
tempa.push(str.substr(ib+j,32));
}
M.push(tempa);
}
//////////////////////////////////////////////////////
//main loop goes here!
var a, b, c, d, e, f, g, h, t1, t2;
for(var i=0; i<M.length; i++){
//Message schedule have length of 64
var W = [];
var temp;
//prepare for message diggest,, compression etc...
//////////////////////////////////////////////////////
for(var tb=0; tb<64; tb++){
if(tb < 16){
W.push(parseInt(M[i][tb], 2) % Math.pow(2,32));
}else{
temp = (sha256TOOLS.omega1(W[tb-2]) + W[tb-7] + sha256TOOLS.omega0(W[tb-15]) + W[tb-16]) % Math.pow(2,32);
W.push(temp);
}
}
//////////////////////////////////////////////////////
//real computation between
//ints mod 2 ** 32
//////////////////////////////////////////////////////
a = H[0];
b = H[1];
c = H[2];
d = H[3];
e = H[4];
f = H[5];
g = H[6];
h = H[7];
for(var t=0; t<64; t++){
t1 = (h + sha256TOOLS.sigma1(e) + sha256TOOLS.ch(e,f,g) + K[t] + W[t]) % Math.pow(2,32);
t2 = (sha256TOOLS.sigma0(a) + sha256TOOLS.maj(a,b,c)) % Math.pow(2,32);
h = g;
g = f;
f = e;
e = (d + t1) % Math.pow(2,32);
d = c;
c = b;
b = a;
a = (t1 + t2) % Math.pow(2,32);
}
H[0] = (a + H[0]) % Math.pow(2,32);
H[1] = (b + H[1]) % Math.pow(2,32);
H[2] = (c + H[2]) % Math.pow(2,32);
H[3] = (d + H[3]) % Math.pow(2,32);
H[4] = (e + H[4]) % Math.pow(2,32);
H[5] = (f + H[5]) % Math.pow(2,32);
H[6] = (g + H[6]) % Math.pow(2,32);
H[7] = (h + H[7]) % Math.pow(2,32);
}
//////////////////////////////////////////////////////
var output = "";
for(var ic=0; ic<8; ic++){
var tempb = (H[ic] >>> 0).toString(16);
output += (sha256TOOLS.addFrontZero(tempb, 8));
}
return output;
});
1 Answer 1
Bug
JavaScript strings are Unicode. Your code assumes that the characters in the string are less than 256. If a character is over 256 the resulting binary encoded string will be the wrong length and the hash will fail.
Typed arrays and DataView
There is no need to convert the numbers to a string of zeros and ones. The conversion is a massive CPU overhead, chews up RAM (16bytes for every character) and there is a lot of additional code converting to and from that string.
Use typed arrays and DataView
to pack the string into 32Bit integers. The arrays, apart from Uint8ClampedArray
will automatically mask the number as they are written so you do not need to do all the % (2 ** 32)
operations.
The max string length in JS is \2ドル^{53}\$ the check you do is a little overkill as just to build a string of size \2ドル^{64}\$ would take many 100s of years on a top end machine.
Minor points
- You can pad a string with zeros using
String.padStart
. egbinStr = number.toString(2).padStart(32, "0");
- Use the power operator
**
rather thanMath.pow
. egMath.pow(2, 32) === 2 ** 32
- Avoid repeating the same calculation. eg Create a constant
const int32Mod = 2 ** 32
to hold the modulo rather than calculate it every time
Rewrite
I started from scratch and used typed arrays and a few other methods to speed things up. The rewrite will create a hash in about 1/15th the time and uses a lot less memory.
Rather than use the class syntax to create the tools (which is not secure) I have encapsulated the tools and hash function via closure to prevent interception of any data to be hashed.
The function using the Uint8Array
will mask out the top 8 bits of Javascripts string characters. It would be simple to modify to allow for the hash to work on the full 16bits per character using Uint16Array
const sha256 = (() => {
const stringFillArray = (str, arr, i = 0) => { while(i < str.length) { arr[i] = str.charCodeAt(i++) } }
const H = [0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19];
const K = [0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2];
const ch = (x, y, z) => (x & y) ^ (~x & z);
const maj = (x, y, z) => (x & y) ^ (x & z) ^ (y & z);
const sigma0 = x => (((x >>> 2) | (x << 30)) ^ ((x >>> 13) | (x << 19)) ^ ((x >>> 22) | (x << 10)));
const sigma1 = x => (((x >>> 6) | (x << 26)) ^ ((x >>> 11) | (x << 21)) ^ ((x >>> 25) | (x << 7)));
const omega0 = x => (((x >>> 7) | (x << 25)) ^ ((x >>> 18) | (x << 14)) ^ (x >>> 3));
const omega1 = x => (((x >>> 17) | (x << 15)) ^ ((x >>> 19) | (x << 13)) ^ (x >>> 10));
const buf32 = new Array(64);
const hTemp = new Int32Array(8);
const totals = new Int32Array(2);
const o1 = omega0, o2 = omega1, s1 = sigma0, s2 = sigma1, t = hTemp, b = buf32; // Aliases
const hashIt = string => {
var i = 0, j, result = [];
const hashed = new Uint32Array(H);
const packChunk = i => o2(b[i - 2]) + b[i - 7] + o1(b[i - 15]) + b[i - 16];
const hashVals = (i = 0) => {
while (i < 64) {
totals[0] = t[7] + s2(t[4]) + ch(t[4], t[5], t[6]) + K[i] + b[i++];
totals[1] = s1(t[0]) + maj(t[0], t[1], t[2]);
t[7] = t[6];
t[6] = t[5];
t[5] = t[4];
t[4] = t[3] + totals[0];
t[3] = t[2];
t[2] = t[1];
t[1] = t[0];
t[0] = totals[0] + totals[1];
}
};
const sumVals = (i = 0) => { while (i < 8) { hashed[i] = t[i] + hashed[i++] } };
const stringBuf = new ArrayBuffer(((string.length / 64 | 0) + 1) * 64);
const stringView = new DataView(stringBuf);
const bytes = new Uint8Array(stringBuf);
const words = new Int32Array(stringBuf);
stringFillArray(string, bytes);
bytes[string.length] = 0x80;
stringView.setUint32(bytes.length - 4, string.length * 8);
while (i < words.length) {
j = 0;
while (j < 16) { buf32[j] = stringView.getInt32((i + (j++)) * 4) }
while (j < 64) { buf32[j] = packChunk(j++) }
hTemp.set(hashed);
hashVals();
sumVals();
i += 16;
}
i = 0;
while (i < 8) { result[i] = hashed[i++].toString(16).padStart(8, "0") }
return result.join("");
};
return str => hashIt(str);
})();
-
\$\begingroup\$ I've placed a remark about the encoding / update functionality below the question that may be interesting to you as well. \$\endgroup\$Maarten Bodewes– Maarten Bodewes2020年01月27日 22:28:54 +00:00Commented Jan 27, 2020 at 22:28
var a, b, c, d, e, f, g, h, t1, t2;
Why not use an array like you did forH
? \$\endgroup\$update
method to allow it to process data piecemeal. This is really important especially if the input data is not present in total in the first place (authenticating multiple fields, for instance, or implementing HMAC). \$\endgroup\$