Skip to content

Commit

Permalink
Merge pull request #147 from supercharge/base-hasher-refinements
Browse files Browse the repository at this point in the history
Hasher refinements
  • Loading branch information
marcuspoehls authored Oct 30, 2023
2 parents a8810b0 + ae261e8 commit 247d102
Show file tree
Hide file tree
Showing 10 changed files with 285 additions and 51 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [4.0.0](https://github.com/supercharge/framework/compare/v3.20.4...v4.0.0) - 2023-xx-xx

### Added
- `@supercharge/hashing`
- add `createHash` method: create a Node.js `Hash` instance for a given input
- add `md5` method: create a Node.js MD5 hash
- add `sha256` method: create a Node.js SHA256 hash
- add `sha512` method: create a Node.js SHA512 hash

### Updated
- bump dependencies
- `@supercharge/contracts`
Expand All @@ -17,6 +24,10 @@
- the `isMissing(key)` method now determines whether a value for a given `key` is `undefined` (related to `has(key)`, because `isMissing` is doing the opposite of `has`)
- rename the `add(key, value)` method to `set(key, value)`
- remove the `add(key, value)` method
- `@supercharge/hashing`
- removed `bcrypt` package from being installed automatically, users must install it explicitely when the hashing driver should use bcrypt
- hashing options require a factory function to return the hash driver constructor



## [3.20.4](https://github.com/supercharge/framework/compare/v3.20.3...v3.20.4) - 2023-10-15
Expand Down
33 changes: 33 additions & 0 deletions packages/contracts/src/hashing/base-hasher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

import type { BinaryLike, Encoding, Hash } from 'node:crypto'
import { HashBuilderCallback } from './hash-builder.js'

export interface BaseHasher {
/**
* Creates and returns a Node.js `Hash` instance for the given `algorithm`
* and the related `input` with (optional) `inputEncoding`. When `input`
* is a string and `inputEncoding` is omitted, it defaults to `utf8`.
*/
createHash (algorithm: string, input: string | BinaryLike, inputEncoding?: Encoding): Hash

/**
* Returns an MD5 hash instance for the given `content`.
*/
md5 (input: BinaryLike): string
md5 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
md5 (input: string, inputEncoding: Encoding): Hash

/**
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
*/
sha256 (input: BinaryLike): string
sha256 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
sha256 (input: string, inputEncoding: Encoding): Hash

/**
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
*/
sha512 (input: BinaryLike): string
sha512 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
sha512 (input: string, inputEncoding: Encoding): Hash
}
14 changes: 14 additions & 0 deletions packages/contracts/src/hashing/hash-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

import type { BinaryToTextEncoding, Encoding } from 'node:crypto'

export type HashBuilderCallback = (hashBuilder: HashBuilder) => unknown

export interface HashBuilderOptions {
inputEncoding?: Encoding
outputEncoding: BinaryToTextEncoding
}

export interface HashBuilder {
inputEncoding(inputEncoding: Encoding): this
toString(outputEncoding: BinaryToTextEncoding): void
}
34 changes: 3 additions & 31 deletions packages/contracts/src/hashing/hasher.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@

import type { BinaryLike, Encoding, Hash } from 'node:crypto'
import { BaseHasher } from './base-hasher.js'

export type HasherCtor = new(...args: any[]) => Hasher
export type HasherCtor = new (...args: any[]) => Hasher

export interface Hasher {
export interface Hasher extends BaseHasher {
/**
* Hash the given `value`.
*/
Expand All @@ -18,32 +18,4 @@ export interface Hasher {
* Determine whether the given hash value has been hashed using the configured options.
*/
needsRehash (hashedValue: string): boolean

/**
* Creates and returns a Node.js `Hash` instance for the given `algorithm`
* and the related `input` with (optional) `inputEncoding`. When `input`
* is a string and `inputEncoding` is omitted, it defaults to `utf8`.
*/
createHash (algorithm: string, input: string | BinaryLike, inputEncoding?: Encoding): Hash

/**
* Returns an MD5 hash instance for the given `content`.
*/
md5 (input: BinaryLike): Hash
md5 (input: string, inputEncoding: Encoding): Hash
md5 (input: string | BinaryLike, inputEncoding?: Encoding): Hash

/**
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
*/
sha256 (input: BinaryLike): Hash
sha256 (input: string, inputEncoding: Encoding): Hash
sha256 (input: string | BinaryLike, inputEncoding?: Encoding): Hash

/**
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
*/
sha512 (input: BinaryLike): Hash
sha512 (input: string, inputEncoding: Encoding): Hash
sha512 (input: string | BinaryLike, inputEncoding?: Encoding): Hash
}
2 changes: 2 additions & 0 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ export { EnvStore } from './env/env.js'
export { Bootstrapper, BootstrapperCtor } from './core/bootstrapper.js'
export { ErrorHandler, ErrorHandlerCtor } from './core/error-handler.js'

export { HashBuilder, HashBuilderCallback, HashBuilderOptions } from './hashing/hash-builder.js'
export { HashConfig } from './hashing/config.js'
export { BaseHasher } from './hashing/base-hasher.js'
export { Hasher } from './hashing/hasher.js'

export { BodyparserConfig, BodyparserOptions } from './http/bodyparser-config.js'
Expand Down
50 changes: 40 additions & 10 deletions packages/hashing/src/base-hasher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@

import { HashBuilder } from './hash-builder.js'
import Crypto, { BinaryLike, Encoding, Hash } from 'node:crypto'
import { BaseHasher as BaseHasherContract, HashBuilderCallback, HashBuilderOptions } from '@supercharge/contracts'

export class BaseHasher {
export class BaseHasher implements BaseHasherContract {
/**
* Creates and returns a Node.js `Hash` instance for the given `algorithm`
* and the related `input` with (optional) `inputEncoding`. When `input`
Expand All @@ -16,27 +18,55 @@ export class BaseHasher {
/**
* Returns an MD5 hash instance for the given `content`.
*/
md5 (input: BinaryLike): Hash
md5 (input: BinaryLike): string
md5 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
md5 (input: string, inputEncoding: Encoding): Hash
md5 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
return this.createHash('md5', input, inputEncoding)
md5 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
return this.hash('md5', input, inputEncodingOrHashBuilder)
}

/**
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
*/
sha256 (input: BinaryLike): Hash
sha256 (input: BinaryLike): string
sha256 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
sha256 (input: string, inputEncoding: Encoding): Hash
sha256 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
return this.createHash('sha256', input, inputEncoding)
sha256 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
return this.hash('sha256', input, inputEncodingOrHashBuilder)
}

/**
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
*/
sha512 (input: BinaryLike): Hash
sha512 (input: BinaryLike): string
sha512 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
sha512 (input: string, inputEncoding: Encoding): Hash
sha512 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
return this.createHash('sha512', input, inputEncoding)
sha512 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
return this.hash('sha512', input, inputEncodingOrHashBuilder)
}

/**
* Returns the hashed string value or the `Hash` instance, depending on the
* user input. This function resolves a hash builder callback and creates
* the hash value for the provided algorithm and i/o encoding options.
*/
private hash (algorithm: string, input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
if (typeof inputEncodingOrHashBuilder === 'string') {
return this.createHash(algorithm, input, inputEncodingOrHashBuilder)
}

if (typeof inputEncodingOrHashBuilder === 'function') {
const hashBuilderOptions: HashBuilderOptions = { outputEncoding: 'base64' }
const builder = new HashBuilder(hashBuilderOptions)
inputEncodingOrHashBuilder(builder)

return this
.createHash(algorithm, input, hashBuilderOptions.inputEncoding)
.digest(hashBuilderOptions.outputEncoding)
}

return this
.createHash(algorithm, input, inputEncodingOrHashBuilder)
.digest('base64')
}
}
27 changes: 27 additions & 0 deletions packages/hashing/src/hash-builder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@

import { HashBuilder as HashBuilderContract, HashBuilderOptions } from '@supercharge/contracts'
import { BinaryToTextEncoding, Encoding } from 'crypto'

export class HashBuilder implements HashBuilderContract {
/**
* Stores the hash builder options.
*/
private readonly options: HashBuilderOptions

constructor (options: HashBuilderOptions) {
this.options = options
}

inputEncoding (inputEncoding: Encoding): this {
this.options.inputEncoding = inputEncoding
return this
}

digest (encoding: BinaryToTextEncoding): void {
this.options.outputEncoding = encoding
}

toString (encoding: BinaryToTextEncoding): void {
this.digest(encoding)
}
}
26 changes: 16 additions & 10 deletions packages/hashing/src/hash-manager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

import type { BinaryLike, Encoding, Hash } from 'node:crypto'
import { Manager } from '@supercharge/manager'
import { Application, Hasher, HashConfig } from '@supercharge/contracts'
import { Application, Hasher, HashConfig, HashBuilderCallback } from '@supercharge/contracts'

export class HashManager extends Manager<Application> implements Hasher {
/**
Expand Down Expand Up @@ -81,27 +81,33 @@ export class HashManager extends Manager<Application> implements Hasher {
/**
* Returns an MD5 hash instance for the given `content`.
*/
md5 (input: BinaryLike): Hash
md5 (input: BinaryLike): string
md5 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
md5 (input: string, inputEncoding: Encoding): Hash
md5 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
return this.driver().md5(input, inputEncoding)
md5 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
// @ts-expect-error TODO
return this.driver().md5(input, inputEncodingOrHashBuilder)
}

/**
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
*/
sha256 (input: BinaryLike): Hash
sha256 (input: BinaryLike): string
sha256 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
sha256 (input: string, inputEncoding: Encoding): Hash
sha256 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
return this.driver().sha256(input, inputEncoding)
sha256 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
// @ts-expect-error TODO
return this.driver().sha256(input, inputEncodingOrHashBuilder)
}

