|
| 1 | +// https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8?permalink_comment_id=4535312 |
| 2 | +// A simple, open-source, HMAC-SHA256 implementation in pure JavaScript. Designed for efficient minification. |
| 3 | +// Feel free to choose whatever license you find most permissible, but I offer no warranty for the code. |
| 4 | +// It's 100% free to do with as you please. |
| 5 | + |
| 6 | +// To ensure cross-browser support even without a proper SubtleCrypto |
| 7 | +// impelmentation (or without access to the impelmentation, as is the case with |
| 8 | +// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256 |
| 9 | +// HMAC signatures using nothing but raw JavaScript |
| 10 | + |
| 11 | +/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */ |
| 12 | + |
| 13 | +// By giving internal functions names that we can mangle, future calls to |
| 14 | +// them are reduced to a single byte (minor space savings in minified file) |
| 15 | +var uint8Array = Uint8Array; |
| 16 | +var uint32Array = Uint32Array; |
| 17 | +var pow = Math.pow; |
| 18 | + |
| 19 | +// Will be initialized below |
| 20 | +// Using a Uint32Array instead of a simple array makes the minified code |
| 21 | +// a bit bigger (we lose our `unshift()` hack), but comes with huge |
| 22 | +// performance gains |
| 23 | +var DEFAULT_STATE = new uint32Array(8); |
| 24 | +var ROUND_CONSTANTS = []; |
| 25 | + |
| 26 | +// Reusable object for expanded message |
| 27 | +// Using a Uint32Array instead of a simple array makes the minified code |
| 28 | +// 7 bytes larger, but comes with huge performance gains |
| 29 | +var M = new uint32Array(64); |
| 30 | + |
| 31 | +// After minification the code to compute the default state and round |
| 32 | +// constants is smaller than the output. More importantly, this serves as a |
| 33 | +// good educational aide for anyone wondering where the magic numbers come |
| 34 | +// from. No magic numbers FTW! |
| 35 | +function getFractionalBits(n) |
| 36 | +{ |
| 37 | + return ((n - (n | 0)) * pow(2, 32)) | 0; |
| 38 | +} |
| 39 | + |
| 40 | +var n = 2, nPrime = 0; |
| 41 | +while (nPrime < 64) |
| 42 | +{ |
| 43 | + // isPrime() was in-lined from its original function form to save |
| 44 | + // a few bytes |
| 45 | + var isPrime = true; |
| 46 | + // Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes |
| 47 | + // var sqrtN = pow(n, 1 / 2); |
| 48 | + // So technically to determine if a number is prime you only need to |
| 49 | + // check numbers up to the square root. However this function only runs |
| 50 | + // once and we're only computing the first 64 primes (up to 311), so on |
| 51 | + // any modern CPU this whole function runs in a couple milliseconds. |
| 52 | + // By going to n / 2 instead of sqrt(n) we net 8 byte savings and no |
| 53 | + // scaling performance cost |
| 54 | + for (var factor = 2; factor <= n / 2; factor++) |
| 55 | + { |
| 56 | + if (n % factor === 0) |
| 57 | + { |
| 58 | + isPrime = false; |
| 59 | + } |
| 60 | + } |
| 61 | + if (isPrime) |
| 62 | + { |
| 63 | + if (nPrime < 8) |
| 64 | + { |
| 65 | + DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2)); |
| 66 | + } |
| 67 | + ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3)); |
| 68 | + |
| 69 | + nPrime++; |
| 70 | + } |
| 71 | + |
| 72 | + n++; |
| 73 | +} |
| 74 | + |
| 75 | +// For cross-platform support we need to ensure that all 32-bit words are |
| 76 | +// in the same endianness. A UTF-8 TextEncoder will return BigEndian data, |
| 77 | +// so upon reading or writing to our ArrayBuffer we'll only swap the bytes |
| 78 | +// if our system is LittleEndian (which is about 99% of CPUs) |
| 79 | +var LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0]; |
| 80 | + |
| 81 | +function convertEndian(word) |
| 82 | +{ |
| 83 | + if (LittleEndian) |
| 84 | + { |
| 85 | + return ( |
| 86 | + // byte 1 -> byte 4 |
| 87 | + (word >>> 24) | |
| 88 | + // byte 2 -> byte 3 |
| 89 | + (((word >>> 16) & 0xff) << 8) | |
| 90 | + // byte 3 -> byte 2 |
| 91 | + ((word & 0xff00) << 8) | |
| 92 | + // byte 4 -> byte 1 |
| 93 | + (word << 24) |
| 94 | + ); |
| 95 | + } |
| 96 | + else |
| 97 | + { |
| 98 | + return word; |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +function rightRotate(word, bits) |
| 103 | +{ |
| 104 | + return (word >>> bits) | (word << (32 - bits)); |
| 105 | +} |
| 106 | + |
| 107 | +function sha256(data) |
| 108 | +{ |
| 109 | + // Copy default state |
| 110 | + var STATE = DEFAULT_STATE.slice(); |
| 111 | + |
| 112 | + // Caching this reduces occurrences of ".length" in minified JavaScript |
| 113 | + // 3 more byte savings! :D |
| 114 | + var legth = data.length; |
| 115 | + |
| 116 | + // Pad data |
| 117 | + var bitLength = legth * 8; |
| 118 | + var newBitLength = (512 - ((bitLength + 64) % 512) - 1) + bitLength + 65; |
| 119 | + |
| 120 | + // "bytes" and "words" are stored BigEndian |
| 121 | + var bytes = new uint8Array(newBitLength / 8); |
| 122 | + var words = new uint32Array(bytes.buffer); |
| 123 | + |
| 124 | + bytes.set(data, 0); |
| 125 | + // Append a 1 |
| 126 | + bytes[legth] = 0b10000000; |
| 127 | + // Store length in BigEndian |
| 128 | + words[words.length - 1] = convertEndian(bitLength); |
| 129 | + |
| 130 | + // Loop iterator (avoid two instances of "var") -- saves 2 bytes |
| 131 | + var round; |
| 132 | + |
| 133 | + // Process blocks (512 bits / 64 bytes / 16 words at a time) |
| 134 | + for (var block = 0; block < newBitLength / 32; block += 16) |
| 135 | + { |
| 136 | + var workingState = STATE.slice(); |
| 137 | + |
| 138 | + // Rounds |
| 139 | + for (round = 0; round < 64; round++) |
| 140 | + { |
| 141 | + var MRound; |
| 142 | + // Expand message |
| 143 | + if (round < 16) |
| 144 | + { |
| 145 | + // Convert to platform Endianness for later math |
| 146 | + MRound = convertEndian(words[block + round]); |
| 147 | + } |
| 148 | + else |
| 149 | + { |
| 150 | + var gamma0x = M[round - 15]; |
| 151 | + var gamma1x = M[round - 2]; |
| 152 | + MRound = |
| 153 | + M[round - 7] + M[round - 16] + ( |
| 154 | + rightRotate(gamma0x, 7) ^ |
| 155 | + rightRotate(gamma0x, 18) ^ |
| 156 | + (gamma0x >>> 3) |
| 157 | + ) + ( |
| 158 | + rightRotate(gamma1x, 17) ^ |
| 159 | + rightRotate(gamma1x, 19) ^ |
| 160 | + (gamma1x >>> 10) |
| 161 | + ) |
| 162 | + ; |
| 163 | + } |
| 164 | + |
| 165 | + // M array matches platform endianness |
| 166 | + M[round] = MRound |= 0; |
| 167 | + |
| 168 | + // Computation |
| 169 | + var t1 = |
| 170 | + ( |
| 171 | + rightRotate(workingState[4], 6) ^ |
| 172 | + rightRotate(workingState[4], 11) ^ |
| 173 | + rightRotate(workingState[4], 25) |
| 174 | + ) + |
| 175 | + ( |
| 176 | + (workingState[4] & workingState[5]) ^ |
| 177 | + (~workingState[4] & workingState[6]) |
| 178 | + ) + workingState[7] + MRound + ROUND_CONSTANTS[round] |
| 179 | + ; |
| 180 | + var t2 = |
| 181 | + ( |
| 182 | + rightRotate(workingState[0], 2) ^ |
| 183 | + rightRotate(workingState[0], 13) ^ |
| 184 | + rightRotate(workingState[0], 22) |
| 185 | + ) + |
| 186 | + ( |
| 187 | + (workingState[0] & workingState[1]) ^ |
| 188 | + (workingState[2] & (workingState[0] ^ |
| 189 | + workingState[1])) |
| 190 | + ) |
| 191 | + ; |
| 192 | + |
| 193 | + for (var i = 7; i > 0; i--) |
| 194 | + { |
| 195 | + workingState[i] = workingState[i - 1]; |
| 196 | + } |
| 197 | + workingState[0] = (t1 + t2) | 0; |
| 198 | + workingState[4] = (workingState[4] + t1) | 0; |
| 199 | + } |
| 200 | + |
| 201 | + // Update state |
| 202 | + for (round = 0; round < 8; round++) |
| 203 | + { |
| 204 | + STATE[round] = (STATE[round] + workingState[round]) | 0; |
| 205 | + } |
| 206 | + } |
| 207 | + |
| 208 | + // Finally the state needs to be converted to BigEndian for output |
| 209 | + // And we want to return a Uint8Array, not a Uint32Array |
| 210 | + return new uint8Array(new uint32Array( |
| 211 | + STATE.map(function(val) { return convertEndian(val); }) |
| 212 | + ).buffer); |
| 213 | +} |
| 214 | + |
| 215 | +function hmac(key, data) |
| 216 | +{ |
| 217 | + if (key.length > 64) |
| 218 | + key = sha256(key); |
| 219 | + |
| 220 | + if (key.length < 64) |
| 221 | + { |
| 222 | + const tmp = new Uint8Array(64); |
| 223 | + tmp.set(key, 0); |
| 224 | + key = tmp; |
| 225 | + } |
| 226 | + |
| 227 | + // Generate inner and outer keys |
| 228 | + var innerKey = new Uint8Array(64); |
| 229 | + var outerKey = new Uint8Array(64); |
| 230 | + for (var i = 0; i < 64; i++) |
| 231 | + { |
| 232 | + innerKey[i] = 0x36 ^ key[i]; |
| 233 | + outerKey[i] = 0x5c ^ key[i]; |
| 234 | + } |
| 235 | + |
| 236 | + // Append the innerKey |
| 237 | + var msg = new Uint8Array(data.length + 64); |
| 238 | + msg.set(innerKey, 0); |
| 239 | + msg.set(data, 64); |
| 240 | + |
| 241 | + // Has the previous message and append the outerKey |
| 242 | + var result = new Uint8Array(64 + 32); |
| 243 | + result.set(outerKey, 0); |
| 244 | + result.set(sha256(msg), 64); |
| 245 | + |
| 246 | + // Hash the previous message |
| 247 | + return sha256(result); |
| 248 | +} |
| 249 | + |
| 250 | +// Convert a string to a Uint8Array, SHA-256 it, and convert back to string |
| 251 | +const encoder = new TextEncoder("utf-8"); |
| 252 | + |
| 253 | +function sign(inputKey, inputData) |
| 254 | +{ |
| 255 | + const key = typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey; |
| 256 | + const data = typeof inputData === "string" ? encoder.encode(inputData) : inputData; |
| 257 | + return hmac(key, data); |
| 258 | +} |
| 259 | + |
| 260 | +function hash(str) |
| 261 | +{ |
| 262 | + return hex(sha256(encoder.encode(str))); |
| 263 | +} |
| 264 | + |
| 265 | +function hex(bin) |
| 266 | +{ |
| 267 | + return bin.reduce((acc, val) => |
| 268 | + acc + ("00" + val.toString(16)).substr(-2) |
| 269 | + , ""); |
| 270 | +} |
0 commit comments