Spaces:
Paused
Paused
| import { config as appConfig } from '../config/index.js'; | |
| import { | |
| randomBytes, | |
| createCipheriv, | |
| createDecipheriv, | |
| createHash, | |
| pbkdf2Sync, | |
| randomUUID, | |
| } from 'crypto'; | |
| import { genSalt, hash, compare } from 'bcrypt'; | |
| import { deflateSync, inflateSync } from 'zlib'; | |
| import { createLogger } from '../logging/logger.js'; | |
| import { fromUrlSafeBase64, toUrlSafeBase64 } from './general.js'; | |
| const logger = createLogger('crypto'); | |
| const saltRounds = 10; | |
| export const compressData = (data: string): Buffer => { | |
| return deflateSync(Buffer.from(data, 'utf-8'), { | |
| level: 9, | |
| }); | |
| }; | |
| export const decompressData = (data: Buffer): string => { | |
| return inflateSync(data).toString('utf-8'); | |
| }; | |
| const encryptData = ( | |
| secretKey: Buffer, | |
| data: Buffer | |
| ): { i: string; d: string } => { | |
| // Then encrypt the compressed data | |
| const iv = randomBytes(16); | |
| const cipher = createCipheriv('aes-256-cbc', secretKey, iv); | |
| const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); | |
| return { | |
| i: iv.toString('base64'), | |
| d: encryptedData.toString('base64'), | |
| }; | |
| }; | |
| const decryptData = ( | |
| secretKey: Buffer, | |
| encryptedData: Buffer, | |
| iv: Buffer | |
| ): Buffer => { | |
| const decipher = createDecipheriv('aes-256-cbc', secretKey, iv); | |
| // Decrypt the data | |
| const decryptedData = Buffer.concat([ | |
| decipher.update(encryptedData), | |
| decipher.final(), | |
| ]); | |
| return decryptedData; | |
| }; | |
| type SuccessResponse = { | |
| success: true; | |
| data: string; | |
| error: null; | |
| }; | |
| type ErrorResponse = { | |
| success: false; | |
| error: string; | |
| data: null; | |
| }; | |
| export type Response = SuccessResponse | ErrorResponse; | |
| export function isEncrypted(data: string): boolean { | |
| try { | |
| // parse the data as json | |
| const json = JSON.parse(fromUrlSafeBase64(data)); | |
| return ['aioEncrypt', 'a'].includes(json.type || json.t); | |
| } catch (error) { | |
| return false; | |
| } | |
| } | |
| /** | |
| * Encrypts a string using AES-256-CBC encryption, returns a string in the format "iv:encrypted" where | |
| * iv and encrypted are url encoded. | |
| * @param data Data to encrypt | |
| * @param secretKey Secret key used for encryption | |
| * @returns Encrypted data or error message | |
| */ | |
| export function encryptString(data: string, secretKey?: Buffer): Response { | |
| if (!secretKey) { | |
| secretKey = Buffer.from(appConfig.bootstrap.secretKey, 'hex'); | |
| } | |
| try { | |
| const compressed = compressData(data); | |
| const { i, d: e } = encryptData(secretKey, compressed); | |
| return { | |
| success: true, | |
| data: toUrlSafeBase64(JSON.stringify({ i, e, t: 'a' })), | |
| error: null, | |
| }; | |
| } catch (error: any) { | |
| logger.error(`Failed to encrypt data: ${error.message}`); | |
| return { | |
| success: false, | |
| error: error.message, | |
| data: null, | |
| }; | |
| } | |
| } | |
| /** | |
| * Decrypts a string using AES-256-CBC encryption | |
| * @param data Encrypted data to decrypt | |
| * @param secretKey Secret key used for encryption | |
| * @returns Decrypted data or error message | |
| */ | |
| export function decryptString(data: string, secretKey?: Buffer): Response { | |
| if (!secretKey) { | |
| secretKey = Buffer.from(appConfig.bootstrap.secretKey, 'hex'); | |
| } | |
| try { | |
| if (!isEncrypted(data)) { | |
| throw new Error('The data was not in an expected encrypted format'); | |
| } | |
| const json = JSON.parse(fromUrlSafeBase64(data)); | |
| const iv = Buffer.from(json.iv || json.i, 'base64'); | |
| const encrypted = Buffer.from(json.encrypted || json.e, 'base64'); | |
| const decrypted = decryptData(secretKey, encrypted, iv); | |
| const decompressed = decompressData(decrypted); | |
| return { | |
| success: true, | |
| data: decompressed, | |
| error: null, | |
| }; | |
| } catch (error: any) { | |
| logger.error(`Failed to decrypt data: ${error.message}`); | |
| return { | |
| success: false, | |
| error: error.message, | |
| data: null, | |
| }; | |
| } | |
| } | |
| export function getSimpleTextHash(text: string): string { | |
| return createHash('sha256').update(text).digest('hex'); | |
| } | |
| /** | |
| * Creates a secure hash of text using PBKDF2 | |
| * @param text Text to hash | |
| * @returns Object containing the hash and salt used | |
| */ | |
| export async function getTextHash(text: string): Promise<string> { | |
| return await hash(text, await genSalt(saltRounds)); | |
| } | |
| /** | |
| * Verifies if the provided text matches a previously generated hash | |
| * @param text Text to verify | |
| * @param storedHash Previously generated hash | |
| * @returns Boolean indicating if the text matches the hash | |
| */ | |
| export async function verifyHash( | |
| text: string, | |
| storedHash: string | |
| ): Promise<boolean> { | |
| return compare(text, storedHash); | |
| } | |
| /** | |
| * Derives a 64 character hex string from a password using PBKDF2 | |
| * @param password Password to derive key from | |
| * @param salt Optional salt, will be generated if not provided | |
| * @returns Object containing the key and salt used | |
| */ | |
| export async function deriveKey( | |
| password: string, | |
| salt?: string | |
| ): Promise<{ key: Buffer; salt: string }> { | |
| salt = salt || (await genSalt(saltRounds)); | |
| const key = pbkdf2Sync( | |
| Buffer.from(password, 'utf-8'), | |
| Buffer.from(salt, 'hex'), | |
| 100000, | |
| 32, | |
| 'sha512' | |
| ); | |
| return { key, salt }; | |
| } | |
| export function generateUUID(): string { | |
| return randomUUID(); | |
| } | |