Skip to content

Commit

Permalink
Merge pull request #8 from frytg/dev/hmac-keys
Browse files Browse the repository at this point in the history
fix: optimize key handling for HMAC
  • Loading branch information
frytg authored Dec 16, 2024
2 parents b1dcda3 + 0f46f68 commit 4487a49
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 40 deletions.
6 changes: 5 additions & 1 deletion crypto/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Crypto Changelog

## 2024-12-16 - 0.0.2

- fix: optimize key handling for HMAC

## 2024-12-12 - 0.0.1

- feat: added hash and hmac functions for SHA-256 and SHA-512
- feat: added hash and HMAC functions for SHA-256 and SHA-512
1 change: 1 addition & 0 deletions crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

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

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

Expand Down
2 changes: 1 addition & 1 deletion crypto/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://jsr.io/schema/config-file.v1.json",
"name": "@frytg/crypto",
"version": "0.0.1",
"version": "0.0.2",
"exports": {
"./hash": "./hash.ts",
"./hmac": "./hmac.ts"
Expand Down
16 changes: 14 additions & 2 deletions crypto/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@ import { createHash } from 'node:crypto'

const HEX_ENCODING = 'hex'

/**
* @module hash
* SHA-256 and SHA-512 hash functions
*
* @example
* ```ts
* import { hashSha512 } from '@frytg/crypto/hash'
*
* hashSha512('hello world')
* ```
*/

/**
* Returns a SHA-256 hash of the input string (as hexadecimal string)
* @param str - The string to hash
Expand All @@ -11,7 +23,7 @@ const HEX_ENCODING = 'hex'
* ```ts
* import { hashSha256 } from '@frytg/crypto/hash'
*
* hashSha256('hello')
* hashSha256('hello world')
* ```
*/
export const hashSha256 = (str: string): string => createHash('sha256').update(str).digest(HEX_ENCODING)
Expand All @@ -25,7 +37,7 @@ export const hashSha256 = (str: string): string => createHash('sha256').update(s
* ```ts
* import { hashSha512 } from '@frytg/crypto/hash'
*
* hashSha512('hello')
* hashSha512('hello world')
* ```
*/
export const hashSha512 = (str: string): string => createHash('sha512').update(str).digest(HEX_ENCODING)
55 changes: 42 additions & 13 deletions crypto/hmac.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { Buffer } from 'node:buffer'
import { test } from '@cross/test'
import { assertEquals } from '@std/assert'
import { assertEquals, assertThrows } from '@std/assert'

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

test('hmacSha256 - generates correct HMAC SHA-256 hashes', () => {
const testCases = [
{
input: 'hello',
key: 'secret',
expected: '88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b',
key: '0123456789abcdef',
expected: '58341de110352e89a9dfe341aede35073e34b5640f006ed94208efa321d68994',
},
{
input: '',
key: 'secret',
expected: 'f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169',
key: '0123456789abcdef',
expected: 'f2f24bb00417d3d905c2fc9b659fbe5310af55be93eb00524fc2021e3cc29a88',
},
{
input: 'The quick brown fox jumps over the lazy dog',
key: 'secret',
expected: '54cd5b827c0ec938fa072a29b177469c843317b095591dc846767aa338bac600',
key: '0123456789abcdef',
expected: '5a4921e469387c921b75e6f135db948ab94a0ee5d28c8d8706df5df3c09a8095',
},
]

Expand All @@ -32,21 +32,21 @@ test('hmacSha512 - generates correct HMAC SHA-512 hashes', () => {
const testCases = [
{
input: 'hello',
key: 'secret',
key: '0123456789abcdef',
expected:
'db1595ae88a62fd151ec1cba81b98c39df82daae7b4cb9820f446d5bf02f1dcfca6683d88cab3e273f5963ab8ec469a746b5b19086371239f67d1e5f99a79440',
'e603296d6ec667b62905984498c87cee7c35625fac4517108d74ac169ab0a6727a65d4786cd11c3c0851b8505983714f58ee2156f32093e9360cb275539802e9',
},
{
input: '',
key: 'secret',
key: '0123456789abcdef',
expected:
'b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901',
'acae8450151bdbb810f41200da1bf26ef2756037bcdc930b014cbbc5fccb3b9ddc0cdcee6b05fa07d88e65af87d202e6dd8d0c5303a8a0866a2a5ce505779808',
},
{
input: 'The quick brown fox jumps over the lazy dog',
key: 'secret',
key: '0123456789abcdef',
expected:
'76af3588620ef6e2c244d5a360e080c0d649b6dd6b82ccd115eeefee8ff403bcee9aeb08618db9a2a94a9e80c7996bb2cb0c00f6e69de38ed8af2758ef39df0a',
'10e7297c19413a9129c9ac57779baa43b273198bce8b27b2e3e3e764c2792d430f46742bf57d1c9d8c6593e70c891d384472a508ac44a2ec92effff1ff850ba4',
},
]

Expand Down Expand Up @@ -75,3 +75,32 @@ test('bufferFromHex - converts hex strings to Buffer correctly', () => {
assertEquals(bufferFromHex(input), expected, `bufferFromHex("${input}") should return correct Buffer`)
}
})

test('bufferFromHex - validates hex strings correctly', () => {
// Valid hex strings should work
const validHexes = ['0123456789abcdef', 'ABCDEF', '', '00', 'ff', 'deadbeef']

for (const hex of validHexes) {
assertEquals(typeof bufferFromHex(hex), 'object', `bufferFromHex should accept valid hex string "${hex}"`)
}

// Invalid hex strings should throw
const invalidHexes = [
'0123456789abcdefg', // invalid hex char
'0123456789abcdef0', // odd length
'xyz', // non-hex chars
'gh', // non-hex chars
' ', // whitespace
'12 34', // spaces
'12-34', // dashes
]

for (const hex of invalidHexes) {
assertThrows(
() => bufferFromHex(hex),
Error,
'Invalid hex string',
`bufferFromHex should reject invalid hex string "${hex}"`,
)
}
})
45 changes: 35 additions & 10 deletions crypto/hmac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,55 @@ import { Buffer } from 'node:buffer'
import { createHmac } from 'node:crypto'

const HEX_ENCODING = 'hex'
const HEX_REGEX = /^[0-9a-fA-F]*$/

/**
* @module hmac
* HMAC SHA-256 and SHA-512 hash functions
*
* @example
* ```ts
* import { hmacSha512 } from '@frytg/crypto/hmac'
*
* hmacSha512('hello world', '0123456789abcdef')
* ```
*/

/**
* Returns a HMAC SHA-256 hash of the input string (as hexadecimal string)
* @param str - The string to hash
* @param key - The key to use for the HMAC
* @param key - The key to use for the HMAC, when a string is provided, it will be converted to a Buffer
* @returns HMAC SHA-256 hash of the input string (hexadecimal)
*
* @example
* ```ts
* import { hmacSha256 } from '@frytg/crypto/hmac'
*
* hmacSha256('hello', 'my-secret-key')
* hmacSha256('hello world', '0123456789abcdef')
* ```
*/
export const hmacSha256 = (str: string | Buffer, key: string | Buffer): string =>
createHmac('sha256', key).update(str).digest(HEX_ENCODING)
export const hmacSha256 = (str: string | Buffer, key: string | Buffer): string => {
const keyBuffer = typeof key === 'string' ? bufferFromHex(key) : key
return createHmac('sha256', keyBuffer).update(str).digest(HEX_ENCODING)
}

/**
* Returns a HMAC SHA-512 hash of the input string (as hexadecimal string)
* @param str - The string to hash
* @param key - The key to use for the HMAC
* @param key - The key to use for the HMAC, when a string is provided, it will be converted to a Buffer
* @returns HMAC SHA-512 hash of the input string (hexadecimal)
*
* @example
* ```ts
* import { hmacSha512 } from '@frytg/crypto/hmac'
*
* hmacSha512('hello', 'my-secret-key')
* hmacSha512('hello world', '0123456789abcdef')
* ```
*/
export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string =>
createHmac('sha512', key).update(str).digest(HEX_ENCODING)
export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string => {
const keyBuffer = typeof key === 'string' ? bufferFromHex(key) : key
return createHmac('sha512', keyBuffer).update(str).digest(HEX_ENCODING)
}

/**
* Converts a hexadecimal string to a Buffer for use with HMAC
Expand All @@ -45,7 +62,15 @@ export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string =
* ```ts
* import { hmacSha512, bufferFromHex } from '@frytg/crypto/hmac'
*
* hmacSha512('hello', bufferFromHex('0123456789abcdef'))
* hmacSha512('hello world', bufferFromHex('0123456789abcdef'))
* ```
*/
export const bufferFromHex = (hex: string): Buffer => Buffer.from(hex, HEX_ENCODING)
export const bufferFromHex = (hex: string): Buffer => {
// check if hex string is valid
if (!HEX_REGEX.test(hex)) throw new Error('Invalid hex string')

// check if hex string is even length
if (hex.length % 2 !== 0) throw new Error('Invalid hex string length')

return Buffer.from(hex, HEX_ENCODING)
}
2 changes: 1 addition & 1 deletion deno.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@
"@types/node": "npm:@types/node@^22.10.2",
"@cross/test": "jsr:@cross/test@^0.0.10",
"@std/assert": "jsr:@std/assert@^1.0.9",
"sinon": "npm:sinon@^17.0.1"
"sinon": "npm:sinon@^19.0.2"
}
}
24 changes: 12 additions & 12 deletions deno.lock

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

0 comments on commit 4487a49

Please sign in to comment.