审查视图

app/utils/hmac.ts 7.5 KB
202304001 authored
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
// From https://gist.github.com/guillermodlpa/f6d955f838e9b10d1ef95b8e259b2c58
// From https://gist.github.com/stevendesu/2d52f7b5e1f1184af3b667c0b5e054b8

// To ensure cross-browser support even without a proper SubtleCrypto
// impelmentation (or without access to the impelmentation, as is the case with
// Chrome loaded over HTTP instead of HTTPS), this library can create SHA-256
// HMAC signatures using nothing but raw JavaScript

/* eslint-disable no-magic-numbers, id-length, no-param-reassign, new-cap */

// By giving internal functions names that we can mangle, future calls to
// them are reduced to a single byte (minor space savings in minified file)
const uint8Array = Uint8Array;
const uint32Array = Uint32Array;
const pow = Math.pow;

// Will be initialized below
// Using a Uint32Array instead of a simple array makes the minified code
// a bit bigger (we lose our `unshift()` hack), but comes with huge
// performance gains
const DEFAULT_STATE = new uint32Array(8);
const ROUND_CONSTANTS: number[] = [];

// Reusable object for expanded message
// Using a Uint32Array instead of a simple array makes the minified code
// 7 bytes larger, but comes with huge performance gains
const M = new uint32Array(64);

// After minification the code to compute the default state and round
// constants is smaller than the output. More importantly, this serves as a
// good educational aide for anyone wondering where the magic numbers come
// from. No magic numbers FTW!
function getFractionalBits(n: number) {
  return ((n - (n | 0)) * pow(2, 32)) | 0;
}

let n = 2;
let nPrime = 0;
while (nPrime < 64) {
  // isPrime() was in-lined from its original function form to save
  // a few bytes
  let isPrime = true;
  // Math.sqrt() was replaced with pow(n, 1/2) to save a few bytes
  // var sqrtN = pow(n, 1 / 2);
  // So technically to determine if a number is prime you only need to
  // check numbers up to the square root. However this function only runs
  // once and we're only computing the first 64 primes (up to 311), so on
  // any modern CPU this whole function runs in a couple milliseconds.
  // By going to n / 2 instead of sqrt(n) we net 8 byte savings and no
  // scaling performance cost
  for (let factor = 2; factor <= n / 2; factor++) {
    if (n % factor === 0) {
      isPrime = false;
    }
  }
  if (isPrime) {
    if (nPrime < 8) {
      DEFAULT_STATE[nPrime] = getFractionalBits(pow(n, 1 / 2));
    }
    ROUND_CONSTANTS[nPrime] = getFractionalBits(pow(n, 1 / 3));

    nPrime++;
  }

  n++;
}

// For cross-platform support we need to ensure that all 32-bit words are
// in the same endianness. A UTF-8 TextEncoder will return BigEndian data,
// so upon reading or writing to our ArrayBuffer we'll only swap the bytes
// if our system is LittleEndian (which is about 99% of CPUs)
const LittleEndian = !!new uint8Array(new uint32Array([1]).buffer)[0];

function convertEndian(word: number) {
  if (LittleEndian) {
    return (
      // byte 1 -> byte 4
      (word >>> 24) |
      // byte 2 -> byte 3
      (((word >>> 16) & 0xff) << 8) |
      // byte 3 -> byte 2
      ((word & 0xff00) << 8) |
      // byte 4 -> byte 1
      (word << 24)
    );
  } else {
    return word;
  }
}

function rightRotate(word: number, bits: number) {
  return (word >>> bits) | (word << (32 - bits));
}

