PluginProbe
CryptX / 4.0.4
CryptX v4.0.4
4.2.1 4.2.0 4.1.1 trunk 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.9 2.0 2.1 2.2 2.3 2.3.1 2.3.2 2.3.3 2.4.0 2.4.1 2.4.2 2.4.3 2.4.4 All 93 releases
cryptx / js / cryptx.js

cryptx.js in CryptX 4.0.4, at js/cryptx.js

459 lines 12.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Secure CryptX Library - Fixed for backward compatibility
3 */
4
5 // Configuration constants
6 const ITERATIONS = window.cryptxConfig?.iterations || 100000; // fallback to old value
7 const KEY_LENGTH = window.cryptxConfig?.keyLength || 32;
8 const IV_LENGTH = window.cryptxConfig?.ivLength || 16;
9 const SALT_LENGTH = window.cryptxConfig?.saltLength || 16;
10 const CONFIG = {
11 ALLOWED_PROTOCOLS: ['http:', 'https:', 'mailto:'],
12 MAX_URL_LENGTH: 2048,
13 ENCRYPTION_KEY_SIZE: 32,
14 IV_SIZE: 16
15 };
16
17 /**
18 * Utility functions for secure operations
19 */
20 class SecureUtils {
21 static getSecureRandomBytes(length) {
22 if (typeof crypto === 'undefined' || !crypto.getRandomValues) {
23 throw new Error('Secure random number generation not available');
24 }
25 return crypto.getRandomValues(new Uint8Array(length));
26 }
27
28 static arrayBufferToBase64(buffer) {
29 const bytes = new Uint8Array(buffer);
30 let binary = '';
31 for (let i = 0; i < bytes.byteLength; i++) {
32 binary += String.fromCharCode(bytes[i]);
33 }
34 return btoa(binary);
35 }
36
37 static base64ToArrayBuffer(base64) {
38 const binary = atob(base64);
39 const buffer = new ArrayBuffer(binary.length);
40 const bytes = new Uint8Array(buffer);
41 for (let i = 0; i < binary.length; i++) {
42 bytes[i] = binary.charCodeAt(i);
43 }
44 return buffer;
45 }
46
47 static validateUrl(url) {
48 if (typeof url !== 'string' || url.length === 0) {
49 return null;
50 }
51
52 if (url.length > CONFIG.MAX_URL_LENGTH) {
53 console.error('URL exceeds maximum length');
54 return null;
55 }
56
57 try {
58 const urlObj = new URL(url);
59
60 if (!CONFIG.ALLOWED_PROTOCOLS.includes(urlObj.protocol)) {
61 console.error('Protocol not allowed:', urlObj.protocol);
62 return null;
63 }
64
65 if (urlObj.protocol === 'mailto:') {
66 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
67 if (!emailRegex.test(urlObj.pathname)) {
68 console.error('Invalid email format in mailto URL');
69 return null;
70 }
71 }
72
73 return url;
74 } catch (error) {
75 console.error('Invalid URL format:', error.message);
76 return null;
77 }
78 }
79
80 static escapeJavaScript(str) {
81 if (typeof str !== 'string') {
82 return '';
83 }
84 return str.replace(/\\/g, '\\\\')
85 .replace(/'/g, "\\'")
86 .replace(/"/g, '\\"')
87 .replace(/\n/g, '\\n')
88 .replace(/\r/g, '\\r')
89 .replace(/\t/g, '\\t');
90 }
91 }
92
93 /**
94 * Legacy encryption class - Fixed to match original PHP algorithm
95 */
96 class LegacyEncryption {
97 /**
98 * Decrypts using the original CryptX algorithm (matches PHP version)
99 * @param {string} encryptedString
100 * @returns {string}
101 */
102 static originalDecrypt(encryptedString) {
103 if (typeof encryptedString !== 'string' || encryptedString.length === 0) {
104 throw new Error('Invalid encrypted string');
105 }
106
107 // Constants from original algorithm
108 const UPPER_LIMIT = 8364;
109 const DEFAULT_VALUE = 128;
110
111 let charCode = 0;
112 let decryptedString = "mailto:";
113 let encryptionKey = 0;
114
115 try {
116 for (let i = 0; i < encryptedString.length; i += 2) {
117 if (i + 1 >= encryptedString.length) {
118 break;
119 }
120
121 // Get the salt (encryption key) from current position
122 encryptionKey = parseInt(encryptedString.charAt(i), 10);
123
124 // Handle invalid salt values
125 if (isNaN(encryptionKey)) {
126 encryptionKey = 0;
127 }
128
129 // Get the character code from next position
130 charCode = encryptedString.charCodeAt(i + 1);
131
132 // Apply the same logic as original
133 if (charCode >= UPPER_LIMIT) {
134 charCode = DEFAULT_VALUE;
135 }
136
137 // Decrypt by subtracting the salt
138 const decryptedCharCode = charCode - encryptionKey;
139
140 // Validate the result
141 if (decryptedCharCode < 0 || decryptedCharCode > 1114111) {
142 throw new Error('Invalid character code during decryption');
143 }
144
145 decryptedString += String.fromCharCode(decryptedCharCode);
146 }
147
148 return decryptedString;
149 } catch (error) {
150 throw new Error('Original decryption failed: ' + error.message);
151 }
152 }
153
154 /**
155 * Encrypts using the original CryptX algorithm (matches PHP version)
156 * @param {string} inputString
157 * @returns {string}
158 */
159 static originalEncrypt(inputString) {
160 if (typeof inputString !== 'string' || inputString.length === 0) {
161 throw new Error('Invalid input string');
162 }
163
164 // Remove "mailto:" prefix if present for encryption
165 const cleanInput = inputString.replace(/^mailto:/, '');
166 let crypt = '';
167
168 // ASCII values blacklist (from PHP constant)
169 const ASCII_VALUES_BLACKLIST = ['32', '34', '39', '60', '62', '63', '92', '94', '96', '127'];
170
171 try {
172 for (let i = 0; i < cleanInput.length; i++) {
173 let salt, asciiValue;
174 let attempts = 0;
175 const maxAttempts = 20; // Prevent infinite loops
176
177 do {
178 if (attempts >= maxAttempts) {
179 // Fallback to a safe salt if we can't find a valid one
180 salt = 1;
181 asciiValue = cleanInput.charCodeAt(i) + salt;
182 break;
183 }
184
185 // Generate random number between 0 and 3 (matching PHP rand(0,3))
186 const randomValues = SecureUtils.getSecureRandomBytes(1);
187 salt = randomValues[0] % 4;
188
189 // Get ASCII value and add salt
190 asciiValue = cleanInput.charCodeAt(i) + salt;
191
192 // Check if value exceeds limit (matching PHP logic)
193 if (asciiValue >= 8364) {
194 asciiValue = 128;
195 }
196
197 attempts++;
198 } while (ASCII_VALUES_BLACKLIST.includes(asciiValue.toString()) && attempts < maxAttempts);
199
200 // Append salt and character to result
201 crypt += salt.toString() + String.fromCharCode(asciiValue);
202 }
203
204 return crypt;
205 } catch (error) {
206 throw new Error('Original encryption failed: ' + error.message);
207 }
208 }
209 }
210
211 /**
212 * Modern encryption class using Web Crypto API - PHP Compatible
213 */
214 class SecureEncryption {
215 static async deriveKey(password, salt) {
216 const encoder = new TextEncoder();
217 const keyMaterial = await crypto.subtle.importKey(
218 'raw',
219 encoder.encode(password),
220 { name: 'PBKDF2' },
221 false,
222 ['deriveKey']
223 );
224
225 return crypto.subtle.deriveKey(
226 {
227 name: 'PBKDF2',
228 salt: salt,
229 iterations: ITERATIONS,
230 hash: 'SHA-256'
231 },
232 keyMaterial,
233 { name: 'AES-GCM', length: KEY_LENGTH * 8},
234 false,
235 ['encrypt', 'decrypt']
236 );
237 }
238
239 static async encrypt(plaintext, password) {
240 if (typeof plaintext !== 'string' || typeof password !== 'string') {
241 throw new Error('Both plaintext and password must be strings');
242 }
243
244 const encoder = new TextEncoder();
245 const salt = SecureUtils.getSecureRandomBytes(16);
246 const iv = SecureUtils.getSecureRandomBytes(CONFIG.IV_SIZE);
247
248 const key = await this.deriveKey(password, salt);
249
250 const encrypted = await crypto.subtle.encrypt(
251 { name: 'AES-GCM', iv: iv },
252 key,
253 encoder.encode(plaintext)
254 );
255
256 // Match PHP format: salt(16) + iv(16) + encrypted_data + tag(16)
257 const encryptedArray = new Uint8Array(encrypted);
258 const encryptedData = encryptedArray.slice(0, -16); // Remove tag from encrypted data
259 const tag = encryptedArray.slice(-16); // Get the tag
260
261 const combined = new Uint8Array(salt.length + iv.length + encryptedData.length + tag.length);
262 combined.set(salt, 0);
263 combined.set(iv, salt.length);
264 combined.set(encryptedData, salt.length + iv.length);
265 combined.set(tag, salt.length + iv.length + encryptedData.length);
266
267 return SecureUtils.arrayBufferToBase64(combined.buffer);
268 }
269
270 static async decrypt(encryptedData, password) {
271 if (typeof encryptedData !== 'string' || typeof password !== 'string') {
272 throw new Error('Both encryptedData and password must be strings');
273 }
274
275 try {
276 const combined = SecureUtils.base64ToArrayBuffer(encryptedData);
277 const totalLength = combined.byteLength;
278
279 // PHP format: salt(16) + iv(16) + encrypted_data + tag(16)
280 const saltLength = 16;
281 const ivLength = 16;
282 const tagLength = 16;
283 const encryptedDataLength = totalLength - saltLength - ivLength - tagLength;
284
285 if (totalLength < saltLength + ivLength + tagLength) {
286 throw new Error('Encrypted data too short');
287 }
288
289 const salt = combined.slice(0, saltLength);
290 const iv = combined.slice(saltLength, saltLength + ivLength);
291 const encryptedDataOnly = combined.slice(saltLength + ivLength, saltLength + ivLength + encryptedDataLength);
292 const tag = combined.slice(-tagLength); // Last 16 bytes
293
294 const key = await this.deriveKey(password, new Uint8Array(salt));
295
296 // Reconstruct the encrypted data with tag for Web Crypto API
297 const encryptedWithTag = new Uint8Array(encryptedDataOnly.byteLength + tag.byteLength);
298 encryptedWithTag.set(new Uint8Array(encryptedDataOnly), 0);
299 encryptedWithTag.set(new Uint8Array(tag), encryptedDataOnly.byteLength);
300
301 const decrypted = await crypto.subtle.decrypt(
302 { name: 'AES-GCM', iv: new Uint8Array(iv) },
303 key,
304 encryptedWithTag
305 );
306
307 const decoder = new TextDecoder();
308 return decoder.decode(decrypted);
309 } catch (error) {
310 throw new Error('Decryption failed: ' + error.message);
311 }
312 }
313 }
314
315 /**
316 * Main CryptX functions with backward compatibility
317 */
318
319 /**
320 * Securely decrypts and validates a URL before navigation
321 * @param {string} encryptedUrl
322 * @param {string} password
323 */
324 async function secureDecryptAndNavigate(encryptedUrl, password = 'default_key') {
325 if (typeof encryptedUrl !== 'string' || encryptedUrl.length === 0) {
326 console.error('Invalid encrypted URL provided');
327 return;
328 }
329
330 try {
331 let decryptedUrl;
332
333 // Try modern decryption first, then fall back to original algorithm
334 try {
335 decryptedUrl = await SecureEncryption.decrypt(encryptedUrl, password);
336 } catch (modernError) {
337 console.warn('Modern decryption failed, trying original algorithm');
338 decryptedUrl = LegacyEncryption.originalDecrypt(encryptedUrl);
339 }
340
341 const validatedUrl = SecureUtils.validateUrl(decryptedUrl);
342 if (!validatedUrl) {
343 console.error('Invalid or unsafe URL detected');
344 return;
345 }
346
347 window.location.href = validatedUrl;
348
349 } catch (error) {
350 console.error('Error during URL decryption and navigation:', error.message);
351 }
352 }
353
354 /**
355 * Legacy function for backward compatibility - using original algorithm
356 * @param {string} encryptedString
357 * @returns {string|null}
358 */
359 function DeCryptString(encryptedString) {
360 try {
361 return LegacyEncryption.originalDecrypt(encryptedString);
362 } catch (error) {
363 console.error('Legacy decryption failed:', error.message);
364 return null;
365 }
366 }
367
368 /**
369 * Legacy function for backward compatibility - secured
370 * @param {string} encryptedUrl
371 */
372 function DeCryptX(encryptedUrl) {
373 const decryptedUrl = DeCryptString(encryptedUrl);
374 if (!decryptedUrl) {
375 console.error('Failed to decrypt URL');
376 return;
377 }
378
379 const validatedUrl = SecureUtils.validateUrl(decryptedUrl);
380 if (!validatedUrl) {
381 console.error('Invalid or unsafe URL detected');
382 return;
383 }
384
385 window.location.href = validatedUrl;
386 }
387
388 /**
389 * Generates encrypted email link with proper security
390 * @param {string} emailAddress
391 * @param {string} password
392 * @returns {Promise<string>}
393 */
394 async function generateSecureEmailLink(emailAddress, password = 'default_key') {
395 if (typeof emailAddress !== 'string' || emailAddress.length === 0) {
396 throw new Error('Valid email address required');
397 }
398
399 const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
400 if (!emailRegex.test(emailAddress)) {
401 throw new Error('Invalid email format');
402 }
403
404 const mailtoUrl = `mailto:${emailAddress}`;
405 const encryptedData = await SecureEncryption.encrypt(mailtoUrl, password);
406 const escapedData = SecureUtils.escapeJavaScript(encryptedData);
407
408 return `javascript:secureDecryptAndNavigate('${escapedData}', '${SecureUtils.escapeJavaScript(password)}')`;
409 }
410
411 /**
412 * Legacy function for backward compatibility - using original algorithm
413 * @param {string} emailAddress
414 * @returns {string}
415 */
416 function generateDeCryptXHandler(emailAddress) {
417 if (typeof emailAddress !== 'string' || emailAddress.length === 0) {
418 console.error('Valid email address required');
419 return 'javascript:void(0)';
420 }
421
422 try {
423 const encrypted = LegacyEncryption.originalEncrypt(emailAddress);
424 const escaped = SecureUtils.escapeJavaScript(encrypted);
425 return `javascript:DeCryptX('${escaped}')`;
426 } catch (error) {
427 console.error('Error generating handler:', error.message);
428 return 'javascript:void(0)';
429 }
430 }
431
432 /**
433 * Legacy function - matches original PHP generateHashFromString
434 * @param {string} inputString
435 * @returns {string}
436 */
437 function generateHashFromString(inputString) {
438 try {
439 return LegacyEncryption.originalEncrypt(inputString);
440 } catch (error) {
441 console.error('Error generating hash:', error.message);
442 return '';
443 }
444 }
445
446 // Export functions for module usage
447 if (typeof module !== 'undefined' && module.exports) {
448 module.exports = {
449 secureDecryptAndNavigate,
450 generateSecureEmailLink,
451 DeCryptX,
452 DeCryptString,
453 generateDeCryptXHandler,
454 generateHashFromString,
455 SecureEncryption,
456 LegacyEncryption,
457 SecureUtils
458 };
459 }