Skip to content

Commit

Permalink
feat(crypto): initial commit (#3)
Browse files Browse the repository at this point in the history
* feat(crypto): initial commit

* chore: fix lint

* refactor: unintented changes
  • Loading branch information
climba03003 authored Jan 18, 2024
1 parent ef09e57 commit 5e413ef
Show file tree
Hide file tree
Showing 21 changed files with 1,101 additions and 340 deletions.
115 changes: 115 additions & 0 deletions .github/workflows/ci-crypto.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
name: Continuous Integration - Crypto

on:
push:
paths:
- ".github/workflows/ci-crypto.yml"
- "packages/crypto/**"
pull_request:
paths:
- ".github/workflows/ci-crypto.yml"
- "packages/crypto/**"

jobs:
linter:
name: Lint Code
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Check out repo
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 18

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install

- name: Lint code
run: pnpm --filter "./packages/crypto" run lint

test:
name: Test
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
matrix:
node-version: [18, 20]
os: [macos-latest, ubuntu-latest, windows-latest]
steps:
- name: Check out repo
uses: actions/checkout@v4
with:
persist-credentials: false

- name: Setup Node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
run_install: false

- name: Get pnpm store directory
shell: bash
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- uses: actions/cache@v4
name: Setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-${{ matrix.node-version }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-${{ matrix.node-version }}-pnpm-store-
- name: Install dependencies
run: pnpm install

- name: Run tests
run: pnpm --filter "./packages/crypto" run test

automerge:
name: Automerge Dependabot PRs
if: >
github.event_name == 'pull_request' &&
github.event.pull_request.user.login == 'dependabot[bot]'
needs: test
permissions:
pull-requests: write
contents: write
runs-on: ubuntu-latest
steps:
- uses: fastify/github-action-merge-dependabot@v3
with:
exclude: ${{ inputs.auto-merge-exclude }}
github-token: ${{ secrets.GITHUB_TOKEN }}
target: major
24 changes: 24 additions & 0 deletions packages/crypto/.eslintrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"extends": "standard-with-typescript",
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
// conflict between standard and standard-typescript
"no-void": ["error", { "allowAsStatement": true }]
},
"overrides": [
{
"files": ["**/*.test.ts"],
"rules": {
"@typescript-eslint/no-floating-promises": "off"
}
},
{
"files": ["scripts/*.mjs"],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off"
}
}
]
}
86 changes: 86 additions & 0 deletions packages/crypto/lib/aes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { createCipheriv, createDecipheriv, scryptSync, type BinaryLike, type CipherCCMTypes, type CipherGCM, type CipherGCMTypes, type DecipherGCM } from 'crypto'
import { randomBytes } from './utils'

export function computeKeySize (algorithm: CipherCCMTypes | CipherGCMTypes = 'aes-256-gcm'): number {
switch (algorithm) {
case 'aes-128-ccm':
case 'aes-128-gcm':
return 16
case 'aes-192-ccm':
case 'aes-192-gcm':
return 24
case 'aes-256-ccm':
case 'aes-256-gcm':
case 'chacha20-poly1305':
default:
return 32
}
}

export function computeIVSize (algorithm: CipherCCMTypes | CipherGCMTypes = 'aes-256-gcm'): number {
switch (algorithm) {
case 'chacha20-poly1305':
return 12
case 'aes-128-ccm':
case 'aes-128-gcm':
case 'aes-192-ccm':
case 'aes-192-gcm':
case 'aes-256-ccm':
case 'aes-256-gcm':
default:
return 16
}
}

export interface EncryptionResult {
value: string
iv: string
authTag: string
secret: BinaryLike
salt: BinaryLike
}

export function encrypt (
token: string,
algorithm: CipherCCMTypes | CipherGCMTypes = 'aes-256-gcm',
secret: BinaryLike = randomBytes(32, 'hex'),
salt: BinaryLike = randomBytes(32, 'hex'),
authTagLength = 16
): EncryptionResult {
const key: Buffer = scryptSync(secret, salt, computeKeySize(algorithm))
const ivSize = computeIVSize(algorithm)
const iv: Buffer = Buffer.alloc(ivSize, randomBytes(ivSize), 'binary')
const option: Parameters<typeof createCipheriv> = [algorithm, key, iv, { authTagLength } as any]
const cipher: CipherGCM = createCipheriv.apply(createCipheriv, option) as CipherGCM
cipher.setAAD(Buffer.from(`${String(secret)}${String(salt)}`))

let value = cipher.update(token, 'utf8', 'hex')
value += cipher.final('hex')
return {
value,
iv: iv.toString('hex'),
authTag: cipher.getAuthTag().toString('hex'),
secret,
salt
}
}

export function decrypt (
encrypted: string,
iv: Buffer,
authTag: Buffer,
algorithm: CipherCCMTypes | CipherGCMTypes,
secret: BinaryLike,
salt: BinaryLike,
authTagLength = 16
): string {
const key: Buffer = scryptSync(secret, salt, computeKeySize(algorithm))
const option: Parameters<typeof createDecipheriv> = [algorithm, key, iv, { authTagLength } as any]
const decipher = createDecipheriv.apply(createDecipheriv, option) as DecipherGCM
decipher.setAAD(Buffer.from(`${String(secret)}${String(salt)}`))
decipher.setAuthTag(authTag)

let value = decipher.update(encrypted, 'hex', 'utf8')
value += decipher.final('utf8')
return value
}
60 changes: 60 additions & 0 deletions packages/crypto/lib/base64.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
export const CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
export const URLCHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_ '

