Skip to content

Commit

Permalink
Merge pull request #12 from frytg/dev/crypto-key-base64
Browse files Browse the repository at this point in the history
Dev/crypto key base64
  • Loading branch information
frytg authored Dec 17, 2024
2 parents da48d91 + 74a7ed0 commit f99fee1
Show file tree
Hide file tree
Showing 13 changed files with 343 additions and 70 deletions.
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

0 comments on commit f99fee1

Please sign in to comment.