Skip to content

Commit d1331b6

Browse files
committed
chore: wip
1 parent 08dafea commit d1331b6

File tree

10 files changed

+408
-5
lines changed

10 files changed

+408
-5
lines changed

.github/workflows/release.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: CI
33
on:
44
push:
55
tags:
6-
- 'v*'
6+
- "v*"
77

88
jobs:
99
release:
@@ -22,7 +22,7 @@ jobs:
2222
uses: actions/cache@v4
2323
with:
2424
path: node_modules
25-
key: node-modules-${{ hashFiles('**/bun.lockb') }}
25+
key: node-modules-${{ hashFiles('**/bun.lock') }}
2626
restore-keys: |
2727
node-modules-
2828

packages/crypto/build.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { dts } from 'bun-plugin-dtsx'
2+
3+
await Bun.build({
4+
entrypoints: ['src/index.ts'],
5+
outdir: './dist',
6+
target: 'node',
7+
plugins: [dts()],
8+
})

packages/crypto/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "ts-security-crypto",
3+
"type": "module",
4+
"version": "0.0.0",
5+
"description": "Cryptographic utilities using Bun native features",
6+
"author": "Chris Breuer <[email protected]>",
7+
"license": "MIT",
8+
"exports": {
9+
".": {
10+
"types": "./dist/index.d.ts",
11+
"import": "./dist/index.js"
12+
}
13+
},
14+
"module": "./dist/index.js",
15+
"types": "./dist/index.d.ts",
16+
"files": ["README.md", "dist"],
17+
"scripts": {
18+
"build": "bun build.ts",
19+
"typecheck": "bun tsc --noEmit"
20+
}
21+
}

packages/crypto/src/encrypt.ts

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/**
2+
* AES Encryption/Decryption using Web Crypto API (Bun/Node/Browser compatible)
3+
*/
4+
5+
export interface EncryptOptions {
6+
algorithm?: 'AES-GCM' | 'AES-CBC'
7+
keyLength?: 128 | 192 | 256
8+
iv?: Uint8Array
9+
}
10+
11+
export interface EncryptResult {
12+
encrypted: string
13+
iv: string
14+
algorithm: string
15+
}
16+
17+
/**
18+
* Generate a cryptographic key from a passphrase using PBKDF2
19+
*/
20+
async function deriveKey(
21+
passphrase: string,
22+
salt: Uint8Array,
23+
keyLength: number = 256,
24+
): Promise<CryptoKey> {
25+
const encoder = new TextEncoder()
26+
const passphraseKey = await crypto.subtle.importKey(
27+
'raw',
28+
encoder.encode(passphrase),
29+
{ name: 'PBKDF2' },
30+
false,
31+
['deriveKey'],
32+
)
33+
34+
return await crypto.subtle.deriveKey(
35+
{
36+
name: 'PBKDF2',
37+
salt,
38+
iterations: 100000,
39+
hash: 'SHA-256',
40+
},
41+
passphraseKey,
42+
{ name: 'AES-GCM', length: keyLength },
43+
false,
44+
['encrypt', 'decrypt'],
45+
)
46+
}
47+
48+
/**
49+
* Encrypt a message using AES-GCM
50+
*/
51+
export async function encrypt(
52+
message: string,
53+
passphrase: string,
54+
options?: EncryptOptions,
55+
): Promise<EncryptResult> {
56+
const algorithm = options?.algorithm || 'AES-GCM'
57+
const keyLength = options?.keyLength || 256
58+
const iv = options?.iv || crypto.getRandomValues(new Uint8Array(12))
59+
60+
// Generate salt for key derivation
61+
const salt = crypto.getRandomValues(new Uint8Array(16))
62+
63+
// Derive key from passphrase
64+
const key = await deriveKey(passphrase, salt, keyLength)
65+
66+
// Encrypt the message
67+
const encoder = new TextEncoder()
68+
const encrypted = await crypto.subtle.encrypt(
69+
{
70+
name: algorithm,
71+
iv,
72+
},
73+
key,
74+
encoder.encode(message),
75+
)
76+
77+
// Combine salt + IV + encrypted data
78+
const combined = new Uint8Array(salt.length + iv.length + encrypted.byteLength)
79+
combined.set(salt, 0)
80+
combined.set(iv, salt.length)
81+
combined.set(new Uint8Array(encrypted), salt.length + iv.length)
82+
83+
// Convert to base64
84+
return {
85+
encrypted: Buffer.from(combined).toString('base64'),
86+
iv: Buffer.from(iv).toString('base64'),
87+
algorithm,
88+
}
89+
}
90+
91+
/**
92+
* Decrypt a message using AES-GCM
93+
*/
94+
export async function decrypt(
95+
encryptedData: string,
96+
passphrase: string,
97+
algorithm: string = 'AES-GCM',
98+
): Promise<string> {
99+
// Decode from base64
100+
const combined = Buffer.from(encryptedData, 'base64')
101+
102+
// Extract salt, IV, and encrypted data
103+
const salt = combined.slice(0, 16)
104+
const iv = combined.slice(16, 28) // 12 bytes for GCM
105+
const encrypted = combined.slice(28)
106+
107+
// Derive key from passphrase
108+
const key = await deriveKey(passphrase, salt)
109+
110+
// Decrypt the message
111+
const decrypted = await crypto.subtle.decrypt(
112+
{
113+
name: algorithm,
114+
iv,
115+
},
116+
key,
117+
encrypted,
118+
)
119+
120+
// Convert back to string
121+
const decoder = new TextDecoder()
122+
return decoder.decode(decrypted)
123+
}
124+
125+
/**
126+
* Simple base64 encode (Bun native)
127+
*/
128+
export function base64Encode(input: string): string {
129+
return Buffer.from(input, 'utf-8').toString('base64')
130+
}
131+
132+
/**
133+
* Simple base64 decode (Bun native)
134+
*/
135+
export function base64Decode(input: string): string {
136+
return Buffer.from(input, 'base64').toString('utf-8')
137+
}

