Skip to content

Commit ae261e8

Browse files
committed
(base) hasher refinements
1 parent a8810b0 commit ae261e8

File tree

10 files changed

+285
-51
lines changed

10 files changed

+285
-51
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

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

5+
### Added
6+
- `@supercharge/hashing`
7+
- add `createHash` method: create a Node.js `Hash` instance for a given input
8+
- add `md5` method: create a Node.js MD5 hash
9+
- add `sha256` method: create a Node.js SHA256 hash
10+
- add `sha512` method: create a Node.js SHA512 hash
11+
512
### Updated
613
- bump dependencies
714
- `@supercharge/contracts`
@@ -17,6 +24,10 @@
1724
- 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`)
1825
- rename the `add(key, value)` method to `set(key, value)`
1926
- remove the `add(key, value)` method
27+
- `@supercharge/hashing`
28+
- removed `bcrypt` package from being installed automatically, users must install it explicitely when the hashing driver should use bcrypt
29+
- hashing options require a factory function to return the hash driver constructor
30+
2031

2132

2233
## [3.20.4](https://github.com/supercharge/framework/compare/v3.20.3...v3.20.4) - 2023-10-15
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
2+
import type { BinaryLike, Encoding, Hash } from 'node:crypto'
3+
import { HashBuilderCallback } from './hash-builder.js'
4+
5+
export interface BaseHasher {
6+
/**
7+
* Creates and returns a Node.js `Hash` instance for the given `algorithm`
8+
* and the related `input` with (optional) `inputEncoding`. When `input`
9+
* is a string and `inputEncoding` is omitted, it defaults to `utf8`.
10+
*/
11+
createHash (algorithm: string, input: string | BinaryLike, inputEncoding?: Encoding): Hash
12+
13+
/**
14+
* Returns an MD5 hash instance for the given `content`.
15+
*/
16+
md5 (input: BinaryLike): string
17+
md5 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
18+
md5 (input: string, inputEncoding: Encoding): Hash
19+
20+
/**
21+
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
22+
*/
23+
sha256 (input: BinaryLike): string
24+
sha256 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
25+
sha256 (input: string, inputEncoding: Encoding): Hash
26+
27+
/**
28+
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
29+
*/
30+
sha512 (input: BinaryLike): string
31+
sha512 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
32+
sha512 (input: string, inputEncoding: Encoding): Hash
33+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
2+
import type { BinaryToTextEncoding, Encoding } from 'node:crypto'
3+
4+
export type HashBuilderCallback = (hashBuilder: HashBuilder) => unknown
5+
6+
export interface HashBuilderOptions {
7+
inputEncoding?: Encoding
8+
outputEncoding: BinaryToTextEncoding
9+
}
10+
11+
export interface HashBuilder {
12+
inputEncoding(inputEncoding: Encoding): this
13+
toString(outputEncoding: BinaryToTextEncoding): void
14+
}
Lines changed: 3 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11

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

4-
export type HasherCtor = new(...args: any[]) => Hasher
4+
export type HasherCtor = new (...args: any[]) => Hasher
55

6-
export interface Hasher {
6+
export interface Hasher extends BaseHasher {
77
/**
88
* Hash the given `value`.
99
*/
@@ -18,32 +18,4 @@ export interface Hasher {
1818
* Determine whether the given hash value has been hashed using the configured options.
1919
*/
2020
needsRehash (hashedValue: string): boolean
21-
22-
/**
23-
* Creates and returns a Node.js `Hash` instance for the given `algorithm`
24-
* and the related `input` with (optional) `inputEncoding`. When `input`
25-
* is a string and `inputEncoding` is omitted, it defaults to `utf8`.
26-
*/
27-
createHash (algorithm: string, input: string | BinaryLike, inputEncoding?: Encoding): Hash
28-
29-
/**
30-
* Returns an MD5 hash instance for the given `content`.
31-
*/
32-
md5 (input: BinaryLike): Hash
33-
md5 (input: string, inputEncoding: Encoding): Hash
34-
md5 (input: string | BinaryLike, inputEncoding?: Encoding): Hash
35-
36-
/**
37-
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
38-
*/
39-
sha256 (input: BinaryLike): Hash
40-
sha256 (input: string, inputEncoding: Encoding): Hash
41-
sha256 (input: string | BinaryLike, inputEncoding?: Encoding): Hash
42-
43-
/**
44-
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
45-
*/
46-
sha512 (input: BinaryLike): Hash
47-
sha512 (input: string, inputEncoding: Encoding): Hash
48-
sha512 (input: string | BinaryLike, inputEncoding?: Encoding): Hash
4921
}

packages/contracts/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ export { EnvStore } from './env/env.js'
2020
export { Bootstrapper, BootstrapperCtor } from './core/bootstrapper.js'
2121
export { ErrorHandler, ErrorHandlerCtor } from './core/error-handler.js'
2222

23+
export { HashBuilder, HashBuilderCallback, HashBuilderOptions } from './hashing/hash-builder.js'
2324
export { HashConfig } from './hashing/config.js'
25+
export { BaseHasher } from './hashing/base-hasher.js'
2426
export { Hasher } from './hashing/hasher.js'
2527

2628
export { BodyparserConfig, BodyparserOptions } from './http/bodyparser-config.js'

packages/hashing/src/base-hasher.ts

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11

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

4-
export class BaseHasher {
6+
export class BaseHasher implements BaseHasherContract {
57
/**
68
* Creates and returns a Node.js `Hash` instance for the given `algorithm`
79
* and the related `input` with (optional) `inputEncoding`. When `input`
@@ -16,27 +18,55 @@ export class BaseHasher {
1618
/**
1719
* Returns an MD5 hash instance for the given `content`.
1820
*/
19-
md5 (input: BinaryLike): Hash
21+
md5 (input: BinaryLike): string
22+
md5 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
2023
md5 (input: string, inputEncoding: Encoding): Hash
21-
md5 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
22-
return this.createHash('md5', input, inputEncoding)
24+
md5 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
25+
return this.hash('md5', input, inputEncodingOrHashBuilder)
2326
}
2427

2528
/**
2629
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
2730
*/
28-
sha256 (input: BinaryLike): Hash
31+
sha256 (input: BinaryLike): string
32+
sha256 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
2933
sha256 (input: string, inputEncoding: Encoding): Hash
30-
sha256 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
31-
return this.createHash('sha256', input, inputEncoding)
34+
sha256 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
35+
return this.hash('sha256', input, inputEncodingOrHashBuilder)
3236
}
3337

3438
/**
3539
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
3640
*/
37-
sha512 (input: BinaryLike): Hash
41+
sha512 (input: BinaryLike): string
42+
sha512 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
3843
sha512 (input: string, inputEncoding: Encoding): Hash
39-
sha512 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
40-
return this.createHash('sha512', input, inputEncoding)
44+
sha512 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
45+
return this.hash('sha512', input, inputEncodingOrHashBuilder)
46+
}
47+
48+
/**
49+
* Returns the hashed string value or the `Hash` instance, depending on the
50+
* user input. This function resolves a hash builder callback and creates
51+
* the hash value for the provided algorithm and i/o encoding options.
52+
*/
53+
private hash (algorithm: string, input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
54+
if (typeof inputEncodingOrHashBuilder === 'string') {
55+
return this.createHash(algorithm, input, inputEncodingOrHashBuilder)
56+
}
57+
58+
if (typeof inputEncodingOrHashBuilder === 'function') {
59+
const hashBuilderOptions: HashBuilderOptions = { outputEncoding: 'base64' }
60+
const builder = new HashBuilder(hashBuilderOptions)
61+
inputEncodingOrHashBuilder(builder)
62+
63+
return this
64+
.createHash(algorithm, input, hashBuilderOptions.inputEncoding)
65+
.digest(hashBuilderOptions.outputEncoding)
66+
}
67+
68+
return this
69+
.createHash(algorithm, input, inputEncodingOrHashBuilder)
70+
.digest('base64')
4171
}
4272
}

packages/hashing/src/hash-builder.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
import { HashBuilder as HashBuilderContract, HashBuilderOptions } from '@supercharge/contracts'
3+
import { BinaryToTextEncoding, Encoding } from 'crypto'
4+
5+
export class HashBuilder implements HashBuilderContract {
6+
/**
7+
* Stores the hash builder options.
8+
*/
9+
private readonly options: HashBuilderOptions
10+
11+
constructor (options: HashBuilderOptions) {
12+
this.options = options
13+
}
14+
15+
inputEncoding (inputEncoding: Encoding): this {
16+
this.options.inputEncoding = inputEncoding
17+
return this
18+
}
19+
20+
digest (encoding: BinaryToTextEncoding): void {
21+
this.options.outputEncoding = encoding
22+
}
23+
24+
toString (encoding: BinaryToTextEncoding): void {
25+
this.digest(encoding)
26+
}
27+
}

packages/hashing/src/hash-manager.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
import type { BinaryLike, Encoding, Hash } from 'node:crypto'
33
import { Manager } from '@supercharge/manager'
4-
import { Application, Hasher, HashConfig } from '@supercharge/contracts'
4+
import { Application, Hasher, HashConfig, HashBuilderCallback } from '@supercharge/contracts'
55

66
export class HashManager extends Manager<Application> implements Hasher {
77
/**
@@ -81,27 +81,33 @@ export class HashManager extends Manager<Application> implements Hasher {
8181
/**
8282
* Returns an MD5 hash instance for the given `content`.
8383
*/
84-
md5 (input: BinaryLike): Hash
84+
md5 (input: BinaryLike): string
85+
md5 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
8586
md5 (input: string, inputEncoding: Encoding): Hash
86-
md5 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
87-
return this.driver().md5(input, inputEncoding)
87+
md5 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
88+
// @ts-expect-error TODO
89+
return this.driver().md5(input, inputEncodingOrHashBuilder)
8890
}
8991

9092
/**
9193
* Returns a SHA256 hash instance using SHA-2 for the given `content`.
9294
*/
93-
sha256 (input: BinaryLike): Hash
95+
sha256 (input: BinaryLike): string
96+
sha256 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
9497
sha256 (input: string, inputEncoding: Encoding): Hash
95-
sha256 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
96-
return this.driver().sha256(input, inputEncoding)
98+
sha256 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
99+
// @ts-expect-error TODO
100+
return this.driver().sha256(input, inputEncodingOrHashBuilder)
97101
}
98102

99103
/**
100104
* Returns a SHA512 hash instance using SHA-2 for the given `content`.
101105
*/
102-
sha512 (input: BinaryLike): Hash
106+
sha512 (input: BinaryLike): string
107+
sha512 (input: BinaryLike, hashBuilder: HashBuilderCallback): string
103108
sha512 (input: string, inputEncoding: Encoding): Hash
104-
sha512 (input: string | BinaryLike, inputEncoding?: Encoding): Hash {
105-
return this.driver().sha512(input, inputEncoding)
109+
sha512 (input: string | BinaryLike, inputEncodingOrHashBuilder?: Encoding | HashBuilderCallback): Hash | string {
110+
// @ts-expect-error TODO
111+
return this.driver().sha512(input, inputEncodingOrHashBuilder)
106112
}
107113
}

packages/hashing/test/base-hasher.js

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
2+
import { test } from 'uvu'
3+
import { expect } from 'expect'
4+
import { BaseHasher } from '../dist/base-hasher.js'
5+
import { Hash } from 'crypto'
6+
7+
test('createHash', async () => {
8+
const hasher = new BaseHasher()
9+
10+
const hash = hasher.createHash('sha256', 'supercharge')
11+
expect(hash instanceof Hash).toBe(true)
12+
expect(hash.digest('base64').endsWith('=')).toBe(true)
13+
})
14+
15+
test('md5 with value', async () => {
16+
const hasher = new BaseHasher()
17+
18+
const md5 = hasher.md5('supercharge')
19+
expect(typeof md5 === 'string').toBe(true)
20+
expect(md5.endsWith('=')).toBe(true)
21+
})
22+
23+
test('md5 with input encoding', async () => {
24+
const hasher = new BaseHasher()
25+
26+
const md5 = hasher.md5('supercharge', 'utf8')
27+
expect(md5 instanceof Hash).toBe(true)
28+
})
29+
30+
test('md5 with hash builder callback', async () => {
31+
const hasher = new BaseHasher()
32+
33+
const md5 = hasher.md5('supercharge', hash => hash.toString('hex'))
34+
expect(typeof md5 === 'string').toBe(true)
35+
})
36+
37+
test('sha256 with value', async () => {
38+
const hasher = new BaseHasher()
39+
40+
const sha256 = hasher.sha256('supercharge')
41+
expect(typeof sha256 === 'string').toBe(true)
42+
expect(sha256.endsWith('=')).toBe(true)
43+
})
44+
45+
test('sha256 with input encoding', async () => {
46+
const hasher = new BaseHasher()
47+
48+
const sha256 = hasher.sha256('supercharge', 'utf8')
49+
expect(sha256 instanceof Hash).toBe(true)
50+
})
51+
52+
test('sha256 with hash builder callback', async () => {
53+
const hasher = new BaseHasher()
54+
55+
const sha256 = hasher.sha256('supercharge', hash => hash.toString('hex'))
56+
expect(typeof sha256 === 'string').toBe(true)
57+
})
58+
59+
test('sha512 with value', async () => {
60+
const hasher = new BaseHasher()
61+
62+
const sha512 = hasher.sha512('supercharge')
63+
expect(typeof sha512 === 'string').toBe(true)
64+
expect(sha512.endsWith('=')).toBe(true)
65+
})
66+
67+
test('sha512 with input encoding', async () => {
68+
const hasher = new BaseHasher()
69+
70+
const sha512 = hasher.sha512('supercharge', 'utf8')
71+
expect(sha512 instanceof Hash).toBe(true)
72+
})
73+
74+
test('sha512 with hash builder callback', async () => {
75+
const hasher = new BaseHasher()
76+
77+
const sha512 = hasher.sha512('supercharge', hash => hash.toString('hex'))
78+
expect(typeof sha512 === 'string').toBe(true)
79+
})
80+
81+
test('hashes are different from each other', async () => {
82+
const hasher = new BaseHasher()
83+
const input = 'supercharge'
84+
85+
expect(hasher.md5(input)).toEqual(hasher.md5(input))
86+
expect(hasher.md5(input)).not.toEqual(hasher.sha256(input))
87+
expect(hasher.md5(input)).not.toEqual(hasher.sha512(input))
88+
89+
expect(hasher.sha256(input)).toEqual(hasher.sha256(input))
90+
expect(hasher.sha256(input)).not.toEqual(hasher.md5(input))
91+
expect(hasher.sha256(input)).not.toEqual(hasher.sha512(input))
92+
93+
expect(hasher.sha512(input)).toEqual(hasher.sha512(input))
94+
expect(hasher.sha512(input)).not.toEqual(hasher.md5(input))
95+
expect(hasher.sha512(input)).not.toEqual(hasher.sha256(input))
96+
})
97+
98+
test('hash builder', async () => {
99+
const hasher = new BaseHasher()
100+
101+
const sha512 = hasher.sha512('supercharge', hash => {
102+
hash
103+
.inputEncoding('utf8')
104+
.toString('hex')
105+
})
106+
107+
expect(typeof sha512 === 'string').toBe(true)
108+
})
109+
110+
test.run()

0 commit comments

Comments
 (0)