├── .github ├── FUNDING.yml └── workflows │ ├── node-aught.yml │ ├── node-pretest.yml │ ├── node-tens.yml │ ├── node-twenties.yml │ ├── rebase.yml │ └── require-allow-edits.yml ├── .gitignore ├── .npmrc ├── .nycrc ├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ljharb] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: npm/hash-base 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/workflows/node-aught.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: node.js < 10' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | tests: 7 | uses: ljharb/actions/.github/workflows/node.yml@main 8 | with: 9 | range: '0.10 - 9' 10 | type: minors 11 | command: npm run tests-only 12 | -------------------------------------------------------------------------------- /.github/workflows/node-pretest.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: pretest/posttest' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | tests: 7 | uses: ljharb/actions/.github/workflows/pretest.yml@main 8 | -------------------------------------------------------------------------------- /.github/workflows/node-tens.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: node.js >= 10' 2 | 3 | on: [pull_request, push] 4 | 5 | jobs: 6 | tests: 7 | uses: ljharb/actions/.github/workflows/node.yml@main 8 | with: 9 | range: '>= 10' 10 | type: minors 11 | command: npm run tests-only 12 | -------------------------------------------------------------------------------- /.github/workflows/node-twenties.yml: -------------------------------------------------------------------------------- 1 | name: 'Tests: node.js >= 20' 2 | 3 | on: [pull_request, push] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | tests: 10 | uses: ljharb/actions/.github/workflows/node.yml@main 11 | with: 12 | range: '>= 20' 13 | type: minors 14 | command: npm run tests-only 15 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Rebase 2 | 3 | on: [pull_request_target] 4 | 5 | permissions: 6 | contents: read 7 | 8 | jobs: 9 | _: 10 | permissions: 11 | contents: write # for ljharb/rebase to push code to rebase 12 | pull-requests: read # for ljharb/rebase to get info about PR 13 | 14 | name: "Automatic Rebase" 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - uses: ljharb/rebase@master 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /.github/workflows/require-allow-edits.yml: -------------------------------------------------------------------------------- 1 | name: Require “Allow Edits” 2 | 3 | on: [pull_request_target] 4 | 5 | jobs: 6 | _: 7 | name: "Require “Allow Edits”" 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: ljharb/require-allow-edits@main 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | node_modules 4 | 5 | npm-debug.log 6 | 7 | # Only apps should have lockfiles 8 | npm-shrinkwrap.json 9 | package-lock.json 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "all": true, 3 | "check-coverage": false, 4 | "reporter": ["text-summary", "text", "html", "json"], 5 | "exclude": [ 6 | "coverage", 7 | "test" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Kirill Fomichev 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hash-base 2 | 3 | [![npm Package](https://img.shields.io/npm/v/hash-base.svg?style=flat-square)](https://www.npmjs.org/package/hash-base) 4 | [![Build Status](https://img.shields.io/travis/crypto-browserify/hash-base.svg?branch=master&style=flat-square)](https://travis-ci.org/crypto-browserify/hash-base) 5 | [![Dependency status](https://img.shields.io/david/crypto-browserify/hash-base.svg?style=flat-square)](https://david-dm.org/crypto-browserify/hash-base#info=dependencies) 6 | 7 | Abstract base class to inherit from if you want to create streams implementing the same API as node crypto [Hash][1] (for [Cipher][2] / [Decipher][3] check [crypto-browserify/cipher-base][4]). 8 | 9 | ## Example 10 | 11 | ```js 12 | const HashBase = require('hash-base'); 13 | const inherits = require('inherits'); 14 | 15 | // our hash function is XOR sum of all bytes 16 | function MyHash () { 17 | HashBase.call(this, 1); // in bytes 18 | 19 | this._sum = 0x00; 20 | }; 21 | 22 | inherits(MyHash, HashBase) 23 | 24 | MyHash.prototype._update = function () { 25 | for (let i = 0; i < this._block.length; ++i) { 26 | this._sum ^= this._block[i]; 27 | } 28 | }; 29 | 30 | MyHash.prototype._digest = function () { 31 | return this._sum; 32 | }; 33 | 34 | const data = Buffer.from([0x00, 0x42, 0x01]); 35 | const hash = new MyHash().update(data).digest(); 36 | console.log(hash); // => 67 37 | ``` 38 | You also can check [source code](index.js) or [crypto-browserify/md5.js][5] 39 | 40 | ## LICENSE 41 | 42 | MIT 43 | 44 | [1]: https://nodejs.org/api/crypto.html#crypto_class_hash 45 | [2]: https://nodejs.org/api/crypto.html#crypto_class_cipher 46 | [3]: https://nodejs.org/api/crypto.html#crypto_class_decipher 47 | [4]: https://github.com/crypto-browserify/cipher-base 48 | [5]: https://github.com/crypto-browserify/md5.js 49 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | var Buffer = require('safe-buffer').Buffer 3 | var Transform = require('readable-stream').Transform 4 | var inherits = require('inherits') 5 | 6 | function HashBase (blockSize) { 7 | Transform.call(this) 8 | 9 | this._block = Buffer.allocUnsafe(blockSize) 10 | this._blockSize = blockSize 11 | this._blockOffset = 0 12 | this._length = [0, 0, 0, 0] 13 | 14 | this._finalized = false 15 | } 16 | 17 | inherits(HashBase, Transform) 18 | 19 | HashBase.prototype._transform = function (chunk, encoding, callback) { 20 | var error = null 21 | try { 22 | this.update(chunk, encoding) 23 | } catch (err) { 24 | error = err 25 | } 26 | 27 | callback(error) 28 | } 29 | 30 | HashBase.prototype._flush = function (callback) { 31 | var error = null 32 | try { 33 | this.push(this.digest()) 34 | } catch (err) { 35 | error = err 36 | } 37 | 38 | callback(error) 39 | } 40 | 41 | var useUint8Array = typeof Uint8Array !== 'undefined' 42 | var useArrayBuffer = typeof ArrayBuffer !== 'undefined' && 43 | typeof Uint8Array !== 'undefined' && 44 | ArrayBuffer.isView && 45 | (Buffer.prototype instanceof Uint8Array || Buffer.TYPED_ARRAY_SUPPORT) 46 | 47 | function toBuffer (data, encoding) { 48 | // No need to do anything for exact instance 49 | // This is only valid when safe-buffer.Buffer === buffer.Buffer, i.e. when Buffer.from/Buffer.alloc existed 50 | if (data instanceof Buffer) return data 51 | 52 | // Convert strings to Buffer 53 | if (typeof data === 'string') return Buffer.from(data, encoding) 54 | 55 | /* 56 | * Wrap any TypedArray instances and DataViews 57 | * Makes sense only on engines with full TypedArray support -- let Buffer detect that 58 | */ 59 | if (useArrayBuffer && ArrayBuffer.isView(data)) { 60 | if (data.byteLength === 0) return Buffer.alloc(0) // Bug in Node.js <6.3.1, which treats this as out-of-bounds 61 | var res = Buffer.from(data.buffer, data.byteOffset, data.byteLength) 62 | // Recheck result size, as offset/length doesn't work on Node.js <5.10 63 | // We just go to Uint8Array case if this fails 64 | if (res.byteLength === data.byteLength) return res 65 | } 66 | 67 | /* 68 | * Uint8Array in engines where Buffer.from might not work with ArrayBuffer, just copy over 69 | * Doesn't make sense with other TypedArray instances 70 | */ 71 | if (useUint8Array && data instanceof Uint8Array) return Buffer.from(data) 72 | 73 | /* 74 | * Old Buffer polyfill on an engine that doesn't have TypedArray support 75 | * Also, this is from a different Buffer polyfill implementation then we have, as instanceof check failed 76 | * Convert to our current Buffer implementation 77 | */ 78 | if ( 79 | Buffer.isBuffer(data) && 80 | data.constructor && 81 | typeof data.constructor.isBuffer === 'function' && 82 | data.constructor.isBuffer(data) 83 | ) { 84 | return Buffer.from(data) 85 | } 86 | 87 | throw new TypeError('The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.') 88 | } 89 | 90 | HashBase.prototype.update = function (data, encoding) { 91 | if (this._finalized) throw new Error('Digest already called') 92 | 93 | data = toBuffer(data, encoding) // asserts correct input type 94 | 95 | // consume data 96 | var block = this._block 97 | var offset = 0 98 | while (this._blockOffset + data.length - offset >= this._blockSize) { 99 | for (var i = this._blockOffset; i < this._blockSize;) block[i++] = data[offset++] 100 | this._update() 101 | this._blockOffset = 0 102 | } 103 | while (offset < data.length) block[this._blockOffset++] = data[offset++] 104 | 105 | // update length 106 | for (var j = 0, carry = data.length * 8; carry > 0; ++j) { 107 | this._length[j] += carry 108 | carry = (this._length[j] / 0x0100000000) | 0 109 | if (carry > 0) this._length[j] -= 0x0100000000 * carry 110 | } 111 | 112 | return this 113 | } 114 | 115 | HashBase.prototype._update = function () { 116 | throw new Error('_update is not implemented') 117 | } 118 | 119 | HashBase.prototype.digest = function (encoding) { 120 | if (this._finalized) throw new Error('Digest already called') 121 | this._finalized = true 122 | 123 | var digest = this._digest() 124 | if (encoding !== undefined) digest = digest.toString(encoding) 125 | 126 | // reset state 127 | this._block.fill(0) 128 | this._blockOffset = 0 129 | for (var i = 0; i < 4; ++i) this._length[i] = 0 130 | 131 | return digest 132 | } 133 | 134 | HashBase.prototype._digest = function () { 135 | throw new Error('_digest is not implemented') 136 | } 137 | 138 | module.exports = HashBase 139 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hash-base", 3 | "version": "3.1.0", 4 | "description": "abstract base class for hash-streams", 5 | "keywords": [ 6 | "hash", 7 | "stream" 8 | ], 9 | "homepage": "https://github.com/crypto-browserify/hash-base", 10 | "bugs": { 11 | "url": "https://github.com/crypto-browserify/hash-base/issues" 12 | }, 13 | "license": "MIT", 14 | "author": "Kirill Fomichev (https://github.com/fanatid)", 15 | "files": [ 16 | "index.js" 17 | ], 18 | "main": "index.js", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/crypto-browserify/hash-base.git" 22 | }, 23 | "scripts": { 24 | "lint": "standard", 25 | "pretest": "npm run lint", 26 | "test": "npm run tests-only", 27 | "tests-only": "nyc tape 'test/**/*.js'", 28 | "posttest": "npx npm@'>=10.2' audit --production" 29 | }, 30 | "dependencies": { 31 | "inherits": "^2.0.4", 32 | "readable-stream": "^2.3.8", 33 | "safe-buffer": "^5.2.1" 34 | }, 35 | "devDependencies": { 36 | "nyc": "^10.3.2", 37 | "standard": "^14.3.3", 38 | "tape": "^5.9.0" 39 | }, 40 | "engines": { 41 | "node": ">= 0.10" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | var test = require('tape') 4 | var HashBase = require('../') 5 | var Buffer = require('safe-buffer').Buffer 6 | 7 | var utf8text = 'УТФ-8 text' 8 | var utf8buf = Buffer.from(utf8text, 'utf8') 9 | function noop () {} 10 | 11 | test('HashBase#_transform', function (t) { 12 | t.test('should use HashBase#update', function (t) { 13 | t.plan(3) 14 | var base = new HashBase(64) 15 | base.update = function () { 16 | t.same(arguments[0], utf8text) 17 | t.same(arguments[1], 'utf8') 18 | } 19 | base._transform(utf8text, 'utf8', function (err) { 20 | t.same(err, null) 21 | }) 22 | 23 | t.end() 24 | }) 25 | 26 | t.test('should handle error in HashBase#update', function (t) { 27 | t.plan(1) 28 | var err = new Error('hey') 29 | var base = new HashBase(64) 30 | base.update = function () { throw err } 31 | base._transform(Buffer.allocUnsafe(0), 'buffer', function (_err) { 32 | t.true(_err === err) 33 | }) 34 | 35 | t.end() 36 | }) 37 | 38 | t.end() 39 | }) 40 | 41 | test('HashBase#_flush', function (t) { 42 | t.test('should use HashBase#digest', function (t) { 43 | t.plan(2) 44 | var buffer = Buffer.allocUnsafe(0) 45 | var base = new HashBase(64) 46 | base.push = function (data) { t.true(data === buffer) } 47 | base.digest = function () { return buffer } 48 | base._flush(function (err) { t.same(err, null) }) 49 | 50 | t.end() 51 | }) 52 | 53 | t.test('should handle errors in HashBase#digest', function (t) { 54 | t.plan(1) 55 | var base = new HashBase(64) 56 | var err = new Error('hey') 57 | base.digest = function () { throw err } 58 | base._flush(function (_err) { t.true(_err === err) }) 59 | 60 | t.end() 61 | }) 62 | 63 | t.end() 64 | }) 65 | 66 | test('HashBase#update', function (t) { 67 | t.test('only string or buffer is allowed', function (t) { 68 | var base = new HashBase(64) 69 | t.throws(function () { 70 | base.update(null) 71 | }, /^TypeError: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView.$/) 72 | t.end() 73 | }) 74 | 75 | t.test('should throw error after HashBase#digest', function (t) { 76 | var base = new HashBase(64) 77 | base._digest = noop 78 | base.digest() 79 | t.throws(function () { 80 | base.update('') 81 | }, /^Error: Digest already called$/) 82 | t.end() 83 | }) 84 | 85 | t.test('should use HashBase#_update', function (t) { 86 | t.plan(1) 87 | 88 | var base = new HashBase(64) 89 | base._update = t.pass 90 | base.update(Buffer.allocUnsafe(64)) 91 | 92 | t.end() 93 | }) 94 | 95 | t.test('default encoding is utf8', function (t) { 96 | t.plan(1) 97 | 98 | var buffer = Buffer.allocUnsafe(64) 99 | buffer.fill(0) 100 | utf8buf.copy(buffer) 101 | var base = new HashBase(64) 102 | base._update = function () { t.same(this._block, buffer) } 103 | base.update(buffer.toString('utf8')) 104 | 105 | t.end() 106 | }) 107 | 108 | t.test('decode string with custom encoding', function (t) { 109 | t.plan(1) 110 | var buffer = Buffer.allocUnsafe(64) 111 | buffer.fill(0x42) 112 | var base = new HashBase(64) 113 | base._update = function () { t.same(this._block, buffer) } 114 | base.update(buffer.toString('hex'), 'hex') 115 | 116 | t.end() 117 | }) 118 | 119 | t.test('data length is more than 2^32 bits', function (t) { 120 | var base = new HashBase(64) 121 | base._length = [Math.pow(2, 32) - 1, 0, 0, 0] 122 | base.update(Buffer.allocUnsafe(1)) 123 | t.same(base._length, [7, 1, 0, 0]) 124 | 125 | t.end() 126 | }) 127 | 128 | t.test('should return `this`', function (t) { 129 | var base = new HashBase(64) 130 | t.same(base.update(Buffer.allocUnsafe(0)), base) 131 | 132 | t.end() 133 | }) 134 | 135 | t.test( 136 | 'handle UInt16Array', 137 | { 138 | skip: !( 139 | ArrayBuffer.isView && 140 | (Buffer.prototype instanceof Uint8Array || Buffer.TYPED_ARRAY_SUPPORT) 141 | ) && 'ArrayBuffer.isView and TypedArray fully supported' 142 | }, 143 | function (t) { 144 | var base = new HashBase(64) 145 | 146 | base._update = noop 147 | base.update(new Uint16Array([1234, 512])) 148 | t.same(base._block.slice(0, base._blockOffset), Buffer.from('d2040002', 'hex')) 149 | 150 | t.end() 151 | } 152 | ) 153 | 154 | t.end() 155 | }) 156 | 157 | test('HashBase#_update', function (t) { 158 | t.test('is not implemented', function (t) { 159 | var base = new HashBase(64) 160 | t.throws(function () { 161 | base._update() 162 | }, /^Error: _update is not implemented$/) 163 | t.end() 164 | }) 165 | 166 | t.end() 167 | }) 168 | 169 | test('HashBase#digest', function (t) { 170 | t.test('should throw error on second call', function (t) { 171 | var base = new HashBase(64) 172 | base._digest = noop 173 | base.digest() 174 | t.throws(function () { 175 | base.digest() 176 | }, /^Error: Digest already called$/) 177 | t.end() 178 | }) 179 | 180 | t.test('should use HashBase#_digest', function (t) { 181 | t.plan(1) 182 | 183 | var base = new HashBase(64) 184 | base._digest = t.pass 185 | base.digest() 186 | 187 | t.end() 188 | }) 189 | 190 | t.test('should return buffer by default', function (t) { 191 | var base = new HashBase(64) 192 | 193 | base._digest = function () { return utf8buf } 194 | t.same(base.digest(), utf8buf) 195 | 196 | t.end() 197 | }) 198 | 199 | t.test('should encode result with custom encoding', function (t) { 200 | var base = new HashBase(64) 201 | base._digest = function () { return utf8buf } 202 | t.same(base.digest('utf8'), utf8text) 203 | 204 | t.end() 205 | }) 206 | 207 | t.end() 208 | }) 209 | 210 | test('HashBase#_digest', function (t) { 211 | t.test('is not implemented', function (t) { 212 | var base = new HashBase(64) 213 | t.throws(function () { 214 | base._digest() 215 | }, /^Error: _digest is not implemented$/) 216 | t.end() 217 | }) 218 | 219 | t.end() 220 | }) 221 | --------------------------------------------------------------------------------