packages/crypto/src/hash.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/**
2+
* Hashing utilities using Bun native crypto
3+
*/
4+
5+
export interface HashOptions {
6+
algorithm?: 'md5' | 'sha1' | 'sha256' | 'sha512' | 'blake2b256'
7+
encoding?: 'hex' | 'base64'
8+
}
9+
10+
/**
11+
* Generate a hash using Bun's native crypto
12+
*/
13+
export function hash(input: string | Buffer, options?: HashOptions): string {
14+
const algorithm = options?.algorithm || 'sha256'
15+
const encoding = options?.encoding || 'hex'
16+
17+
const hasher = new Bun.CryptoHasher(algorithm)
18+
hasher.update(input)
19+
20+
return hasher.digest(encoding)
21+
}
22+
23+
/**
24+
* Generate MD5 hash
25+
*/
26+
export function md5(input: string | Buffer, encoding: 'hex' | 'base64' = 'hex'): string {
27+
return hash(input, { algorithm: 'md5', encoding })
28+
}
29+
30+
/**
31+
* Generate SHA1 hash
32+
*/
33+
export function sha1(input: string | Buffer, encoding: 'hex' | 'base64' = 'hex'): string {
34+
return hash(input, { algorithm: 'sha1', encoding })
35+
}
36+
37+
/**
38+
* Generate SHA256 hash
39+
*/
40+
export function sha256(input: string | Buffer, encoding: 'hex' | 'base64' = 'hex'): string {
41+
return hash(input, { algorithm: 'sha256', encoding })
42+
}
43+
44+
/**
45+
* Generate SHA512 hash
46+
*/
47+
export function sha512(input: string | Buffer, encoding: 'hex' | 'base64' = 'hex'): string {
48+
return hash(input, { algorithm: 'sha512', encoding })
49+
}
50+
51+
/**
52+
* Generate BLAKE2b-256 hash
53+
*/
54+
export function blake2b256(input: string | Buffer, encoding: 'hex' | 'base64' = 'hex'): string {
55+
return hash(input, { algorithm: 'blake2b256', encoding })
56+
}
57+
58+
/**
59+
* Generate HMAC
60+
*/
61+
export function hmac(
62+
input: string | Buffer,
63+
key: string,
64+
algorithm: 'sha256' | 'sha512' = 'sha256',
65+
encoding: 'hex' | 'base64' = 'hex',
66+
): string {
67+
const hasher = new Bun.CryptoHasher(algorithm, key)
68+
hasher.update(input)
69+
return hasher.digest(encoding)
70+
}

packages/crypto/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './encrypt'
2+
export * from './hash'
3+
export * from './password'
4+
export * from './key'

packages/crypto/src/key.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/**
2+
* Key generation utilities using native crypto
3+
*/
4+
5+
/**
6+
* Generate a random application key
7+
*/
8+
export function generateKey(length: number = 32): string {
9+
const random = crypto.getRandomValues(new Uint8Array(length))
10+
const base64 = Buffer.from(random).toString('base64')
11+
return `base64:${base64}`
12+
}
13+
14+
/**
15+
* Generate a random hex string
16+
*/
17+
export function generateHex(length: number = 32): string {
18+
const random = crypto.getRandomValues(new Uint8Array(length))
19+
return Buffer.from(random).toString('hex')
20+
}
21+
22+
/**
23+
* Generate a random UUID v4
24+
*/
25+
export function generateUUID(): string {
26+
return crypto.randomUUID()
27+
}
28+
29+
/**
30+
* Generate random bytes
31+
*/
32+
export function randomBytes(length: number): Uint8Array {
33+
return crypto.getRandomValues(new Uint8Array(length))
34+
}
35+
36+
/**
37+
* Generate a secure random integer between min and max (inclusive)
38+
*/
39+
export function randomInt(min: number, max: number): number {
40+
const range = max - min + 1
41+
const bytesNeeded = Math.ceil(Math.log2(range) / 8)
42+
const maxValid = Math.floor(256 ** bytesNeeded / range) * range - 1
43+
44+
let randomValue: number
45+
do {
46+
const randomBytes = crypto.getRandomValues(new Uint8Array(bytesNeeded))
47+
randomValue = randomBytes.reduce((acc, byte, i) => acc + byte * (256 ** i), 0)
48+
} while (randomValue > maxValid)
49+
50+
return min + (randomValue % range)
51+
}

0 commit comments

Comments
 (0)