function sha256(data: Uint8Array) {
  // Copy default state
  const STATE = DEFAULT_STATE.slice();

  // Caching this reduces occurrences of ".length" in minified JavaScript
  // 3 more byte savings! :D
  const legth = data.length;

  // Pad data
  const bitLength = legth * 8;
  const newBitLength = 512 - ((bitLength + 64) % 512) - 1 + bitLength + 65;

  // "bytes" and "words" are stored BigEndian
  const bytes = new uint8Array(newBitLength / 8);
  const words = new uint32Array(bytes.buffer);

  bytes.set(data, 0);
  // Append a 1
  bytes[legth] = 0b10000000;
  // Store length in BigEndian
  words[words.length - 1] = convertEndian(bitLength);

  // Loop iterator (avoid two instances of "var") -- saves 2 bytes
  let round;

  // Process blocks (512 bits / 64 bytes / 16 words at a time)
  for (let block = 0; block < newBitLength / 32; block += 16) {
    const workingState = STATE.slice();

    // Rounds
    for (round = 0; round < 64; round++) {
      let MRound;
      // Expand message
      if (round < 16) {
        // Convert to platform Endianness for later math
        MRound = convertEndian(words[block + round]);
      } else {
        const gamma0x = M[round - 15];
        const gamma1x = M[round - 2];
        MRound =
          M[round - 7] +
          M[round - 16] +
          (rightRotate(gamma0x, 7) ^
            rightRotate(gamma0x, 18) ^
            (gamma0x >>> 3)) +
          (rightRotate(gamma1x, 17) ^
            rightRotate(gamma1x, 19) ^
            (gamma1x >>> 10));
      }

      // M array matches platform endianness
      M[round] = MRound |= 0;

      // Computation
      const t1 =
        (rightRotate(workingState[4], 6) ^
          rightRotate(workingState[4], 11) ^
          rightRotate(workingState[4], 25)) +
        ((workingState[4] & workingState[5]) ^
          (~workingState[4] & workingState[6])) +
        workingState[7] +
        MRound +
        ROUND_CONSTANTS[round];
      const t2 =
        (rightRotate(workingState[0], 2) ^
          rightRotate(workingState[0], 13) ^
          rightRotate(workingState[0], 22)) +
        ((workingState[0] & workingState[1]) ^
          (workingState[2] & (workingState[0] ^ workingState[1])));
      for (let i = 7; i > 0; i--) {
        workingState[i] = workingState[i - 1];
      }
      workingState[0] = (t1 + t2) | 0;
      workingState[4] = (workingState[4] + t1) | 0;
    }

    // Update state
    for (round = 0; round < 8; round++) {
      STATE[round] = (STATE[round] + workingState[round]) | 0;
    }
  }

  // Finally the state needs to be converted to BigEndian for output
  // And we want to return a Uint8Array, not a Uint32Array
  return new uint8Array(
    new uint32Array(
      STATE.map(function (val) {
        return convertEndian(val);
      }),
    ).buffer,
  );
}

function hmac(key: Uint8Array, data: ArrayLike<number>) {
  if (key.length > 64) key = sha256(key);

  if (key.length < 64) {
    const tmp = new Uint8Array(64);
    tmp.set(key, 0);
    key = tmp;
  }

  // Generate inner and outer keys
  const innerKey = new Uint8Array(64);
  const outerKey = new Uint8Array(64);
  for (let i = 0; i < 64; i++) {
    innerKey[i] = 0x36 ^ key[i];
    outerKey[i] = 0x5c ^ key[i];
  }

  // Append the innerKey
  const msg = new Uint8Array(data.length + 64);
  msg.set(innerKey, 0);
  msg.set(data, 64);

  // Has the previous message and append the outerKey
  const result = new Uint8Array(64 + 32);
  result.set(outerKey, 0);
  result.set(sha256(msg), 64);

  // Hash the previous message
  return sha256(result);
}

// Convert a string to a Uint8Array, SHA-256 it, and convert back to string
const encoder = new TextEncoder();

export function sign(
  inputKey: string | Uint8Array,
  inputData: string | Uint8Array,
) {
  const key =
    typeof inputKey === "string" ? encoder.encode(inputKey) : inputKey;
  const data =
    typeof inputData === "string" ? encoder.encode(inputData) : inputData;
  return hmac(key, data);
}

export function hex(bin: Uint8Array) {
  return bin.reduce((acc, val) => {
    const hexVal = "00" + val.toString(16);
    return acc + hexVal.substring(hexVal.length - 2);
  }, "");
}

export function hash(str: string) {
  return hex(sha256(encoder.encode(str)));
}

export function hashWithSecret(str: string, secret: string) {
  return hex(sign(secret, str)).toString();
}