Aethel Bot OSS repository! aethel.xyz
bot fun ai discord discord-bot aethel
at dev 4.7 kB view raw
1import * as config from '@/config'; 2import crypto from 'crypto'; 3import logger from './logger'; 4 5const ALGO = 'aes-256-gcm'; 6const KEY = crypto.createHash('sha256').update(config.API_KEY_ENCRYPTION_SECRET).digest(); 7const IV_LENGTH = 12; 8const MAX_ENCRYPTED_LENGTH = 10000; 9 10class EncryptionError extends Error { 11 constructor( 12 message: string, 13 public readonly operation: 'encrypt' | 'decrypt', 14 ) { 15 super(message); 16 this.name = 'EncryptionError'; 17 } 18} 19 20function encrypt(text: string): string { 21 if (!text || typeof text !== 'string') { 22 throw new EncryptionError('Invalid input: text must be a non-empty string', 'encrypt'); 23 } 24 25 if (text.length > 5000) { 26 throw new EncryptionError('Input text too large for encryption', 'encrypt'); 27 } 28 29 try { 30 const iv = crypto.randomBytes(IV_LENGTH); 31 const cipher = crypto.createCipheriv(ALGO, KEY, iv); 32 let encrypted = cipher.update(text, 'utf8', 'base64'); 33 encrypted += cipher.final('base64'); 34 const tag = cipher.getAuthTag(); 35 36 const result = `${iv.toString('base64')}:${tag.toString('base64')}:${encrypted}`; 37 38 if (result.length > MAX_ENCRYPTED_LENGTH) { 39 throw new EncryptionError('Encrypted data exceeds maximum length', 'encrypt'); 40 } 41 42 logger.debug('Data encrypted successfully'); 43 return result; 44 } catch (err) { 45 if (err instanceof EncryptionError) { 46 throw err; 47 } 48 logger.error('Encryption failed:', err); 49 throw new EncryptionError('Failed to encrypt data', 'encrypt'); 50 } 51} 52 53function decrypt(encrypted: string): string { 54 if (!encrypted || typeof encrypted !== 'string') { 55 throw new EncryptionError( 56 'Invalid input: encrypted data must be a non-empty string', 57 'decrypt', 58 ); 59 } 60 61 if (encrypted.length > MAX_ENCRYPTED_LENGTH) { 62 throw new EncryptionError('Encrypted data exceeds maximum length', 'decrypt'); 63 } 64 65 try { 66 const parts = encrypted.split(':'); 67 if (parts.length !== 3) { 68 logger.warn('Invalid encrypted data format - expected 3 parts separated by colons'); 69 throw new EncryptionError('Invalid encrypted data format', 'decrypt'); 70 } 71 72 const [ivB64, tagB64, data] = parts; 73 74 if (!ivB64 || !tagB64 || !data) { 75 throw new EncryptionError('Missing encryption components', 'decrypt'); 76 } 77 78 let iv: Buffer, tag: Buffer; 79 try { 80 iv = Buffer.from(ivB64, 'base64'); 81 tag = Buffer.from(tagB64, 'base64'); 82 } catch { 83 throw new EncryptionError('Invalid base64 encoding in encrypted data', 'decrypt'); 84 } 85 86 if (iv.length !== IV_LENGTH) { 87 throw new EncryptionError( 88 `Invalid IV length: expected ${IV_LENGTH}, got ${iv.length}`, 89 'decrypt', 90 ); 91 } 92 93 if (tag.length !== 16) { 94 throw new EncryptionError( 95 `Invalid auth tag length: expected 16, got ${tag.length}`, 96 'decrypt', 97 ); 98 } 99 100 const decipher = crypto.createDecipheriv(ALGO, KEY, iv); 101 decipher.setAuthTag(tag); 102 103 let decrypted = decipher.update(data, 'base64', 'utf8'); 104 decrypted += decipher.final('utf8'); 105 106 logger.debug('Data decrypted successfully'); 107 return decrypted; 108 } catch (error) { 109 if (error instanceof EncryptionError) { 110 throw error; 111 } 112 113 if (error instanceof Error) { 114 if (error.message.includes('Unsupported state or unable to authenticate data')) { 115 logger.warn( 116 'Authentication failed during decryption - data may be corrupted or key changed', 117 ); 118 throw new EncryptionError( 119 'Authentication failed - data may be corrupted or encryption key changed', 120 'decrypt', 121 ); 122 } 123 if (error.message.includes('Invalid key length')) { 124 logger.error('Invalid encryption key length'); 125 throw new EncryptionError('Invalid encryption key configuration', 'decrypt'); 126 } 127 } 128 129 logger.error('Decryption failed:', error); 130 throw new EncryptionError('Failed to decrypt data', 'decrypt'); 131 } 132} 133 134function canDecrypt(encrypted: string): boolean { 135 try { 136 decrypt(encrypted); 137 return true; 138 } catch { 139 return false; 140 } 141} 142 143function isValidEncryptedFormat(encrypted: string): boolean { 144 if (!encrypted || typeof encrypted !== 'string') { 145 return false; 146 } 147 148 const parts = encrypted.split(':'); 149 if (parts.length !== 3) { 150 return false; 151 } 152 153 const [ivB64, tagB64, data] = parts; 154 155 try { 156 const iv = Buffer.from(ivB64, 'base64'); 157 const tag = Buffer.from(tagB64, 'base64'); 158 159 return iv.length === IV_LENGTH && tag.length === 16 && data.length > 0; 160 } catch { 161 return false; 162 } 163} 164 165export { encrypt, decrypt, canDecrypt, isValidEncryptedFormat, EncryptionError };