chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// @pdsjs/spaces/blake3 - vendored BLAKE3 with extendable output.
//
// This is the only hand-written cryptographic primitive in pds.js. WebCrypto
// has no BLAKE3, and LtHash (proposal 0016) is defined in terms of it. Verified
// against the official BLAKE3 test vectors in test/fixtures/blake3-vectors.json;
// do not modify without re-running packages/spaces/test/blake3.test.js.
//
// Transliterated from the BLAKE3 reference implementation
// (https://github.com/BLAKE3-team/BLAKE3, reference_impl/reference_impl.rs),
// per the spec at https://github.com/BLAKE3-team/BLAKE3-specs. Only the
// unkeyed, extendable-output mode is implemented — no keyed hash, no
// derive-key mode.
const IV = new Uint32Array([
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
0x1f83d9ab, 0x5be0cd19,
]);
const MSG_PERMUTATION = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8];
const CHUNK_START = 1 << 0;
const CHUNK_END = 1 << 1;
const PARENT = 1 << 2;
const ROOT = 1 << 3;
const BLOCK_LEN = 64;
const CHUNK_LEN = 1024;
/**
* A deferred compression: the final block of a chunk or parent, not yet
* compressed, so the ROOT flag can still be applied to the last one.
* @typedef {Object} Output
* @property {Uint32Array} cv
* @property {Uint32Array} block
* @property {number} counter
* @property {number} blockLen
* @property {number} flags
*/
/**
* @param {number} x
* @param {number} n
* @returns {number}
*/
const rotr = (x, n) => ((x >>> n) | (x << (32 - n))) >>> 0;
/**
* The quarter-round. Rotations are 16, 12, 8, 7.
* @param {Uint32Array} s - 16-word compression state, mutated in place
* @param {number} a
* @param {number} b
* @param {number} c
* @param {number} d
* @param {number} mx
* @param {number} my
* @returns {void}
*/
function g(s, a, b, c, d, mx, my) {
s[a] = (s[a] + s[b] + mx) >>> 0;
s[d] = rotr(s[d] ^ s[a], 16);
s[c] = (s[c] + s[d]) >>> 0;
s[b] = rotr(s[b] ^ s[c], 12);
s[a] = (s[a] + s[b] + my) >>> 0;
s[d] = rotr(s[d] ^ s[a], 8);
s[c] = (s[c] + s[d]) >>> 0;
s[b] = rotr(s[b] ^ s[c], 7);
}
/**
* Four column mixes then four diagonal mixes.
* @param {Uint32Array} s - 16-word compression state, mutated in place
* @param {Uint32Array} m - 16 message words
* @returns {void}
*/
function round(s, m) {
g(s, 0, 4, 8, 12, m[0], m[1]);
g(s, 1, 5, 9, 13, m[2], m[3]);
g(s, 2, 6, 10, 14, m[4], m[5]);
g(s, 3, 7, 11, 15, m[6], m[7]);
g(s, 0, 5, 10, 15, m[8], m[9]);
g(s, 1, 6, 11, 12, m[10], m[11]);
g(s, 2, 7, 8, 13, m[12], m[13]);
g(s, 3, 4, 9, 14, m[14], m[15]);
}
/**
* @param {Uint32Array} m - 16 message words
* @returns {Uint32Array} the message words reordered by MSG_PERMUTATION
*/
function permute(m) {
const out = new Uint32Array(16);
for (let i = 0; i < 16; i++) out[i] = m[MSG_PERMUTATION[i]];
return out;
}
/**
* @param {Uint32Array} cv - 8-word chaining value
* @param {Uint32Array} block - 16 message words
* @param {number} counter
* @param {number} blockLen
* @param {number} flags
* @returns {Uint32Array} all 16 state words — the first 8 are the chaining
* value, all 16 are needed for extended output.
*/
function compress(cv, block, counter, blockLen, flags) {
const s = new Uint32Array(16);
s.set(cv, 0);
s[8] = IV[0];
s[9] = IV[1];
s[10] = IV[2];
s[11] = IV[3];
s[12] = counter >>> 0;
s[13] = Math.floor(counter / 2 ** 32) >>> 0;
s[14] = blockLen;
s[15] = flags;
let m = block;
for (let i = 0; i < 7; i++) {
round(s, m);
if (i < 6) m = permute(m);
}
for (let i = 0; i < 8; i++) {
s[i] = (s[i] ^ s[i + 8]) >>> 0;
s[i + 8] = (s[i + 8] ^ cv[i]) >>> 0;
}
return s;
}
/**
* Read 16 little-endian uint32s from a 64-byte block.
* @param {Uint8Array} bytes
* @param {number} offset
* @returns {Uint32Array}
*/
function wordsFromBlock(bytes, offset) {
const m = new Uint32Array(16);
for (let i = 0; i < 16; i++) {
const o = offset + i * 4;
m[i] =
(bytes[o] |
(bytes[o + 1] << 8) |
(bytes[o + 2] << 16) |
(bytes[o + 3] << 24)) >>>
0;
}
return m;
}
/**
* @param {Output} o
* @returns {Uint32Array} the 8-word chaining value
*/
function outputChainingValue(o) {
return compress(o.cv, o.block, o.counter, o.blockLen, o.flags).slice(0, 8);
}
/**
* @param {Output} o
* @param {number} outLen - output length in bytes
* @returns {Uint8Array}
*/
function rootOutputBytes(o, outLen) {
const out = new Uint8Array(outLen);
let i = 0;
let counter = 0;
while (i < outLen) {
const words = compress(o.cv, o.block, counter, o.blockLen, o.flags | ROOT);
for (let w = 0; w < 16 && i < outLen; w++) {
for (let b = 0; b < 4 && i < outLen; b++) {
out[i++] = (words[w] >>> (8 * b)) & 0xff;
}
}
counter++;
}
return out;
}
/**
* @param {Uint8Array} input
* @param {number} start - byte offset of the chunk within input
* @param {number} len - chunk length in bytes
* @param {number} chunkCounter
* @returns {Output}
*/
function chunkOutput(input, start, len, chunkCounter) {
let cv = IV.slice(0, 8);
let blockFlags = CHUNK_START;
let pos = 0;
while (len - pos > BLOCK_LEN) {
const m = wordsFromBlock(input, start + pos);
cv = compress(cv, m, chunkCounter, BLOCK_LEN, blockFlags).slice(0, 8);
pos += BLOCK_LEN;
blockFlags = 0;
}
const blockLen = len - pos;
const padded = new Uint8Array(BLOCK_LEN);
padded.set(input.subarray(start + pos, start + len));
return {
cv,
block: wordsFromBlock(padded, 0),
counter: chunkCounter,
blockLen,
flags: blockFlags | CHUNK_END,
};
}
/**
* @param {Uint32Array} leftCv - 8-word chaining value
* @param {Uint32Array} rightCv - 8-word chaining value
* @returns {Output}
*/
function parentOutput(leftCv, rightCv) {
const block = new Uint32Array(16);
block.set(leftCv, 0);
block.set(rightCv, 8);
return {
cv: IV.slice(0, 8),
block,
counter: 0,
blockLen: BLOCK_LEN,
flags: PARENT,
};
}
/**
* Unkeyed BLAKE3 with extendable output.
* @param {Uint8Array} input
* @param {number} [outLen] - output length in bytes, default 32
* @returns {Uint8Array}
*/
export function blake3(input, outLen = 32) {
const chunks = Math.max(1, Math.ceil(input.length / CHUNK_LEN));
/** @type {Uint32Array[]} */
const stack = [];
/** @type {Output|undefined} */
let output;
for (let i = 0; i < chunks; i++) {
const start = i * CHUNK_LEN;
const len = Math.min(CHUNK_LEN, Math.max(0, input.length - start));
const chunk = chunkOutput(input, start, len, i);
// The last chunk stays an Output so the ROOT flag can reach it.
if (i === chunks - 1) {
output = chunk;
break;
}
// Merge while the completed-chunk count has a trailing zero bit.
let cv = outputChainingValue(chunk);
let n = i + 1;
while ((n & 1) === 0) {
// Guaranteed non-empty: n's trailing zero count never exceeds the
// number of merges already pushed onto the stack.
cv = outputChainingValue(
parentOutput(/** @type {Uint32Array} */ (stack.pop()), cv),
);
n >>= 1;
}
stack.push(cv);
}
// At least one chunk always runs, so `output` is set by the time either
// loop below reads it.
while (stack.length > 0) {
output = parentOutput(
/** @type {Uint32Array} */ (stack.pop()),
outputChainingValue(/** @type {Output} */ (output)),
);
}
return rootOutputBytes(/** @type {Output} */ (output), outLen);
}