Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use secure sha256 algorithm #12

Merged
merged 3 commits into from
Jul 14, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 14 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@
[![Codecov][codecov-src]][codecov-href]
[![bundle size][bundle-src]][bundle-href]

> Super fast hashing library based on murmurhash3 written in Vanilla JS
> Super fast hashing library written in Vanilla JS
## Usage

@@ -27,22 +27,22 @@ Import:

```js
// ESM
import { hash, objectHash, murmurHash } from 'ohash'
import { hash, objectHash, murmurHash, sha256 } from 'ohash'

// CommonJS
const { hash, objectHash, murmurHash } = require('ohash')
const { hash, objectHash, murmurHash, sha256 } = require('ohash')
```

### `hash(object, options?)`

Converts object value into a string hash using `objectHash` and then applies `murmurHash`.
Converts object value into a string hash using `objectHash` and then applies `sha256` (trimmed by length of 10).

Usage:

```js
import { hash } from 'ohash'

// "2736179692"
// "7596ed03b7"
console.log(hash({ foo: 'bar'}))
```

@@ -61,7 +61,7 @@ console.log(objectHash({ foo: 'bar'}))

### `murmurHash(str)`

Converts input string (of any length) into a 32-bit positive integer using MurmurHash3.
Converts input string (of any length) into a 32-bit positive integer using [MurmurHash3]((https://en.wikipedia.org/wiki/MurmurHash)).

Usage:

@@ -72,21 +72,16 @@ import { murmurHash } from 'ohash'
console.log(murmurHash('Hello World'))
```

## What is MurmurHash
### `sha256`

[MurmurHash](https://en.wikipedia.org/wiki/MurmurHash) is a non-cryptographic hash function created by Austin Appleby.
Create a secure [SHA 256](https://en.wikipedia.org/wiki/SHA-2) digest from input string.

According to [murmurhash website](https://sites.google.com/site/murmurhash):

✅ Extremely simple - compiles down to ~52 instructions on x86.

✅ Excellent distribution - Passes chi-squared tests for practically all keysets & bucket sizes.

✅ Excellent avalanche behavior - Maximum bias is under 0.5%.

✅ Excellent collision resistance - Passes Bob Jenkin's frog.c torture-test. No collisions possible for 4-byte keys, no small (1- to 7-bit) differentials.
```js
import { sha256 } from 'ohash'

✅ Excellent performance
// "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"
console.log(sha256('Hello World'))
```

## 💻 Development

@@ -102,7 +97,7 @@ Made with 💛
Published under [MIT License](./LICENSE).

Based on [puleos/object-hash](https://github.com/puleos/object-hash) by [Scott Puleo](https://github.com/puleos/), and implementations from [perezd/node-murmurhash](perezd/node-murmurhash) and
[garycourt/murmurhash-js](https://github.com/garycourt/murmurhash-js) by [Gary Court](mailto:gary.court@gmail.com) and [Austin Appleby](mailto:aappleby@gmail.com).
[garycourt/murmurhash-js](https://github.com/garycourt/murmurhash-js) by [Gary Court](mailto:gary.court@gmail.com) and [Austin Appleby](mailto:aappleby@gmail.com) and [brix/crypto-js](https://github.com/brix/crypto-js).

<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/ohash?style=flat-square
174 changes: 174 additions & 0 deletions src/crypto/core.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// Based on https://github.com/brix/crypto-js 4.1.1 (MIT)

export class WordArray {
words: number[]
sigBytes: number

constructor (words?, sigBytes?) {
words = this.words = words || []

if (sigBytes !== undefined) {
this.sigBytes = sigBytes
} else {
this.sigBytes = words.length * 4
}
}

toString (encoder?) {
return (encoder || Hex).stringify(this)
}

concat (wordArray) {
// Clamp excess bits
this.clamp()

// Concat
if (this.sigBytes % 4) {
// Copy one byte at a time
for (let i = 0; i < wordArray.sigBytes; i++) {
const thatByte = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xFF
this.words[(this.sigBytes + i) >>> 2] |= thatByte << (24 - ((this.sigBytes + i) % 4) * 8)
}
} else {
// Copy one word at a time
for (let j = 0; j < wordArray.sigBytes; j += 4) {
this.words[(this.sigBytes + j) >>> 2] = wordArray.words[j >>> 2]
}
}
this.sigBytes += wordArray.sigBytes

// Chainable
return this
}

clamp () {
// Clamp
this.words[this.sigBytes >>> 2] &= 0xFFFFFFFF << (32 - (this.sigBytes % 4) * 8)
this.words.length = Math.ceil(this.sigBytes / 4)
}

clone () {
return new WordArray(this.words.slice(0))
}
}

export const Hex = {
stringify (wordArray) {
// Convert
const hexChars = []
for (let i = 0; i < wordArray.sigBytes; i++) {
const bite = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xFF
hexChars.push((bite >>> 4).toString(16))
hexChars.push((bite & 0x0F).toString(16))
}

return hexChars.join('')
}
}

export const Latin1 = {
parse (latin1Str) {
// Shortcut
const latin1StrLength = latin1Str.length

// Convert
const words = []
for (let i = 0; i < latin1StrLength; i++) {
words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xFF) << (24 - (i % 4) * 8)
}

return new WordArray(words, latin1StrLength)
}
}

export const Utf8 = {
parse (utf8Str) {
return Latin1.parse(unescape(encodeURIComponent(utf8Str)))
}
}

export class BufferedBlockAlgorithm {
_data: WordArray
_nDataBytes: number
_minBufferSize: number = 0
blockSize = 512 / 32

constructor () {
this.reset()
}

reset () {
// Initial values
this._data = new WordArray()
this._nDataBytes = 0
}

_append (data) {
// Convert string to WordArray, else assume WordArray already
if (typeof data === 'string') {
data = Utf8.parse(data)
}

// Append
this._data.concat(data)
this._nDataBytes += data.sigBytes
}

_doProcessBlock (_dataWords, _offset) {}

_process (doFlush?: Boolean) {
let processedWords

// Count blocks ready
let nBlocksReady = this._data.sigBytes / (this.blockSize * 4 /* bytes */)
if (doFlush) {
// Round up to include partial blocks
nBlocksReady = Math.ceil(nBlocksReady)
} else {
// Round down to include only full blocks,
// less the number of blocks that must remain in the buffer
nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0)
}

// Count words ready
const nWordsReady = nBlocksReady * this.blockSize

// Count bytes ready
const nBytesReady = Math.min(nWordsReady * 4, this._data.sigBytes)

// Process blocks
if (nWordsReady) {
for (let offset = 0; offset < nWordsReady; offset += this.blockSize) {
// Perform concrete-algorithm logic
this._doProcessBlock(this._data.words, offset)
}

// Remove processed words
processedWords = this._data.words.splice(0, nWordsReady)
this._data.sigBytes -= nBytesReady
}

// Return processed words
return new WordArray(processedWords, nBytesReady)
}
}

export class Hasher extends BufferedBlockAlgorithm {
update (messageUpdate) {
// Append
this._append(messageUpdate)

// Update the hash
this._process()

// Chainable
return this
}

finalize (messageUpdate) {
// Final message update
if (messageUpdate) {
this._append(messageUpdate)
}
}
}
File renamed without changes.
145 changes: 145 additions & 0 deletions src/crypto/sha256.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Based on https://github.com/brix/crypto-js 4.1.1 (MIT)

import { WordArray, Hasher } from './core'

// Initialization and round constants tables
const H = []
const K = [];

// Compute constants
(function () {
function isPrime (n) {
const sqrtN = Math.sqrt(n)
for (let factor = 2; factor <= sqrtN; factor++) {
if (!(n % factor)) {
return false
}
}

return true
}

function getFractionalBits (n) {
return ((n - (n | 0)) * 0x100000000) | 0
}

let n = 2
let nPrime = 0
while (nPrime < 64) {
if (isPrime(n)) {
if (nPrime < 8) {
H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2))
}
K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3))

nPrime++
}

n++
}
}())

// Reusable object
const W = []

/**
* SHA-256 hash algorithm.
*/
export class SHA256 extends Hasher {
_hash: WordArray

constructor () {
super()
this.reset()
}

reset () {
super.reset()
this._hash = new WordArray(H.slice(0))
}

_doProcessBlock (M, offset) {
// Shortcut
const H = this._hash.words

// Working variables
let a = H[0]
let b = H[1]
let c = H[2]
let d = H[3]
let e = H[4]
let f = H[5]
let g = H[6]
let h = H[7]

// Computation
for (let i = 0; i < 64; i++) {
if (i < 16) {
W[i] = M[offset + i] | 0
} else {
const gamma0x = W[i - 15]
const gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^
((gamma0x << 14) | (gamma0x >>> 18)) ^
(gamma0x >>> 3)

const gamma1x = W[i - 2]
const gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^
((gamma1x << 13) | (gamma1x >>> 19)) ^
(gamma1x >>> 10)

W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]
}

const ch = (e & f) ^ (~e & g)
const maj = (a & b) ^ (a & c) ^ (b & c)

const sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22))
const sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25))

const t1 = h + sigma1 + ch + K[i] + W[i]
const t2 = sigma0 + maj

h = g
g = f
f = e
e = (d + t1) | 0
d = c
c = b
b = a
a = (t1 + t2) | 0
}

// Intermediate hash value
H[0] = (H[0] + a) | 0
H[1] = (H[1] + b) | 0
H[2] = (H[2] + c) | 0
H[3] = (H[3] + d) | 0
H[4] = (H[4] + e) | 0
H[5] = (H[5] + f) | 0
H[6] = (H[6] + g) | 0
H[7] = (H[7] + h) | 0
}

finalize (messageUpdate) {
super.finalize(messageUpdate)

const nBitsTotal = this._nDataBytes * 8
const nBitsLeft = this._data.sigBytes * 8

// Add padding
this._data.words[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32)
this._data.words[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000)
this._data.words[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal
this._data.sigBytes = this._data.words.length * 4

// Hash final blocks
this._process()

// Return final computed hash
return this._hash
}
}

export function sha256 (message: string) {
return new SHA256().finalize(message).toString()
}
4 changes: 2 additions & 2 deletions src/hash.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { objectHash, HashOptions } from './object-hash'
import { murmurHash } from './murmur'
import { sha256 } from './crypto/sha256'

/**
* Hash any JS value into a string
@@ -10,5 +10,5 @@ import { murmurHash } from './murmur'
*/
export function hash (object: any, options: HashOptions = {}): string {
const hashed = typeof object === 'string' ? object : objectHash(object, options)
return String(murmurHash(hashed))
return sha256(hashed).substr(0, 10)
}
7 changes: 4 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './murmur'
export * from './object-hash'
export * from './hash'
export { objectHash } from './object-hash'
export { hash } from './hash'
export { murmurHash } from './crypto/murmur'
export { sha256 } from './crypto/sha256'
9 changes: 7 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { expect, it } from 'vitest'
import { murmurHash, objectHash, hash } from '../src'
import { murmurHash, objectHash, hash, sha256 } from '../src'

it('murmurHash', () => {
expect(murmurHash('Hello World')).toMatchInlineSnapshot('2708020327')
})

it('sha256', () => {
expect(sha256('Hello World')).toMatchInlineSnapshot('"a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e"')
expect(sha256('')).toMatchInlineSnapshot('"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"')
})

it('objectHash', () => {
expect(objectHash({ foo: 'bar' })).toMatchInlineSnapshot('"object:1:string:3:foo:string:3:bar,"')
})

it('hash', () => {
expect(hash({ foo: 'bar' })).toMatchInlineSnapshot('"2736179692"')
expect(hash({ foo: 'bar' })).toMatchInlineSnapshot('"7596ed03b7"')
})
3,581 changes: 3,581 additions & 0 deletions yarn.lock

Large diffs are not rendered by default.