diff --git a/crypto/CHANGELOG.md b/crypto/CHANGELOG.md index e07b753..c14239b 100644 --- a/crypto/CHANGELOG.md +++ b/crypto/CHANGELOG.md @@ -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 diff --git a/crypto/README.md b/crypto/README.md index 6fa45b0..6e30f35 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -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) diff --git a/crypto/deno.jsonc b/crypto/deno.jsonc index 6bcc18e..e00fa98 100644 --- a/crypto/deno.jsonc +++ b/crypto/deno.jsonc @@ -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" diff --git a/crypto/hash.ts b/crypto/hash.ts index 2242c20..cdc05a5 100644 --- a/crypto/hash.ts +++ b/crypto/hash.ts @@ -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) diff --git a/crypto/hmac.test.ts b/crypto/hmac.test.ts index 4c9fb57..b89fb31 100644 --- a/crypto/hmac.test.ts +++ b/crypto/hmac.test.ts @@ -1,6 +1,6 @@ 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' @@ -8,18 +8,18 @@ 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}"`, + ) + } +}) diff --git a/crypto/hmac.ts b/crypto/hmac.ts index 294c125..d7c56d6 100644 --- a/crypto/hmac.ts +++ b/crypto/hmac.ts @@ -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) +} diff --git a/deno.jsonc b/deno.jsonc index 28f6e91..58d196b 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -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" } } diff --git a/deno.lock b/deno.lock index 15773d7..2dfcb9d 100644 --- a/deno.lock +++ b/deno.lock @@ -12,7 +12,7 @@ "npm:@types/node@*": "22.5.4", "npm:@types/node@^22.10.2": "22.10.2", "npm:luxon@^3.5.0": "3.5.0", - "npm:sinon@^17.0.1": "17.0.2", + "npm:sinon@^19.0.2": "19.0.2", "npm:winston@^3.17.0": "3.17.0" }, "jsr": { @@ -99,8 +99,8 @@ "type-detect@4.0.8" ] }, - "@sinonjs/fake-timers@11.3.1": { - "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "@sinonjs/fake-timers@13.0.5": { + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", "dependencies": [ "@sinonjs/commons" ] @@ -164,8 +164,8 @@ "text-hex" ] }, - "diff@5.2.0": { - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==" + "diff@7.0.0": { + "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==" }, "enabled@2.0.0": { "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" @@ -214,8 +214,8 @@ "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "nise@5.1.9": { - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "nise@6.1.1": { + "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", "dependencies": [ "@sinonjs/commons", "@sinonjs/fake-timers", @@ -230,8 +230,8 @@ "fn.name" ] }, - "path-to-regexp@6.3.0": { - "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + "path-to-regexp@8.2.0": { + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==" }, "readable-stream@3.6.2": { "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", @@ -253,8 +253,8 @@ "is-arrayish" ] }, - "sinon@17.0.2": { - "integrity": "sha512-uihLiaB9FhzesElPDFZA7hDcNABzsVHwr3YfmM9sBllVwab3l0ltGlRV1XhpNfIacNDLGD1QRZNLs5nU5+hTuA==", + "sinon@19.0.2": { + "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", "dependencies": [ "@sinonjs/commons", "@sinonjs/fake-timers", @@ -388,7 +388,7 @@ "jsr:@std/assert@^1.0.9", "npm:@biomejs/biome@^1.9.4", "npm:@types/node@^22.10.2", - "npm:sinon@^17.0.1" + "npm:sinon@^19.0.2" ], "members": { "check-required-env": {