From 3aea9d87bf7b7fd1486a7110b1d98636a71e9b9a Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Tue, 17 Dec 2024 17:11:10 +0100 Subject: [PATCH 1/4] 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 From 1e71ba66ba0353df7a394ffdab81b20fd8882eff Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Tue, 17 Dec 2024 17:57:46 +0100 Subject: [PATCH 2/4] feat: add key generation --- crypto/CHANGELOG.md | 1 + crypto/README.md | 17 ++++++++ crypto/deno.jsonc | 4 +- crypto/generate-default-keys.ts | 16 ++++++++ crypto/generate-key.test.ts | 71 +++++++++++++++++++++++++++++++++ crypto/generate-key.ts | 57 ++++++++++++++++++++++++++ crypto/hash.ts | 8 ++-- crypto/hmac.ts | 20 +++++----- deno.jsonc | 2 +- 9 files changed, 180 insertions(+), 16 deletions(-) create mode 100644 crypto/generate-default-keys.ts create mode 100644 crypto/generate-key.test.ts create mode 100644 crypto/generate-key.ts diff --git a/crypto/CHANGELOG.md b/crypto/CHANGELOG.md index 830e453..ca40257 100644 --- a/crypto/CHANGELOG.md +++ b/crypto/CHANGELOG.md @@ -3,6 +3,7 @@ ## 2024-12-17 - 0.1.0 - feat: add `bufferFromBase64` +- feat: add key generation ## 2024-12-16 - 0.0.3 diff --git a/crypto/README.md b/crypto/README.md index 6e30f35..ac78996 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -8,6 +8,23 @@ This package provides functions to compute SHA-256 and SHA-512 hashes using [nat - [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) +- [generate default keys](https://jsr.io/@frytg/crypto/doc/generate-default-keys) +- [generate key](https://jsr.io/@frytg/crypto/doc/generate-key) + +## Generate default keys + +You can generate default keys for common key sizes by running the following command: + +```bash +deno run jsr:@frytg/crypto/generate-default-keys +``` + +The output will be printed to the console. + +Those can be used for HMAC or encryption key generation. +Google Cloud Storage for example uses 32 byte keys (in base64) for [customer-supplied](https://cloud.google.com/storage/docs/encryption/customer-supplied-keys) [encryption keys](https://cloud.google.com/storage/docs/encryption/using-customer-supplied-keys#storage-upload-encrypted-object-nodejs). + +For SHA-256 the recommended key size is 32 bytes (256 bits) and for SHA-512 it is 64 bytes (512 bits) (see [RFC 2104](https://www.rfc-editor.org/rfc/rfc2104#section-2) - section 2 and 3). ## Author diff --git a/crypto/deno.jsonc b/crypto/deno.jsonc index 23de6c9..52073cf 100644 --- a/crypto/deno.jsonc +++ b/crypto/deno.jsonc @@ -4,7 +4,9 @@ "version": "0.1.0", "exports": { "./hash": "./hash.ts", - "./hmac": "./hmac.ts" + "./hmac": "./hmac.ts", + "./generate-default-keys": "./generate-default-keys.ts", + "./generate-key": "./generate-key.ts" }, "publish": { "exclude": ["*.test.ts"] diff --git a/crypto/generate-default-keys.ts b/crypto/generate-default-keys.ts new file mode 100644 index 0000000..9d314da --- /dev/null +++ b/crypto/generate-default-keys.ts @@ -0,0 +1,16 @@ +/** + * @module + * Run key generation for default key sizes and print the results to the console + * + * @example + * ```bash + * deno run jsr:@frytg/crypto/generate-default-keys + * ``` + */ + +// load module +import { generateKey } from './generate-key.ts' + +generateKey(32) +generateKey(64) +generateKey(128) diff --git a/crypto/generate-key.test.ts b/crypto/generate-key.test.ts new file mode 100644 index 0000000..050661f --- /dev/null +++ b/crypto/generate-key.test.ts @@ -0,0 +1,71 @@ +// load packages +import { Buffer } from 'node:buffer' +import { test } from '@cross/test' +import { assertEquals, assertExists } from '@std/assert' + +// load module +import { generateKey } from './generate-key.ts' + +test('generateKey - generates keys of correct length', () => { + const testCases = [{ bytes: 16 }, { bytes: 32 }, { bytes: 64 }, { bytes: 128 }] + + for (const { bytes } of testCases) { + const key = generateKey(bytes, true) + + // Check buffer length + assertEquals(key.buffer.length, bytes, `Buffer should be ${bytes} bytes long`) + + // Check hex length (2 characters per byte) + assertEquals(key.hex.length, bytes * 2, `Hex string should be ${bytes * 2} characters long`) + } +}) + +test('generateKey - generates different keys each time', () => { + const keys = new Set() + const numKeys = 100 + const bytes = 32 + + // Generate multiple keys + for (let i = 0; i < numKeys; i++) { + const key = generateKey(bytes, true) + keys.add(key.hex) + } + + // All keys should be unique + assertEquals(keys.size, numKeys, `All ${numKeys} generated keys should be unique`) +}) + +test('generateKey - returns consistent encodings', () => { + const key = generateKey(32, true) + + // Buffer to base64 + assertEquals(key.base64, key.buffer.toString('base64'), 'base64 encoding should match Buffer.toString("base64")') + + // Buffer to hex + assertEquals(key.hex, key.buffer.toString('hex'), 'hex encoding should match Buffer.toString("hex")') + + // base64 back to buffer + assertEquals(Buffer.from(key.base64, 'base64'), key.buffer, 'base64 string should convert back to original buffer') + + // hex back to buffer + assertEquals(Buffer.from(key.hex, 'hex'), key.buffer, 'hex string should convert back to original buffer') +}) + +test('generateKey - uses default length of 32 bytes', () => { + const key = generateKey(undefined, true) + assertEquals(key.buffer.length, 32, 'Default key length should be 32 bytes') +}) + +test('generateKey - returns object with required properties', () => { + const key = generateKey(32, true) + + // Check that all properties exist + assertExists(key.buffer, 'Should have buffer property') + assertExists(key.base64, 'Should have base64 property') + assertExists(key.hex, 'Should have hex property') + + // Check property types + assertEquals(key.buffer instanceof Buffer, true, 'buffer should be Buffer instance') + assertEquals(typeof key.base64, 'string', 'base64 should be string') + assertEquals(typeof key.hex, 'string', 'hex should be string') +}) diff --git a/crypto/generate-key.ts b/crypto/generate-key.ts new file mode 100644 index 0000000..7a910ac --- /dev/null +++ b/crypto/generate-key.ts @@ -0,0 +1,57 @@ +// deno-lint-ignore-file no-console +/** + * @module + * {@linkcode generateKey | Generate a key} + * + * @example + * ```ts + * import { generateKey } from '@frytg/crypto/generate-key' + * + * generateKey(32, true) + * ``` + */ + +// demo provided by +// https://cloud.google.com/storage/docs/encryption/using-customer-supplied-keys#storage-generate-encryption-key-nodejs + +// load packages +import type { Buffer } from 'node:buffer' +import crypto from 'node:crypto' + +/** + * Generates a key of the specified number of bytes and prints the key in base64 and hex to the console + * + * @param {number} bytes - The number of bytes to generate + * @param {boolean} skipConsole - Whether to skip printing to the console + * @returns {Object} The key in base64 and hex + * + * @example + * ```ts + * import { generateKey } from '@frytg/crypto/generate-key' + * + * generateKey(32, true) + * ``` + */ +export const generateKey = (bytes = 32, skipConsole = false): { buffer: Buffer; base64: string; hex: string } => { + // generate key + if (!skipConsole) console.group(`Generating ${bytes} byte key...\n`) + + // generate key + const buffer = crypto.randomBytes(bytes) + + // encode key in base64 + const encodedKeyBase64 = buffer.toString('base64') + if (!skipConsole) console.log(`Base 64 encoded key:\n${encodedKeyBase64}\n`) + + // encode key in hex + const encodedKeyHex = buffer.toString('hex') + if (!skipConsole) console.log(`Hex encoded key:\n${encodedKeyHex}\n`) + + if (!skipConsole) console.groupEnd() + + return { + buffer, + base64: encodedKeyBase64, + hex: encodedKeyHex, + } +} diff --git a/crypto/hash.ts b/crypto/hash.ts index 92463d4..e3b41c1 100644 --- a/crypto/hash.ts +++ b/crypto/hash.ts @@ -17,8 +17,8 @@ const HEX_ENCODING = 'hex' /** * Returns a SHA-256 hash of the input string (as hexadecimal string) - * @param str - The string to hash - * @returns SHA-256 hash of the input string (hexadecimal) + * @param {string} str - The string to hash + * @returns {string} SHA-256 hash of the input string (hexadecimal) * * @example SHA-256 * ```ts @@ -31,8 +31,8 @@ export const hashSha256 = (str: string): string => createHash('sha256').update(s /** * Returns a SHA-512 hash of the input string (as hexadecimal string) - * @param str - The string to hash - * @returns SHA-512 hash of the input string (hexadecimal) + * @param {string} str - The string to hash + * @returns {string} SHA-512 hash of the input string (hexadecimal) * * @example SHA-512 * ```ts diff --git a/crypto/hmac.ts b/crypto/hmac.ts index d28c96d..741ca5d 100644 --- a/crypto/hmac.ts +++ b/crypto/hmac.ts @@ -21,9 +21,9 @@ const HEX_REGEX = /^[0-9a-fA-F]*$/ /** * 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, when a string is provided, it will be converted to a Buffer - * @returns HMAC SHA-256 hash of the input string (hexadecimal) + * @param {string | Buffer} str - The string to hash + * @param {string | Buffer} key - The key to use for the HMAC, when a string is provided, it will be converted to a Buffer + * @returns {string} HMAC SHA-256 hash of the input string (hexadecimal) * * @example * ```ts @@ -39,9 +39,9 @@ export const hmacSha256 = (str: string | Buffer, key: string | Buffer): string = /** * 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, when a string is provided, it will be converted to a Buffer - * @returns HMAC SHA-512 hash of the input string (hexadecimal) + * @param {string | Buffer} str - The string to hash + * @param {string | Buffer} key - The key to use for the HMAC, when a string is provided, it will be converted to a Buffer + * @returns {string} HMAC SHA-512 hash of the input string (hexadecimal) * * @example * ```ts @@ -57,8 +57,8 @@ export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string = /** * Converts a base64 string to a Buffer for use with HMAC - * @param base64 - The base64 string to convert - * @returns Buffer + * @param {string} base64 - The base64 string to convert + * @returns {Buffer} * * @example * ```ts @@ -76,8 +76,8 @@ export const bufferFromBase64 = (base64: string): Buffer => { /** * Converts a hexadecimal string to a Buffer for use with HMAC - * @param hex - The hexadecimal string to convert - * @returns Buffer + * @param {string} hex - The hexadecimal string to convert + * @returns {Buffer} * * @example * ```ts diff --git a/deno.jsonc b/deno.jsonc index 58d196b..b1b0a35 100644 --- a/deno.jsonc +++ b/deno.jsonc @@ -16,7 +16,7 @@ }, "tasks": { "check": "deno fmt --check && deno lint && biome lint", - "test": "deno test --allow-sys --allow-env" + "test": "deno test --allow-sys --allow-env --clean --coverage" }, "lint": { "rules": { From 7268e4a2efe422640beafdf87b56cf4778159902 Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Tue, 17 Dec 2024 20:16:44 +0100 Subject: [PATCH 3/4] fix: unhandled rejections for invalid base64 --- crypto/hmac-buffer.test.ts | 8 +++++--- crypto/hmac.ts | 16 +++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/crypto/hmac-buffer.test.ts b/crypto/hmac-buffer.test.ts index b570f2a..1070490 100644 --- a/crypto/hmac-buffer.test.ts +++ b/crypto/hmac-buffer.test.ts @@ -31,7 +31,7 @@ test('bufferFromBase64 - converts base64 strings to Buffer correctly', () => { } }) -test('bufferFromBase64 - validates base64 strings correctly', () => { +test('bufferFromBase64 - validates valid base64 strings correctly', () => { // Valid base64 strings should work const validBase64 = [ 'aGVsbG8=', // normal case @@ -46,10 +46,12 @@ test('bufferFromBase64 - validates base64 strings correctly', () => { assertEquals( typeof bufferFromBase64(base64), 'object', - `bufferFromBase64 should accept valid base64 string "${base64}"`, + `bufferFromBase64 should accept valid base64 string "${base64}".`, ) } +}) +test('bufferFromBase64 - validates invalid base64 strings correctly', () => { // Invalid base64 strings should throw const invalidBase64 = [ '!@#$', // invalid characters @@ -62,7 +64,7 @@ test('bufferFromBase64 - validates base64 strings correctly', () => { () => bufferFromBase64(base64), Error, 'base64', // error message should include - `bufferFromBase64 should reject invalid base64 string "${base64}"`, + `bufferFromBase64 should reject invalid base64 string "${base64}".`, ) } }) diff --git a/crypto/hmac.ts b/crypto/hmac.ts index 741ca5d..cae3a5f 100644 --- a/crypto/hmac.ts +++ b/crypto/hmac.ts @@ -11,10 +11,9 @@ */ // load packages -import { Buffer } from 'node:buffer' +import { Buffer, atob } 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]*$/ @@ -67,11 +66,18 @@ export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string = * hmacSha512('hello world', bufferFromBase64('MDEyMzQ1Njc4OWFiY2RlZg==')) * ``` */ -export const bufferFromBase64 = (base64: string): Buffer => { +export const bufferFromBase64 = (base64: string, preferNativeError = false): Buffer => { // check if base64 string is valid - if (!BASE64_REGEX.test(base64)) throw new Error('Invalid base64 string') + if (!BASE64_REGEX.test(base64)) throw new Error('Invalid base64 characters') - return Buffer.from(base64, BASE64_ENCODING) + try { + // we're using atob because Buffer.from(base64, 'base64') throws unhandled rejections on some systems + return Buffer.from(atob(base64)) + } catch (error) { + // the native error varies between runtimes, so the default is to throw our own error + if (preferNativeError) throw error + throw new Error('Invalid base64 string') + } } /** From 74a7ed054ccee7827891728a9c646243b1444dbe Mon Sep 17 00:00:00 2001 From: Daniel Freytag Date: Tue, 17 Dec 2024 20:27:13 +0100 Subject: [PATCH 4/4] fix: properly validate hmac strings --- crypto/hmac-buffer.test.ts | 4 +++- crypto/hmac.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/crypto/hmac-buffer.test.ts b/crypto/hmac-buffer.test.ts index 1070490..2744412 100644 --- a/crypto/hmac-buffer.test.ts +++ b/crypto/hmac-buffer.test.ts @@ -90,14 +90,16 @@ test('bufferFromHex - converts hex strings to Buffer correctly', () => { } }) -test('bufferFromHex - validates hex strings correctly', () => { +test('bufferFromHex - validates valid 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}"`) } +}) +test('bufferFromHex - validates invalid hex strings correctly', () => { // Invalid hex strings should throw const invalidHexes = [ '0123456789abcdefg', // invalid hex char diff --git a/crypto/hmac.ts b/crypto/hmac.ts index cae3a5f..3408e44 100644 --- a/crypto/hmac.ts +++ b/crypto/hmac.ts @@ -11,6 +11,7 @@ */ // load packages +// deno-fmt-ignore import { Buffer, atob } from 'node:buffer' import { createHmac } from 'node:crypto'