diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a7190b1..8beeeab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,7 +17,7 @@ permissions: contents: read env: - JSR_DEPENDENCIES: "@cross/test @std/assert @std/fmt @frytg/logger @frytg/dates" + JSR_DEPENDENCIES: "@cross/test @std/assert @std/fmt @frytg/logger" NPM_DEPENDENCIES: "luxon sinon" jobs: diff --git a/.gitignore b/.gitignore index 5854978..21b6ced 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ node_modules/ vendor/ tmp +.dev/ diff --git a/README.md b/README.md index fa070df..71f3890 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,18 @@ This repository is work in progress. - [`@frytg/dates`](./dates/README.md) - Date utilities around Luxon - [`@frytg/logger`](./logger/README.md) - Pre-configuredWinston logging wrapper +## More Tooling + +Planned for this utility package: + +- `hashes` - sha256, sha512, etc. + +Other tools that I regularly use and don't feel the need to optimize or re-create in this utility package: + +- [`axios`](https://github.com/axios/axios) - _Promise based HTTP client for the browser and node.js_ +- [`hono`](https://jsr.io/@hono/hono) - _small, simple, and ultrafast web framework built on Web Standards_ +- [`undici`](https://github.com/nodejs/undici) - performant HTTP/1.1 client for Node.js + ## Lint Use `deno fmt`, `deno lint` and `biome lint` to check the code. @@ -43,13 +55,6 @@ deno publish --dry-run Then once everything is pushed or merged on `main`, run the GitHub actions workflow to publish the packages to JSR (see [_publishing packages_](https://jsr.io/docs/publishing-packages) for more details). -## More Tooling - -Other tools that I regularly use and don't feel the need to optimize or re-create in this utility package: - -- [`axios`](https://github.com/axios/axios) - _Promise based HTTP client for the browser and node.js_ -- [`hono`](https://jsr.io/@hono/hono) - _small, simple, and ultrafast web framework built on Web Standards_ - ## Author Created by [@frytg](https://github.com/frytg) / [frytg.digital](https://www.frytg.digital) diff --git a/check-required-env/README.md b/check-required-env/README.md index 58ab0ed..8bf9017 100644 --- a/check-required-env/README.md +++ b/check-required-env/README.md @@ -6,7 +6,7 @@ Simply check if a certain required environment variable is set. If not, throw an error and exit the process. ```ts -import { checkRequiredEnv } from 'jsr:@frytg/check-required-env'; +import { checkRequiredEnv } from '@frytg/check-required-env'; checkRequiredEnv('MY_IMPORTANT_ENV_VAR'); ``` diff --git a/check-required-env/check-required-env.ts b/check-required-env/check-required-env.ts index 4cddff9..b09f5ce 100644 --- a/check-required-env/check-required-env.ts +++ b/check-required-env/check-required-env.ts @@ -10,7 +10,7 @@ import logger from '@frytg/logger' * * @example * ```ts - * import { checkRequiredEnv } from 'jsr:@frytg/check-required-env' + * import { checkRequiredEnv } from '@frytg/check-required-env' * * checkRequiredEnv('MY_IMPORTANT_ENV_VAR') * ``` diff --git a/crypto/CHANGELOG.md b/crypto/CHANGELOG.md new file mode 100644 index 0000000..e07b753 --- /dev/null +++ b/crypto/CHANGELOG.md @@ -0,0 +1,5 @@ +# Crypto Changelog + +## 2024-12-12 - 0.0.1 + +- feat: added hash and hmac functions for SHA-256 and SHA-512 diff --git a/crypto/README.md b/crypto/README.md new file mode 100644 index 0000000..6fa45b0 --- /dev/null +++ b/crypto/README.md @@ -0,0 +1,17 @@ +# Crypto utilities + +[![JSR @frytg/crypto](https://jsr.io/badges/@frytg/crypto)](https://jsr.io/@frytg/crypto) +[![ci](https://github.com/frytg/utility/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/frytg/utility/actions/workflows/test.yml) + +This package provides functions to compute SHA-256 and SHA-512 hashes using [native Node crypto](https://nodejs.org/api/crypto.html). + +- [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) + +## Author + +Created by [@frytg](https://github.com/frytg) / [frytg.digital](https://www.frytg.digital) + +## License + +[Unlicense](https://github.com/frytg/utility/blob/main/LICENSE) - also see [unlicense.org](https://unlicense.org) diff --git a/crypto/hash.test.ts b/crypto/hash.test.ts new file mode 100644 index 0000000..68520aa --- /dev/null +++ b/crypto/hash.test.ts @@ -0,0 +1,49 @@ +import { test } from '@cross/test' +import { assertEquals } from '@std/assert' + +import { hashSha256, hashSha512 } from './hash.ts' + +test('hashSha256 - generates correct SHA-256 hashes', () => { + const testCases = [ + { + input: 'hello', + expected: '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824', + }, + { + input: '', + expected: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', + }, + { + input: 'The quick brown fox jumps over the lazy dog', + expected: 'd7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592', + }, + ] + + for (const { input, expected } of testCases) { + assertEquals(hashSha256(input), expected, `hashSha256("${input}") should return "${expected}"`) + } +}) + +test('hashSha512 - generates correct SHA-512 hashes', () => { + const testCases = [ + { + input: 'hello', + expected: + '9b71d224bd62f3785d96d46ad3ea3d73319bfbc2890caadae2dff72519673ca72323c3d99ba5c11d7c7acc6e14b8c5da0c4663475c2e5c3adef46f73bcdec043', + }, + { + input: '', + expected: + 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e', + }, + { + input: 'The quick brown fox jumps over the lazy dog', + expected: + '07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6', + }, + ] + + for (const { input, expected } of testCases) { + assertEquals(hashSha512(input), expected, `hashSha512("${input}") should return "${expected}"`) + } +}) diff --git a/crypto/hash.ts b/crypto/hash.ts new file mode 100644 index 0000000..2242c20 --- /dev/null +++ b/crypto/hash.ts @@ -0,0 +1,31 @@ +import { createHash } from 'node:crypto' + +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) + * + * @example SHA-256 + * ```ts + * import { hashSha256 } from '@frytg/crypto/hash' + * + * hashSha256('hello') + * ``` + */ +export const hashSha256 = (str: string): string => createHash('sha256').update(str).digest(HEX_ENCODING) + +/** + * 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) + * + * @example SHA-512 + * ```ts + * import { hashSha512 } from '@frytg/crypto/hash' + * + * hashSha512('hello') + * ``` + */ +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 new file mode 100644 index 0000000..4c9fb57 --- /dev/null +++ b/crypto/hmac.test.ts @@ -0,0 +1,77 @@ +import { Buffer } from 'node:buffer' +import { test } from '@cross/test' +import { assertEquals } 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', + }, + { + input: '', + key: 'secret', + expected: 'f9e66e179b6747ae54108f82f8ade8b3c25d76fd30afde6c395822c530196169', + }, + { + input: 'The quick brown fox jumps over the lazy dog', + key: 'secret', + expected: '54cd5b827c0ec938fa072a29b177469c843317b095591dc846767aa338bac600', + }, + ] + + for (const { input, key, expected } of testCases) { + assertEquals(hmacSha256(input, key), expected, `hmacSha256("${input}", "${key}") should return "${expected}"`) + } +}) + +test('hmacSha512 - generates correct HMAC SHA-512 hashes', () => { + const testCases = [ + { + input: 'hello', + key: 'secret', + expected: + 'db1595ae88a62fd151ec1cba81b98c39df82daae7b4cb9820f446d5bf02f1dcfca6683d88cab3e273f5963ab8ec469a746b5b19086371239f67d1e5f99a79440', + }, + { + input: '', + key: 'secret', + expected: + 'b0e9650c5faf9cd8ae02276671545424104589b3656731ec193b25d01b07561c27637c2d4d68389d6cf5007a8632c26ec89ba80a01c77a6cdd389ec28db43901', + }, + { + input: 'The quick brown fox jumps over the lazy dog', + key: 'secret', + expected: + '76af3588620ef6e2c244d5a360e080c0d649b6dd6b82ccd115eeefee8ff403bcee9aeb08618db9a2a94a9e80c7996bb2cb0c00f6e69de38ed8af2758ef39df0a', + }, + ] + + for (const { input, key, expected } of testCases) { + 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`) + } +}) diff --git a/crypto/hmac.ts b/crypto/hmac.ts new file mode 100644 index 0000000..294c125 --- /dev/null +++ b/crypto/hmac.ts @@ -0,0 +1,51 @@ +// load package +import { Buffer } from 'node:buffer' +import { createHmac } from 'node:crypto' + +const HEX_ENCODING = 'hex' + +/** + * 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 + * @returns HMAC SHA-256 hash of the input string (hexadecimal) + * + * @example + * ```ts + * import { hmacSha256 } from '@frytg/crypto/hmac' + * + * hmacSha256('hello', 'my-secret-key') + * ``` + */ +export const hmacSha256 = (str: string | Buffer, key: string | Buffer): string => + createHmac('sha256', key).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 + * @returns HMAC SHA-512 hash of the input string (hexadecimal) + * + * @example + * ```ts + * import { hmacSha512 } from '@frytg/crypto/hmac' + * + * hmacSha512('hello', 'my-secret-key') + * ``` + */ +export const hmacSha512 = (str: string | Buffer, key: string | Buffer): string => + createHmac('sha512', key).update(str).digest(HEX_ENCODING) + +/** + * Converts a hexadecimal string to a Buffer for use with HMAC + * @param hex - The hexadecimal string to convert + * @returns Buffer + * + * @example + * ```ts + * import { hmacSha512, bufferFromHex } from '@frytg/crypto/hmac' + * + * hmacSha512('hello', bufferFromHex('0123456789abcdef')) + * ``` + */ +export const bufferFromHex = (hex: string): Buffer => Buffer.from(hex, HEX_ENCODING) diff --git a/crypto/jsr.jsonc b/crypto/jsr.jsonc new file mode 100644 index 0000000..0ab3d5f --- /dev/null +++ b/crypto/jsr.jsonc @@ -0,0 +1,12 @@ +{ + "$schema": "https://jsr.io/schema/config-file.v1.json", + "name": "@frytg/crypto", + "version": "0.0.1", + "exports": { + "hash": "./hash.ts", + "hmac": "./hmac.ts" + }, + "publish": { + "exclude": ["*.test.ts"] + } +} diff --git a/dates/deno.jsonc b/dates/deno.jsonc index 5141f2b..220e5b4 100644 --- a/dates/deno.jsonc +++ b/dates/deno.jsonc @@ -6,5 +6,8 @@ "imports": { "@std/fmt": "jsr:@std/fmt@^1.0.3", "luxon": "npm:luxon@^3.5.0" + }, + "publish": { + "exclude": ["*.test.ts"] } } diff --git a/logger/README.md b/logger/README.md index e22b895..92539e8 100644 --- a/logger/README.md +++ b/logger/README.md @@ -16,7 +16,7 @@ Debug logs will only be logged if the env `STAGE` is set to `dev`. ## Usage ```ts -import logger from 'jsr:@frytg/logger'; +import logger from '@frytg/logger'; ``` ```ts diff --git a/logger/logger.ts b/logger/logger.ts index 743cb5c..7c4bffc 100644 --- a/logger/logger.ts +++ b/logger/logger.ts @@ -67,7 +67,7 @@ const formatConfigLocal: Logform.Format = format.combine( * * @example basic log * ```ts - * import { logger } from 'jsr:@frytg/logger' + * import { logger } from '@frytg/logger' * * logger.log({ * level: 'debug',