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

Dev/crypto key base64 #12

Merged
merged 4 commits into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 5 additions & 0 deletions crypto/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Crypto Changelog

## 2024-12-17 - 0.1.0

- feat: add `bufferFromBase64`
- feat: add key generation

## 2024-12-16 - 0.0.3

- fix: optimize JSDoc for Deno
Expand Down
17 changes: 17 additions & 0 deletions crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 4 additions & 2 deletions crypto/deno.jsonc
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"$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"
"./hmac": "./hmac.ts",
"./generate-default-keys": "./generate-default-keys.ts",
"./generate-key": "./generate-key.ts"
},
"publish": {
"exclude": ["*.test.ts"]
Expand Down
16 changes: 16 additions & 0 deletions crypto/generate-default-keys.ts
Original file line number Diff line number Diff line change
@@ -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)
71 changes: 71 additions & 0 deletions crypto/generate-key.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
57 changes: 57 additions & 0 deletions crypto/generate-key.ts
Original file line number Diff line number Diff line change
@@ -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,
}
}
2 changes: 2 additions & 0 deletions crypto/hash.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down
9 changes: 5 additions & 4 deletions crypto/hash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
* ```
*/

// load packages
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)
* @param {string} str - The string to hash
* @returns {string} SHA-256 hash of the input string (hexadecimal)
*
* @example SHA-256
* ```ts
Expand All @@ -30,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
Expand Down
122 changes: 122 additions & 0 deletions crypto/hmac-buffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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 valid 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}".`,
)
}
})

test('bufferFromBase64 - validates invalid base64 strings correctly', () => {
// 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 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
'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}"`,
)
}
})
Loading
Loading