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

Optional xxHash support and native SHA2-256 from browser Web Crypto API or Node.js crypto #15

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
11 changes: 10 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,19 @@
"@types/node": "^18.0.4",
"c8": "latest",
"eslint": "latest",
"hash-wasm": "^4.9.0",
"standard-version": "latest",
"typescript": "latest",
"unbuild": "latest",
"vitest": "latest"
},
"packageManager": "pnpm@7.5.2"
"packageManager": "pnpm@7.5.2",
"peerDependencies": {
"hash-wasm": "^4.9.0"
},
"peerDependenciesMeta": {
"hash-wasm": {
"optional": true
}
}
}
8 changes: 7 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/hash-fn.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type HashFn = (message: string) => Promise<string> | string;
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { objectHash } from './object-hash'
export { hash } from './hash'
export { murmurHash } from './crypto/murmur'
export { sha256 } from './crypto/sha256'
export { useHash } from './use-hash'
41 changes: 41 additions & 0 deletions src/init-sha.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import type { HashFn } from './hash-fn.type'
import { toBase64 } from './to-base64'
import { sha256base64 } from './crypto/sha256'

export const ALGORITHM = 'SHA-256'
export const MAX_LENGTH = 10

function getWebCryptoHash (digest: SubtleCrypto['digest']): HashFn {
const encoder = new TextEncoder()

return message => digest(ALGORITHM, encoder.encode(message))
.then(arrayBuffer => toBase64(arrayBuffer).slice(0, MAX_LENGTH))
}

/**
* Init SHA2-256
* @description Works with Web Crypto API, Node.js `crypto` module and JavaScript
*/
export async function initSha (): Promise<HashFn> {
// Web Crypto API, global: browser, Node.js >= 17.6.0 with `--experimental-global-webcrypto` flag
if (typeof crypto !== 'undefined' && crypto?.subtle) {
return getWebCryptoHash(crypto.subtle.digest)
}

// Web Crypto API: Node.js >= 15
try {
const { webcrypto } = await import('crypto')
return getWebCryptoHash(webcrypto.subtle.digest)
} catch (_e) {
}

// Crypto API: Node.js
try {
const { createHash } = await import('crypto')
return message => createHash(ALGORITHM).update(message).digest('base64').slice(0, MAX_LENGTH)
} catch (_e) {
}

// Fallback to JavaScript
return message => sha256base64(message).slice(0, MAX_LENGTH)
}
18 changes: 18 additions & 0 deletions src/init-xx-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { HashFn } from './hash-fn.type'

/**
* Init xxHash
* @description Only if `hash-wasm` peer dependency is installed
*/
export async function initXxHash (): Promise<HashFn> {
const { createXXHash32 } = await import('hash-wasm')
const { encodeBase64 } = await import('hash-wasm/lib/util')

const hasher = await createXXHash32()

return (message) => {
hasher.init()
hasher.update(message)
return encodeBase64(hasher.digest('binary'))
}
}
55 changes: 55 additions & 0 deletions src/to-base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

export function encodeBase64 (data: Uint8Array, pad = true): string {
const len = data.length
const extraBytes = len % 3
const parts = []

const len2 = len - extraBytes
for (let i = 0; i < len2; i += 3) {
const tmp = ((data[i] << 16) & 0xFF0000) +
((data[i + 1] << 8) & 0xFF00) +
(data[i + 2] & 0xFF)

const triplet = base64Chars.charAt((tmp >> 18) & 0x3F) +
base64Chars.charAt((tmp >> 12) & 0x3F) +
base64Chars.charAt((tmp >> 6) & 0x3F) +
base64Chars.charAt(tmp & 0x3F)

parts.push(triplet)
}

if (extraBytes === 1) {
const tmp = data[len - 1]
const a = base64Chars.charAt(tmp >> 2)
const b = base64Chars.charAt((tmp << 4) & 0x3F)

parts.push(`${a}${b}`)
if (pad) {
parts.push('==')
}
} else if (extraBytes === 2) {
const tmp = (data[len - 2] << 8) + data[len - 1]
const a = base64Chars.charAt(tmp >> 10)
const b = base64Chars.charAt((tmp >> 4) & 0x3F)
const c = base64Chars.charAt((tmp << 2) & 0x3F)
parts.push(`${a}${b}${c}`)
if (pad) {
parts.push('=')
}
}

return parts.join('')
}

