├── blake3-wasm ├── .npmignore ├── tsconfig.json ├── tsconfig.esm.json ├── src │ ├── node │ │ ├── index.ts │ │ ├── hash-fn.ts │ │ └── hash-instance.ts │ ├── browser │ │ ├── index.ts │ │ ├── hash.ts │ │ ├── encoding.ts │ │ ├── hash-fn.ts │ │ └── hash-instance.ts │ └── wasm │ │ └── blake3.c └── package.json ├── blake3 ├── browser.mts ├── .gitignore ├── .npmignore ├── tsconfig.esm.json ├── tsconfig.json ├── set-node-module.cts ├── index.cts ├── index.mts ├── package.json ├── node.test.cts ├── browser.test.cts └── test-helpers.cts ├── blake3-internal ├── .npmignore ├── tsconfig.esm.json ├── tsconfig.json ├── src │ ├── index.ts │ ├── hash-fn.ts │ ├── hash-oneshots.ts │ ├── hash-reader.ts │ ├── wasm-types.ts │ └── hash-instance.ts └── package.json ├── .remarkrc ├── blake3-native ├── .npmignore ├── tsconfig.esm.json ├── tsconfig.json ├── src │ ├── index.ts │ ├── hash-fn.ts │ ├── hash-instance.ts │ └── native.cc ├── package.json └── binding.gyp ├── .gitmodules ├── .vscode ├── extensions.json ├── c_cpp_properties.json ├── settings.json └── launch.json ├── tsconfig.esm.json ├── tsconfig.base.json ├── .github └── workflows │ ├── ci.yml │ └── build-neon.yml ├── benchmark.js ├── LICENSE ├── package.json ├── .devcontainer ├── devcontainer.json ├── Dockerfile └── base.Dockerfile ├── .gitignore ├── changelog.md ├── Makefile └── readme.md /blake3-wasm/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist 3 | !/esm 4 | -------------------------------------------------------------------------------- /blake3/browser.mts: -------------------------------------------------------------------------------- 1 | export * from 'blake3-wasm'; 2 | -------------------------------------------------------------------------------- /blake3-internal/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist 3 | !/esm 4 | -------------------------------------------------------------------------------- /blake3/.gitignore: -------------------------------------------------------------------------------- 1 | /*.cjs 2 | /*.mjs 3 | /*.map 4 | /*.d.* 5 | -------------------------------------------------------------------------------- /.remarkrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "remark-toc": { "tight": true } 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /blake3/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/*.{cjs,mjs,map} 3 | !/*.d.{cts,mts} 4 | /test* 5 | /*.test.* 6 | -------------------------------------------------------------------------------- /blake3-native/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist 3 | !/esm 4 | !/c 5 | !/binding.gyp 6 | !/src/native.cc 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "blake3-src"] 2 | path = blake3-src 3 | url = git@github.com:connor4312/BLAKE3-fork.git 4 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "rust-lang.rust" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ESNext", 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /blake3-wasm/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /blake3-internal/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.esm.json", 3 | "compilerOptions": { 4 | "outDir": "./esm" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /blake3-internal/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /blake3-native/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.esm.json", 3 | "compilerOptions": { 4 | "outDir": "./esm" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /blake3-native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /blake3-wasm/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.esm.json", 3 | "compilerOptions": { 4 | "outDir": "./esm" 5 | }, 6 | "include": ["src"] 7 | } 8 | -------------------------------------------------------------------------------- /blake3-native/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@c4312/blake3-internal'; 2 | export * from './hash-fn'; 3 | export * from './hash-instance'; 4 | 5 | export const load = () => Promise.resolve(); 6 | -------------------------------------------------------------------------------- /blake3/tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "ES2022" 5 | }, 6 | "include": ["*.mts"], 7 | "exclude": ["*.d.*"] 8 | } 9 | -------------------------------------------------------------------------------- /blake3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "CommonJS" 5 | }, 6 | "include": ["*.cts"], 7 | "exclude": ["*.d.*"] 8 | } 9 | -------------------------------------------------------------------------------- /blake3-internal/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hash-fn.js'; 2 | export * from './hash-instance.js'; 3 | export * from './hash-oneshots.js'; 4 | export * from './hash-reader.js'; 5 | export * from './wasm-types.js'; 6 | -------------------------------------------------------------------------------- /blake3-wasm/src/node/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@c4312/blake3-internal'; 2 | export { deriveKey, hash, keyedHash } from './hash-fn'; 3 | export * from './hash-instance'; 4 | 5 | import { setWasm } from '@c4312/blake3-internal'; 6 | 7 | //@ts-ignore 8 | import blake3 from '../wasm/blake3'; 9 | 10 | /** Loads the WebAssembly hashing code. This *must* be called before any hashing methods. */ 11 | export const load = async () => { 12 | setWasm(await blake3()); 13 | }; 14 | -------------------------------------------------------------------------------- /blake3-wasm/src/browser/index.ts: -------------------------------------------------------------------------------- 1 | export * from '@c4312/blake3-internal'; 2 | export { deriveKey, hash, keyedHash } from './hash-fn.js'; 3 | export * from './hash-instance.js'; 4 | import { setWasm } from '@c4312/blake3-internal'; 5 | 6 | //@ts-ignore 7 | import blake3 from '../../esm/wasm/blake3.mjs'; 8 | 9 | /** Loads the WebAssembly hashing code. This *must* be called before any hashing methods. */ 10 | export const load = async () => { 11 | setWasm(await blake3()); 12 | }; 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "forceConsistentCasingInFileNames": true, 4 | "sourceMap": true, 5 | "inlineSources": true, 6 | "declaration": true, 7 | "noImplicitReturns": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "esModuleInterop": true, 11 | "strict": true, 12 | "moduleResolution": "node", 13 | "newLine": "LF", 14 | "module": "commonjs", 15 | "target": "ES2021", 16 | "lib": ["ES2021", "dom"], 17 | "types": ["mocha", "node"] 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /blake3-internal/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c4312/blake3-internal", 3 | "version": "3.0.0", 4 | "description": "Internal shared package for BLAKE3 bindings", 5 | "engines": { 6 | "node": ">=16" 7 | }, 8 | "gypfile": true, 9 | "module": "./esm/index", 10 | "main": "./dist/index", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/connor4312/blake3.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/connor4312/blake3/issues" 17 | }, 18 | "homepage": "https://github.com/connor4312/blake3#readme", 19 | "author": "Connor Peet ", 20 | "license": "MIT" 21 | } 22 | -------------------------------------------------------------------------------- /blake3/set-node-module.cts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | 3 | const files = ['index.cjs', 'index.d.cts', 'index.mjs', 'index.d.mts']; 4 | const nativeModule = '@c4312/blake3-native'; 5 | const wasmModule = 'blake3-wasm'; 6 | 7 | let targetModule: string; 8 | try { 9 | require(nativeModule); 10 | targetModule = nativeModule; 11 | } catch { 12 | targetModule = wasmModule; 13 | } 14 | 15 | for (const file of files) { 16 | const path = `${__dirname}/${file}`; 17 | const content = fs 18 | .readFileSync(path, 'utf-8') 19 | .replaceAll(nativeModule, targetModule) 20 | .replaceAll(wasmModule, targetModule); 21 | 22 | fs.writeFileSync(path, content); 23 | } 24 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "includePath": [ 6 | "${workspaceFolder}/**", 7 | "${workspaceFolder}/node_modules/node-addon-api", 8 | "/home/node/emsdk/upstream/emscripten/system/include/**", 9 | "/usr/local/include/node" 10 | ], 11 | "defines": [ 12 | "NODE_ADDON_API_ENABLE_MAYBE" 13 | ], 14 | "compilerPath": "/usr/bin/gcc", 15 | "cStandard": "gnu17", 16 | "cppStandard": "gnu++14", 17 | "intelliSenseMode": "linux-gcc-x64" 18 | } 19 | ], 20 | "version": 4 21 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "editor.defaultFormatter": "esbenp.prettier-vscode", 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": true 6 | }, 7 | "typescript.tsdk": "node_modules/typescript/lib", 8 | "files.associations": { 9 | "blake3.h": "c", 10 | "blake3_impl.h": "c", 11 | "stddef.h": "c", 12 | "immintrin.h": "c", 13 | "emscripten.h": "c", 14 | "stdlib.h": "c" 15 | }, 16 | "C_Cpp.default.includePath": [ 17 | "/home/node/emsdk/upstream/emscripten/system/include/**", 18 | "${workspaceFolder}/BLAKE3/c/*" 19 | ], 20 | "[c]": { 21 | "editor.defaultFormatter": "ms-vscode.cpptools" 22 | }, 23 | "[cpp]": { 24 | "editor.defaultFormatter": "ms-vscode.cpptools" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /blake3/index.cts: -------------------------------------------------------------------------------- 1 | // Bunlers add the `process.browser` flag to indicate the build enviroment. 2 | // Throw a verbose error if we see people bundling the Node.js build in their 3 | // browser, since it probably won't work for them (or at least not give them) 4 | // nice tree shaking and such. 5 | // 6 | // Note that we don't check the presence of window/document, since those can 7 | // be emulated in common test scenarios (e.g. jest's default setup with jsdom). 8 | if ((process as any).browser) { 9 | throw new Error( 10 | 'You tried to import the Node.js version of blake3, instead of the browser ' + 11 | 'version, in your build. You can fix this by importing "blake3/browser" ' + 12 | 'instead of "blake3"', 13 | ); 14 | } 15 | 16 | export * from '@c4312/blake3-native'; 17 | -------------------------------------------------------------------------------- /blake3/index.mts: -------------------------------------------------------------------------------- 1 | // Bunlers add the `process.browser` flag to indicate the build enviroment. 2 | // Throw a verbose error if we see people bundling the Node.js build in their 3 | // browser, since it probably won't work for them (or at least not give them) 4 | // nice tree shaking and such. 5 | // 6 | // Note that we don't check the presence of window/document, since those can 7 | // be emulated in common test scenarios (e.g. jest's default setup with jsdom). 8 | if ((process as any).browser) { 9 | throw new Error( 10 | 'You tried to import the Node.js version of blake3, instead of the browser ' + 11 | 'version, in your build. You can fix this by importing "blake3/browser" ' + 12 | 'instead of "blake3"', 13 | ); 14 | } 15 | 16 | export * from '@c4312/blake3-native'; 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | 5 | jobs: 6 | build_and_test: 7 | name: Test 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '12.x' 14 | - uses: actions-rs/toolchain@v1 15 | with: 16 | toolchain: nightly 17 | target: wasm32-unknown-unknown 18 | - run: npm install --ci 19 | - uses: actions-rs/cargo@v1 20 | with: 21 | command: install 22 | args: wasm-pack 23 | - run: make 24 | - run: npm test 25 | 26 | - uses: actions/upload-artifact@v1 27 | if: failure() 28 | with: 29 | name: pkg 30 | path: pkg 31 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "args": [ 15 | "--recursive", "dist/**/*.test.js", "--timeout", "5000", 16 | ], 17 | "program": "${workspaceFolder}/node_modules/bin/.mocha", 18 | "preLaunchTask": "tsc: build - tsconfig.json", 19 | "outFiles": [ 20 | "${workspaceFolder}/dist/**/*.js" 21 | ] 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /blake3-wasm/src/browser/hash.ts: -------------------------------------------------------------------------------- 1 | import { BrowserEncoding, mustGetEncoder } from './encoding.js'; 2 | 3 | /** 4 | * Hash returned from functions in the browser. 5 | */ 6 | export class Hash extends Uint8Array { 7 | /** 8 | * A constant-time comparison against the other hash/array. 9 | */ 10 | public equals(other: unknown): boolean { 11 | if (!(other instanceof Uint8Array)) { 12 | return false; 13 | } 14 | 15 | if (other.length !== this.length) { 16 | return false; 17 | } 18 | 19 | let cmp = 0; 20 | for (let i = 0; i < this.length; i++) { 21 | cmp |= this[i] ^ other[i]; 22 | } 23 | 24 | return cmp === 0; 25 | } 26 | 27 | public toString(encoding: BrowserEncoding = 'hex'): string { 28 | return mustGetEncoder(encoding)(this); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /benchmark.js: -------------------------------------------------------------------------------- 1 | const { hash: hashWasm, load } = require('./dist/node-wasm'); 2 | const { hash: hashNative } = require('./dist/node-native'); 3 | const { createHash } = require('crypto'); 4 | const b = require('benny'); 5 | 6 | (async () => { 7 | await load(); 8 | 9 | 10 | [ 11 | { size: '64B', data: Buffer.alloc(64) }, 12 | { size: '64KB', data: Buffer.alloc(1024 * 64) }, 13 | { size: '6MB', data: Buffer.alloc(1024 * 1024 * 6) }, 14 | ].forEach(({ size, data }) => 15 | b.suite( 16 | size, 17 | b.add('blake3 wasm', () => hashWasm(data)), 18 | b.add('blake3 native', () => hashNative(data)), 19 | ...['md5', 'sha1', 'sha256'].map((alg) => 20 | b.add(alg, () => createHash(alg).update(data).digest()), 21 | ), 22 | b.cycle(), 23 | ), 24 | ); 25 | })(); 26 | -------------------------------------------------------------------------------- /blake3-native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@c4312/blake3-native", 3 | "version": "3.0.0", 4 | "description": "BLAKE3 hashing for JavaScript: native Node bindings only", 5 | "engines": { 6 | "node": ">=16" 7 | }, 8 | "keywords": [ 9 | "blake3", 10 | "node-addon", 11 | "hash" 12 | ], 13 | "gypfile": true, 14 | "module": "./esm/index", 15 | "main": "./dist/index", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/connor4312/blake3.git" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/connor4312/blake3/issues" 22 | }, 23 | "homepage": "https://github.com/connor4312/blake3#readme", 24 | "author": "Connor Peet ", 25 | "license": "MIT", 26 | "dependencies": { 27 | "@c4312/blake3-internal": "2.1.7", 28 | "node-addon-api": "^5.0.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blake3-wasm/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blake3-wasm", 3 | "version": "3.0.0", 4 | "description": "BLAKE3 hashing for JavaScript: WebAssembly bindings only", 5 | "engines": { 6 | "node": ">=16" 7 | }, 8 | "keywords": [ 9 | "blake3", 10 | "webassembly", 11 | "wasm", 12 | "hash" 13 | ], 14 | "browser": "./esm/browser/index", 15 | "module": "./esm/node/index", 16 | "main": "./dist/node/index", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/connor4312/blake3.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/connor4312/blake3/issues" 23 | }, 24 | "homepage": "https://github.com/connor4312/blake3#readme", 25 | "author": "Connor Peet ", 26 | "license": "MIT", 27 | "dependencies": { 28 | "@c4312/blake3-internal": "2.1.7" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /blake3-internal/src/hash-fn.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Options passed to hash functions. 3 | */ 4 | export interface IBaseHashOptions { 5 | /** 6 | * Length of the desired hash, in bytes. Note that when encoding the output 7 | * as a string, this is *not* the string length. 8 | */ 9 | length?: number; 10 | } 11 | 12 | /** 13 | * Default hash length, in bytes, unless otherwise specified. 14 | */ 15 | export const defaultHashLength = 32; 16 | 17 | /** 18 | * A type that can be hashed. 19 | */ 20 | export type HashInput = Uint8Array | ArrayBuffer | SharedArrayBuffer | string; 21 | 22 | const textEncoder = new TextEncoder(); 23 | 24 | /** 25 | * Converts the input to an Uint8Array. 26 | * @hidden 27 | */ 28 | export const inputToArray = (input: HashInput) => 29 | input instanceof Uint8Array 30 | ? input 31 | : typeof input === 'string' 32 | ? textEncoder.encode(input) 33 | : new Uint8Array(input); 34 | -------------------------------------------------------------------------------- /blake3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blake3", 3 | "version": "3.0.0", 4 | "description": "BLAKE3 hashing for JavaScript", 5 | "engines": { 6 | "node": ">=16" 7 | }, 8 | "keywords": [ 9 | "blake3", 10 | "webassembly", 11 | "wasm", 12 | "hash" 13 | ], 14 | "browser": "./browser.mjs", 15 | "module": "./index.mjs", 16 | "main": "./index.cjs", 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/connor4312/blake3.git" 20 | }, 21 | "scripts": { 22 | "postinstall": "node set-node-module.cjs" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/connor4312/blake3/issues" 26 | }, 27 | "homepage": "https://github.com/connor4312/blake3#readme", 28 | "author": "Connor Peet ", 29 | "license": "MIT", 30 | "dependencies": { 31 | "blake3-wasm": "2.1.7" 32 | }, 33 | "optionalDependencies": { 34 | "@c4312/blake3-native": "2.1.7" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /blake3-wasm/src/browser/encoding.ts: -------------------------------------------------------------------------------- 1 | // A small collection of encodings for convenience of use in the browser. 2 | 3 | export type BrowserEncoding = 'hex' | 'base64' | 'utf8'; 4 | 5 | const decoder = new TextDecoder(); 6 | 7 | const encoders: { [K in BrowserEncoding]: (data: Uint8Array) => string } = { 8 | // certainly not the fastest, but hashes are pretty small 9 | base64: (data) => btoa(String.fromCharCode(...data)), 10 | 11 | hex: (data) => { 12 | let out = ''; 13 | for (const byte of data) { 14 | if (byte < 0x10) { 15 | out += '0'; 16 | } 17 | 18 | out += byte.toString(16); 19 | } 20 | 21 | return out; 22 | }, 23 | 24 | utf8: (data) => decoder.decode(data), 25 | }; 26 | 27 | /** 28 | * @hidden 29 | */ 30 | export const mustGetEncoder = (encoding: BrowserEncoding) => { 31 | const encoder = encoders[encoding]; 32 | if (!encoder) { 33 | throw new Error(`Unknown encoding ${encoding}`); 34 | } 35 | 36 | return encoder; 37 | }; 38 | -------------------------------------------------------------------------------- /blake3-native/binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "blake3", 5 | "cflags!": [ 6 | "-fno-exceptions", 7 | "-march=native", 8 | "-O3" 9 | ], 10 | "cflags_cc!": [ 11 | "-fno-exceptions", 12 | "-march=native", 13 | "-O3" 14 | ], 15 | "sources": [ 16 | "src/native.cc", 17 | "c/blake3.c", 18 | "c/blake3_dispatch.c", 19 | "c/blake3_portable.c", 20 | "f.endsWith(process.platform==='win32'?'_msvc.asm':'_unix.S')).map(f=>'c/'+f).join(' ')\")", 21 | ], 22 | "include_dirs": [ 23 | " { 4 | scratch.grow(input.length + hashLength); 5 | const wasm = getWasm(); 6 | 7 | const hashIsInScratch = hashLength <= MAX_SCRATCH_SIZE; 8 | const inputIsInScratch = hashLength + input.length <= MAX_SCRATCH_SIZE; 9 | const scratchAddr = scratch.grow( 10 | (hashIsInScratch ? hashLength : 0) + (inputIsInScratch ? input.length : 0), 11 | ); 12 | 13 | const hashAddr = hashIsInScratch ? scratchAddr : wasm._malloc(hashLength); 14 | const inputAddr = inputIsInScratch 15 | ? hashIsInScratch 16 | ? scratchAddr + hashLength 17 | : scratchAddr 18 | : wasm._malloc(input.length); 19 | 20 | wasm.HEAPU8.set(input, inputAddr); 21 | 22 | wasm.ccall( 23 | 'hash_oneshot', 24 | null, 25 | ['number', 'number', 'number', 'number'], 26 | [inputAddr, input.length, hashAddr, hashLength], 27 | ); 28 | 29 | if (!inputIsInScratch) { 30 | wasm._free(inputAddr); 31 | } 32 | if (!hashIsInScratch) { 33 | wasm._free(hashAddr); 34 | } 35 | 36 | // kinda use after free, but as long as we (internal caller) consumes it 37 | // synchronously, it's fine. 38 | return wasm.HEAPU8.subarray(hashAddr, hashAddr + hashLength); 39 | }; 40 | -------------------------------------------------------------------------------- /blake3-native/src/hash-fn.ts: -------------------------------------------------------------------------------- 1 | import { defaultHashLength, HashInput, IBaseHashOptions, inputToArray } from '@c4312/blake3-internal'; 2 | import { createDeriveKey, createHash, createKeyed } from './hash-instance'; 3 | 4 | /** 5 | * Returns a blake3 hash of the input. 6 | */ 7 | export function hash( 8 | input: HashInput, 9 | { length = defaultHashLength }: IBaseHashOptions = {}, 10 | ): Buffer { 11 | const hash = createHash(); 12 | hash.update(inputToArray(input)); 13 | return hash.digest({ length }); 14 | } 15 | 16 | /** 17 | * Given cryptographic key material and a context string, services a subkey of 18 | * any length. See {@link https://docs.rs/blake3/0.1.3/blake3/fn.derive_key.html} 19 | * for more information. 20 | */ 21 | export function deriveKey( 22 | context: HashInput, 23 | material: HashInput, 24 | { length = defaultHashLength }: IBaseHashOptions = {}, 25 | ) { 26 | const derive = createDeriveKey(context); 27 | derive.update(inputToArray(material)); 28 | const digest = derive.digest({ length }); 29 | derive.dispose(); 30 | return digest; 31 | } 32 | 33 | /** 34 | * The keyed hash function. See {@link https://docs.rs/blake3/0.1.3/blake3/fn.keyed_hash.html}. 35 | */ 36 | export function keyedHash( 37 | key: HashInput, 38 | input: HashInput, 39 | { length = defaultHashLength }: IBaseHashOptions = {}, 40 | ) { 41 | const keyed = createKeyed(key); 42 | keyed.update(inputToArray(input)); 43 | const digest = keyed.digest({ length }); 44 | keyed.dispose(); 45 | return digest; 46 | } 47 | -------------------------------------------------------------------------------- /blake3-wasm/src/node/hash-fn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultHashLength, 3 | HashInput, 4 | hashOneShot, 5 | IBaseHashOptions, 6 | inputToArray, 7 | } from '@c4312/blake3-internal'; 8 | import { createDeriveKey, createKeyed } from './hash-instance'; 9 | 10 | /** 11 | * Returns a blake3 hash of the input. 12 | */ 13 | export function hash( 14 | input: HashInput, 15 | { length = defaultHashLength }: IBaseHashOptions = {}, 16 | ): Buffer { 17 | return Buffer.from(hashOneShot(inputToArray(input), length)); 18 | } 19 | 20 | /** 21 | * Given cryptographic key material and a context string, services a subkey of 22 | * any length. See {@link https://docs.rs/blake3/0.1.3/blake3/fn.derive_key.html} 23 | * for more information. 24 | */ 25 | export function deriveKey( 26 | context: HashInput, 27 | material: HashInput, 28 | { length = defaultHashLength }: IBaseHashOptions = {}, 29 | ) { 30 | const derive = createDeriveKey(context); 31 | derive.update(inputToArray(material)); 32 | const digest = derive.digest({ length }); 33 | derive.dispose(); 34 | return digest; 35 | } 36 | 37 | /** 38 | * The keyed hash function. See {@link https://docs.rs/blake3/0.1.3/blake3/fn.keyed_hash.html}. 39 | */ 40 | export function keyedHash( 41 | key: HashInput, 42 | input: HashInput, 43 | { length = defaultHashLength }: IBaseHashOptions = {}, 44 | ) { 45 | const keyed = createKeyed(key); 46 | keyed.update(inputToArray(input)); 47 | const digest = keyed.digest({ length }); 48 | keyed.dispose(); 49 | return digest; 50 | } 51 | -------------------------------------------------------------------------------- /blake3-wasm/src/browser/hash-fn.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defaultHashLength, 3 | HashInput, 4 | hashOneShot, 5 | IBaseHashOptions, 6 | inputToArray, 7 | } from '@c4312/blake3-internal'; 8 | import { createDeriveKey, createKeyed } from './hash-instance.js'; 9 | import { Hash } from './hash.js'; 10 | 11 | /** 12 | * Returns a blake3 hash of the input. 13 | */ 14 | export function hash( 15 | input: HashInput, 16 | { length = defaultHashLength }: IBaseHashOptions = {}, 17 | ): Hash { 18 | const hash = new Hash(length); 19 | hash.set(hashOneShot(inputToArray(input), length)); 20 | return hash; 21 | } 22 | 23 | /** 24 | * Given cryptographic key material and a context string, services a subkey of 25 | * any length. See {@link https://docs.rs/blake3/0.1.3/blake3/fn.derive_key.html} 26 | * for more information. 27 | */ 28 | export function deriveKey( 29 | context: HashInput, 30 | material: HashInput, 31 | { length = defaultHashLength }: IBaseHashOptions = {}, 32 | ) { 33 | const derive = createDeriveKey(context); 34 | derive.update(inputToArray(material)); 35 | const digest = derive.digest({ length }); 36 | derive.dispose(); 37 | return digest; 38 | } 39 | 40 | /** 41 | * The keyed hash function. See {@link https://docs.rs/blake3/0.1.3/blake3/fn.keyed_hash.html}. 42 | */ 43 | export function keyedHash( 44 | key: HashInput, 45 | input: HashInput, 46 | { length = defaultHashLength }: IBaseHashOptions = {}, 47 | ) { 48 | const keyed = createKeyed(inputToArray(key)); 49 | keyed.update(inputToArray(input)); 50 | const digest = keyed.digest({ length }); 51 | keyed.dispose(); 52 | return digest; 53 | } 54 | -------------------------------------------------------------------------------- /blake3-internal/src/hash-reader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The maximum number of bytes that can be read from the hash. 3 | * 4 | * Calculated out 2^64-1, since `Xn` syntax (for `Xn ** Yn`) requires TS 5 | * targeting esnext/es2020 which includes features that Node 10 doesn't 6 | * yet supported. 7 | */ 8 | export const maxHashBytes = 2n ** 64n - 1n; 9 | 10 | /** 11 | * The HashReader is a type returned from any of the hash functions. 12 | * 13 | * You can use it as an iterator, but note that slices returned from the 14 | * iteration cannot be used outside. of where they were returned from. 15 | */ 16 | export interface IHashReader extends Iterable> { 17 | /** 18 | * Returns the position of the reader in the hash. Can be written to to seek. 19 | */ 20 | position: bigint; 21 | 22 | /** 23 | * Reads data from the hash into the target array. The target will always 24 | * be completely filled with data, unless the end of the hash bytes is 25 | * reached. The number of written bytes is returned. 26 | */ 27 | readInto(target: Uint8Array): number; 28 | 29 | /** 30 | * Reads and returns the given number of bytes from the hash, advancing 31 | * the position of the reader. 32 | */ 33 | read(bytes: number): T; 34 | 35 | /** 36 | * Returns a view of the given number of bytes from the reader. The view can 37 | * be used synchronously, but must not be reused later. This is more 38 | * efficient when using the webassembly version of the module. 39 | * 40 | * Fewer bytes may be returned than requested, if the number is large (>1MB). 41 | */ 42 | view(bytes: number): Readonly; 43 | } 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | rs/*/target 2 | **/*.rs.bk 3 | Cargo.lock 4 | 5 | 6 | # Created by https://www.gitignore.io/api/node 7 | 8 | ### Node ### 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # parcel-bundler cache (https://parceljs.org/) 69 | .cache 70 | 71 | # next.js build output 72 | .next 73 | 74 | # nuxt.js build output 75 | .nuxt 76 | 77 | # vuepress build output 78 | .vuepress/dist 79 | 80 | # Serverless directories 81 | .serverless 82 | 83 | 84 | # End of https://www.gitignore.io/api/node 85 | 86 | /*.tgz 87 | /*.cpuprofile 88 | /targets.json 89 | /*/build 90 | /*/dist 91 | /*/esm 92 | /blake3-native/c 93 | -------------------------------------------------------------------------------- /blake3-wasm/src/wasm/blake3.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | void EMSCRIPTEN_KEEPALIVE hash_oneshot(const void *input, size_t input_len, 7 | void *out, size_t out_len) { 8 | blake3_hasher h; 9 | blake3_hasher_init(&h); 10 | blake3_hasher_update(&h, input, input_len); 11 | blake3_hasher_finalize_seek(&h, 0, out, out_len); 12 | } 13 | 14 | blake3_hasher *EMSCRIPTEN_KEEPALIVE clone_hasher(blake3_hasher *h) { 15 | blake3_hasher *h2 = malloc(sizeof(blake3_hasher)); 16 | *h2 = *h; 17 | return h2; 18 | } 19 | 20 | blake3_hasher *EMSCRIPTEN_KEEPALIVE hasher_init(int x) { 21 | blake3_hasher *h = malloc(sizeof(blake3_hasher)); 22 | blake3_hasher_init(h); 23 | return h; 24 | } 25 | 26 | blake3_hasher *EMSCRIPTEN_KEEPALIVE hasher_init_keyed(const uint8_t *key) { 27 | blake3_hasher *h = malloc(sizeof(blake3_hasher)); 28 | blake3_hasher_init_keyed(h, key); 29 | return h; 30 | } 31 | 32 | blake3_hasher *EMSCRIPTEN_KEEPALIVE hasher_init_derive(const void *context, 33 | size_t context_len) { 34 | blake3_hasher *h = malloc(sizeof(blake3_hasher)); 35 | blake3_hasher_init_derive_key_raw(h, context, context_len); 36 | return h; 37 | } 38 | 39 | void EMSCRIPTEN_KEEPALIVE hasher_update(blake3_hasher *h, const void *input, 40 | size_t input_len) { 41 | blake3_hasher_update(h, input, input_len); 42 | } 43 | 44 | void EMSCRIPTEN_KEEPALIVE hasher_read(blake3_hasher *h, uint32_t seek0, 45 | uint32_t seek1, void *out, 46 | size_t out_len) { 47 | // Bigint support in wasm is still experimental in supported Node.js versions. 48 | // https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-pass-int64-t-and-uint64-t-values-from-js-into-wasm-functions 49 | uint64_t seek = (uint64_t)seek0 << 32 | (uint64_t)seek1; 50 | blake3_hasher_finalize_seek(h, seek, out, out_len); 51 | } 52 | -------------------------------------------------------------------------------- /blake3-wasm/src/browser/hash-instance.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getWasm, 3 | HashInput, 4 | HashRaw, 5 | IBaseHashOptions, 6 | inputToArray, 7 | WasmHasher, 8 | } from '@c4312/blake3-internal'; 9 | import { BrowserEncoding, mustGetEncoder } from './encoding.js'; 10 | import { Hash } from './hash.js'; 11 | 12 | /** 13 | * @inheritdoc 14 | */ 15 | export class BrowserHasher extends WasmHasher { 16 | protected alloc(n: number): Hash { 17 | return new Hash(n); 18 | } 19 | 20 | /** 21 | * @inheritdoc 22 | * @override 23 | */ 24 | public update(data: HashInput): this { 25 | return super.update(inputToArray(data)); 26 | } 27 | 28 | /** 29 | * Returns a digest of the hash with the given encoding. 30 | */ 31 | public digest(options?: IBaseHashOptions): Hash; 32 | public digest(encoding: undefined, options: IBaseHashOptions): Hash; 33 | public digest(encoding: BrowserEncoding, options?: IBaseHashOptions): string; 34 | public digest( 35 | encoding?: IBaseHashOptions | BrowserEncoding, 36 | options?: IBaseHashOptions, 37 | ): string | Hash { 38 | let resolvedOpts: IBaseHashOptions | undefined; 39 | let resolvedEnc: BrowserEncoding | undefined; 40 | if (encoding && typeof encoding === 'object') { 41 | resolvedOpts = encoding; 42 | resolvedEnc = undefined; 43 | } else { 44 | resolvedOpts = options; 45 | resolvedEnc = encoding; 46 | } 47 | 48 | const result = super.digest(resolvedOpts); 49 | return resolvedEnc ? mustGetEncoder(resolvedEnc)(result) : result; 50 | } 51 | } 52 | 53 | /** 54 | * A Node.js crypto-like createHash method. 55 | */ 56 | export const createHash = () => new BrowserHasher(getWasm(), HashRaw.default()); 57 | 58 | /** 59 | * A Node.js crypto-like createHash method. 60 | */ 61 | export const createKeyed = (key: HashInput) => 62 | new BrowserHasher(getWasm(), HashRaw.keyed(inputToArray(key))); 63 | 64 | /** 65 | * Construct a new Hasher for the key derivation function. 66 | */ 67 | export const createDeriveKey = (context: HashInput) => 68 | new BrowserHasher(getWasm(), HashRaw.derive(inputToArray(context))); 69 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 2 | ARG VARIANT=16-bullseye 3 | FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | # Install Rust 6 | RUN su node -c "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y" 7 | ENV PATH="/home/node/.cargo/bin:${PATH}" 8 | RUN su node -c "cargo install wasm-pack --version 0.9.1" 9 | 10 | 11 | # Enable Playwright https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#running-puppeteer-in-docker 12 | RUN apt-get update \ 13 | && apt-get install -y wget gnupg \ 14 | && wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 15 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 16 | && apt-get update \ 17 | && apt-get install -y google-chrome-stable fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf libxss1 \ 18 | --no-install-recommends \ 19 | && rm -rf /var/lib/apt/lists/* 20 | ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true 21 | 22 | # Install Emscripten https://emscripten.org/docs/getting_started/downloads.html#sdk-download-and-install 23 | RUN su node -c "git clone https://github.com/emscripten-core/emsdk.git /home/node/emsdk \ 24 | && cd /home/node/emsdk \ 25 | && ./emsdk install latest \ 26 | && ./emsdk activate latest" 27 | RUN su node -c "echo \"source /home/node/emsdk/emsdk_env.sh\" >> /home/node/.bashrc" 28 | 29 | # Install 30 | RUN su node -c "cd /home/node && wget https://github.com/WebAssembly/binaryen/releases/download/version_110/binaryen-version_110-x86_64-linux.tar.gz -O binaryen.tar.gz && tar -xvf binaryen.tar.gz" 31 | ENV PATH="/home/node/binaryen-version_110/bin:${PATH}" 32 | 33 | # [Optional] Uncomment this section to install additional OS packages. 34 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 35 | # && apt-get -y install --no-install-recommends 36 | 37 | # [Optional] Uncomment if you want to install an additional version of node using nvm 38 | # ARG EXTRA_NODE_VERSION=10 39 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 40 | 41 | # [Optional] Uncomment if you want to install more global node modules 42 | # RUN su node -c "npm install -g " 43 | -------------------------------------------------------------------------------- /blake3-native/src/hash-instance.ts: -------------------------------------------------------------------------------- 1 | import { HashInput, IBaseHashOptions, IHasher, IHashReader, inputToArray } from '@c4312/blake3-internal'; 2 | import { Transform, TransformCallback } from 'stream'; 3 | 4 | //@ts-ignore 5 | import * as native from '../build/Release/blake3'; 6 | 7 | export const createHash = () => new NodeHash(native.createHash()); 8 | export const createKeyed = (key: HashInput) => new NodeHash(native.createKeyed(inputToArray(key))); 9 | export const createDeriveKey = (material: HashInput) => 10 | new NodeHash(native.createDeriveKey(inputToArray(material))); 11 | 12 | /** 13 | * @inheritdoc 14 | */ 15 | export class NodeHash extends Transform implements IHasher { 16 | constructor( 17 | private readonly hash: { 18 | update(data: Uint8Array): void; 19 | digest(length?: number): Buffer; 20 | reader(): IHashReader; 21 | }, 22 | ) { 23 | super(); 24 | } 25 | 26 | /** 27 | * @reader 28 | */ 29 | public reader() { 30 | return this.hash.reader(); 31 | } 32 | 33 | /** 34 | * @inheritdoc 35 | */ 36 | public update(data: HashInput, encoding?: BufferEncoding): this { 37 | this.hash.update( 38 | encoding && typeof data === 'string' ? Buffer.from(data, encoding) : inputToArray(data), 39 | ); 40 | return this; 41 | } 42 | 43 | /** 44 | * @inheritdoc 45 | */ 46 | public dispose(): void { 47 | // no-op 48 | } 49 | 50 | /** 51 | * @inheritdoc 52 | */ 53 | public digest(encoding?: IBaseHashOptions): Buffer; 54 | public digest(encoding: undefined, options: IBaseHashOptions): Buffer; 55 | public digest(encoding: BufferEncoding, options?: IBaseHashOptions): string; 56 | public digest( 57 | encoding?: IBaseHashOptions | BufferEncoding, 58 | options?: IBaseHashOptions, 59 | ): string | Buffer { 60 | let resolvedOpts: IBaseHashOptions | undefined; 61 | let resolvedEnc: BufferEncoding | undefined; 62 | if (encoding && typeof encoding === 'object') { 63 | resolvedOpts = encoding; 64 | resolvedEnc = undefined; 65 | } else { 66 | resolvedOpts = options; 67 | resolvedEnc = encoding; 68 | } 69 | 70 | const result = this.hash.digest(resolvedOpts?.length); 71 | return resolvedEnc ? result.toString(resolvedEnc) : result; 72 | } 73 | 74 | /** 75 | * @inheritdoc 76 | * @hidden 77 | */ 78 | override _transform(chunk: Buffer | string, encoding: string, callback: TransformCallback): void { 79 | this.update(chunk, encoding as BufferEncoding); 80 | callback(); 81 | } 82 | 83 | /** 84 | * @inheritdoc 85 | * @hidden 86 | */ 87 | override _flush(callback: TransformCallback): void { 88 | callback(null, this.digest()); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.0.0 - 2022-10-08 4 | 5 | - **breaking:** Node 16, or a modern browser, is required 6 | - **breaking:** native module is now built post-download with node-gyp, removing most build headaches and allowing architecture-specific optimizations. You do therefore need [node-gyp dependencies](https://github.com/nodejs/node-gyp#installation) to use the native module. (However, it automatically falls back to wasm if node-gyp building fails) 7 | - **breaking**: _all_ consumers must await `blake3.load()` before using hash functions 8 | - calling `dispose()` on hash instances is now optional 9 | - blake3 now uses SIMD operations both for native hashing and in WebAssembly 10 | 11 | ## 2.1.7 - 2021-11-10 12 | 13 | - fix publishing failure in 2.1.6 14 | 15 | ## 2.1.6 - 2021-11-10 16 | 17 | - build for node 16 18 | 19 | ## 2.1.5 - 2020-11-28 20 | 21 | - fix version matching on postinstall 22 | - build for node 15 23 | 24 | ## 2.1.4 - 2020-05-11 25 | 26 | - add support for Node 14 27 | - validate native bindings before switching to them 28 | 29 | ## 2.1.3 - 2020-03-06 30 | 31 | - update to BLAKE3@0.2, providing significant speed improvements for native Node 32 | - fixed missing file causing installation of native Node extensions to fail 33 | 34 | ## 2.1.2 - 2020-03-01 35 | 36 | - fix missing createKeyed function in browsers 37 | 38 | ## 2.1.1 - 2020-02-11 39 | 40 | - allow importing the module without a bundler or with tools like Parcel 41 | 42 | ## 2.1.0 - 2020-01-23 43 | 44 | - add keyed hash and key derivation support (fixes [#2](https://github.com/connor4312/blake3/issues/2), [#9](https://github.com/connor4312/blake3/issues/9)) 45 | - fixed a bug in hex encoding in the browser 46 | 47 | ## 2.0.1 - 2020-01-23 48 | 49 | - fix browser bundle to use pure es modules (fixes [#8](https://github.com/connor4312/blake3/issues/8)) 50 | 51 | ## 2.0.0 - 2020-01-19 52 | 53 | - **breaking** the simple `hash` function no longer takes an encoding in its second parameter. Use `hash(data).toString()` instead. 54 | - allow configuring the hash length in `hash()` and `hasher.digest()` 55 | - add `using()` helper to deal with disposables 56 | - add `dispose: boolean` option to Hashers' `.digest()` methods, so that you can read a head while continuing to update it 57 | - add `hasher.reader()` method to retrieve a hash reader object, which allows seeking through and reading arbitrary amounts of data from the hash 58 | - add helper methods for encoding and constant-time equality checking in the returned Hash within the browser 59 | 60 | ## 1.2.0 - 2020-01-14 61 | 62 | - add native Node.js bindings 63 | 64 | ## 1.2.0-0 - 2020-01-14 (prerelease) 65 | 66 | - add native Node.js bindings 67 | 68 | ## 1.1.0 - 2020-01-12 69 | 70 | - add support for usage in browsers 71 | - fix potential memory unsafety in WebAssembly land 72 | - add support for output encoding in `Hash.digest([encoding])` 73 | 74 | ## 1.0.0 - 2020-01-09 75 | 76 | Initial release 77 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TARGETS=nodejs browser web 2 | MODE=dev 3 | 4 | BLAKE3_SRC = blake3-src/c/blake3.c blake3-src/c/blake3_dispatch.c blake3-src/c/blake3_portable.c blake3-src/c/blake3_sse41.c 5 | 6 | EM_WASM_SRC = blake3-wasm/src/wasm/blake3.c $(BLAKE3_SRC) 7 | EM_WASM_OUT = blake3-wasm/dist/wasm/blake3.js blake3-wasm/esm/wasm/blake3.mjs 8 | 9 | BINDING_CONFIG = blake3-native/build/Makefile 10 | BINDING_SRC = $(wildcard blake3-native/src/*.cc) $(BLAKE3_SRC) $(BINDING_CONFIG) 11 | BINDING_SRC_CP = blake3-native/c 12 | BINDING_OUT = blake3-native/build/Release/blake3.node 13 | 14 | TS_INTERNAL_SRC = $(wildcard blake3-internal/src/*.ts) 15 | TS_INTERNAL_OUT = blake3-internal/dist/index.js 16 | 17 | TS_WASM_SRC = $(wildcard blake3-wasm/src/*.ts) $(TS_INTERNAL_OUT) 18 | TS_WASM_OUT = blake3-wasm/dist/browser/index.js 19 | 20 | TS_NATIVE_SRC = $(wildcard blake3-native/src/*.ts) $(TS_INTERNAL_OUT) 21 | TS_NATIVE_OUT = blake3-native/dist/index.js 22 | 23 | TS_ROOT_SRC = $(wildcard blake3/*.cts) $(wildcard blake3/*.mts) $(TS_WASM_OUT) $(TS_NATIVE_OUT) 24 | TS_ROOT_OUT = blake3/index.cjs 25 | 26 | ALL_OUT = $(EM_WASM_OUT) $(BINDING_OUT) $(TS_ROOT_OUT) 27 | 28 | define wasm-compile = 29 | emcc -O3 -msimd128 -msse4.1 $^ -o $@ \ 30 | -sEXPORTED_FUNCTIONS=_malloc,_free -sEXPORTED_RUNTIME_METHODS=ccall -Iblake3-src/c -sMODULARIZE -s 'EXPORT_NAME="createMyModule"' \ 31 | -sASSERTIONS=0 --profiling \ 32 | -DIS_WASM -DBLAKE3_NO_AVX512 -DBLAKE3_NO_SSE2 -DBLAKE3_NO_AVX2 33 | endef 34 | 35 | all: $(ALL_OUT) 36 | 37 | blake3-wasm/dist/wasm/blake3.js: $(EM_WASM_SRC) 38 | mkdir -p $(dir $@) 39 | $(wasm-compile) 40 | 41 | blake3-wasm/esm/wasm/blake3.mjs: $(EM_WASM_SRC) 42 | mkdir -p $(dir $@) 43 | $(wasm-compile) 44 | 45 | $(BINDING_CONFIG): 46 | cd blake3-native && node-gyp configure 47 | 48 | $(BINDING_SRC_CP): $(BINDING_SRC) 49 | cp -R blake3-src/c blake3-native 50 | 51 | $(BINDING_OUT): $(BINDING_SRC_CP) 52 | cd blake3-native && node-gyp build 53 | 54 | $(TS_INTERNAL_OUT): $(TS_INTERNAL_SRC) 55 | cd blake3-internal && ../node_modules/.bin/tsc -p tsconfig.json 56 | cd blake3-internal && ../node_modules/.bin/tsc -p tsconfig.esm.json 57 | 58 | $(TS_WASM_OUT): $(TS_WASM_SRC) 59 | cd blake3-wasm && ../node_modules/.bin/tsc -p tsconfig.json 60 | cd blake3-wasm && ../node_modules/.bin/tsc -p tsconfig.esm.json 61 | 62 | $(TS_NATIVE_OUT): $(TS_NATIVE_SRC) 63 | cd blake3-native && ../node_modules/.bin/tsc -p tsconfig.json 64 | cd blake3-native && ../node_modules/.bin/tsc -p tsconfig.esm.json 65 | 66 | $(TS_ROOT_OUT): $(TS_ROOT_SRC) 67 | cd blake3 && ../node_modules/.bin/tsc -p tsconfig.json 68 | cd blake3 && ../node_modules/.bin/tsc -p tsconfig.esm.json 69 | 70 | test: $(ALL_OUT) 71 | cd blake3 && node ../node_modules/.bin/mocha "*.test.cjs" --timeout 5000 $(TEST_ARGS) 72 | 73 | clean: 74 | rm -rf ./*/build ./*/dist ./*/esm 75 | 76 | fmt: 77 | node ./node_modules/.bin/remark readme.md -f -o readme.md 78 | node ./node_modules/.bin/prettier --write "ts/**/*.ts" "*.md" 79 | 80 | pack: 81 | npm pack -w blake3 -w blake3-native -w blake3-internal -w blake3-wasm 82 | 83 | .PHONY: all clean test fmt get-native pack 84 | -------------------------------------------------------------------------------- /.devcontainer/base.Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 2 | ARG VARIANT=16-bullseye 3 | FROM node:${VARIANT} 4 | 5 | # [Option] Install zsh 6 | ARG INSTALL_ZSH="true" 7 | # [Option] Upgrade OS packages to their latest versions 8 | ARG UPGRADE_PACKAGES="true" 9 | 10 | # Install needed packages, yarn, nvm and setup non-root user. Use a separate RUN statement to add your own dependencies. 11 | ARG USERNAME=node 12 | ARG USER_UID=1000 13 | ARG USER_GID=$USER_UID 14 | ARG NPM_GLOBAL=/usr/local/share/npm-global 15 | ENV NVM_DIR=/usr/local/share/nvm 16 | ENV NVM_SYMLINK_CURRENT=true \ 17 | PATH=${NPM_GLOBAL}/bin:${NVM_DIR}/current/bin:${PATH} 18 | COPY library-scripts/*.sh library-scripts/*.env /tmp/library-scripts/ 19 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 20 | # Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131 21 | && apt-get purge -y imagemagick imagemagick-6-common \ 22 | # Install common packages, non-root user, update yarn and install nvm 23 | && bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" \ 24 | # Install yarn, nvm 25 | && rm -rf /opt/yarn-* /usr/local/bin/yarn /usr/local/bin/yarnpkg \ 26 | && bash /tmp/library-scripts/node-debian.sh "${NVM_DIR}" "none" "${USERNAME}" \ 27 | # Configure global npm install location, use group to adapt to UID/GID changes 28 | && if ! cat /etc/group | grep -e "^npm:" > /dev/null 2>&1; then groupadd -r npm; fi \ 29 | && usermod -a -G npm ${USERNAME} \ 30 | && umask 0002 \ 31 | && mkdir -p ${NPM_GLOBAL} \ 32 | && touch /usr/local/etc/npmrc \ 33 | && chown ${USERNAME}:npm ${NPM_GLOBAL} /usr/local/etc/npmrc \ 34 | && chmod g+s ${NPM_GLOBAL} \ 35 | && npm config -g set prefix ${NPM_GLOBAL} \ 36 | && sudo -u ${USERNAME} npm config -g set prefix ${NPM_GLOBAL} \ 37 | # Install eslint 38 | && su ${USERNAME} -c "umask 0002 && npm install -g eslint" \ 39 | && npm cache clean --force > /dev/null 2>&1 \ 40 | # Install python-is-python3 on bullseye to prevent node-gyp regressions 41 | && . /etc/os-release \ 42 | && if [ "${VERSION_CODENAME}" = "bullseye" ]; then apt-get -y install --no-install-recommends python-is-python3; fi \ 43 | # Clean up 44 | && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /root/.gnupg /tmp/library-scripts 45 | 46 | # [Optional] Uncomment this section to install additional OS packages. 47 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 48 | # && apt-get -y install --no-install-recommends 49 | 50 | # [Optional] Uncomment if you want to install an additional version of node using nvm 51 | # ARG EXTRA_NODE_VERSION=10 52 | # RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" 53 | 54 | # [Optional] Uncomment if you want to install more global node modules 55 | # RUN su node -c "npm install -g "" -------------------------------------------------------------------------------- /blake3-wasm/src/node/hash-instance.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getWasm, 3 | HashInput, 4 | HashRaw, 5 | IBaseHashOptions, 6 | IHasher, 7 | inputToArray, 8 | WasmHasher, 9 | } from '@c4312/blake3-internal'; 10 | import { Transform, TransformCallback } from 'stream'; 11 | 12 | export interface INodeHash extends IHasher { 13 | /** 14 | * @inheritdoc 15 | * @override 16 | */ 17 | update(data: HashInput, encoding?: BufferEncoding): this; 18 | 19 | /** 20 | * @inheritdoc 21 | * @override 22 | */ 23 | digest(options?: IBaseHashOptions): Buffer; 24 | 25 | /** 26 | * Returns a digest of the hash with the given set of hash options. 27 | */ 28 | digest(encoding: undefined, options: IBaseHashOptions): Buffer; 29 | 30 | /** 31 | * Returns a digest of the hash with the given encoding. 32 | */ 33 | digest(encoding: BufferEncoding, options?: IBaseHashOptions): string; 34 | } 35 | 36 | class BufferHash extends WasmHasher { 37 | protected alloc(n: number): Buffer { 38 | return Buffer.allocUnsafe(n); 39 | } 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | export class NodeHash extends Transform implements IHasher { 46 | private readonly hash: BufferHash; 47 | 48 | constructor(wasmModule: any, hasher: HashRaw) { 49 | super(); 50 | this.hash = new BufferHash(wasmModule, hasher); 51 | } 52 | 53 | /** 54 | * @reader 55 | */ 56 | public reader() { 57 | return this.hash.reader(); 58 | } 59 | 60 | /** 61 | * @inheritdoc 62 | */ 63 | public update(data: HashInput, encoding?: BufferEncoding): this { 64 | this.hash.update( 65 | encoding && typeof data === 'string' ? Buffer.from(data, encoding) : inputToArray(data), 66 | ); 67 | return this; 68 | } 69 | 70 | /** 71 | * @inheritdoc 72 | */ 73 | public dispose(): void { 74 | this.hash.dispose(); 75 | } 76 | 77 | /** 78 | * @inheritdoc 79 | */ 80 | public digest(encoding?: IBaseHashOptions): Buffer; 81 | public digest(encoding: undefined, options: IBaseHashOptions): Buffer; 82 | public digest(encoding: BufferEncoding, options?: IBaseHashOptions): string; 83 | public digest( 84 | encoding?: IBaseHashOptions | BufferEncoding, 85 | options?: IBaseHashOptions, 86 | ): string | Buffer { 87 | let resolvedOpts: IBaseHashOptions | undefined; 88 | let resolvedEnc: BufferEncoding | undefined; 89 | if (encoding && typeof encoding === 'object') { 90 | resolvedOpts = encoding; 91 | resolvedEnc = undefined; 92 | } else { 93 | resolvedOpts = options; 94 | resolvedEnc = encoding; 95 | } 96 | 97 | const result = this.hash.digest(resolvedOpts); 98 | return resolvedEnc ? result.toString(resolvedEnc) : result; 99 | } 100 | 101 | /** 102 | * @inheritdoc 103 | * @hidden 104 | */ 105 | override _transform(chunk: Buffer | string, encoding: string, callback: TransformCallback): void { 106 | this.update(chunk, encoding as BufferEncoding); 107 | callback(); 108 | } 109 | 110 | /** 111 | * @inheritdoc 112 | * @hidden 113 | */ 114 | override _flush(callback: TransformCallback): void { 115 | callback(null, this.digest()); 116 | } 117 | } 118 | 119 | /** 120 | * A Node.js crypto-like createHash method. 121 | */ 122 | export const createHash = () => new NodeHash(getWasm(), HashRaw.default()); 123 | 124 | /** 125 | * Construct a new Hasher for the keyed hash function. 126 | */ 127 | export const createKeyed = (key: HashInput) => 128 | new NodeHash(getWasm(), HashRaw.keyed(inputToArray(key))); 129 | 130 | /** 131 | * Construct a new Hasher for the key derivation function. 132 | */ 133 | export const createDeriveKey = (context: HashInput) => 134 | new NodeHash(getWasm(), HashRaw.derive(inputToArray(context))); 135 | -------------------------------------------------------------------------------- /blake3-internal/src/wasm-types.ts: -------------------------------------------------------------------------------- 1 | export interface WasmModule { 2 | HEAPU8: Uint8Array; 3 | _free(addr: number): void; 4 | _malloc(bytes: number): number; 5 | ccall(...args: unknown[]): T; 6 | } 7 | 8 | let globalWasm: WasmModule | undefined; 9 | 10 | export const setWasm = (m: WasmModule) => (globalWasm = m); 11 | 12 | export const getWasm = () => { 13 | if (!globalWasm) { 14 | throw new Error(`Make sure to await blake3.load() before trying to use any functions`); 15 | } 16 | 17 | return globalWasm; 18 | }; 19 | 20 | const memoryDeallocator = new FinalizationRegistry<{ 21 | wasm: WasmModule; 22 | address: number; 23 | }>((v) => v.wasm._free(v.address)); 24 | 25 | export const MAX_SCRATCH_SIZE = 1024 * 1024; 26 | 27 | class Scratch { 28 | private addr?: number; 29 | private scratchSize = 32; 30 | 31 | public get size() { 32 | return this.scratchSize; 33 | } 34 | 35 | grow(n: number): number { 36 | let grew = false; 37 | while (this.scratchSize < n && this.scratchSize < MAX_SCRATCH_SIZE) { 38 | this.scratchSize <<= 1; 39 | grew = true; 40 | } 41 | 42 | if (!grew && this.addr !== undefined) { 43 | return this.addr; 44 | } 45 | 46 | const wasm = getWasm(); 47 | if (this.addr !== undefined) { 48 | memoryDeallocator.unregister(this); 49 | wasm._free(this.addr); 50 | } 51 | 52 | return (this.addr = wasm._malloc(this.scratchSize)); 53 | } 54 | } 55 | 56 | export const scratch = new Scratch(); 57 | 58 | export class HashRaw { 59 | public static default() { 60 | return new HashRaw(getWasm().ccall('hasher_init', 'number', [], [])); 61 | } 62 | 63 | public static keyed(key: Uint8Array | Uint8ClampedArray) { 64 | if (key.byteLength !== 32) { 65 | throw new RangeError(`BLAKE3 key must be exactly 32 bytes long`); 66 | } 67 | const wasm = getWasm(); 68 | const saddr = scratch.grow(32); 69 | wasm.HEAPU8.set(key, saddr); 70 | return new HashRaw(wasm.ccall('hasher_init_keyed', 'number', ['number'], [saddr])); 71 | } 72 | 73 | public static derive(key: Uint8Array) { 74 | const wasm = getWasm(); 75 | const saddr = scratch.grow(key.byteLength); 76 | wasm.HEAPU8.set(key, saddr); 77 | return new HashRaw( 78 | wasm.ccall('hasher_init_derive', 'number', ['number', 'number'], [saddr, key.byteLength]), 79 | ); 80 | } 81 | 82 | private addr?: { wasm: WasmModule; address: number }; 83 | public readonly scratch = scratch; 84 | 85 | constructor(private hasher: number) { 86 | this.addr = { wasm: getWasm(), address: hasher }; 87 | memoryDeallocator.register(this, this.addr, this); 88 | } 89 | 90 | update(address: number, length: number) { 91 | if (!this.addr) { 92 | throw new Error('Cannot use a hash after disposing it'); 93 | } 94 | 95 | this.addr.wasm.ccall( 96 | 'hasher_update', 97 | null, 98 | ['number', 'number', 'number'], 99 | [this.hasher, address, length], 100 | ); 101 | } 102 | 103 | read(seek: bigint, address: number, length: number) { 104 | if (!this.addr) { 105 | throw new Error('Cannot use a hash after disposing it'); 106 | } 107 | 108 | this.addr.wasm.ccall( 109 | 'hasher_read', 110 | null, 111 | ['number', 'number', 'number', 'number', 'number'], 112 | [this.hasher, Number(seek >> 32n), Number(seek & 0xffffffffn), address, length], 113 | ); 114 | } 115 | 116 | clone() { 117 | if (!this.addr) { 118 | throw new Error('Cannot use a hash after disposing it'); 119 | } 120 | 121 | const addr = this.addr.wasm.ccall('clone_hasher', null, ['number'], [this.hasher]); 122 | return new HashRaw(addr); 123 | } 124 | 125 | public dispose() { 126 | if (this.addr) { 127 | memoryDeallocator.unregister(this); 128 | this.addr.wasm._free(this.addr.address); 129 | this.addr = undefined; 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /blake3-internal/src/hash-instance.ts: -------------------------------------------------------------------------------- 1 | import { defaultHashLength, HashInput, IBaseHashOptions, inputToArray } from './hash-fn.js'; 2 | import { IHashReader, maxHashBytes } from './hash-reader.js'; 3 | import { HashRaw, WasmModule } from './wasm-types.js'; 4 | 5 | /** 6 | * A blake3 hash. Quite similar to Node's crypto hashing. 7 | * 8 | * Note that you must call {@link IHash#dispose} or {@link IHash#done} when 9 | * you're finished with it to free memory. 10 | */ 11 | export interface IHasher { 12 | /** 13 | * Adds the given data to the hash. 14 | * @throws {Error} if {@link IHash#digest} has already been called. 15 | */ 16 | update(data: HashInput): this; 17 | 18 | /** 19 | * Returns a digest of the hash. 20 | * 21 | * If `dispose: false` is given in the options, the hash will not 22 | * automatically be disposed of, allowing you to continue updating 23 | * it after obtaining the current reader. 24 | */ 25 | digest(options?: IBaseHashOptions): T; 26 | 27 | /** 28 | * Returns a {@link HashReader} for the current hash. 29 | * 30 | * If `dispose: false` is given in the options, the hash will not 31 | * automatically be disposed of, allowing you to continue updating 32 | * it after obtaining the current reader. 33 | */ 34 | reader(): IHashReader; 35 | 36 | /** 37 | * Frees memory associated with the hasher. As of 3.x, this is not 38 | * required to be call since we do so in a finalizer. However, if hashing 39 | * large amounts of data synchronously, it may be necessary. 40 | */ 41 | dispose(): void; 42 | } 43 | 44 | export abstract class WasmHasher implements IHasher { 45 | private tookReader = false; 46 | 47 | /** @inhernal */ 48 | constructor(private readonly wasmModule: WasmModule, private hash: HashRaw) {} 49 | 50 | /** 51 | * Allocates a new binary array of the return type. 52 | */ 53 | protected abstract alloc(n: number): Binary; 54 | 55 | /** @inheritdoc */ 56 | public update(data: HashInput): this { 57 | // reuse of the hasher is allowed after digesting, but we don't want to 58 | // change any readers that were already taken. So clone the hash if the 59 | // user ends up doing this. 60 | if (this.tookReader) { 61 | this.hash = this.hash.clone(); 62 | this.tookReader = false; 63 | } 64 | 65 | const arr = inputToArray(data); 66 | const saddr = this.hash.scratch.grow(arr.byteLength); 67 | const step = this.hash.scratch.size; 68 | for (let i = 0; i < arr.byteLength; i += step) { 69 | this.wasmModule.HEAPU8.set(arr.subarray(i, Math.min(arr.length, i + step)), saddr); 70 | this.hash.update(saddr, arr.byteLength); 71 | } 72 | return this; 73 | } 74 | 75 | /** @inheritdoc */ 76 | public digest({ length = defaultHashLength }: IBaseHashOptions = {}): Binary { 77 | const out = this.alloc(length); 78 | const saddr = this.hash.scratch.grow(length); 79 | const step = this.hash.scratch.size; 80 | for (let i = 0; i < length; i += step) { 81 | const n = Math.min(length - i, step); 82 | this.hash.read(0n, saddr, n); 83 | out.set(this.wasmModule.HEAPU8.subarray(saddr, saddr + n), i); 84 | } 85 | 86 | return out; 87 | } 88 | 89 | /** @inheritdoc */ 90 | public reader(): IHashReader { 91 | const hash = this.hash; 92 | this.tookReader = true; 93 | let position = 0n; 94 | 95 | const reader: IHashReader = { 96 | get position() { 97 | return position; 98 | }, 99 | set position(value) { 100 | if (value > maxHashBytes || value < 0n) { 101 | throw new RangeError(`Hash reader position must be within [0, ${maxHashBytes}]`); 102 | } 103 | position = value; 104 | }, 105 | readInto: (target) => { 106 | const remaining = Number(maxHashBytes - position); 107 | 108 | const length = remaining > target.byteLength ? target.byteLength : remaining; 109 | const saddr = this.hash.scratch.grow(length); 110 | const step = this.hash.scratch.size; 111 | for (let i = 0; i < length; i += step) { 112 | const n = Math.min(length - i, step); 113 | this.hash.read(position, saddr, n); 114 | position += BigInt(n); 115 | target.set(this.wasmModule.HEAPU8.subarray(saddr, saddr + n), i); 116 | } 117 | 118 | return length; 119 | }, 120 | read: (bytes) => { 121 | bytes = Math.min(bytes, Number(maxHashBytes - position)); 122 | const out = this.alloc(bytes); 123 | const saddr = hash.scratch.grow(bytes); 124 | const step = hash.scratch.size; 125 | for (let i = 0; i < bytes; i += step) { 126 | const n = Math.min(bytes - i, step); 127 | hash.read(position, saddr, n); 128 | position += BigInt(n); 129 | out.set(this.wasmModule.HEAPU8.subarray(saddr, saddr + n), i); 130 | } 131 | 132 | return out; 133 | }, 134 | view: (bytes) => { 135 | bytes = Math.min(bytes, Number(maxHashBytes - position)); 136 | const saddr = hash.scratch.grow(bytes); 137 | hash.read(position, saddr, Math.min(bytes, hash.scratch.size)); 138 | position += BigInt(bytes); 139 | return this.wasmModule.HEAPU8.subarray(saddr, saddr + bytes); 140 | }, 141 | *[Symbol.iterator]() { 142 | const stepSize = 1024n; 143 | while (position < maxHashBytes) { 144 | const bytes = maxHashBytes - stepSize < stepSize ? maxHashBytes - stepSize : stepSize; 145 | yield reader.view(Number(bytes)); 146 | } 147 | }, 148 | }; 149 | 150 | return reader; 151 | } 152 | 153 | public dispose() { 154 | this.hash.dispose(); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /.github/workflows/build-neon.yml: -------------------------------------------------------------------------------- 1 | name: Generate Binaries 2 | 'on': 3 | push: 4 | branches: 5 | - generate-binary 6 | jobs: 7 | build: 8 | name: Build 9 | runs-on: '${{ matrix.os }}' 10 | strategy: 11 | matrix: 12 | os: 13 | - macos-latest 14 | - ubuntu-latest 15 | - windows-latest 16 | steps: 17 | - uses: actions/checkout@master 18 | - run: mkdir dist 19 | - uses: actions-rs/toolchain@v1 20 | with: 21 | target: wasm32-unknown-unknown 22 | toolchain: nightly 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: v17.1.0 26 | - shell: powershell 27 | name: use npm 6 on node 15 28 | run: npm install -g npm@6 29 | if: matrix.os == 'windows-latest' 30 | - shell: powershell 31 | name: patch node-gyp for VS 2019 32 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 33 | if: matrix.os == 'windows-latest' 34 | - run: npm install neon-cli rimraf 35 | - run: ../node_modules/.bin/neon build --release 36 | working-directory: rs 37 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-102.node' 38 | - uses: actions/setup-node@v1 39 | with: 40 | node-version: v16.13.0 41 | - shell: powershell 42 | name: use npm 6 on node 15 43 | run: npm install -g npm@6 44 | if: matrix.os == 'windows-latest' 45 | - shell: powershell 46 | name: patch node-gyp for VS 2019 47 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 48 | if: matrix.os == 'windows-latest' 49 | - run: ./node_modules/.bin/rimraf rs/native/target 50 | - run: ../node_modules/.bin/neon build --release 51 | working-directory: rs 52 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-93.node' 53 | - uses: actions/setup-node@v1 54 | with: 55 | node-version: v15.14.0 56 | - shell: powershell 57 | name: use npm 6 on node 15 58 | run: npm install -g npm@6 59 | if: matrix.os == 'windows-latest' 60 | - shell: powershell 61 | name: patch node-gyp for VS 2019 62 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 63 | if: matrix.os == 'windows-latest' 64 | - run: ./node_modules/.bin/rimraf rs/native/target 65 | - run: ../node_modules/.bin/neon build --release 66 | working-directory: rs 67 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-88.node' 68 | - uses: actions/setup-node@v1 69 | with: 70 | node-version: v14.18.1 71 | - shell: powershell 72 | name: patch node-gyp for VS 2019 73 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 74 | if: matrix.os == 'windows-latest' 75 | - run: ./node_modules/.bin/rimraf rs/native/target 76 | - run: ../node_modules/.bin/neon build --release 77 | working-directory: rs 78 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-83.node' 79 | - uses: actions/setup-node@v1 80 | with: 81 | node-version: v13.14.0 82 | - shell: powershell 83 | name: patch node-gyp for VS 2019 84 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 85 | if: matrix.os == 'windows-latest' 86 | - run: ./node_modules/.bin/rimraf rs/native/target 87 | - run: ../node_modules/.bin/neon build --release 88 | working-directory: rs 89 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-79.node' 90 | - uses: actions/setup-node@v1 91 | with: 92 | node-version: v12.22.7 93 | - shell: powershell 94 | name: patch node-gyp for VS 2019 95 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 96 | if: matrix.os == 'windows-latest' 97 | - run: ./node_modules/.bin/rimraf rs/native/target 98 | - run: ../node_modules/.bin/neon build --release 99 | working-directory: rs 100 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-72.node' 101 | - uses: actions/setup-node@v1 102 | with: 103 | node-version: v11.15.0 104 | - shell: powershell 105 | name: patch node-gyp for VS 2019 106 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 107 | if: matrix.os == 'windows-latest' 108 | - run: ./node_modules/.bin/rimraf rs/native/target 109 | - run: ../node_modules/.bin/neon build --release 110 | working-directory: rs 111 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-67.node' 112 | - uses: actions/setup-node@v1 113 | with: 114 | node-version: v10.24.1 115 | - shell: powershell 116 | name: patch node-gyp for VS 2019 117 | run: "npm install --global node-gyp@latest\r\nnpm prefix -g | % {npm config set node_gyp \"$_\\node_modules\\node-gyp\\bin\\node-gyp.js\"}" 118 | if: matrix.os == 'windows-latest' 119 | - run: ./node_modules/.bin/rimraf rs/native/target 120 | - run: ../node_modules/.bin/neon build --release 121 | working-directory: rs 122 | - run: 'mv rs/native/index.node dist/${{ matrix.os }}-64.node' 123 | - uses: actions/upload-artifact@v1 124 | with: 125 | name: dist 126 | path: dist 127 | -------------------------------------------------------------------------------- /blake3/node.test.cts: -------------------------------------------------------------------------------- 1 | import * as native from '@c4312/blake3-native'; 2 | import * as wasm from 'blake3-wasm'; 3 | import { expect } from 'chai'; 4 | import { ReadableStreamBuffer } from 'stream-buffers'; 5 | import { IHashReader, maxHashBytes } from './index.cjs'; 6 | import { hello48, inputs, ogTestVectors } from './test-helpers.cjs'; 7 | 8 | function suite({ 9 | hash, 10 | createHash, 11 | keyedHash, 12 | deriveKey, 13 | createDeriveKey, 14 | createKeyed, 15 | }: typeof wasm | typeof native) { 16 | describe('encoding', () => { 17 | it('hashes a buffer', () => { 18 | expect(hash(Buffer.from(inputs.hello.input))).to.deep.equal(inputs.hello.hash); 19 | }); 20 | 21 | it('hashes a string', () => { 22 | expect(hash(inputs.hello.input)).to.deep.equal(inputs.hello.hash); 23 | }); 24 | 25 | it('hashes an arraybuffer', () => { 26 | const buf = Buffer.from(inputs.hello.input); 27 | expect(hash(new Uint8Array(buf).buffer)).to.deep.equal(inputs.hello.hash); 28 | }); 29 | 30 | it('customizes the output length', () => { 31 | expect(hash(inputs.hello.input, { length: 16 })).to.deep.equal( 32 | inputs.hello.hash.slice(0, 16), 33 | ); 34 | }); 35 | }); 36 | 37 | describe('memory-safety (#5)', () => { 38 | it('hash', () => { 39 | const hashA = hash('hello'); 40 | const hashB = hash('goodbye'); 41 | expect(hashA.toString('hex')).to.equal( 42 | 'ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f', 43 | ); 44 | expect(hashB.toString('hex')).to.equal( 45 | 'f94a694227c5f31a07551908ad5fb252f5f0964030df5f2f200adedfae4d9b69', 46 | ); 47 | }); 48 | 49 | it('hasher', () => { 50 | const hasherA = createHash(); 51 | const hasherB = createHash(); 52 | hasherA.update('hel'); 53 | hasherB.update('good'); 54 | hasherA.update('lo'); 55 | hasherB.update('bye'); 56 | 57 | const hashA = hasherA.digest(); 58 | const hashB = hasherB.digest(); 59 | expect(hashA.toString('hex')).to.equal( 60 | 'ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f', 61 | ); 62 | expect(hashB.toString('hex')).to.equal( 63 | 'f94a694227c5f31a07551908ad5fb252f5f0964030df5f2f200adedfae4d9b69', 64 | ); 65 | }); 66 | }); 67 | 68 | describe('hasher', () => { 69 | it('digests', (callback) => { 70 | const buffer = new ReadableStreamBuffer(); 71 | buffer.put(Buffer.from(inputs.large.input)); 72 | buffer.stop(); 73 | 74 | const hash = createHash(); 75 | 76 | buffer.on('data', (b) => hash.update(b)); 77 | buffer.on('end', () => { 78 | const actual = hash.digest(); 79 | expect(actual).to.deep.equal(inputs.large.hash); 80 | callback(); 81 | }); 82 | }); 83 | 84 | it('is a transform stream', (callback) => { 85 | const buffer = new ReadableStreamBuffer(); 86 | buffer.put(Buffer.from(inputs.large.input)); 87 | buffer.stop(); 88 | 89 | buffer 90 | .pipe(createHash()) 91 | .on('error', callback) 92 | .on('data', (hash) => { 93 | expect(hash).to.deep.equal(inputs.large.hash); 94 | callback(); 95 | }); 96 | }); 97 | 98 | it('customizes the output length', () => { 99 | const hash = createHash(); 100 | hash.update(inputs.hello.input); 101 | expect(hash.digest('hex', { length: 16 })).to.equal( 102 | inputs.hello.hash.slice(0, 16).toString('hex'), 103 | ); 104 | }); 105 | 106 | it('allows taking incremental hashes', () => { 107 | const hasher = createHash(); 108 | hasher.update('hel'); 109 | 110 | const hashA = hasher.digest(undefined); 111 | const readA = hasher.reader(); 112 | 113 | hasher.update('lo'); 114 | const hashB = hasher.digest(undefined); 115 | const readB = hasher.reader(); 116 | 117 | const expectedA = Buffer.from( 118 | '3121c5bb1b9193123447ac7cfda042f67f967e7a8cf5c12e7570e25529746e4a', 119 | 'hex', 120 | ); 121 | expect(hashA).to.deep.equal(expectedA); 122 | expect(readA.read(32)).to.deep.equal(expectedA); 123 | 124 | expect(hashB).to.deep.equal(inputs.hello.hash); 125 | expect(readB.read(32)).to.deep.equal(inputs.hello.hash); 126 | 127 | hasher.dispose(); 128 | }); 129 | }); 130 | 131 | describe('reader', () => { 132 | let reader: IHashReader; 133 | beforeEach(() => { 134 | const hash = createHash(); 135 | hash.update(inputs.hello.input); 136 | reader = hash.reader(); 137 | }); 138 | 139 | it('implements readInto() and advances', () => { 140 | const actual = Buffer.alloc(32); 141 | reader.readInto(actual.subarray(0, 10)); 142 | reader.readInto(actual.subarray(10)); 143 | expect(actual).to.deep.equal(inputs.hello.hash); 144 | expect(reader.position).to.equal(BigInt(32)); 145 | }); 146 | 147 | it('implements read() and advances', () => { 148 | const actual = reader.read(32); 149 | expect(actual).to.deep.equal(inputs.hello.hash); 150 | expect(reader.position).to.equal(BigInt(32)); 151 | 152 | const actualNext = reader.read(16); 153 | expect(actualNext).to.deep.equal(hello48.slice(32)); 154 | expect(reader.position).to.equal(BigInt(48)); 155 | }); 156 | 157 | it('manually sets position', () => { 158 | reader.position = BigInt(32); 159 | const actual = reader.read(16); 160 | expect(actual).to.deep.equal(hello48.slice(32)); 161 | }); 162 | 163 | it('throws if set out of range', () => { 164 | expect(() => (reader.position = BigInt(-1))).to.throw(RangeError); 165 | expect(() => (reader.position = BigInt('18446744073709551616'))).to.throw(RangeError); 166 | 167 | reader.position = maxHashBytes - BigInt(1); 168 | expect(reader.read(2)).to.have.lengthOf(1); 169 | expect(reader.read(2)).to.have.lengthOf(0); 170 | }); 171 | }); 172 | 173 | describe('original test vectors', () => { 174 | for (const { inputLen, expectedDerive, expectedKeyed, expectedHash } of ogTestVectors.cases) { 175 | describe(`${inputLen}`, async () => { 176 | const input = Buffer.alloc(inputLen); 177 | for (let i = 0; i < inputLen; i++) { 178 | input[i] = i % 251; 179 | } 180 | 181 | it('hash()', () => { 182 | expect(hash(input, { length: expectedHash.length / 2 }).toString('hex')).to.equal( 183 | expectedHash, 184 | ); 185 | }); 186 | 187 | it('deriveKey()', () => { 188 | expect( 189 | deriveKey(ogTestVectors.context, input, { length: expectedDerive.length / 2 }).toString( 190 | 'hex', 191 | ), 192 | ).to.equal(expectedDerive); 193 | }); 194 | 195 | it('createDeriveKey()', (callback) => { 196 | const buffer = new ReadableStreamBuffer(); 197 | buffer.put(Buffer.from(input)); 198 | buffer.stop(); 199 | const hash = createDeriveKey(ogTestVectors.context); 200 | buffer.on('data', (b) => hash.update(b)); 201 | buffer.on('end', () => { 202 | const actual = hash.digest({ length: expectedDerive.length / 2 }).toString('hex'); 203 | expect(actual).to.equal(expectedDerive); 204 | callback(); 205 | }); 206 | }); 207 | 208 | it('keyedHash()', () => { 209 | expect( 210 | keyedHash(Buffer.from(ogTestVectors.key), input, { 211 | length: expectedKeyed.length / 2, 212 | }).toString('hex'), 213 | ).to.equal(expectedKeyed); 214 | }); 215 | 216 | it('createKeyed()', (callback) => { 217 | const buffer = new ReadableStreamBuffer(); 218 | buffer.put(Buffer.from(input)); 219 | buffer.stop(); 220 | const hash = createKeyed(Buffer.from(ogTestVectors.key)); 221 | buffer.on('data', (b) => hash.update(b)); 222 | buffer.on('end', () => { 223 | const actual = hash.digest({ length: expectedDerive.length / 2 }).toString('hex'); 224 | expect(actual).to.equal(expectedKeyed); 225 | callback(); 226 | }); 227 | }); 228 | }); 229 | } 230 | }); 231 | } 232 | 233 | describe('node.js wasm', () => { 234 | before(async () => { 235 | await wasm.load(); 236 | }); 237 | suite(wasm); 238 | }); 239 | describe('node.js native', () => suite(native)); 240 | -------------------------------------------------------------------------------- /blake3/browser.test.cts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { mkdirSync, writeFileSync } from 'fs'; 3 | import { createServer, Server } from 'http'; 4 | import { AddressInfo } from 'net'; 5 | import { tmpdir } from 'os'; 6 | import { resolve } from 'path'; 7 | import { Browser, chromium, Page } from 'playwright'; 8 | import handler from 'serve-handler'; 9 | import webpack from 'webpack'; 10 | import { hello48, inputs, ogTestVectors } from './test-helpers.cjs'; 11 | 12 | // Much of the browser code is also used in Node's wasm. We test things more 13 | // thoroughly there because tests are easier to write and debug, these tests 14 | // are primarily for sanity and checking browser-specific behavior. 15 | describe('browser', () => { 16 | const addInputs = `window.inputs = ${JSON.stringify(inputs)}`; 17 | 18 | describe('webpack', () => { 19 | const testDir = resolve(tmpdir(), 'blake3-browser-test'); 20 | let server: Server; 21 | let browser: Browser; 22 | let page: Page; 23 | 24 | /** 25 | * Builds the browser lib into the testDir. 26 | */ 27 | async function buildWebpack() { 28 | try { 29 | mkdirSync(testDir); 30 | } catch { 31 | // already exists, probably 32 | } 33 | 34 | writeFileSync( 35 | resolve(testDir, 'entry-src.js'), 36 | `import("blake3/browser").then(b3 => b3.load().then(() => window.blake3 = b3));`, 37 | ); 38 | 39 | const stats = await new Promise((res, rej) => 40 | webpack( 41 | { 42 | mode: 'production', 43 | devtool: 'source-map', 44 | entry: resolve(testDir, 'entry-src.js'), 45 | output: { 46 | path: testDir, 47 | filename: 'main.js', 48 | }, 49 | resolve: { 50 | alias: { 51 | 'blake3/browser': resolve(__dirname, 'browser.mjs'), 52 | }, 53 | }, 54 | }, 55 | (err, stats) => (err ? rej(err) : res(stats!)), 56 | ), 57 | ); 58 | 59 | if (stats.hasErrors()) { 60 | throw stats.toString('errors-only'); 61 | } 62 | 63 | writeFileSync(resolve(testDir, 'index.html'), ``); 64 | } 65 | 66 | async function serve() { 67 | server = createServer((req, res) => handler(req, res, { public: testDir })); 68 | await new Promise((resolve) => server.listen(0, resolve)); 69 | } 70 | 71 | before(async function () { 72 | await buildWebpack(); 73 | await serve(); 74 | 75 | this.timeout(20 * 1000); 76 | 77 | const { port } = server.address() as AddressInfo; 78 | browser = await chromium.launch(); 79 | page = await browser.newPage(); 80 | await page.goto(`http://localhost:${port}`); 81 | await page.waitForFunction('!!window.blake3'); 82 | await page.evaluate(addInputs); 83 | }); 84 | 85 | runTests({ 86 | get page() { 87 | return page; 88 | }, 89 | }); 90 | 91 | after(async () => { 92 | await browser?.close(); 93 | server?.close(); 94 | }); 95 | }); 96 | }); 97 | 98 | function runTests(opts: { page: Page }) { 99 | it('hashes a string', async () => { 100 | const result = await opts.page.evaluate('blake3.hash(inputs.large.input).toString("hex")'); 101 | expect(result).to.equal(inputs.large.hash.toString('hex')); 102 | }); 103 | 104 | describe('input encoding', () => { 105 | it('hashes a uint8array', async () => { 106 | const contents = [...new Uint8Array(Buffer.from(inputs.hello.input))]; 107 | const result = await opts.page.evaluate( 108 | `blake3.hash(new Uint8Array([${contents.join(',')}])).toString("hex")`, 109 | ); 110 | expect(result).to.equal(inputs.hello.hash.toString('hex')); 111 | }); 112 | 113 | it('hashes a string', async () => { 114 | const result = await opts.page.evaluate('blake3.hash(inputs.large.input).toString("hex")'); 115 | expect(result).to.equal(inputs.large.hash.toString('hex')); 116 | }); 117 | 118 | it('customizes output length', async () => { 119 | const result = await opts.page.evaluate( 120 | 'blake3.hash(inputs.hello.input, { length: 16 }).toString("hex")', 121 | ); 122 | expect(result).to.equal(inputs.hello.hash.slice(0, 16).toString('hex')); 123 | }); 124 | }); 125 | 126 | describe('output encoding', () => { 127 | const tcases = [ 128 | { encoding: 'hex', expected: inputs.hello.hash.toString('hex') }, 129 | { encoding: 'base64', expected: inputs.hello.hash.toString('base64') }, 130 | { encoding: 'utf8', expected: inputs.hello.hash.toString('utf8') }, 131 | ]; 132 | 133 | tcases.forEach(({ encoding, expected }) => 134 | it(encoding, async () => { 135 | const result = await opts.page.evaluate( 136 | `blake3.hash(inputs.hello.input).toString("${encoding}")`, 137 | ); 138 | expect(result).to.equal(expected); 139 | }), 140 | ); 141 | 142 | it('raw', async () => { 143 | const result = (await opts.page.evaluate(`blake3.hash(inputs.hello.input)`)) as { 144 | length: number; 145 | [n: number]: number; 146 | }; 147 | const actual = Buffer.alloc(32); 148 | for (let i = 0; i < actual.length; i++) { 149 | actual[i] = result[i]; // it comes as a plain object, we need to convert it to a buffer 150 | } 151 | expect(actual).to.deep.equal(inputs.hello.hash); 152 | }); 153 | }); 154 | 155 | describe('hash class', () => { 156 | it('digests', async () => { 157 | const result = await opts.page.evaluate(`(() => { 158 | const hash = blake3.createHash(); 159 | ${[...Buffer.from(inputs.hello.input)] 160 | .map((byte) => `hash.update(new Uint8Array([${byte}]));`) 161 | .join('\n')} 162 | return hash.digest('hex'); 163 | })()`); 164 | 165 | expect(result).to.equal(inputs.hello.hash.toString('hex')); 166 | }); 167 | 168 | it('customizes the output length', async () => { 169 | const result = await opts.page.evaluate(`(() => { 170 | const hash = blake3.createHash(); 171 | hash.update(${JSON.stringify(inputs.hello.input)}); 172 | return hash.digest('hex', { length: 16 }); 173 | })()`); 174 | 175 | expect(result).to.equal(inputs.hello.hash.slice(0, 16).toString('hex')); 176 | }); 177 | 178 | it('returns a hash instance from digest', async () => { 179 | const result = await opts.page.evaluate(`(() => { 180 | const hash = blake3.createHash(); 181 | ${[...Buffer.from(inputs.hello.input)] 182 | .map((byte) => `hash.update(new Uint8Array([${byte}]));`) 183 | .join('\n')} 184 | return hash.digest('hex'); 185 | })()`); 186 | 187 | expect(result).to.equal(inputs.hello.hash.toString('hex')); 188 | }); 189 | }); 190 | 191 | describe('reader', () => { 192 | it('is sane with a Hash', async () => { 193 | const result = await opts.page.evaluate(`(() => { 194 | const hash = blake3.createHash(); 195 | hash.update("hello"); 196 | 197 | const reader = hash.reader(); 198 | return reader.read(48).toString('hex'); 199 | })()`); 200 | 201 | expect(result).to.deep.equal(hello48.toString('hex')); 202 | }); 203 | }); 204 | 205 | describe('original test vectors', () => { 206 | for (const { 207 | inputLen, 208 | expectedDerive, 209 | expectedHash, 210 | expectedKeyed, 211 | } of ogTestVectors.cases.slice(0, 6)) { 212 | describe(`${inputLen}`, async () => { 213 | const input = Buffer.alloc(inputLen); 214 | for (let i = 0; i < inputLen; i++) { 215 | input[i] = i % 251; 216 | } 217 | 218 | const inputStr = `new Uint8Array([${input.join(',')}])`; 219 | 220 | it('hash()', async () => { 221 | const result = await opts.page.evaluate(`blake3.hash( 222 | ${inputStr}, 223 | { length: ${expectedHash.length / 2} } 224 | ).toString("hex")`); 225 | 226 | expect(result).to.equal(expectedHash); 227 | }); 228 | 229 | it('deriveKey()', async () => { 230 | const result = await opts.page.evaluate(`blake3.deriveKey( 231 | ${JSON.stringify(ogTestVectors.context)}, 232 | ${inputStr}, 233 | { length: ${expectedHash.length / 2} } 234 | ).toString("hex")`); 235 | 236 | expect(result).to.equal(expectedDerive); 237 | }); 238 | 239 | it('createKeyed()', async () => { 240 | const result = await opts.page.evaluate(`(() => { 241 | const hasher = blake3.createKeyed(new Uint8Array([${Buffer.from(ogTestVectors.key).join( 242 | ',', 243 | )}])); 244 | hasher.update(${inputStr}); 245 | return hasher.digest({ length: ${expectedHash.length / 2} }).toString('hex'); 246 | })()`); 247 | 248 | expect(result).to.equal(expectedKeyed); 249 | }); 250 | 251 | it('keyedHash()', async () => { 252 | const result = await opts.page.evaluate(`blake3.keyedHash( 253 | new Uint8Array([${Buffer.from(ogTestVectors.key).join(',')}]), 254 | ${inputStr}, 255 | { length: ${expectedHash.length / 2} } 256 | ).toString("hex")`); 257 | 258 | expect(result).to.equal(expectedKeyed); 259 | }); 260 | }); 261 | } 262 | }); 263 | } 264 | -------------------------------------------------------------------------------- /blake3-native/src/native.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | static const size_t KEY_REQUIRED_LENGTH = 32; 5 | static const size_t DEFAULT_HASH_LENGTH = 32; 6 | static const size_t ITERATOR_STEP_SIZE = 1024; 7 | static const uint64_t MAX_HASH_POSITION = 18446744073709551615U; 8 | 9 | struct InstanceData { 10 | Napi::FunctionReference *HashReader; 11 | Napi::FunctionReference *Hash; 12 | }; 13 | 14 | class HashReader : public Napi::ObjectWrap { 15 | public: 16 | static Napi::Function Init(Napi::Env &env, Napi::Object &exports) { 17 | Napi::Function func = DefineClass( 18 | env, "HashReader", 19 | {InstanceAccessor("position", &HashReader::GetPosition, 20 | &HashReader::SetPosition), 21 | InstanceMethod("readInto", &HashReader::ReadInto), 22 | InstanceMethod("read", &HashReader::Read), 23 | InstanceMethod("view", &HashReader::Read), 24 | InstanceMethod(Napi::Symbol::WellKnown(env, "iterator").Unwrap(), 25 | &HashReader::Iterator)}); 26 | 27 | return func; 28 | } 29 | 30 | HashReader(const Napi::CallbackInfo &info) 31 | : Napi::ObjectWrap(info) { 32 | this->position = 0; 33 | } 34 | 35 | void SetHasher(blake3_hasher *hasher) { this->hasher = *hasher; } 36 | 37 | private: 38 | /** 39 | * Returns the position of the reader in the hash. Can be written to to seek. 40 | */ 41 | Napi::Value GetPosition(const Napi::CallbackInfo &info) { 42 | return Napi::BigInt::New(info.Env(), this->position); 43 | } 44 | 45 | /** 46 | * Returns the position of the reader in the hash. Can be written to to seek. 47 | */ 48 | void SetPosition(const Napi::CallbackInfo &info, const Napi::Value &value) { 49 | Napi::Env env = info.Env(); 50 | if (!value.IsBigInt()) { 51 | Napi::TypeError::New(env, "Expected position to be a bigint") 52 | .ThrowAsJavaScriptException(); 53 | return; 54 | } 55 | 56 | bool lossless; 57 | uint64_t position = info[0].As().Uint64Value(&lossless); 58 | if (!lossless || position > MAX_HASH_POSITION) { 59 | Napi::RangeError::New(env, "Cannot seek past the max hash position") 60 | .ThrowAsJavaScriptException(); 61 | return; 62 | } 63 | 64 | this->position = position; 65 | } 66 | 67 | Napi::Value ReadInto(const Napi::CallbackInfo &info) { 68 | Napi::Env env = info.Env(); 69 | if (info.Length() <= 0) { 70 | Napi::TypeError::New(env, "Argument expected") 71 | .ThrowAsJavaScriptException(); 72 | return Napi::Number::New(env, 0); 73 | } 74 | 75 | auto arg0 = info[0]; 76 | if (!arg0.IsTypedArray()) { 77 | Napi::TypeError::New(env, "Expected a typed array") 78 | .ThrowAsJavaScriptException(); 79 | return Napi::Number::New(env, 0); 80 | } 81 | 82 | Napi::TypedArray ta = arg0.As(); 83 | uint64_t readBytes = 84 | std::min((uint64_t)ta.ByteLength(), MAX_HASH_POSITION - this->position); 85 | 86 | blake3_hasher_finalize_seek( 87 | &this->hasher, this->position, 88 | ((uint8_t *)ta.ArrayBuffer().Data()) + ta.ByteOffset(), readBytes); 89 | this->position += readBytes; 90 | return Napi::Number::New(env, readBytes); 91 | } 92 | 93 | Napi::Value Read(const Napi::CallbackInfo &info) { 94 | Napi::Env env = info.Env(); 95 | if (info.Length() <= 0) { 96 | Napi::TypeError::New(env, "Argument expected") 97 | .ThrowAsJavaScriptException(); 98 | return Napi::Number::New(env, 0); 99 | } 100 | 101 | auto arg0 = info[0]; 102 | if (!arg0.IsNumber()) { 103 | Napi::TypeError::New(env, "Expected a number array") 104 | .ThrowAsJavaScriptException(); 105 | return Napi::Number::New(env, 0); 106 | } 107 | 108 | uint64_t requestedBytes = 109 | (uint64_t)(std::max((int64_t)0, arg0.As().Int64Value())); 110 | uint64_t readBytes = 111 | std::min(requestedBytes, MAX_HASH_POSITION - this->position); 112 | 113 | Napi::Buffer out = 114 | Napi::Buffer::New(env, (size_t)readBytes); 115 | blake3_hasher_finalize_seek(&this->hasher, this->position, out.Data(), 116 | out.Length()); 117 | this->position += readBytes; 118 | return out; 119 | } 120 | 121 | Napi::Value Iterator(const Napi::CallbackInfo &info) { 122 | Napi::Env env = info.Env(); 123 | Napi::Object iterator = Napi::Object::New(env); 124 | 125 | Napi::Object iteratorResult = Napi::Object::New(env); 126 | Napi::Buffer view = 127 | Napi::Buffer::New(env, ITERATOR_STEP_SIZE); 128 | 129 | iterator["next"] = 130 | Napi::Function::New(env, [&](const Napi::CallbackInfo &info) { 131 | Napi::Env env = info.Env(); 132 | uint64_t readBytes = std::min((uint64_t)ITERATOR_STEP_SIZE, 133 | MAX_HASH_POSITION - this->position); 134 | blake3_hasher_finalize_seek(&this->hasher, this->position, 135 | view.Data(), readBytes); 136 | this->position += readBytes; 137 | 138 | iteratorResult["done"] = 139 | Napi::Boolean::New(env, this->position == MAX_HASH_POSITION); 140 | if (readBytes < ITERATOR_STEP_SIZE) { 141 | iteratorResult["value"] = view.Get("subarray") 142 | .Unwrap() 143 | .As() 144 | .Call({ 145 | Napi::Number::New(env, 0), 146 | Napi::Number::New(env, readBytes), 147 | }) 148 | .Unwrap(); 149 | } else { 150 | iteratorResult["value"] = view; 151 | } 152 | 153 | return iteratorResult; 154 | }); 155 | 156 | return iterator; 157 | } 158 | 159 | uint64_t position; 160 | blake3_hasher hasher; 161 | }; 162 | 163 | class Hash : public Napi::ObjectWrap { 164 | public: 165 | static Napi::Function Init(Napi::Env &env, Napi::Object &exports) { 166 | Napi::Function func = 167 | DefineClass(env, "Hash", 168 | {InstanceMethod("update", &Hash::Update), 169 | InstanceMethod("digest", &Hash::Digest), 170 | InstanceMethod("reader", &Hash::Reader)}); 171 | 172 | exports.Set("createHash", Napi::Function::New(env, &Hash::CreateHash)); 173 | exports.Set("createKeyed", Napi::Function::New(env, &Hash::CreateKeyed)); 174 | exports.Set("createDeriveKey", 175 | Napi::Function::New(env, &Hash::CreateDeriveKey)); 176 | 177 | return func; 178 | } 179 | 180 | Hash(const Napi::CallbackInfo &info) : Napi::ObjectWrap(info) {} 181 | 182 | static Napi::Value CreateHash(const Napi::CallbackInfo &info) { 183 | struct InstanceData *id = info.Env().GetInstanceData(); 184 | Napi::Value out = id->Hash->New({}).Unwrap(); 185 | Hash *hash = Unwrap(out.As()); 186 | blake3_hasher_init(&hash->hasher); 187 | return out; 188 | } 189 | 190 | static Napi::Value CreateKeyed(const Napi::CallbackInfo &info) { 191 | Napi::Env env = info.Env(); 192 | if (info.Length() <= 0) { 193 | Napi::TypeError::New(env, "Argument expected") 194 | .ThrowAsJavaScriptException(); 195 | return env.Undefined(); 196 | } 197 | 198 | auto arg0 = info[0]; 199 | if (!arg0.IsTypedArray()) { 200 | Napi::TypeError::New(env, "First argument should be a typed array") 201 | .ThrowAsJavaScriptException(); 202 | return env.Undefined(); 203 | } 204 | 205 | Napi::TypedArray ta = arg0.As(); 206 | if (ta.ByteLength() != KEY_REQUIRED_LENGTH) { 207 | Napi::TypeError::New(env, "BLAKE3 key must be exactly 32 bytes") 208 | .ThrowAsJavaScriptException(); 209 | return env.Undefined(); 210 | } 211 | 212 | struct InstanceData *id = env.GetInstanceData(); 213 | Napi::Value out = id->Hash->New({}).Unwrap(); 214 | Hash *hash = Unwrap(out.As()); 215 | blake3_hasher_init_keyed( 216 | &hash->hasher, ((uint8_t *)ta.ArrayBuffer().Data()) + ta.ByteOffset()); 217 | return out; 218 | } 219 | 220 | static Napi::Value CreateDeriveKey(const Napi::CallbackInfo &info) { 221 | Napi::Env env = info.Env(); 222 | if (info.Length() <= 0) { 223 | Napi::TypeError::New(env, "Argument expected") 224 | .ThrowAsJavaScriptException(); 225 | return env.Undefined(); 226 | } 227 | 228 | auto arg0 = info[0]; 229 | if (!arg0.IsTypedArray()) { 230 | Napi::TypeError::New(env, "First argument should be a typed array") 231 | .ThrowAsJavaScriptException(); 232 | return env.Undefined(); 233 | } 234 | 235 | Napi::TypedArray ta = arg0.As(); 236 | struct InstanceData *id = env.GetInstanceData(); 237 | Napi::Value out = id->Hash->New({}).Unwrap(); 238 | Hash *hash = Unwrap(out.As()); 239 | blake3_hasher_init_derive_key_raw( 240 | &hash->hasher, ((uint8_t *)ta.ArrayBuffer().Data()) + ta.ByteOffset(), 241 | ta.ByteLength()); 242 | return out; 243 | } 244 | 245 | private: 246 | Napi::Value Update(const Napi::CallbackInfo &info) { 247 | Napi::Env env = info.Env(); 248 | if (info.Length() <= 0) { 249 | Napi::TypeError::New(env, "Argument expected") 250 | .ThrowAsJavaScriptException(); 251 | return info.This(); 252 | } 253 | 254 | auto arg0 = info[0]; 255 | if (arg0.IsTypedArray()) { 256 | Napi::TypedArray ta = arg0.As(); 257 | blake3_hasher_update( 258 | &this->hasher, ((uint8_t *)ta.ArrayBuffer().Data()) + ta.ByteOffset(), 259 | ta.ByteLength()); 260 | } else { 261 | Napi::TypeError::New(env, "Expected a string or typed array") 262 | .ThrowAsJavaScriptException(); 263 | } 264 | 265 | return info.This(); 266 | } 267 | 268 | Napi::Value Digest(const Napi::CallbackInfo &info) { 269 | Napi::Env env = info.Env(); 270 | 271 | size_t length = DEFAULT_HASH_LENGTH; 272 | if (info[0].IsNumber()) { 273 | length = (size_t)(info[0].As().Uint32Value()); 274 | } 275 | 276 | Napi::Buffer out = Napi::Buffer::New(env, (size_t)length); 277 | blake3_hasher_finalize_seek(&this->hasher, 0, out.Data(), out.Length()); 278 | return out; 279 | } 280 | 281 | Napi::Value Reader(const Napi::CallbackInfo &info) { 282 | struct InstanceData *id = info.Env().GetInstanceData(); 283 | Napi::Value out = id->HashReader->New({}).Unwrap(); 284 | 285 | HashReader *reader = 286 | Napi::ObjectWrap::Unwrap(out.As()); 287 | reader->SetHasher(&this->hasher); 288 | 289 | return out; 290 | } 291 | 292 | blake3_hasher hasher; 293 | }; 294 | 295 | Napi::Object Init(Napi::Env env, Napi::Object exports) { 296 | struct InstanceData *id = new struct InstanceData; 297 | 298 | id->HashReader = new Napi::FunctionReference(); 299 | *(id->HashReader) = Napi::Persistent(HashReader::Init(env, exports)); 300 | 301 | id->Hash = new Napi::FunctionReference(); 302 | *(id->Hash) = Napi::Persistent(Hash::Init(env, exports)); 303 | 304 | env.SetInstanceData(id); 305 | 306 | return exports; 307 | } 308 | 309 | NODE_API_MODULE(addon, Init) 310 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # BLAKE3 2 | 3 | [BLAKE3](https://github.com/BLAKE3-team/BLAKE3) running in JavaScript (node.js and browsers) via native bindings, where available, or WebAssembly. 4 | 5 | npm install blake3 6 | 7 | Additionally, there's a flavor of the package which is identical except that it will not download native Node.js bindings and use only WebAssembly: 8 | 9 | npm install blake3-wasm 10 | 11 | ## Table of Contents 12 | 13 | - [Quickstart](#quickstart) 14 | - [API](#api) 15 | - [Node.js](#nodejs) 16 | - [`hash(data: BinaryLike, options?: { length: number }): Buffer`](#hashdata-binarylike-options--length-number--buffer) 17 | - [`keyedHash(key: BinaryLike, data: BinaryLike, options?: { length: number }): Buffer`](#keyedhashkey-binarylike-data-binarylike-options--length-number--buffer) 18 | - [`deriveKey(context: BinaryLike, material: BinaryLike, options?: { length: number }): Buffer`](#derivekeycontext-binarylike-material-binarylike-options--length-number--buffer) 19 | - [Hasher](#hasher) 20 | - [`createHash(): Hasher`](#createhash-hasher) 21 | - [`createKeyed(key: BinaryLike): Hasher`](#createkeyedkey-binarylike-hasher) 22 | - [`createDeriveKey(context: BinaryLike): Hasher`](#createderivekeycontext-binarylike-hasher) 23 | - [`hasher.update(data: BinaryLike): this`](#hasherupdatedata-binarylike-this) 24 | - [`hasher.digest(encoding?: string, options?: { length: number })): Buffer | string`](#hasherdigestencoding-string-options--length-number--buffer--string) 25 | - [`hasher.reader(): HashReader`](#hasherreader-hashreader) 26 | - [`hasher.dispose()`](#hasherdispose) 27 | - [HashReader](#hashreader) 28 | - [`reader.position: bigint`](#readerposition-bigint) 29 | - [`reader.readInto(target: Uint8Array): number`](#readerreadintotarget-uint8array-number) 30 | - [`reader.read(bytes: number): Buffer`](#readerreadbytes-number-buffer) 31 | - [`reader.view(target: Buffer): Readonly`](#readerviewtarget-buffer-readonlyuint8array) 32 | - [`reader[Symbol.iterator]`](#readersymboliterator) 33 | - [Browser](#browser) 34 | - [`hash(data: BinaryLike, options?: { length: number }): Hash`](#hashdata-binarylike-options--length-number--hash) 35 | - [`keyedHash(key: BinaryLike, data: BinaryLike, options?: { length: number }): Hash`](#keyedhashkey-binarylike-data-binarylike-options--length-number--hash) 36 | - [`deriveKey(context: BinaryLike, material: BinaryLike, options?: { length: number }): Hash`](#derivekeycontext-binarylike-material-binarylike-options--length-number--hash) 37 | - [`Hash`](#hash) 38 | - [`hash.equals(other: Uint8Array)`](#hashequalsother-uint8array) 39 | - [`hash.toString(encoding: 'hex' | 'base64' | 'utf8'): string`](#hashtostringencoding-hex--base64--utf8-string) 40 | - [Hasher](#hasher-1) 41 | - [`createHash(): Hasher`](#createhash-hasher-1) 42 | - [`createKeyed(key: BinaryLike): Hasher`](#createkeyedkey-binarylike-hasher-1) 43 | - [`createDeriveKey(context: BinaryLike): Hasher`](#createderivekeycontext-binarylike-hasher-1) 44 | - [`hasher.update(data: BinaryLike): this`](#hasherupdatedata-binarylike-this-1) 45 | - [`hasher.digest(encoding?: 'hex' | 'base64' | 'utf8', options?: { length: number })): Hash | string`](#hasherdigestencoding-hex--base64--utf8-options--length-number--hash--string) 46 | - [`hasher.reader(): HashReader`](#hasherreader-hashreader-1) 47 | - [`hasher.dispose()`](#hasherdispose-1) 48 | - [HashReader](#hashreader-1) 49 | - [`reader.position: bigint`](#readerposition-bigint-1) 50 | - [`reader.readInto(target: Uint8Array): number`](#readerreadintotarget-uint8array-number-1) 51 | - [`reader.read(bytes: number): Hash`](#readerreadbytes-number-hash) 52 | - [`reader.view(target: Buffer): Readonly`](#readerviewtarget-buffer-readonlyuint8array-1) 53 | - [`reader[Symbol.iterator]`](#readersymboliterator-1) 54 | - [Speed](#speed) 55 | - [Other (JS) Implementations](#other-js-implementations) 56 | - [Contributing](#contributing) 57 | - [Publishing](#publishing) 58 | 59 | ## Quickstart 60 | 61 | If you're on Node, import the module via 62 | 63 | ```js 64 | const blake3 = require('blake3'); 65 | 66 | blake3.hash('foo'); // => Buffer 67 | ``` 68 | 69 | If you're in the browser, import `blake3/browser`. This includes a WebAssembly binary, so you probably want to import it asynchronously, like so: 70 | 71 | ```js 72 | import('blake3/browser').then((blake3) => { 73 | blake3.hash('foo'); // => Uint8Array 74 | }); 75 | ``` 76 | 77 | The API is very similar in Node.js and browsers, but Node supports and returns Buffers and a wider range of input and output encoding. 78 | 79 | More complete example: 80 | 81 | ```js 82 | const { hash, createHash } = require('blake3'); 83 | 84 | hash('some string'); // => hash a string to a uint8array 85 | 86 | // Update incrementally (Node and Browsers): 87 | const hash = createHash(); 88 | stream.on('data', (d) => hash.update(d)); 89 | stream.on('error', (err) => { 90 | // hashes use unmanaged memory in WebAssembly, always free them if you don't digest()! 91 | hash.dispose(); 92 | throw err; 93 | }); 94 | stream.on('end', () => finishedHash(hash.digest())); 95 | 96 | // Or, in Node, it's also a transform stream: 97 | createReadStream('file.txt') 98 | .pipe(createHash()) 99 | .on('data', (hash) => console.log(hash.toString('hex'))); 100 | ``` 101 | 102 | ## API 103 | 104 | ### Node.js 105 | 106 | The Node API can be imported via `require('blake3')`. 107 | 108 | #### `hash(data: BinaryLike, options?: { length: number }): Buffer` 109 | 110 | Returns a hash for the given data. The data can be a string, buffer, typedarray, array buffer, or array. By default, it generates the first 32 bytes of the hash for the data, but this is configurable. It returns a Buffer. 111 | 112 | #### `keyedHash(key: BinaryLike, data: BinaryLike, options?: { length: number }): Buffer` 113 | 114 | Returns keyed a hash for the given data. The key must be exactly 32 bytes. The data can be a string, buffer, typedarray, array buffer, or array. By default, it generates the first 32 bytes of the hash for the data, but this is configurable. It returns a Buffer. 115 | 116 | For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.keyed_hash.html). 117 | 118 | #### `deriveKey(context: BinaryLike, material: BinaryLike, options?: { length: number }): Buffer` 119 | 120 | The key derivation function. The data can be a string, buffer, typedarray, array buffer, or array. By default, it generates the first 32 bytes of the hash for the data, but this is configurable. It returns a Buffer. 121 | 122 | For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.derive_key.html). 123 | 124 | #### Hasher 125 | 126 | The hasher is a type that lets you incrementally build a hash. It's compatible with Node's crypto hash instance. For instance, it implements a transform stream, so you could do something like: 127 | 128 | ```js 129 | createReadStream('file.txt') 130 | .pipe(createHash()) 131 | .on('data', (hash) => console.log(hash.toString('hex'))); 132 | ``` 133 | 134 | ##### `createHash(): Hasher` 135 | 136 | Creates a new hasher instance using the standard hash function. 137 | 138 | ##### `createKeyed(key: BinaryLike): Hasher` 139 | 140 | Creates a new hasher instance for a keyed hash. For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.keyed_hash.html). 141 | 142 | ##### `createDeriveKey(context: BinaryLike): Hasher` 143 | 144 | Creates a new hasher instance for the key derivation function. For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.derive_key.html). 145 | 146 | ##### `hasher.update(data: BinaryLike): this` 147 | 148 | Adds data to a hash. The data can be a string, buffer, typedarray, array buffer, or array. 149 | 150 | ##### `hasher.digest(encoding?: string, options?: { length: number })): Buffer | string` 151 | 152 | Returns the hash of the data. If an `encoding` is given, a string will be returned. Otherwise, a Buffer is returned. Optionally, you can specify the requested byte length of the hash. 153 | 154 | ##### `hasher.reader(): HashReader` 155 | 156 | Returns a [HashReader](#HashReader) for the current hash. 157 | 158 | ##### `hasher.dispose()` 159 | 160 | This is a no-op for Node.js. 161 | 162 | #### HashReader 163 | 164 | The hash reader can be returned from hashing functions. Up to 264-1 bytes of data can be read from BLAKE3 hashes; this structure lets you read those. Note that, like `hash`, this is an object which needs to be manually disposed of. 165 | 166 | ##### `reader.position: bigint` 167 | 168 | A property which gets or sets the position of the reader in the output stream. A `RangeError` is thrown if setting this to a value less than 0 or greater than 264-1. Note that this is a bigint, not a standard number. 169 | 170 | ```js 171 | reader.position += 32n; // advance the reader 32 bytes 172 | ``` 173 | 174 | ##### `reader.readInto(target: Uint8Array): number` 175 | 176 | Reads bytes into the target array, filling it up and advancing the reader's position. It returns the number of bytes written, which may be less then the size of the target buffer if position 264-1 is reached. 177 | 178 | ##### `reader.read(bytes: number): Buffer` 179 | 180 | Reads and returns the given number of bytes from the reader, and advances the position. A `RangeError` is thrown if reading this data puts the reader past 264-1 bytes. 181 | 182 | ##### `reader.view(target: Buffer): Readonly` 183 | 184 | Returns a view of the given number of bytes from the reader. The view can be used synchronously, but must not be reused later. This is more efficient when using the webassembly version of the module. 185 | 186 | Fewer bytes may be returned than requested, if the number is large (>1MB). 187 | 188 | ##### `reader[Symbol.iterator]` 189 | 190 | The reader is an `Iterable` of `Readonly`s. Like the `view` method, the iterated arrays will be reused internally on the next iteration, so if you need data, you should copy it out of the iterated array. 191 | 192 | ### Browser 193 | 194 | The browser API can be imported via `import('blake3/browser')`, which works well with Webpack. 195 | 196 | Note that you **must** call the load() method before using any function in the module. 197 | 198 | ```js 199 | import * as blake3 from 'blake3/browser-async'; 200 | 201 | blake3.load().then(() => { 202 | console.log(blake3.hash('hello world')); 203 | }); 204 | ``` 205 | 206 | #### `hash(data: BinaryLike, options?: { length: number }): Hash` 207 | 208 | Returns a hash for the given data. The data can be a string, typedarray, array buffer, or array. By default, it generates the first 32 bytes of the hash for the data, but this is configurable. It returns a [Hash](#Hash) instance. 209 | 210 | #### `keyedHash(key: BinaryLike, data: BinaryLike, options?: { length: number }): Hash` 211 | 212 | Returns keyed a hash for the given data. The key must be exactly 32 bytes. The data can be a string, typedarray, array buffer, or array. By default, it generates the first 32 bytes of the hash for the data, but this is configurable. It returns a [Hash](#Hash) instance. 213 | 214 | For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.keyed_hash.html). 215 | 216 | #### `deriveKey(context: BinaryLike, material: BinaryLike, options?: { length: number }): Hash` 217 | 218 | The key derivation function. The data can be a string, typedarray, array buffer, or array. By default, it generates the first 32 bytes of the hash for the data, but this is configurable. It returns a [Hash](#Hash) instance. 219 | 220 | For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.derive_key.html). 221 | 222 | #### `Hash` 223 | 224 | A Hash is the type returned from hash functions and the hasher in the browser. It's a `Uint8Array` with a few additional helper methods. 225 | 226 | ##### `hash.equals(other: Uint8Array)` 227 | 228 | Returns whether this hash equals the other hash, via a constant-time equality check. 229 | 230 | ##### `hash.toString(encoding: 'hex' | 'base64' | 'utf8'): string` 231 | 232 | #### Hasher 233 | 234 | The hasher is a type that lets you incrementally build a hash. For instance, you can hash a `fetch`ed page like: 235 | 236 | ```js 237 | const res = await fetch('https://example.com'); 238 | const body = await res.body; 239 | 240 | const hasher = blake3.createHash(); 241 | const reader = body.getReader(); 242 | 243 | while (true) { 244 | const { done, value } = await reader.read(); 245 | if (done) { 246 | break; 247 | } 248 | 249 | hasher.update(value); 250 | } 251 | 252 | console.log('Hash of', res.url, 'is', hasher.digest('hex')); 253 | ``` 254 | 255 | Converts the hash to a string with the given encoding. 256 | 257 | ##### `createHash(): Hasher` 258 | 259 | Creates a new hasher instance using the standard hash function. 260 | 261 | ##### `createKeyed(key: BinaryLike): Hasher` 262 | 263 | Creates a new hasher instance for a keyed hash. For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.keyed_hash.html). 264 | 265 | ##### `createDeriveKey(context: BinaryLike): Hasher` 266 | 267 | Creates a new hasher instance for the key derivation function. For more information, see [the blake3 docs](https://docs.rs/blake3/0.1.3/blake3/fn.derive_key.html). 268 | 269 | ##### `hasher.update(data: BinaryLike): this` 270 | 271 | Adds data to a hash. The data can be a string, buffer, typedarray, array buffer, or array. This will throw if called after `digest()` or `dispose()`. 272 | 273 | ##### `hasher.digest(encoding?: 'hex' | 'base64' | 'utf8', options?: { length: number })): Hash | string` 274 | 275 | Returns the hash of the data. If an `encoding` is given, a string will be returned. Otherwise, a [Hash](#hash) is returned. Optionally, you can specify the requested byte length of the hash. 276 | 277 | ##### `hasher.reader(): HashReader` 278 | 279 | Returns a [HashReader](#HashReader) for the current hash. 280 | 281 | ##### `hasher.dispose()` 282 | 283 | Disposes of webassembly-allocated resources. Resources are free automatically via a `FinalizationRegistry` for hashers, but you may call this manually if you run into resource-constraint issues. 284 | 285 | #### HashReader 286 | 287 | The hash reader can be returned from hashing functions. Up to 264-1 bytes of data can be read from BLAKE3 hashes; this structure lets you read those. Note that, like `hash`, this is an object which needs to be manually disposed of. 288 | 289 | ##### `reader.position: bigint` 290 | 291 | A property which gets or sets the position of the reader in the output stream. A `RangeError` is thrown if setting this to a value less than 0 or greater than 264-1. Note that this is a bigint, not a standard number. 292 | 293 | ```js 294 | reader.position += 32n; // advance the reader 32 bytes 295 | ``` 296 | 297 | ##### `reader.readInto(target: Uint8Array): number` 298 | 299 | Reads bytes into the target array, filling it up and advancing the reader's position. It returns the number of bytes written, which may be less then the size of the target buffer if position 264-1 is reached. 300 | 301 | ##### `reader.read(bytes: number): Hash` 302 | 303 | Reads and returns the given number of bytes from the reader, and advances the position. A `RangeError` is thrown if reading this data puts the reader past 264-1 bytes. 304 | 305 | ##### `reader.view(target: Buffer): Readonly` 306 | 307 | Returns a view of the given number of bytes from the reader. The view can be used synchronously, but must not be reused later. This is more efficient when using the webassembly version of the module. 308 | 309 | Fewer bytes may be returned than requested, if the number is large (>1MB). 310 | 311 | ##### `reader[Symbol.iterator]` 312 | 313 | The reader is an `Iterable` of `Readonly`s. Like the `view` method, the iterated arrays will be reused internally on the next iteration, so if you need data, you should copy it out of the iterated array. 314 | 315 | ## Speed 316 | 317 | > Native Node.js bindings are a work in progress. 318 | 319 | You can run benchmarks by installing `npm install -g @c4312/matcha`, then running `matcha benchmark.js`. These are the results running on Node 12 on my MacBook. Blake3 is significantly faster than Node's built-in hashing. 320 | 321 | 276,000 ops/sec > 64B#md5 (4,240x) 322 | 263,000 ops/sec > 64B#sha1 (4,040x) 323 | 271,000 ops/sec > 64B#sha256 (4,160x) 324 | 1,040,000 ops/sec > 64B#blake3 wasm (15,900x) 325 | 625,000 ops/sec > 64B#blake3 native (9,590x) 326 | 327 | 9,900 ops/sec > 64KB#md5 (152x) 328 | 13,900 ops/sec > 64KB#sha1 (214x) 329 | 6,470 ops/sec > 64KB#sha256 (99.2x) 330 | 6,410 ops/sec > 64KB#blake3 wasm (98.4x) 331 | 48,900 ops/sec > 64KB#blake3 native (750x) 332 | 333 | 106 ops/sec > 6MB#md5 (1.63x) 334 | 150 ops/sec > 6MB#sha1 (2.3x) 335 | 69.2 ops/sec > 6MB#sha256 (1.06x) 336 | 65.2 ops/sec > 6MB#blake3 wasm (1x) 337 | 502 ops/sec > 6MB#blake3 native (7.7x) 338 | 339 | ## Other (JS) Implementations 340 | 341 | - [Brooooooklyn/blake-hash](https://github.com/Brooooooklyn/blake-hash) 342 | 343 | ## Contributing 344 | 345 | This build is a little esoteric due to the mixing of languages. We use a `Makefile` to coodinate things. 346 | 347 | To get set up, you'll want to open the repository in VS Code. Make sure you have [Remote Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) installed, and then accept the "Reopen in Container" prompt when opening the folder. This will get the environment set up with everything you need. Then, run `make prepare` to install local dependencies. 348 | 349 | Finally, `make` will create a build for you; you can run `make MODE=release` for a production release, and certainly should if you want to [benchmark it](#speed). 350 | 351 | - Rust code is compiled from `src/lib.rs` to `pkg/browser` and `pkg/node` 352 | - TypeScript code is compiled from `ts/*.ts` into `dist` 353 | 354 | ### Publishing 355 | 356 | In case I get hit by a bus or get other contributors, these are the steps for publishing: 357 | 358 | 1. Get all your code ready to go in master, pushed up to Github. 359 | 2. Run `make prepare-binaries`. This will update the branch `generate-binary`, which kicks off a build via Github actions to create `.node` binaries for every relevant Node.js version. 360 | 3. When the build completes, it'll generate a zip file of artifacts. Download those. 361 | 4. Back on master, run `npm version ` to update the version in git. `git push --tags`. 362 | 5. On Github, upload the contents of the artifacts folder to the release for the newly tagged version. 363 | 6. Run `npm publish`. 364 | -------------------------------------------------------------------------------- /blake3/test-helpers.cts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | 3 | export const hello48 = Buffer.from( 4 | 'ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200fe992405f0d785b599a2e3387f6d34d01', 5 | 'hex', 6 | ); 7 | 8 | export const inputs = { 9 | large: { 10 | input: readFileSync(__dirname + '/../test-input.txt', 'utf-8'), 11 | hash: Buffer.from('2a2cf9cbc9f8d48f7d089273bc2d796a3cd0677b64234dab0c59e6e29d6a7164', 'hex'), 12 | }, 13 | hello: { 14 | input: 'hello', 15 | hash: Buffer.from('ea8f163db38682925e4491c5e58d4bb3506ef8c14eb78a86e908c5624a67200f', 'hex'), 16 | }, 17 | goodbye: { 18 | input: 'goodbye', 19 | hash: Buffer.from('f94a694227c5f31a07551908ad5fb252f5f0964030df5f2f200adedfae4d9b69', 'hex'), 20 | }, 21 | }; 22 | 23 | /** 24 | * Test vectors from the BLAKE3 repo. 25 | * 26 | * > Each test is an input length and three outputs, one for each of the hash, 27 | * > keyedHash, and deriveKey modes. The input in each case is filled with a 28 | * > 251-byte-long repeating pattern: 0, 1, 2, ..., 249, 250, 0, 1, ... The 29 | * > key used with keyedHash is the 32-byte ASCII string given in the 'key' 30 | * > field below. For deriveKey, the test input is used as the input key, and 31 | * > the context string is 'BLAKE3 2019-12-27 6:29:52 example context'. 32 | * > (As good practice for following the security requirements of deriveKey, 33 | * > test runners should make that context string a hardcoded constant, and we 34 | * > do not provided it in machine-readable form.) Outputs are encoded as 35 | * > hexadecimal. Each case is an extended output, and implementations should 36 | * > also check that the first 32 bytes match their default-length output. 37 | */ 38 | export const ogTestVectors = { 39 | key: 'whats the Elvish word for friend', 40 | context: 'BLAKE3 2019-12-27 16:29:52 test vectors context', 41 | cases: [ 42 | { 43 | inputLen: 0, 44 | expectedHash: 45 | 'af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262e00f03e7b69af26b7faaf09fcd333050338ddfe085b8cc869ca98b206c08243a26f5487789e8f660afe6c99ef9e0c52b92e7393024a80459cf91f476f9ffdbda7001c22e159b402631f277ca96f2defdf1078282314e763699a31c5363165421cce14d', 46 | expectedKeyed: 47 | '92b2b75604ed3c761f9d6f62392c8a9227ad0ea3f09573e783f1498a4ed60d26b18171a2f22a4b94822c701f107153dba24918c4bae4d2945c20ece13387627d3b73cbf97b797d5e59948c7ef788f54372df45e45e4293c7dc18c1d41144a9758be58960856be1eabbe22c2653190de560ca3b2ac4aa692a9210694254c371e851bc8f', 48 | expectedDerive: 49 | '2cc39783c223154fea8dfb7c1b1660f2ac2dcbd1c1de8277b0b0dd39b7e50d7d905630c8be290dfcf3e6842f13bddd573c098c3f17361f1f206b8cad9d088aa4a3f746752c6b0ce6a83b0da81d59649257cdf8eb3e9f7d4998e41021fac119deefb896224ac99f860011f73609e6e0e4540f93b273e56547dfd3aa1a035ba6689d89a0', 50 | }, 51 | { 52 | inputLen: 1, 53 | expectedHash: 54 | '2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213c3a6cb8bf623e20cdb535f8d1a5ffb86342d9c0b64aca3bce1d31f60adfa137b358ad4d79f97b47c3d5e79f179df87a3b9776ef8325f8329886ba42f07fb138bb502f4081cbcec3195c5871e6c23e2cc97d3c69a613eba131e5f1351f3f1da786545e5', 55 | expectedKeyed: 56 | '6d7878dfff2f485635d39013278ae14f1454b8c0a3a2d34bc1ab38228a80c95b6568c0490609413006fbd428eb3fd14e7756d90f73a4725fad147f7bf70fd61c4e0cf7074885e92b0e3f125978b4154986d4fb202a3f331a3fb6cf349a3a70e49990f98fe4289761c8602c4e6ab1138d31d3b62218078b2f3ba9a88e1d08d0dd4cea11', 57 | expectedDerive: 58 | 'b3e2e340a117a499c6cf2398a19ee0d29cca2bb7404c73063382693bf66cb06c5827b91bf889b6b97c5477f535361caefca0b5d8c4746441c57617111933158950670f9aa8a05d791daae10ac683cbef8faf897c84e6114a59d2173c3f417023a35d6983f2c7dfa57e7fc559ad751dbfb9ffab39c2ef8c4aafebc9ae973a64f0c76551', 59 | }, 60 | { 61 | inputLen: 1023, 62 | expectedHash: 63 | '10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11a182d27a591b05592b15607500e1e8dd56bc6c7fc063715b7a1d737df5bad3339c56778957d870eb9717b57ea3d9fb68d1b55127bba6a906a4a24bbd5acb2d123a37b28f9e9a81bbaae360d58f85e5fc9d75f7c370a0cc09b6522d9c8d822f2f28f485', 64 | expectedKeyed: 65 | 'c951ecdf03288d0fcc96ee3413563d8a6d3589547f2c2fb36d9786470f1b9d6e890316d2e6d8b8c25b0a5b2180f94fb1a158ef508c3cde45e2966bd796a696d3e13efd86259d756387d9becf5c8bf1ce2192b87025152907b6d8cc33d17826d8b7b9bc97e38c3c85108ef09f013e01c229c20a83d9e8efac5b37470da28575fd755a10', 66 | expectedDerive: 67 | '74a16c1c3d44368a86e1ca6df64be6a2f64cce8f09220787450722d85725dea59c413264404661e9e4d955409dfe4ad3aa487871bcd454ed12abfe2c2b1eb7757588cf6cb18d2eccad49e018c0d0fec323bec82bf1644c6325717d13ea712e6840d3e6e730d35553f59eff5377a9c350bcc1556694b924b858f329c44ee64b884ef00d', 68 | }, 69 | { 70 | inputLen: 1024, 71 | expectedHash: 72 | '42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af71cf8107265ecdaf8505b95d8fcec83a98a6a96ea5109d2c179c47a387ffbb404756f6eeae7883b446b70ebb144527c2075ab8ab204c0086bb22b7c93d465efc57f8d917f0b385c6df265e77003b85102967486ed57db5c5ca170ba441427ed9afa684e', 73 | expectedKeyed: 74 | '75c46f6f3d9eb4f55ecaaee480db732e6c2105546f1e675003687c31719c7ba4a78bc838c72852d4f49c864acb7adafe2478e824afe51c8919d06168414c265f298a8094b1ad813a9b8614acabac321f24ce61c5a5346eb519520d38ecc43e89b5000236df0597243e4d2493fd626730e2ba17ac4d8824d09d1a4a8f57b8227778e2de', 75 | expectedDerive: 76 | '7356cd7720d5b66b6d0697eb3177d9f8d73a4a5c5e968896eb6a6896843027066c23b601d3ddfb391e90d5c8eccdef4ae2a264bce9e612ba15e2bc9d654af1481b2e75dbabe615974f1070bba84d56853265a34330b4766f8e75edd1f4a1650476c10802f22b64bd3919d246ba20a17558bc51c199efdec67e80a227251808d8ce5bad', 77 | }, 78 | { 79 | inputLen: 1025, 80 | expectedHash: 81 | 'd00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444f4c4a22b4b399155358a994e52bf255de60035742ec71bd08ac275a1b51cc6bfe332b0ef84b409108cda080e6269ed4b3e2c3f7d722aa4cdc98d16deb554e5627be8f955c98e1d5f9565a9194cad0c4285f93700062d9595adb992ae68ff12800ab67a', 82 | expectedKeyed: 83 | '357dc55de0c7e382c900fd6e320acc04146be01db6a8ce7210b7189bd664ea69362396b77fdc0d2634a552970843722066c3c15902ae5097e00ff53f1e116f1cd5352720113a837ab2452cafbde4d54085d9cf5d21ca613071551b25d52e69d6c81123872b6f19cd3bc1333edf0c52b94de23ba772cf82636cff4542540a7738d5b930', 84 | expectedDerive: 85 | 'effaa245f065fbf82ac186839a249707c3bddf6d3fdda22d1b95a3c970379bcb5d31013a167509e9066273ab6e2123bc835b408b067d88f96addb550d96b6852dad38e320b9d940f86db74d398c770f462118b35d2724efa13da97194491d96dd37c3c09cbef665953f2ee85ec83d88b88d11547a6f911c8217cca46defa2751e7f3ad', 86 | }, 87 | { 88 | inputLen: 2048, 89 | expectedHash: 90 | 'e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a9a60bf80001410ec9eea6698cd537939fad4749edd484cb541aced55cd9bf54764d063f23f6f1e32e12958ba5cfeb1bf618ad094266d4fc3c968c2088f677454c288c67ba0dba337b9d91c7e1ba586dc9a5bc2d5e90c14f53a8863ac75655461cea8f9', 91 | expectedKeyed: 92 | '879cf1fa2ea0e79126cb1063617a05b6ad9d0b696d0d757cf053439f60a99dd10173b961cd574288194b23ece278c330fbb8585485e74967f31352a8183aa782b2b22f26cdcadb61eed1a5bc144b8198fbb0c13abbf8e3192c145d0a5c21633b0ef86054f42809df823389ee40811a5910dcbd1018af31c3b43aa55201ed4edaac74fe', 93 | expectedDerive: 94 | '7b2945cb4fef70885cc5d78a87bf6f6207dd901ff239201351ffac04e1088a23e2c11a1ebffcea4d80447867b61badb1383d842d4e79645d48dd82ccba290769caa7af8eaa1bd78a2a5e6e94fbdab78d9c7b74e894879f6a515257ccf6f95056f4e25390f24f6b35ffbb74b766202569b1d797f2d4bd9d17524c720107f985f4ddc583', 95 | }, 96 | { 97 | inputLen: 2049, 98 | expectedHash: 99 | '5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b687952256303096de31d71d74103403822a2e0bc1eb193e7aecc9643a76b7bbc0c9f9c52e8783aae98764ca468962b5c2ec92f0c74eb5448d519713e09413719431c802f948dd5d90425a4ecdadece9eb178d80f26efccae630734dff63340285adec2aed3b51073ad3', 100 | expectedKeyed: 101 | '9f29700902f7c86e514ddc4df1e3049f258b2472b6dd5267f61bf13983b78dd5f9a88abfefdfa1e00b418971f2b39c64ca621e8eb37fceac57fd0c8fc8e117d43b81447be22d5d8186f8f5919ba6bcc6846bd7d50726c06d245672c2ad4f61702c646499ee1173daa061ffe15bf45a631e2946d616a4c345822f1151284712f76b2b0e', 102 | expectedDerive: 103 | '2ea477c5515cc3dd606512ee72bb3e0e758cfae7232826f35fb98ca1bcbdf27316d8e9e79081a80b046b60f6a263616f33ca464bd78d79fa18200d06c7fc9bffd808cc4755277a7d5e09da0f29ed150f6537ea9bed946227ff184cc66a72a5f8c1e4bd8b04e81cf40fe6dc4427ad5678311a61f4ffc39d195589bdbc670f63ae70f4b6', 104 | }, 105 | { 106 | inputLen: 3072, 107 | expectedHash: 108 | 'b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd29a3f6b0b978d6608335c09dc94ccf682f9951cdfc501bfe47b9c9189a6fc7b404d120258506341a6d802857322fbd20d3e5dae05b95c88793fa83db1cb08e7d8008d1599b6209d78336e24839724c191b2a52a80448306e0daa84a3fdb566661a37e11', 109 | expectedKeyed: 110 | '044a0e7b172a312dc02a4c9a818c036ffa2776368d7f528268d2e6b5df19177022f302d0529e4174cc507c463671217975e81dab02b8fdeb0d7ccc7568dd22574c783a76be215441b32e91b9a904be8ea81f7a0afd14bad8ee7c8efc305ace5d3dd61b996febe8da4f56ca0919359a7533216e2999fc87ff7d8f176fbecb3d6f34278b', 111 | expectedDerive: 112 | '050df97f8c2ead654d9bb3ab8c9178edcd902a32f8495949feadcc1e0480c46b3604131bbd6e3ba573b6dd682fa0a63e5b165d39fc43a625d00207607a2bfeb65ff1d29292152e26b298868e3b87be95d6458f6f2ce6118437b632415abe6ad522874bcd79e4030a5e7bad2efa90a7a7c67e93f0a18fb28369d0a9329ab5c24134ccb0', 113 | }, 114 | { 115 | inputLen: 3073, 116 | expectedHash: 117 | '7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd39a27ae3b79d68d89da9bf25bc27139ae65a324918a5f9b7828181e52cf373c84f35b639b7fccbb985b6f2fa56aea0c18f531203497b8bbd3a07ceb5926f1cab74d14bd66486d9a91eba99059a98bd1cd25876b2af5a76c3e9eed554ed72ea952b603bf', 118 | expectedKeyed: 119 | '68dede9bef00ba89e43f31a6825f4cf433389fedae75c04ee9f0cf16a427c95a96d6da3fe985054d3478865be9a092250839a697bbda74e279e8a9e69f0025e4cfddd6cfb434b1cd9543aaf97c635d1b451a4386041e4bb100f5e45407cbbc24fa53ea2de3536ccb329e4eb9466ec37093a42cf62b82903c696a93a50b702c80f3c3c5', 120 | expectedDerive: 121 | '72613c9ec9ff7e40f8f5c173784c532ad852e827dba2bf85b2ab4b76f7079081576288e552647a9d86481c2cae75c2dd4e7c5195fb9ada1ef50e9c5098c249d743929191441301c69e1f48505a4305ec1778450ee48b8e69dc23a25960fe33070ea549119599760a8a2d28aeca06b8c5e9ba58bc19e11fe57b6ee98aa44b2a8e6b14a5', 122 | }, 123 | { 124 | inputLen: 4096, 125 | expectedHash: 126 | '015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e9690289e9409ddb1b99768eafe1623da896faf7e1114bebeadc1be30829b6f8af707d85c298f4f0ff4d9438aef948335612ae921e76d411c3a9111df62d27eaf871959ae0062b5492a0feb98ef3ed4af277f5395172dbe5c311918ea0074ce0036454f620', 127 | expectedKeyed: 128 | 'befc660aea2f1718884cd8deb9902811d332f4fc4a38cf7c7300d597a081bfc0bbb64a36edb564e01e4b4aaf3b060092a6b838bea44afebd2deb8298fa562b7b597c757b9df4c911c3ca462e2ac89e9a787357aaf74c3b56d5c07bc93ce899568a3eb17d9250c20f6c5f6c1e792ec9a2dcb715398d5a6ec6d5c54f586a00403a1af1de', 129 | expectedDerive: 130 | '1e0d7f3db8c414c97c6307cbda6cd27ac3b030949da8e23be1a1a924ad2f25b9d78038f7b198596c6cc4a9ccf93223c08722d684f240ff6569075ed81591fd93f9fff1110b3a75bc67e426012e5588959cc5a4c192173a03c00731cf84544f65a2fb9378989f72e9694a6a394a8a30997c2e67f95a504e631cd2c5f55246024761b245', 131 | }, 132 | { 133 | inputLen: 4097, 134 | expectedHash: 135 | '9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb99505f91b0b5600a11251652eacfa9497b31cd3c409ce2e45cfe6c0a016967316c426bd26f619eab5d70af9a418b845c608840390f361630bd497b1ab44019316357c61dbe091ce72fc16dc340ac3d6e009e050b3adac4b5b2c92e722cffdc46501531956', 136 | expectedKeyed: 137 | '00df940cd36bb9fa7cbbc3556744e0dbc8191401afe70520ba292ee3ca80abbc606db4976cfdd266ae0abf667d9481831ff12e0caa268e7d3e57260c0824115a54ce595ccc897786d9dcbf495599cfd90157186a46ec800a6763f1c59e36197e9939e900809f7077c102f888caaf864b253bc41eea812656d46742e4ea42769f89b83f', 138 | expectedDerive: 139 | 'aca51029626b55fda7117b42a7c211f8c6e9ba4fe5b7a8ca922f34299500ead8a897f66a400fed9198fd61dd2d58d382458e64e100128075fc54b860934e8de2e84170734b06e1d212a117100820dbc48292d148afa50567b8b84b1ec336ae10d40c8c975a624996e12de31abbe135d9d159375739c333798a80c64ae895e51e22f3ad', 140 | }, 141 | { 142 | inputLen: 5120, 143 | expectedHash: 144 | '9cadc15fed8b5d854562b26a9536d9707cadeda9b143978f319ab34230535833acc61c8fdc114a2010ce8038c853e121e1544985133fccdd0a2d507e8e615e611e9a0ba4f47915f49e53d721816a9198e8b30f12d20ec3689989175f1bf7a300eee0d9321fad8da232ece6efb8e9fd81b42ad161f6b9550a069e66b11b40487a5f5059', 145 | expectedKeyed: 146 | '2c493e48e9b9bf31e0553a22b23503c0a3388f035cece68eb438d22fa1943e209b4dc9209cd80ce7c1f7c9a744658e7e288465717ae6e56d5463d4f80cdb2ef56495f6a4f5487f69749af0c34c2cdfa857f3056bf8d807336a14d7b89bf62bef2fb54f9af6a546f818dc1e98b9e07f8a5834da50fa28fb5874af91bf06020d1bf0120e', 147 | expectedDerive: 148 | '7a7acac8a02adcf3038d74cdd1d34527de8a0fcc0ee3399d1262397ce5817f6055d0cefd84d9d57fe792d65a278fd20384ac6c30fdb340092f1a74a92ace99c482b28f0fc0ef3b923e56ade20c6dba47e49227166251337d80a037e987ad3a7f728b5ab6dfafd6e2ab1bd583a95d9c895ba9c2422c24ea0f62961f0dca45cad47bfa0d', 149 | }, 150 | { 151 | inputLen: 5121, 152 | expectedHash: 153 | '628bd2cb2004694adaab7bbd778a25df25c47b9d4155a55f8fbd79f2fe154cff96adaab0613a6146cdaabe498c3a94e529d3fc1da2bd08edf54ed64d40dcd6777647eac51d8277d70219a9694334a68bc8f0f23e20b0ff70ada6f844542dfa32cd4204ca1846ef76d811cdb296f65e260227f477aa7aa008bac878f72257484f2b6c95', 154 | expectedKeyed: 155 | '6ccf1c34753e7a044db80798ecd0782a8f76f33563accaddbfbb2e0ea4b2d0240d07e63f13667a8d1490e5e04f13eb617aea16a8c8a5aaed1ef6fbde1b0515e3c81050b361af6ead126032998290b563e3caddeaebfab592e155f2e161fb7cba939092133f23f9e65245e58ec23457b78a2e8a125588aad6e07d7f11a85b88d375b72d', 156 | expectedDerive: 157 | 'b07f01e518e702f7ccb44a267e9e112d403a7b3f4883a47ffbed4b48339b3c341a0add0ac032ab5aaea1e4e5b004707ec5681ae0fcbe3796974c0b1cf31a194740c14519273eedaabec832e8a784b6e7cfc2c5952677e6c3f2c3914454082d7eb1ce1766ac7d75a4d3001fc89544dd46b5147382240d689bbbaefc359fb6ae30263165', 158 | }, 159 | { 160 | inputLen: 6144, 161 | expectedHash: 162 | '3e2e5b74e048f3add6d21faab3f83aa44d3b2278afb83b80b3c35164ebeca2054d742022da6fdda444ebc384b04a54c3ac5839b49da7d39f6d8a9db03deab32aade156c1c0311e9b3435cde0ddba0dce7b26a376cad121294b689193508dd63151603c6ddb866ad16c2ee41585d1633a2cea093bea714f4c5d6b903522045b20395c83', 163 | expectedKeyed: 164 | '3d6b6d21281d0ade5b2b016ae4034c5dec10ca7e475f90f76eac7138e9bc8f1dc35754060091dc5caf3efabe0603c60f45e415bb3407db67e6beb3d11cf8e4f7907561f05dace0c15807f4b5f389c841eb114d81a82c02a00b57206b1d11fa6e803486b048a5ce87105a686dee041207e095323dfe172df73deb8c9532066d88f9da7e', 165 | expectedDerive: 166 | '2a95beae63ddce523762355cf4b9c1d8f131465780a391286a5d01abb5683a1597099e3c6488aab6c48f3c15dbe1942d21dbcdc12115d19a8b8465fb54e9053323a9178e4275647f1a9927f6439e52b7031a0b465c861a3fc531527f7758b2b888cf2f20582e9e2c593709c0a44f9c6e0f8b963994882ea4168827823eef1f64169fef', 167 | }, 168 | { 169 | inputLen: 6145, 170 | expectedHash: 171 | 'f1323a8631446cc50536a9f705ee5cb619424d46887f3c376c695b70e0f0507f18a2cfdd73c6e39dd75ce7c1c6e3ef238fd54465f053b25d21044ccb2093beb015015532b108313b5829c3621ce324b8e14229091b7c93f32db2e4e63126a377d2a63a3597997d4f1cba59309cb4af240ba70cebff9a23d5e3ff0cdae2cfd54e070022', 172 | expectedKeyed: 173 | '9ac301e9e39e45e3250a7e3b3df701aa0fb6889fbd80eeecf28dbc6300fbc539f3c184ca2f59780e27a576c1d1fb9772e99fd17881d02ac7dfd39675aca918453283ed8c3169085ef4a466b91c1649cc341dfdee60e32231fc34c9c4e0b9a2ba87ca8f372589c744c15fd6f985eec15e98136f25beeb4b13c4e43dc84abcc79cd4646c', 174 | expectedDerive: 175 | '379bcc61d0051dd489f686c13de00d5b14c505245103dc040d9e4dd1facab8e5114493d029bdbd295aaa744a59e31f35c7f52dba9c3642f773dd0b4262a9980a2aef811697e1305d37ba9d8b6d850ef07fe41108993180cf779aeece363704c76483458603bbeeb693cffbbe5588d1f3535dcad888893e53d977424bb707201569a8d2', 176 | }, 177 | { 178 | inputLen: 7168, 179 | expectedHash: 180 | '61da957ec2499a95d6b8023e2b0e604ec7f6b50e80a9678b89d2628e99ada77a5707c321c83361793b9af62a40f43b523df1c8633cecb4cd14d00bdc79c78fca5165b863893f6d38b02ff7236c5a9a8ad2dba87d24c547cab046c29fc5bc1ed142e1de4763613bb162a5a538e6ef05ed05199d751f9eb58d332791b8d73fb74e4fce95', 181 | expectedKeyed: 182 | 'b42835e40e9d4a7f42ad8cc04f85a963a76e18198377ed84adddeaecacc6f3fca2f01d5277d69bb681c70fa8d36094f73ec06e452c80d2ff2257ed82e7ba348400989a65ee8daa7094ae0933e3d2210ac6395c4af24f91c2b590ef87d7788d7066ea3eaebca4c08a4f14b9a27644f99084c3543711b64a070b94f2c9d1d8a90d035d52', 183 | expectedDerive: 184 | '11c37a112765370c94a51415d0d651190c288566e295d505defdad895dae223730d5a5175a38841693020669c7638f40b9bc1f9f39cf98bda7a5b54ae24218a800a2116b34665aa95d846d97ea988bfcb53dd9c055d588fa21ba78996776ea6c40bc428b53c62b5f3ccf200f647a5aae8067f0ea1976391fcc72af1945100e2a6dcb88', 185 | }, 186 | { 187 | inputLen: 7169, 188 | expectedHash: 189 | 'a003fc7a51754a9b3c7fae0367ab3d782dccf28855a03d435f8cfe74605e781798a8b20534be1ca9eb2ae2df3fae2ea60e48c6fb0b850b1385b5de0fe460dbe9d9f9b0d8db4435da75c601156df9d047f4ede008732eb17adc05d96180f8a73548522840779e6062d643b79478a6e8dbce68927f36ebf676ffa7d72d5f68f050b119c8', 190 | expectedKeyed: 191 | 'ed9b1a922c046fdb3d423ae34e143b05ca1bf28b710432857bf738bcedbfa5113c9e28d72fcbfc020814ce3f5d4fc867f01c8f5b6caf305b3ea8a8ba2da3ab69fabcb438f19ff11f5378ad4484d75c478de425fb8e6ee809b54eec9bdb184315dc856617c09f5340451bf42fd3270a7b0b6566169f242e533777604c118a6358250f54', 192 | expectedDerive: 193 | '554b0a5efea9ef183f2f9b931b7497995d9eb26f5c5c6dad2b97d62fc5ac31d99b20652c016d88ba2a611bbd761668d5eda3e568e940faae24b0d9991c3bd25a65f770b89fdcadabcb3d1a9c1cb63e69721cacf1ae69fefdcef1e3ef41bc5312ccc17222199e47a26552c6adc460cf47a72319cb5039369d0060eaea59d6c65130f1dd', 194 | }, 195 | { 196 | inputLen: 8192, 197 | expectedHash: 198 | 'aae792484c8efe4f19e2ca7d371d8c467ffb10748d8a5a1ae579948f718a2a635fe51a27db045a567c1ad51be5aa34c01c6651c4d9b5b5ac5d0fd58cf18dd61a47778566b797a8c67df7b1d60b97b19288d2d877bb2df417ace009dcb0241ca1257d62712b6a4043b4ff33f690d849da91ea3bf711ed583cb7b7a7da2839ba71309bbf', 199 | expectedKeyed: 200 | 'dc9637c8845a770b4cbf76b8daec0eebf7dc2eac11498517f08d44c8fc00d58a4834464159dcbc12a0ba0c6d6eb41bac0ed6585cabfe0aca36a375e6c5480c22afdc40785c170f5a6b8a1107dbee282318d00d915ac9ed1143ad40765ec120042ee121cd2baa36250c618adaf9e27260fda2f94dea8fb6f08c04f8f10c78292aa46102', 201 | expectedDerive: 202 | 'ad01d7ae4ad059b0d33baa3c01319dcf8088094d0359e5fd45d6aeaa8b2d0c3d4c9e58958553513b67f84f8eac653aeeb02ae1d5672dcecf91cd9985a0e67f4501910ecba25555395427ccc7241d70dc21c190e2aadee875e5aae6bf1912837e53411dabf7a56cbf8e4fb780432b0d7fe6cec45024a0788cf5874616407757e9e6bef7', 203 | }, 204 | { 205 | inputLen: 8193, 206 | expectedHash: 207 | 'bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3bb2282aa69be089359ea1154b9a9286c4a56af4de975a9aa4a5c497654914d279bea60bb6d2cf7225a2fa0ff5ef56bbe4b149f3ed15860f78b4e2ad04e158e375c1e0c0b551cd7dfc82f1b155c11b6b3ed51ec9edb30d133653bb5709d1dbd55f4e1ff6', 208 | expectedKeyed: 209 | '954a2a75420c8d6547e3ba5b98d963e6fa6491addc8c023189cc519821b4a1f5f03228648fd983aef045c2fa8290934b0866b615f585149587dda2299039965328835a2b18f1d63b7e300fc76ff260b571839fe44876a4eae66cbac8c67694411ed7e09df51068a22c6e67d6d3dd2cca8ff12e3275384006c80f4db68023f24eebba57', 210 | expectedDerive: 211 | 'af1e0346e389b17c23200270a64aa4e1ead98c61695d917de7d5b00491c9b0f12f20a01d6d622edf3de026a4db4e4526225debb93c1237934d71c7340bb5916158cbdafe9ac3225476b6ab57a12357db3abbad7a26c6e66290e44034fb08a20a8d0ec264f309994d2810c49cfba6989d7abb095897459f5425adb48aba07c5fb3c83c0', 212 | }, 213 | { 214 | inputLen: 16384, 215 | expectedHash: 216 | 'f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde49d764c270176e53e97bdffa58d549073f2c660be0e81293767ed4e4929f9ad34bbb39a529334c57c4a381ffd2a6d4bfdbf1482651b172aa883cc13408fa67758a3e47503f93f87720a3177325f7823251b85275f64636a8f1d599c2e49722f42e93893', 217 | expectedKeyed: 218 | '9e9fc4eb7cf081ea7c47d1807790ed211bfec56aa25bb7037784c13c4b707b0df9e601b101e4cf63a404dfe50f2e1865bb12edc8fca166579ce0c70dba5a5c0fc960ad6f3772183416a00bd29d4c6e651ea7620bb100c9449858bf14e1ddc9ecd35725581ca5b9160de04060045993d972571c3e8f71e9d0496bfa744656861b169d65', 219 | expectedDerive: 220 | '160e18b5878cd0df1c3af85eb25a0db5344d43a6fbd7a8ef4ed98d0714c3f7e160dc0b1f09caa35f2f417b9ef309dfe5ebd67f4c9507995a531374d099cf8ae317542e885ec6f589378864d3ea98716b3bbb65ef4ab5e0ab5bb298a501f19a41ec19af84a5e6b428ecd813b1a47ed91c9657c3fba11c406bc316768b58f6802c9e9b57', 221 | }, 222 | { 223 | inputLen: 31744, 224 | expectedHash: 225 | '62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47860cc51f2b0c28a7b77304bd55fe73af663c02d3f52ea053ba43431ca5bab7bfea2f5e9d7121770d88f70ae9649ea713087d1914f7f312147e247f87eb2d4ffef0ac978bf7b6579d57d533355aa20b8b77b13fd09748728a5cc327a8ec470f4013226f', 226 | expectedKeyed: 227 | 'efa53b389ab67c593dba624d898d0f7353ab99e4ac9d42302ee64cbf9939a4193a7258db2d9cd32a7a3ecfce46144114b15c2fcb68a618a976bd74515d47be08b628be420b5e830fade7c080e351a076fbc38641ad80c736c8a18fe3c66ce12f95c61c2462a9770d60d0f77115bbcd3782b593016a4e728d4c06cee4505cb0c08a42ec', 228 | expectedDerive: 229 | '39772aef80e0ebe60596361e45b061e8f417429d529171b6764468c22928e28e9759adeb797a3fbf771b1bcea30150a020e317982bf0d6e7d14dd9f064bc11025c25f31e81bd78a921db0174f03dd481d30e93fd8e90f8b2fee209f849f2d2a52f31719a490fb0ba7aea1e09814ee912eba111a9fde9d5c274185f7bae8ba85d300a2b', 230 | }, 231 | ], 232 | }; 233 | --------------------------------------------------------------------------------