/**
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
*/
sha512 (input: BinaryLike): Hash
sha512 (input: BinaryLike): string
sha512 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
sha512 (input: string, inputEncoding: Encoding): Hash
sha512 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
return this.driver().sha512(input, inputEncoding)
sha512 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
// @ts-expect-error TODO
return this.driver().sha512(input, inputEncodingOrHashBuilder)
}
}
110 changes: 110 additions & 0 deletions packages/hashing/test/base-hasher.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@

import { test } from 'uvu'
import { expect } from 'expect'
import { BaseHasher } from '../dist/base-hasher.js'
import { Hash } from 'crypto'

test('createHash', async () => {
const hasher = new BaseHasher()

const hash = hasher.createHash('sha256', 'supercharge')
expect(hash instanceof Hash).toBe(true)
expect(hash.digest('base64').endsWith('=')).toBe(true)
})

test('md5 with value', async () => {
const hasher = new BaseHasher()

const md5 = hasher.md5('supercharge')
expect(typeof md5 === 'string').toBe(true)
expect(md5.endsWith('=')).toBe(true)
})

test('md5 with input encoding', async () => {
const hasher = new BaseHasher()

const md5 = hasher.md5('supercharge', 'utf8')
expect(md5 instanceof Hash).toBe(true)
})

test('md5 with hash builder callback', async () => {
const hasher = new BaseHasher()

const md5 = hasher.md5('supercharge', hash => hash.toString('hex'))
expect(typeof md5 === 'string').toBe(true)
})

