Aethel Bot OSS repository!
aethel.xyz
bot
fun
ai
discord
discord-bot
aethel
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 };