/**
* Convert ArrayBuffer to base64 string
* @see https://github.com/Daninet/hash-wasm/blob/bd3a205ca5603fc80adf71d0966fc72e8d4fa0ef/lib/util.ts#L103
* @param {ArrayBuffer} arrayBuffer
* @return {string} Base64
*/
export function toBase64 (arrayBuffer: ArrayBuffer) {
const bytes = new Uint8Array(arrayBuffer)

return encodeBase64(bytes)
}
45 changes: 45 additions & 0 deletions src/use-hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { initSha } from './init-sha'
import { initXxHash } from './init-xx-hash'
import { HashOptions, objectHash } from './object-hash'
import type { HashFn } from './hash-fn.type'

let hashFn: Promise<HashFn> | undefined

function initHashFn () {
// Check for cached
if (hashFn) {
return hashFn
}

try {
hashFn = initXxHash()
} catch (_e) {
hashFn = initSha()
}
return hashFn
}

/**
* Use hash
* @description
* @example
* const hash = await useHash();
* const key = await hash(obj);
*/
export async function useHash () {
const hashFn = await initHashFn()

return {
/**
* Hash any JS value into a string
* @param {object} object value to hash
* @param {HashOptions} [options] hashing options
* @return {string | Promise<string>} hash value
* @api public
*/
hash (object: unknown, options?: HashOptions) {
const hashed = typeof object === 'string' ? object : objectHash(object, options)
return hashFn(hashed)
}
}
}
21 changes: 21 additions & 0 deletions test/init-sha.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { initSha } from '../src/init-sha'

describe('initSha()', () => {
it('should return hash function', async () => {
expect(await initSha()).toMatchInlineSnapshot('[Function]')
})

it('should create hash', async () => {
const hash = await initSha()

expect(await hash('test')).toMatchInlineSnapshot('"n4bQgYhMfW"')
})

// See: https://github.com/unjs/ohash/issues/11
it('should prevent `ufo` and `vue` collision', async () => {
const hash = await initSha()

expect(await hash('vue') !== await hash('ufo')).toBeTruthy()
})
})
21 changes: 21 additions & 0 deletions test/init-xx-hash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it } from 'vitest'
import { initXxHash } from '../src/init-xx-hash'

describe('initXxHash64()', () => {
it('should return hash function', async () => {
expect(await initXxHash()).toMatchInlineSnapshot('[Function]')
})

it('should create hash', async () => {
const hash = await initXxHash()

expect(await hash('test')).toMatchInlineSnapshot('"PiAjzw=="')
})

// See: https://github.com/unjs/ohash/issues/11
it('should prevent `ufo` and `vue` collision', async () => {
const hash = await initXxHash()

expect(await hash('vue') !== await hash('ufo')).toBeTruthy()
})
})
11 changes: 11 additions & 0 deletions test/to-base64.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { describe, expect, it } from 'vitest'
import { toBase64 } from '../src/to-base64'

describe('toBase64()', () => {
it('should return Base64 string from ArrayBuffer', () => {
const encoder = new TextEncoder()
const arrayBuffer = encoder.encode('test').buffer

expect(toBase64(arrayBuffer)).toMatchInlineSnapshot('"dGVzdA=="')
})
})
10 changes: 10 additions & 0 deletions test/use-hash.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { describe, expect, it } from 'vitest'
import { useHash } from '../src'

describe('useHash()', () => {
it('should return hash function', async () => {
const { hash } = await useHash()

expect(await hash('test')).toMatchInlineSnapshot('"PiAjzw=="')
})
})