~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
148 lines · 4.6 KB Raw History
// @pdsjs/core/sha256 - incremental SHA-256.
//
// The streamed blob upload path hashes bytes as they arrive, so it needs a
// hash it can feed chunk by chunk. WebCrypto only hashes a complete buffer.
// This implementation is the portable default; platform packages inject a
// native hasher (node:crypto, the Workers DigestStream) where one exists.

// Round constants: fractional parts of the cube roots of the first 64 primes.
const K = new Uint32Array([
  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,
]);

/**
 * @param {number} x
 * @param {number} n
 * @returns {number}
 */
function rotr(x, n) {
  return (x >>> n) | (x << (32 - n));
}

/**
 * Create an incremental SHA-256 hasher.
 * @returns {import('./ports.js').Hasher}
 */
export function createSha256() {
  const h = new Uint32Array([
    0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
    0x1f83d9ab, 0x5be0cd19,
  ]);
  const block = new Uint8Array(64);
  const w = new Uint32Array(64);
  let blockLen = 0;
  let totalBytes = 0;
  /** @type {Uint8Array|null} */
  let result = null;

  function compress() {
    for (let i = 0; i < 16; i += 1) {
      w[i] =
        (block[i * 4] << 24) |
        (block[i * 4 + 1] << 16) |
        (block[i * 4 + 2] << 8) |
        block[i * 4 + 3];
    }
    for (let i = 16; i < 64; i += 1) {
      const x = w[i - 15];
      const y = w[i - 2];
      const s0 = rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3);
      const s1 = rotr(y, 17) ^ rotr(y, 19) ^ (y >>> 10);
      w[i] = (w[i - 16] + s0 + w[i - 7] + s1) | 0;
    }
    let a = h[0];
    let b = h[1];
    let c = h[2];
    let d = h[3];
    let e = h[4];
    let f = h[5];
    let g = h[6];
    let hh = h[7];
    for (let i = 0; i < 64; i += 1) {
      const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
      const ch = (e & f) ^ (~e & g);
      const t1 = (hh + s1 + ch + K[i] + w[i]) | 0;
      const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
      const maj = (a & b) ^ (a & c) ^ (b & c);
      const t2 = (s0 + maj) | 0;
      hh = g;
      g = f;
      f = e;
      e = (d + t1) | 0;
      d = c;
      c = b;
      b = a;
      a = (t1 + t2) | 0;
    }
    h[0] = (h[0] + a) | 0;
    h[1] = (h[1] + b) | 0;
    h[2] = (h[2] + c) | 0;
    h[3] = (h[3] + d) | 0;
    h[4] = (h[4] + e) | 0;
    h[5] = (h[5] + f) | 0;
    h[6] = (h[6] + g) | 0;
    h[7] = (h[7] + hh) | 0;
    blockLen = 0;
  }

  return {
    /** @param {Uint8Array} chunk */
    update(chunk) {
      if (result) throw new Error('This hash is already final.');
      totalBytes += chunk.length;
      let offset = 0;
      while (offset < chunk.length) {
        const take = Math.min(64 - blockLen, chunk.length - offset);
        block.set(chunk.subarray(offset, offset + take), blockLen);
        blockLen += take;
        offset += take;
        if (blockLen === 64) compress();
      }
    },

    digest() {
      if (result) return result;
      block[blockLen] = 0x80;
      blockLen += 1;
      if (blockLen > 56) {
        block.fill(0, blockLen);
        compress();
      }
      block.fill(0, blockLen);
      // Message length in bits, as a 64-bit big-endian integer. The byte
      // count stays a safe integer far past any accepted blob size, so the
      // high word comes from division rather than a bigint.
      const hi = Math.floor(totalBytes / 0x20000000);
      const lo = (totalBytes << 3) >>> 0;
      block[56] = (hi >>> 24) & 0xff;
      block[57] = (hi >>> 16) & 0xff;
      block[58] = (hi >>> 8) & 0xff;
      block[59] = hi & 0xff;
      block[60] = (lo >>> 24) & 0xff;
      block[61] = (lo >>> 16) & 0xff;
      block[62] = (lo >>> 8) & 0xff;
      block[63] = lo & 0xff;
      blockLen = 64;
      compress();
      const out = new Uint8Array(32);
      for (let i = 0; i < 8; i += 1) {
        out[i * 4] = (h[i] >>> 24) & 0xff;
        out[i * 4 + 1] = (h[i] >>> 16) & 0xff;
        out[i * 4 + 2] = (h[i] >>> 8) & 0xff;
        out[i * 4 + 3] = h[i] & 0xff;
      }
      result = out;
      return out;
    },
  };
}