blake3.js 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /**
  2. * Blake3 fast hash is Blake2 with reduced security (round count). Can also be used as MAC & KDF.
  3. *
  4. * It is advertised as "the fastest cryptographic hash". However, it isn't true in JS.
  5. * Why is this so slow? While it should be 6x faster than blake2b, perf diff is only 20%:
  6. *
  7. * * There is only 30% reduction in number of rounds from blake2s
  8. * * Speed-up comes from tree structure, which is parallelized using SIMD & threading.
  9. * These features are not present in JS, so we only get overhead from trees.
  10. * * Parallelization only happens on 1024-byte chunks: there is no benefit for small inputs.
  11. * * It is still possible to make it faster using: a) loop unrolling b) web workers c) wasm
  12. * @module
  13. */
  14. import { SHA256_IV } from "./_md.js";
  15. import { fromBig } from "./_u64.js";
  16. import { BLAKE2, compress } from "./blake2.js";
  17. // prettier-ignore
  18. import { abytes, aexists, anumber, aoutput, clean, createXOFer, swap32IfBE, toBytes, u32, u8 } from "./utils.js";
  19. // Flag bitset
  20. const B3_Flags = {
  21. CHUNK_START: 0b1,
  22. CHUNK_END: 0b10,
  23. PARENT: 0b100,
  24. ROOT: 0b1000,
  25. KEYED_HASH: 0b10000,
  26. DERIVE_KEY_CONTEXT: 0b100000,
  27. DERIVE_KEY_MATERIAL: 0b1000000,
  28. };
  29. const B3_IV = SHA256_IV.slice();
  30. const B3_SIGMA = /* @__PURE__ */ (() => {
  31. const Id = Array.from({ length: 16 }, (_, i) => i);
  32. const permute = (arr) => [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8].map((i) => arr[i]);
  33. const res = [];
  34. for (let i = 0, v = Id; i < 7; i++, v = permute(v))
  35. res.push(...v);
  36. return Uint8Array.from(res);
  37. })();
  38. /** Blake3 hash. Can be used as MAC and KDF. */
  39. export class BLAKE3 extends BLAKE2 {
  40. constructor(opts = {}, flags = 0) {
  41. super(64, opts.dkLen === undefined ? 32 : opts.dkLen);
  42. this.chunkPos = 0; // Position of current block in chunk
  43. this.chunksDone = 0; // How many chunks we already have
  44. this.flags = 0 | 0;
  45. this.stack = [];
  46. // Output
  47. this.posOut = 0;
  48. this.bufferOut32 = new Uint32Array(16);
  49. this.chunkOut = 0; // index of output chunk
  50. this.enableXOF = true;
  51. const { key, context } = opts;
  52. const hasContext = context !== undefined;
  53. if (key !== undefined) {
  54. if (hasContext)
  55. throw new Error('Only "key" or "context" can be specified at same time');
  56. const k = toBytes(key).slice();
  57. abytes(k, 32);
  58. this.IV = u32(k);
  59. swap32IfBE(this.IV);
  60. this.flags = flags | B3_Flags.KEYED_HASH;
  61. }
  62. else if (hasContext) {
  63. const ctx = toBytes(context);
  64. const contextKey = new BLAKE3({ dkLen: 32 }, B3_Flags.DERIVE_KEY_CONTEXT)
  65. .update(ctx)
  66. .digest();
  67. this.IV = u32(contextKey);
  68. swap32IfBE(this.IV);
  69. this.flags = flags | B3_Flags.DERIVE_KEY_MATERIAL;
  70. }
  71. else {
  72. this.IV = B3_IV.slice();
  73. this.flags = flags;
  74. }
  75. this.state = this.IV.slice();
  76. this.bufferOut = u8(this.bufferOut32);
  77. }
  78. // Unused
  79. get() {
  80. return [];
  81. }
  82. set() { }
  83. b2Compress(counter, flags, buf, bufPos = 0) {
  84. const { state: s, pos } = this;
  85. const { h, l } = fromBig(BigInt(counter), true);
  86. // prettier-ignore
  87. const { v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15 } = compress(B3_SIGMA, bufPos, buf, 7, s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], B3_IV[0], B3_IV[1], B3_IV[2], B3_IV[3], h, l, pos, flags);
  88. s[0] = v0 ^ v8;
  89. s[1] = v1 ^ v9;
  90. s[2] = v2 ^ v10;
  91. s[3] = v3 ^ v11;
  92. s[4] = v4 ^ v12;
  93. s[5] = v5 ^ v13;
  94. s[6] = v6 ^ v14;
  95. s[7] = v7 ^ v15;
  96. }
  97. compress(buf, bufPos = 0, isLast = false) {
  98. // Compress last block
  99. let flags = this.flags;
  100. if (!this.chunkPos)
  101. flags |= B3_Flags.CHUNK_START;
  102. if (this.chunkPos === 15 || isLast)
  103. flags |= B3_Flags.CHUNK_END;
  104. if (!isLast)
  105. this.pos = this.blockLen;
  106. this.b2Compress(this.chunksDone, flags, buf, bufPos);
  107. this.chunkPos += 1;
  108. // If current block is last in chunk (16 blocks), then compress chunks
  109. if (this.chunkPos === 16 || isLast) {
  110. let chunk = this.state;
  111. this.state = this.IV.slice();
  112. // If not the last one, compress only when there are trailing zeros in chunk counter
  113. // chunks used as binary tree where current stack is path. Zero means current leaf is finished and can be compressed.
  114. // 1 (001) - leaf not finished (just push current chunk to stack)
  115. // 2 (010) - leaf finished at depth=1 (merge with last elm on stack and push back)
  116. // 3 (011) - last leaf not finished
  117. // 4 (100) - leafs finished at depth=1 and depth=2
  118. for (let last, chunks = this.chunksDone + 1; isLast || !(chunks & 1); chunks >>= 1) {
  119. if (!(last = this.stack.pop()))
  120. break;
  121. this.buffer32.set(last, 0);
  122. this.buffer32.set(chunk, 8);
  123. this.pos = this.blockLen;
  124. this.b2Compress(0, this.flags | B3_Flags.PARENT, this.buffer32, 0);
  125. chunk = this.state;
  126. this.state = this.IV.slice();
  127. }
  128. this.chunksDone++;
  129. this.chunkPos = 0;
  130. this.stack.push(chunk);
  131. }
  132. this.pos = 0;
  133. }
  134. _cloneInto(to) {
  135. to = super._cloneInto(to);
  136. const { IV, flags, state, chunkPos, posOut, chunkOut, stack, chunksDone } = this;
  137. to.state.set(state.slice());
  138. to.stack = stack.map((i) => Uint32Array.from(i));
  139. to.IV.set(IV);
  140. to.flags = flags;
  141. to.chunkPos = chunkPos;
  142. to.chunksDone = chunksDone;
  143. to.posOut = posOut;
  144. to.chunkOut = chunkOut;
  145. to.enableXOF = this.enableXOF;
  146. to.bufferOut32.set(this.bufferOut32);
  147. return to;
  148. }
  149. destroy() {
  150. this.destroyed = true;
  151. clean(this.state, this.buffer32, this.IV, this.bufferOut32);
  152. clean(...this.stack);
  153. }
  154. // Same as b2Compress, but doesn't modify state and returns 16 u32 array (instead of 8)
  155. b2CompressOut() {
  156. const { state: s, pos, flags, buffer32, bufferOut32: out32 } = this;
  157. const { h, l } = fromBig(BigInt(this.chunkOut++));
  158. swap32IfBE(buffer32);
  159. // prettier-ignore
  160. const { v0, v1, v2, v3, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13, v14, v15 } = compress(B3_SIGMA, 0, buffer32, 7, s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7], B3_IV[0], B3_IV[1], B3_IV[2], B3_IV[3], l, h, pos, flags);
  161. out32[0] = v0 ^ v8;
  162. out32[1] = v1 ^ v9;
  163. out32[2] = v2 ^ v10;
  164. out32[3] = v3 ^ v11;
  165. out32[4] = v4 ^ v12;
  166. out32[5] = v5 ^ v13;
  167. out32[6] = v6 ^ v14;
  168. out32[7] = v7 ^ v15;
  169. out32[8] = s[0] ^ v8;
  170. out32[9] = s[1] ^ v9;
  171. out32[10] = s[2] ^ v10;
  172. out32[11] = s[3] ^ v11;
  173. out32[12] = s[4] ^ v12;
  174. out32[13] = s[5] ^ v13;
  175. out32[14] = s[6] ^ v14;
  176. out32[15] = s[7] ^ v15;
  177. swap32IfBE(buffer32);
  178. swap32IfBE(out32);
  179. this.posOut = 0;
  180. }
  181. finish() {
  182. if (this.finished)
  183. return;
  184. this.finished = true;
  185. // Padding
  186. clean(this.buffer.subarray(this.pos));
  187. // Process last chunk
  188. let flags = this.flags | B3_Flags.ROOT;
  189. if (this.stack.length) {
  190. flags |= B3_Flags.PARENT;
  191. swap32IfBE(this.buffer32);
  192. this.compress(this.buffer32, 0, true);
  193. swap32IfBE(this.buffer32);
  194. this.chunksDone = 0;
  195. this.pos = this.blockLen;
  196. }
  197. else {
  198. flags |= (!this.chunkPos ? B3_Flags.CHUNK_START : 0) | B3_Flags.CHUNK_END;
  199. }
  200. this.flags = flags;
  201. this.b2CompressOut();
  202. }
  203. writeInto(out) {
  204. aexists(this, false);
  205. abytes(out);
  206. this.finish();
  207. const { blockLen, bufferOut } = this;
  208. for (let pos = 0, len = out.length; pos < len;) {
  209. if (this.posOut >= blockLen)
  210. this.b2CompressOut();
  211. const take = Math.min(blockLen - this.posOut, len - pos);
  212. out.set(bufferOut.subarray(this.posOut, this.posOut + take), pos);
  213. this.posOut += take;
  214. pos += take;
  215. }
  216. return out;
  217. }
  218. xofInto(out) {
  219. if (!this.enableXOF)
  220. throw new Error('XOF is not possible after digest call');
  221. return this.writeInto(out);
  222. }
  223. xof(bytes) {
  224. anumber(bytes);
  225. return this.xofInto(new Uint8Array(bytes));
  226. }
  227. digestInto(out) {
  228. aoutput(out, this);
  229. if (this.finished)
  230. throw new Error('digest() was already called');
  231. this.enableXOF = false;
  232. this.writeInto(out);
  233. this.destroy();
  234. return out;
  235. }
  236. digest() {
  237. return this.digestInto(new Uint8Array(this.outputLen));
  238. }
  239. }
  240. /**
  241. * BLAKE3 hash function. Can be used as MAC and KDF.
  242. * @param msg - message that would be hashed
  243. * @param opts - `dkLen` for output length, `key` for MAC mode, `context` for KDF mode
  244. * @example
  245. * const data = new Uint8Array(32);
  246. * const hash = blake3(data);
  247. * const mac = blake3(data, { key: new Uint8Array(32) });
  248. * const kdf = blake3(data, { context: 'application name' });
  249. */
  250. export const blake3 = /* @__PURE__ */ createXOFer((opts) => new BLAKE3(opts));
  251. //# sourceMappingURL=blake3.js.map