Skip to content

Commit f605321

Browse files
jxomampcode-com
andcommitted
feat: add Hash.hmac256 for HMAC-SHA256 hashing
Amp-Thread-ID: https://ampcode.com/threads/T-019bfbdf-042d-715c-8aed-fa3b56817d66 Co-authored-by: Amp <amp@ampcode.com>
1 parent 658ecc6 commit f605321

File tree

3 files changed

+170
-0
lines changed

3 files changed

+170
-0
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"ox": minor
3+
---
4+
5+
Added `Hash.hmac256` for computing HMAC-SHA256 hashes.

src/core/Hash.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { hmac } from '@noble/hashes/hmac'
12
import { ripemd160 as noble_ripemd160 } from '@noble/hashes/ripemd160'
23
import { keccak_256 as noble_keccak256 } from '@noble/hashes/sha3'
34
import { sha256 as noble_sha256 } from '@noble/hashes/sha256'
@@ -73,6 +74,66 @@ export declare namespace keccak256 {
7374
| Errors.GlobalErrorType
7475
}
7576

77+
/**
78+
* Calculates the [HMAC-SHA256](https://en.wikipedia.org/wiki/HMAC) of a {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} value.
79+
*
80+
* This function is a re-export of `hmac` from [`@noble/hashes`](https://github.com/paulmillr/noble-hashes), an audited & minimal JS hashing library.
81+
*
82+
* @example
83+
* ```ts twoslash
84+
* import { Hash, Hex } from 'ox'
85+
*
86+
* Hash.hmac256(Hex.fromString('key'), '0xdeadbeef')
87+
* // @log: '0x...'
88+
* ```
89+
*
90+
* @example
91+
* ### Configure Return Type
92+
*
93+
* ```ts twoslash
94+
* import { Hash, Hex } from 'ox'
95+
*
96+
* Hash.hmac256(Hex.fromString('key'), '0xdeadbeef', { as: 'Bytes' })
97+
* // @log: Uint8Array [...]
98+
* ```
99+
*
100+
* @param key - {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} key.
101+
* @param value - {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} value.
102+
* @param options - Options.
103+
* @returns HMAC-SHA256 hash.
104+
*/
105+
export function hmac256<
106+
value extends Hex.Hex | Bytes.Bytes,
107+
as extends 'Hex' | 'Bytes' =
108+
| (value extends Hex.Hex ? 'Hex' : never)
109+
| (value extends Bytes.Bytes ? 'Bytes' : never),
110+
>(
111+
key: Hex.Hex | Bytes.Bytes,
112+
value: value | Hex.Hex | Bytes.Bytes,
113+
options: hmac256.Options<as> = {},
114+
): hmac256.ReturnType<as> {
115+
const { as = typeof value === 'string' ? 'Hex' : 'Bytes' } = options
116+
const bytes = hmac(noble_sha256, Bytes.from(key), Bytes.from(value))
117+
if (as === 'Bytes') return bytes as never
118+
return Hex.fromBytes(bytes) as never
119+
}
120+
121+
export declare namespace hmac256 {
122+
type Options<as extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> = {
123+
/** The return type. @default 'Hex' */
124+
as?: as | 'Hex' | 'Bytes' | undefined
125+
}
126+
127+
type ReturnType<as extends 'Hex' | 'Bytes' = 'Hex' | 'Bytes'> =
128+
| (as extends 'Bytes' ? Bytes.Bytes : never)
129+
| (as extends 'Hex' ? Hex.Hex : never)
130+
131+
type ErrorType =
132+
| Bytes.from.ErrorType
133+
| Hex.fromBytes.ErrorType
134+
| Errors.GlobalErrorType
135+
}
136+
76137
/**
77138
* Calculates the [Ripemd160](https://en.wikipedia.org/wiki/RIPEMD) hash of a {@link ox#Bytes.Bytes} or {@link ox#Hex.Hex} value.
78139
*

src/core/_test/Hash.test.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,109 @@
11
import { Hash } from 'ox'
22
import { describe, expect, test } from 'vitest'
33

4+
describe('hmac256', () => {
5+
test('default', () => {
6+
expect(
7+
Hash.hmac256(
8+
new Uint8Array([107, 101, 121]),
9+
new Uint8Array([72, 101, 108, 108, 111]),
10+
),
11+
).toMatchInlineSnapshot(`
12+
Uint8Array [
13+
199,
14+
11,
15+
159,
16+
77,
17+
102,
18+
91,
19+
214,
20+
41,
21+
116,
22+
175,
23+
200,
24+
53,
25+
130,
26+
222,
27+
129,
28+
14,
29+
114,
30+
164,
31+
26,
32+
88,
33+
219,
34+
130,
35+
197,
36+
56,
37+
169,
38+
215,
39+
52,
40+
201,
41+
38,
42+
109,
43+
50,
44+
30,
45+
]
46+
`)
47+
48+
expect(Hash.hmac256('0x6b6579', '0x48656c6c6f')).toMatchInlineSnapshot(
49+
`"0xc70b9f4d665bd62974afc83582de810e72a41a58db82c538a9d734c9266d321e"`,
50+
)
51+
})
52+
53+
test('as: Hex', () => {
54+
expect(
55+
Hash.hmac256(
56+
new Uint8Array([107, 101, 121]),
57+
new Uint8Array([72, 101, 108, 108, 111]),
58+
{ as: 'Hex' },
59+
),
60+
).toMatchInlineSnapshot(
61+
`"0xc70b9f4d665bd62974afc83582de810e72a41a58db82c538a9d734c9266d321e"`,
62+
)
63+
})
64+
65+
test('as: Bytes', () => {
66+
expect(
67+
Hash.hmac256('0x6b6579', '0x48656c6c6f', { as: 'Bytes' }),
68+
).toMatchInlineSnapshot(`
69+
Uint8Array [
70+
199,
71+
11,
72+
159,
73+
77,
74+
102,
75+
91,
76+
214,
77+
41,
78+
116,
79+
175,
80+
200,
81+
53,
82+
130,
83+
222,
84+
129,
85+
14,
86+
114,
87+
164,
88+
26,
89+
88,
90+
219,
91+
130,
92+
197,
93+
56,
94+
169,
95+
215,
96+
52,
97+
201,
98+
38,
99+
109,
100+
50,
101+
30,
102+
]
103+
`)
104+
})
105+
})
106+
4107
describe('keccak256', () => {
5108
test('default', () => {
6109
expect(Hash.keccak256('0xdeadbeef')).toMatchInlineSnapshot(
@@ -429,6 +532,7 @@ test('exports', () => {
429532
expect(Object.keys(Hash)).toMatchInlineSnapshot(`
430533
[
431534
"keccak256",
535+
"hmac256",
432536
"ripemd160",
433537
"sha256",
434538
"validate",

0 commit comments

Comments
 (0)