Skip to content

Commit 4487a49

Browse files
authored
Merge pull request #8 from frytg/dev/hmac-keys
fix: optimize key handling for HMAC
2 parents b1dcda3 + 0f46f68 commit 4487a49

8 files changed

+111
-40
lines changed

crypto/CHANGELOG.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Crypto Changelog
22

3+
## 2024-12-16 - 0.0.2
4+
5+
- fix: optimize key handling for HMAC
6+
37
## 2024-12-12 - 0.0.1
48

5-
- feat: added hash and hmac functions for SHA-256 and SHA-512
9+
- feat: added hash and HMAC functions for SHA-256 and SHA-512

crypto/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
This package provides functions to compute SHA-256 and SHA-512 hashes using [native Node crypto](https://nodejs.org/api/crypto.html).
77

8+
- [full docs on JSR](https://jsr.io/@frytg/crypto/doc)
89
- [Hash (SHA-256 or SHA-512)](https://jsr.io/@frytg/crypto/doc/hash)
910
- [HMAC (SHA-256 or SHA-512)](https://jsr.io/@frytg/crypto/doc/hmac)
1011

crypto/deno.jsonc

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://jsr.io/schema/config-file.v1.json",
33
"name": "@frytg/crypto",
4-
"version": "0.0.1",
4+
"version": "0.0.2",
55
"exports": {
66
"./hash": "./hash.ts",
77
"./hmac": "./hmac.ts"

crypto/hash.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,18 @@ import { createHash } from 'node:crypto'
22

33
const HEX_ENCODING = 'hex'
44

5+
/**
6+
* @module hash
7+
* SHA-256 and SHA-512 hash functions
8+
*
9+
* @example
10+
* ```ts
11+
* import { hashSha512 } from '@frytg/crypto/hash'
12+
*
13+
* hashSha512('hello world')
14+
* ```
15+
*/
16+
517
/**
618
* Returns a SHA-256 hash of the input string (as hexadecimal string)
719
* @param str - The string to hash
@@ -11,7 +23,7 @@ const HEX_ENCODING = 'hex'
1123
* ```ts
1224
* import { hashSha256 } from '@frytg/crypto/hash'
1325
*
14-
* hashSha256('hello')
26+
* hashSha256('hello world')
1527
* ```
1628
*/
1729
export const hashSha256 = (str: string): string => createHash('sha256').update(str).digest(HEX_ENCODING)
@@ -25,7 +37,7 @@ export const hashSha256 = (str: string): string => createHash('sha256').update(s
2537
* ```ts
2638
* import { hashSha512 } from '@frytg/crypto/hash'
2739
*
28-
* hashSha512('hello')
40+
* hashSha512('hello world')
2941
* ```
3042
*/
3143
export const hashSha512 = (str: string): string => createHash('sha512').update(str).digest(HEX_ENCODING)

crypto/hmac.test.ts

+42-13
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import { Buffer } from 'node:buffer'
22
import { test } from '@cross/test'
3-
import { assertEquals } from '@std/assert'
3+
import { assertEquals, assertThrows } from '@std/assert'
44

55
import { bufferFromHex, hmacSha256, hmacSha512 } from './hmac.ts'
66

77
test('hmacSha256 - generates correct HMAC SHA-256 hashes', () => {
88
const testCases = [
99
{
1010
input: 'hello',
11-
key: 'secret',
12-
expected: '88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b',
11+
key: '0123456789abcdef',
12+
expected: '58341de110352e89a9dfe341aede35073e34b5640f006ed94208efa321d68994',
1313
},
1414
{
1515
input: '',
16-
key: 'secret',
17-
expected: 'f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169',
16+
key: '0123456789abcdef',
17+
expected: 'f2f24bb00417d3d905c2fc9b659fbe5310af55be93eb00524fc2021e3cc29a88',
1818
},
1919
{
2020
input: 'The quick brown fox jumps over the lazy dog',
21-
key: 'secret',
22-
expected: '54cd5b827c0ec938fa072a29b177469c843317b095591dc846767aa338bac600',
21+
key: '0123456789abcdef',
22+
expected: '5a4921e469387c921b75e6f135db948ab94a0ee5d28c8d8706df5df3c09a8095',
2323
},
2424
]
2525

@@ -32,21 +32,21 @@ test('hmacSha512 - generates correct HMAC SHA-512 hashes', () => {
3232
const testCases = [
3333
{
3434
input: 'hello',
35-
key: 'secret',
35+
key: '0123456789abcdef',
3636
expected:
37-
'db1595ae88a62fd151ec1cba81b98c39df82daae7b4cb9820f446d5bf02f1dcfca6683d88cab3e273f5963ab8ec469a746b5b19086371239f67d1e5f99a79440',
37+
'e603296d6ec667b62905984498c87cee7c35625fac4517108d74ac169ab0a6727a65d4786cd11c3c0851b8505983714f58ee2156f32093e9360cb275539802e9',
3838
},
3939
{
4040
input: '',
41-
key: 'secret',
41+
key: '0123456789abcdef',
4242
expected:
43-
'b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901',
43+
'acae8450151bdbb810f41200da1bf26ef2756037bcdc930b014cbbc5fccb3b9ddc0cdcee6b05fa07d88e65af87d202e6dd8d0c5303a8a0866a2a5ce505779808',
4444
},
4545
{
4646
input: 'The quick brown fox jumps over the lazy dog',
47-
key: 'secret',
47+
key: '0123456789abcdef',
4848
expected:
49-
'76af3588620ef6e2c244d5a360e080c0d649b6dd6b82ccd115eeefee8ff403bcee9aeb08618db9a2a94a9e80c7996bb2cb0c00f6e69de38ed8af2758ef39df0a',
49+
'10e7297c19413a9129c9ac57779baa43b273198bce8b27b2e3e3e764c2792d430f46742bf57d1c9d8c6593e70c891d384472a508ac44a2ec92effff1ff850ba4',
5050
},
5151
]
5252

@@ -75,3 +75,32 @@ test('bufferFromHex - converts hex strings to Buffer correctly', () => {
7575
assertEquals(bufferFromHex(input), expected, `bufferFromHex("${input}") should return correct Buffer`)
7676
}
7777
})
78+
79+
test('bufferFromHex - validates hex strings correctly', () => {
80+
// Valid hex strings should work
81+
const validHexes = ['0123456789abcdef', 'ABCDEF', '', '00', 'ff', 'deadbeef']
82+
83+
for (const hex of validHexes) {
84+
assertEquals(typeof bufferFromHex(hex), 'object', `bufferFromHex should accept valid hex string "${hex}"`)
85+
}
86+
87+
// Invalid hex strings should throw
88+
const invalidHexes = [
89+
'0123456789abcdefg', // invalid hex char
90+
'0123456789abcdef0', // odd length
91+
'xyz', // non-hex chars
92+
'gh', // non-hex chars
93+
' ', // whitespace
94+
'12 34', // spaces
95+
'12-34', // dashes
96+
]
97+
98+
for (const hex of invalidHexes) {
99+
assertThrows(
100+
() => bufferFromHex(hex),
101+
Error,
102+
'Invalid hex string',
103+
`bufferFromHex should reject invalid hex string "${hex}"`,
104+
)
105+
}
106+
})

crypto/hmac.ts

+35-10
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,55 @@ import { Buffer } from 'node:buffer'
33
import { createHmac } from 'node:crypto'
44

55
const HEX_ENCODING = 'hex'
6+
const HEX_REGEX = /^[0-9a-fA-F]*$/
7+
8+
/**
9+
* @module hmac
10+
* HMAC SHA-256 and SHA-512 hash functions
11+
*
12+
* @example
13+
* ```ts
14+
* import { hmacSha512 } from '@frytg/crypto/hmac'
15+
*
16+
* hmacSha512('hello world', '0123456789abcdef')
17+
* ```
18+
*/
619

720
/**
821
* Returns a HMAC SHA-256 hash of the input string (as hexadecimal string)
922
* @param str - The string to hash
10-
* @param key - The key to use for the HMAC
23+
* @param key - The key to use for the HMAC, when a string is provided, it will be converted to a Buffer
1124
* @returns HMAC SHA-256 hash of the input string (hexadecimal)
1225
*
1326
* @example
1427
* ```ts
1528
* import { hmacSha256 } from '@frytg/crypto/hmac'
1629
*
17-
* hmacSha256('hello', 'my-secret-key')
30+
* hmacSha256('hello world', '0123456789abcdef')
1831
* ```
1932
*/
20-
export const hmacSha256 = (str: string | Buffer, key: string | Buffer): string =>
21-
createHmac('sha256', key).update(str).digest(HEX_ENCODING)
33+
export const hmacSha256 = (str: string | Buffer, key: string | Buffer): string => {
34+
const keyBuffer = typeof key === 'string' ? bufferFromHex(key) : key
35+
return createHmac('sha256', keyBuffer).update(str).digest(HEX_ENCODING)
36+
}
2237

2338
/**
2439
* Returns a HMAC SHA-512 hash of the input string (as hexadecimal string)
2540
* @param str - The string to hash
26-
* @param key - The key to use for the HMAC
41+
* @param key - The key to use for the HMAC, when a string is provided, it will be converted to a Buffer
2742
* @returns HMAC SHA-512 hash of the input string (hexadecimal)
2843
*
2944
* @example
3045
* ```ts
3146
* import { hmacSha512 } from '@frytg/crypto/hmac'
3247
*
33-
* hmacSha512('hello', 'my-secret-key')
48+
* hmacSha512('hello world', '0123456789abcdef')
3449
* ```
3550
*/
36-
export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string =>
37-
createHmac('sha512', key).update(str).digest(HEX_ENCODING)
51+
export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string => {
52+
const keyBuffer = typeof key === 'string' ? bufferFromHex(key) : key
53+
return createHmac('sha512', keyBuffer).update(str).digest(HEX_ENCODING)
54+
}
3855

3956
/**
4057
* Converts a hexadecimal string to a Buffer for use with HMAC
@@ -45,7 +62,15 @@ export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string =
4562
* ```ts
4663
* import { hmacSha512, bufferFromHex } from '@frytg/crypto/hmac'
4764
*
48-
* hmacSha512('hello', bufferFromHex('0123456789abcdef'))
65+
* hmacSha512('hello world', bufferFromHex('0123456789abcdef'))
4966
* ```
5067
*/
51-
export const bufferFromHex = (hex: string): Buffer => Buffer.from(hex, HEX_ENCODING)
68+
export const bufferFromHex = (hex: string): Buffer => {
69+
// check if hex string is valid
70+
if (!HEX_REGEX.test(hex)) throw new Error('Invalid hex string')
71+
72+
// check if hex string is even length
73+
if (hex.length % 2 !== 0) throw new Error('Invalid hex string length')
74+
75+
return Buffer.from(hex, HEX_ENCODING)
76+
}

deno.jsonc

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@
3636
"@types/node": "npm:@types/node@^22.10.2",
3737
"@cross/test": "jsr:@cross/test@^0.0.10",
3838
"@std/assert": "jsr:@std/assert@^1.0.9",
39-
"sinon": "npm:sinon@^17.0.1"
39+
"sinon": "npm:sinon@^19.0.2"
4040
}
4141
}

deno.lock

+12-12
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)