From 3aea9d87bf7b7fd1486a7110b1d98636a71e9b9a Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Tue, 17 Dec 2024 17:11:10 +0100 Subject: [PATCH] feat: add `bufferFromBase64` --- README.md | 1 + crypto/CHANGELOG.md | 4 ++ crypto/deno.jsonc | 2 +- crypto/hash.test.ts | 2 + crypto/hash.ts | 1 + crypto/hmac-buffer.test.ts | 118 +++++++++++++++++++++++++++++++++++++ crypto/hmac.test.ts | 57 ++---------------- crypto/hmac.ts | 23 +++++++- 8 files changed, 153 insertions(+), 55 deletions(-) create mode 100644 crypto/hmac-buffer.test.ts diff --git a/README.md b/README.md index 71f3890..a649f1e 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ This repository is work in progress. ## Tools - [`@frytg/check-required-env`](./check-required-env/README.md) - Check a required environment variable +- [`@frytg/crypto`](./crypto/README.md) - Crypto utilities (hash, hmac, etc.) - [`@frytg/dates`](./dates/README.md) - Date utilities around Luxon - [`@frytg/logger`](./logger/README.md) - Pre-configuredWinston logging wrapper diff --git a/crypto/CHANGELOG.md b/crypto/CHANGELOG.md index 3f5ac43..830e453 100644 --- a/crypto/CHANGELOG.md +++ b/crypto/CHANGELOG.md @@ -1,5 +1,9 @@ # Crypto Changelog +## 2024-12-17 - 0.1.0 + +- feat: add `bufferFromBase64` + ## 2024-12-16 - 0.0.3 - fix: optimize JSDoc for Deno diff --git a/crypto/deno.jsonc b/crypto/deno.jsonc index b4d3b53..23de6c9 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.3", + "version": "0.1.0", "exports": { "./hash": "./hash.ts", "./hmac": "./hmac.ts" diff --git a/crypto/hash.test.ts b/crypto/hash.test.ts index 68520aa..8d9ff95 100644 --- a/crypto/hash.test.ts +++ b/crypto/hash.test.ts @@ -1,6 +1,8 @@ +// load packages import { test } from '@cross/test' import { assertEquals } from '@std/assert' +// load module import { hashSha256, hashSha512 } from './hash.ts' test('hashSha256 - generates correct SHA-256 hashes', () => { diff --git a/crypto/hash.ts b/crypto/hash.ts index 934ddc4..92463d4 100644 --- a/crypto/hash.ts +++ b/crypto/hash.ts @@ -10,6 +10,7 @@ * ``` */ +// load packages import { createHash } from 'node:crypto' const HEX_ENCODING = 'hex' diff --git a/crypto/hmac-buffer.test.ts b/crypto/hmac-buffer.test.ts new file mode 100644 index 0000000..b570f2a --- /dev/null +++ b/crypto/hmac-buffer.test.ts @@ -0,0 +1,118 @@ +// load packages +import { Buffer } from 'node:buffer' +import { test } from '@cross/test' +import { assertEquals, assertThrows } from '@std/assert' + +// load module +import { bufferFromBase64, bufferFromHex } from './hmac.ts' + +test('bufferFromBase64 - converts base64 strings to Buffer correctly', () => { + const testCases = [ + { + input: 'aGVsbG8=', // "hello" + expected: Buffer.from('hello'), + }, + { + input: '', // empty string + expected: Buffer.from(''), + }, + { + input: 'YWJjZGVmMTIzNDU2', // "abcdef123456" + expected: Buffer.from('abcdef123456'), + }, + { + input: 'Zm9vIGJhcg==', // "foo bar" + expected: Buffer.from('foo bar'), + }, + ] + + for (const { input, expected } of testCases) { + assertEquals(bufferFromBase64(input), expected, `bufferFromBase64("${input}") should return correct Buffer`) + } +}) + +test('bufferFromBase64 - validates base64 strings correctly', () => { + // Valid base64 strings should work + const validBase64 = [ + 'aGVsbG8=', // normal case + '', // empty string + 'YQ==', // single char padding + 'YWI=', // double char padding + 'YWJj', // no padding needed + 'YWJjZA==', // standard padding + ] + + for (const base64 of validBase64) { + assertEquals( + typeof bufferFromBase64(base64), + 'object', + `bufferFromBase64 should accept valid base64 string "${base64}"`, + ) + } + + // Invalid base64 strings should throw + const invalidBase64 = [ + '!@#$', // invalid characters + 'hello', // not base64 + 'YW JjZA==', // spaces + ] + + for (const base64 of invalidBase64) { + assertThrows( + () => bufferFromBase64(base64), + Error, + 'base64', // error message should include + `bufferFromBase64 should reject invalid base64 string "${base64}"`, + ) + } +}) + +test('bufferFromHex - converts hex strings to Buffer correctly', () => { + const testCases = [ + { + input: '0123456789abcdef', + expected: Buffer.from([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]), + }, + { + input: 'ff00ff00', + expected: Buffer.from([0xff, 0x00, 0xff, 0x00]), + }, + { + input: '', + expected: Buffer.from([]), + }, + ] + + for (const { input, expected } of testCases) { + 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', // error message should include + `bufferFromHex should reject invalid hex string "${hex}"`, + ) + } +}) diff --git a/crypto/hmac.test.ts b/crypto/hmac.test.ts index b89fb31..5e0a7e9 100644 --- a/crypto/hmac.test.ts +++ b/crypto/hmac.test.ts @@ -1,8 +1,9 @@ -import { Buffer } from 'node:buffer' +// load packages import { test } from '@cross/test' -import { assertEquals, assertThrows } from '@std/assert' +import { assertEquals } from '@std/assert' -import { bufferFromHex, hmacSha256, hmacSha512 } from './hmac.ts' +// load module +import { hmacSha256, hmacSha512 } from './hmac.ts' test('hmacSha256 - generates correct HMAC SHA-256 hashes', () => { const testCases = [ @@ -54,53 +55,3 @@ test('hmacSha512 - generates correct HMAC SHA-512 hashes', () => { assertEquals(hmacSha512(input, key), expected, `hmacSha512("${input}", "${key}") should return "${expected}"`) } }) - -test('bufferFromHex - converts hex strings to Buffer correctly', () => { - const testCases = [ - { - input: '0123456789abcdef', - expected: Buffer.from([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef]), - }, - { - input: 'ff00ff00', - expected: Buffer.from([0xff, 0x00, 0xff, 0x00]), - }, - { - input: '', - expected: Buffer.from([]), - }, - ] - - for (const { input, expected } of testCases) { - 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 5bff835..d28c96d 100644 --- a/crypto/hmac.ts +++ b/crypto/hmac.ts @@ -10,10 +10,12 @@ * ``` */ -// load package +// load packages import { Buffer } from 'node:buffer' import { createHmac } from 'node:crypto' +const BASE64_ENCODING = 'base64' +const BASE64_REGEX = /^[0-9a-zA-Z+/=]*$/ const HEX_ENCODING = 'hex' const HEX_REGEX = /^[0-9a-fA-F]*$/ @@ -53,6 +55,25 @@ export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string = return createHmac('sha512', keyBuffer).update(str).digest(HEX_ENCODING) } +/** + * Converts a base64 string to a Buffer for use with HMAC + * @param base64 - The base64 string to convert + * @returns Buffer + * + * @example + * ```ts + * import { hmacSha512, bufferFromBase64 } from '@frytg/crypto/hmac' + * + * hmacSha512('hello world', bufferFromBase64('MDEyMzQ1Njc4OWFiY2RlZg==')) + * ``` + */ +export const bufferFromBase64 = (base64: string): Buffer => { + // check if base64 string is valid + if (!BASE64_REGEX.test(base64)) throw new Error('Invalid base64 string') + + return Buffer.from(base64, BASE64_ENCODING) +} + /** * Converts a hexadecimal string to a Buffer for use with HMAC * @param hex - The hexadecimal string to convert