-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat(crypto): initial commit * chore: fix lint * refactor: unintented changes
- Loading branch information
1 parent
ef09e57
commit 5e413ef
Showing
21 changed files
with
1,101 additions
and
340 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"type": "module" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'))) | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
Oops, something went wrong.