Skip to content

Instantly share code, notes, and snippets.

@panudetjt
Last active August 2, 2025 07:26
Show Gist options
  • Save panudetjt/a077af3fb47153d460347c4f67cbaf08 to your computer and use it in GitHub Desktop.
Save panudetjt/a077af3fb47153d460347c4f67cbaf08 to your computer and use it in GitHub Desktop.
Encrypt/Decrypt by AI
import { base64url } from 'jose'
async function getCrypto(): Promise<SubtleCrypto> {
if (typeof globalThis !== 'undefined' && globalThis.crypto?.subtle) {
return globalThis.crypto.subtle
}
if (typeof window !== 'undefined' && window.crypto?.subtle) {
return window.crypto.subtle
}
const { webcrypto } = await import('node:crypto')
return webcrypto.subtle as SubtleCrypto
}
async function pbkdf2(password: string, salt: Uint8Array, iterations: number): Promise<Uint8Array> {
const subtle = await getCrypto()
const encoder = new TextEncoder()
const passwordBytes = encoder.encode(password)
// Import password as key material
const keyMaterial = await subtle.importKey(
'raw',
passwordBytes,
'PBKDF2',
false,
['deriveKey']
)
// Derive key using PBKDF2 (JOSE standard)
const derivedKey = await subtle.deriveKey(
{
name: 'PBKDF2',
salt,
iterations,
hash: 'SHA-256'
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
true,
['encrypt', 'decrypt']
)
// Export as raw bytes for use with AES-GCM
const keyBytes = await subtle.exportKey('raw', derivedKey)
return new Uint8Array(keyBytes)
}
async function aesGcmEncrypt(plaintext: Uint8Array, key: Uint8Array, iv: Uint8Array): Promise<Uint8Array> {
const subtle = await getCrypto()
const cryptoKey = await subtle.importKey(
'raw',
key,
{ name: 'AES-GCM' },
false,
['encrypt']
)
const encrypted = await subtle.encrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
plaintext
)
return new Uint8Array(encrypted)
}
async function aesGcmDecrypt(ciphertext: Uint8Array, key: Uint8Array, iv: Uint8Array): Promise<Uint8Array> {
const subtle = await getCrypto()
const cryptoKey = await subtle.importKey(
'raw',
key,
{ name: 'AES-GCM' },
false,
['decrypt']
)
const decrypted = await subtle.decrypt(
{ name: 'AES-GCM', iv },
cryptoKey,
ciphertext
)
return new Uint8Array(decrypted)
}
export async function encryptSecret(payload: Record<string, any>, keyText: string): Promise<string> {
const encoder = new TextEncoder()
const plaintext = encoder.encode(JSON.stringify(payload))
// Generate random salt and IV (following JOSE standards)
const salt = new Uint8Array(16)
const iv = new Uint8Array(12) // GCM recommended IV size
for (let i = 0; i < 16; i++) {
salt[i] = Math.floor(Math.random() * 256)
}
for (let i = 0; i < 12; i++) {
iv[i] = Math.floor(Math.random() * 256)
}
// Use PBKDF2 with 100k iterations (JOSE recommendation)
const key = await pbkdf2(keyText, salt, 100000)
// Encrypt using AES-GCM (JOSE standard)
const ciphertext = await aesGcmEncrypt(plaintext, key, iv)
// Create JWE-like structure
const jwePayload = {
protected: base64url.encode(new TextEncoder().encode(JSON.stringify({
alg: 'PBES2-HS256+A128KW',
enc: 'A256GCM'
}))),
encrypted_key: '',
iv: base64url.encode(iv),
ciphertext: base64url.encode(ciphertext),
tag: '',
salt: base64url.encode(salt)
}
return base64url.encode(new TextEncoder().encode(JSON.stringify(jwePayload)))
}
export async function decryptSecret(encryptedData: string, keyText: string): Promise<Record<string, any>> {
const dataStr = new TextDecoder().decode(base64url.decode(encryptedData))
const jwePayload = JSON.parse(dataStr)
const salt = base64url.decode(jwePayload.salt)
const iv = base64url.decode(jwePayload.iv)
const ciphertext = base64url.decode(jwePayload.ciphertext)
// Derive key using same PBKDF2 parameters
const key = await pbkdf2(keyText, salt, 100000)
try {
// Decrypt using AES-GCM
const plaintext = await aesGcmDecrypt(ciphertext, key, iv)
const decoder = new TextDecoder()
const payloadString = decoder.decode(plaintext)
return JSON.parse(payloadString)
} catch (error) {
throw new Error('Invalid key or corrupted data')
}
}
import { describe, it, expect } from 'vitest'
import { encryptSecret, decryptSecret } from './crypto'
describe('crypto utilities', () => {
const testPayload = {
userId: '12345',
username: 'testuser',
roles: ['admin', 'user'],
metadata: {
lastLogin: '2024-01-01T00:00:00Z',
permissions: ['read', 'write']
}
}
it('should encrypt and decrypt with simple key', async () => {
const key = 'my-simple-key'
const encrypted = await encryptSecret(testPayload, key)
expect(encrypted).toBeTruthy()
expect(typeof encrypted).toBe('string')
const decrypted = await decryptSecret(encrypted, key)
expect(decrypted).toEqual(testPayload)
})
it('should encrypt and decrypt with long key', async () => {
const key = 'this-is-a-very-long-key-that-is-longer-than-32-characters-and-should-be-truncated'
const encrypted = await encryptSecret(testPayload, key)
const decrypted = await decryptSecret(encrypted, key)
expect(decrypted).toEqual(testPayload)
})
it('should encrypt and decrypt with short key', async () => {
const key = 'short'
const encrypted = await encryptSecret(testPayload, key)
const decrypted = await decryptSecret(encrypted, key)
expect(decrypted).toEqual(testPayload)
})
it('should encrypt and decrypt with special characters key', async () => {
const key = 'ключ-กุญแจ-🔑-密钥'
const encrypted = await encryptSecret(testPayload, key)
const decrypted = await decryptSecret(encrypted, key)
expect(decrypted).toEqual(testPayload)
})
it('should fail with wrong key', async () => {
const correctKey = 'correct-key'
const wrongKey = 'wrong-key'
const encrypted = await encryptSecret(testPayload, correctKey)
await expect(decryptSecret(encrypted, wrongKey)).rejects.toThrow()
})
it('should encrypt different data with same key', async () => {
const key = 'same-key'
const payload1 = { data: 'first' }
const payload2 = { data: 'second' }
const encrypted1 = await encryptSecret(payload1, key)
const encrypted2 = await encryptSecret(payload2, key)
expect(encrypted1).not.toBe(encrypted2)
const decrypted1 = await decryptSecret(encrypted1, key)
const decrypted2 = await decryptSecret(encrypted2, key)
expect(decrypted1).toEqual(payload1)
expect(decrypted2).toEqual(payload2)
})
it('should handle empty string payload', async () => {
const key = 'test-key'
const payload = { message: '' }
const encrypted = await encryptSecret(payload, key)
const decrypted = await decryptSecret(encrypted, key)
expect(decrypted).toEqual(payload)
})
it('should handle complex nested objects', async () => {
const key = 'complex-key'
const payload = {
level1: {
level2: {
level3: {
array: [1, 2, 3, { nested: true }],
boolean: false,
null: null,
number: 42.5
}
}
}
}
const encrypted = await encryptSecret(payload, key)
const decrypted = await decryptSecret(encrypted, key)
expect(decrypted).toEqual(payload)
})
})
import { base64url } from 'jose'
function simpleHash(input: Uint8Array): Uint8Array {
const hash = new Uint8Array(32)
for (let i = 0; i < input.length; i++) {
const idx1 = i % 32
const idx2 = (i + 7) % 32
hash[idx1] ^= input[i] || 0
hash[idx2] ^= (input[i] || 0) * 17
}
for (let round = 0; round < 1000; round++) {
const temp = new Uint8Array(32)
for (let i = 0; i < 32; i++) {
temp[i] = (hash[i] || 0) ^ (hash[(i + 1) % 32] || 0) ^ (hash[(i + 7) % 32] || 0) ^ (round & 0xFF)
}
hash.set(temp)
}
return hash
}
function xorEncrypt(data: Uint8Array, key: Uint8Array): Uint8Array {
const result = new Uint8Array(data.length)
for (let i = 0; i < data.length; i++) {
result[i] = (data[i] || 0) ^ (key[i % key.length] || 0)
}
return result
}
function createKeyFromText(keyText: string): Uint8Array {
const encoder = new TextEncoder()
const keyData = encoder.encode(keyText)
return simpleHash(keyData)
}
export async function encryptSecret(payload: Record<string, any>, keyText: string): Promise<string> {
const encoder = new TextEncoder()
const payloadString = JSON.stringify(payload)
const plaintext = encoder.encode(payloadString)
const key = createKeyFromText(keyText)
const iv = new Uint8Array(16)
for (let i = 0; i < 16; i++) {
iv[i] = Math.floor(Math.random() * 256)
}
const checksum = simpleHash(encoder.encode(payloadString + keyText))
const keyWithIv = simpleHash(new Uint8Array([...key, ...iv]))
const encrypted = xorEncrypt(plaintext, keyWithIv)
const combined = new Uint8Array(checksum.length + iv.length + encrypted.length)
combined.set(checksum)
combined.set(iv, checksum.length)
combined.set(encrypted, checksum.length + iv.length)
return base64url.encode(combined)
}
export async function decryptSecret(encryptedData: string, keyText: string): Promise<Record<string, any>> {
const combined = base64url.decode(encryptedData)
const key = createKeyFromText(keyText)
const expectedChecksum = combined.slice(0, 32)
const iv = combined.slice(32, 48)
const encrypted = combined.slice(48)
const keyWithIv = simpleHash(new Uint8Array([...key, ...iv]))
const decrypted = xorEncrypt(encrypted, keyWithIv)
const decoder = new TextDecoder()
const payloadString = decoder.decode(decrypted)
try {
const payload = JSON.parse(payloadString)
const encoder = new TextEncoder()
const actualChecksum = simpleHash(encoder.encode(payloadString + keyText))
for (let i = 0; i < 32; i++) {
if (expectedChecksum[i] !== actualChecksum[i]) {
throw new Error('Invalid key or corrupted data')
}
}
return payload
} catch (e) {
if (e instanceof SyntaxError) {
throw new Error('Invalid key or corrupted data')
}
throw e
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment