Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: optimize key handling for HMAC #8

Merged
merged 1 commit into from
Dec 16, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
@@ -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)

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"
16 changes: 14 additions & 2 deletions crypto/hash.ts
Original file line number Diff line number Diff line change
@@ -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
@@ -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)
@@ -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',
},
]

@@ -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',
},
]

@@ -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
@@ -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
@@ -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
@@ -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.