test('sha256 with value', async () => {
const hasher = new BaseHasher()

const sha256 = hasher.sha256('supercharge')
expect(typeof sha256 === 'string').toBe(true)
expect(sha256.endsWith('=')).toBe(true)
})

test('sha256 with input encoding', async () => {
const hasher = new BaseHasher()

const sha256 = hasher.sha256('supercharge', 'utf8')
expect(sha256 instanceof Hash).toBe(true)
})

test('sha256 with hash builder callback', async () => {
const hasher = new BaseHasher()

const sha256 = hasher.sha256('supercharge', hash => hash.toString('hex'))
expect(typeof sha256 === 'string').toBe(true)
})

test('sha512 with value', async () => {
const hasher = new BaseHasher()

const sha512 = hasher.sha512('supercharge')
expect(typeof sha512 === 'string').toBe(true)
expect(sha512.endsWith('=')).toBe(true)
})

test('sha512 with input encoding', async () => {
const hasher = new BaseHasher()

const sha512 = hasher.sha512('supercharge', 'utf8')
expect(sha512 instanceof Hash).toBe(true)
})

test('sha512 with hash builder callback', async () => {
const hasher = new BaseHasher()

const sha512 = hasher.sha512('supercharge', hash => hash.toString('hex'))
expect(typeof sha512 === 'string').toBe(true)
})

test('hashes are different from each other', async () => {
const hasher = new BaseHasher()
const input = 'supercharge'

expect(hasher.md5(input)).toEqual(hasher.md5(input))
expect(hasher.md5(input)).not.toEqual(hasher.sha256(input))
expect(hasher.md5(input)).not.toEqual(hasher.sha512(input))

expect(hasher.sha256(input)).toEqual(hasher.sha256(input))
expect(hasher.sha256(input)).not.toEqual(hasher.md5(input))
expect(hasher.sha256(input)).not.toEqual(hasher.sha512(input))

expect(hasher.sha512(input)).toEqual(hasher.sha512(input))
expect(hasher.sha512(input)).not.toEqual(hasher.md5(input))
expect(hasher.sha512(input)).not.toEqual(hasher.sha256(input))
})

test('hash builder', async () => {
const hasher = new BaseHasher()

const sha512 = hasher.sha512('supercharge', hash => {
hash
.inputEncoding('utf8')
.toString('hex')
})

expect(typeof sha512 === 'string').toBe(true)
})

test.run()
Loading

0 comments on commit 247d102

Please sign in to comment.