export type Base64Charset = 'default' | 'url' | 'custom'

export function encodeReplaceCharset (
value: string,
charset: Base64Charset = 'default',
custom: string = URLCHARSET
): string {
if (charset === 'default') return value
const replaced = []
for (let i = value.length - 1; i >= 0; i--) {
const char = value.charAt(i)
const indexOf = CHARSET.indexOf(char)
replaced.push(custom.charAt(indexOf))
}
return replaced
.reverse()
.join('')
.trim()
}

export function encode (
value: string | Buffer | Uint8Array,
charset: Base64Charset = 'default',
custom: string = URLCHARSET
): string {
// Cast Uint8Array to Buffer
if (value instanceof Uint8Array) {
value = Buffer.from(value)
}
// Allocate Buffer
value = Buffer.alloc(value.length, value as Buffer | string)
return encodeReplaceCharset(value.toString('base64'), charset, custom)
}

export function decodeReplaceCharset (
value: string,
charset: Base64Charset = 'default',
custom: string = URLCHARSET
): string {
if (charset === 'default') return value
let replaced: string[] | string = []
for (let i = value.length - 1; i >= 0; i--) {
const char = value[i]
const indexOf = custom.indexOf(char)
replaced.push(CHARSET[indexOf])
}
replaced = replaced.reverse().join('')
while (replaced.length % 4 > 0) {
replaced += '='
}
return replaced
}

export function decode (value: string, charset: Base64Charset = 'default', custom: string = URLCHARSET): string {
const val = Buffer.from(decodeReplaceCharset(value, charset, custom), 'base64')
return val.toString('utf8')
}
43 changes: 43 additions & 0 deletions packages/crypto/lib/hash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createHash, type BinaryLike, type BinaryToTextEncoding } from 'crypto'

export function hash (
value: BinaryLike,
encoding: BinaryToTextEncoding = 'hex',
algorithm = 'sha512'
): string {
const hash = createHash(algorithm)
hash.update(value)
return hash.digest(encoding)
}

export function md5 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'md5')
}

export function sha1 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'sha1')
}

export function sha224 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'sha224')
}

export function sha256 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'sha256')
}

export function sha384 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'sha384')
}

export function sha512 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'sha512')
}

export function shake128 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'shake128')
}

export function shake256 (value: string, encoding: BinaryToTextEncoding = 'hex'): string {
return hash(value, encoding, 'shake256')
}
5 changes: 5 additions & 0 deletions packages/crypto/lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * as AES from './aes'
export * as Base64 from './base64'
export * from './hash'
export * as Scrypt from './scrypt'
export * from './utils'
3 changes: 3 additions & 0 deletions packages/crypto/lib/mjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"type": "module"
}
32 changes: 32 additions & 0 deletions packages/crypto/lib/scrypt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { randomBytes, scrypt, timingSafeEqual } from 'crypto'

export async function hash (value: string, keylen = 32, cost = 65536, blockSize = 8, parallelization = 1): Promise<string> {
// salt is limited to at least 16 bytes long
const salt = randomBytes(Math.min(16, keylen / 2))
const maxmem = 128 * cost * blockSize * 2
return await new Promise(function (resolve, reject) {
scrypt(value, salt, Number(keylen), { cost, blockSize, parallelization, maxmem }, function (error, key) {
/* istanbul ignore next */
if (error !== null) reject(error)
resolve(`$scrypt$L=${String(keylen)}$N=${String(Math.log2(cost))},r=${String(blockSize)},p=${String(parallelization)}$${salt.toString('base64url')}$${key.toString('base64url')}`)
})
})
}

export const REGEXP = /^\$scrypt\$L=(\d+)\$N=(\d+),r=(\d+),p=(\d+)\$([A-Za-z0-9_-]+)\$([A-Za-z0-9_-]+)$/

export async function compare (value: string, hashed: string): Promise<boolean> {
const array = REGEXP.exec(hashed)
if (array === null) throw new Error('Invalid Scrypt Hash Format.')
const [,keylen, cost, blockSize, parallelization, salt, hash] = array
const maxmem = 128 * Math.pow(2, Number(cost)) * Number(blockSize) * 2
return await new Promise(function (resolve, reject) {
scrypt(value, Buffer.from(salt, 'base64url'), Number(keylen), {
cost: Math.pow(2, Number(cost)), blockSize: Number(blockSize), parallelization: Number(parallelization), maxmem
}, function (error, key) {
/* istanbul ignore next */
if (error !== null) reject(error)
resolve(timingSafeEqual(key, Buffer.from(hash, 'base64url')))
})
})
}
22 changes: 22 additions & 0 deletions packages/crypto/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import * as crypto from 'crypto'

export type RandomBytesEncode = BufferEncoding | 'buffer'

export function randomBytes (size: number, encoding?: 'buffer'): Buffer
export function randomBytes (size: number, encoding: BufferEncoding): string
export function randomBytes (size: number, encoding: RandomBytesEncode = 'buffer'): string | Buffer {
const randomBytes = crypto.randomBytes(size)
if (encoding === 'buffer') {
return randomBytes
} else {
return randomBytes.toString(encoding)
}
}

export function randomNum (digit: number = 6): string {
return crypto.randomInt(0, Math.pow(10, digit)).toString().padStart(digit, '0')
}

export function randomUUID (): string {
return crypto.randomUUID()
}
Loading

0 comments on commit 5e413ef

Please sign in to comment.