PluginProbe
CryptX / 3.3.3.2
CryptX v3.3.3.2
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
← All changes | js/cryptx.js +19 -743 4.2.0 → 3.3.3.2 View file →
@@ -1,746 +1,22 @@
1 -/**
2 - * Secure CryptX Library - Fixed for backward compatibility
3 - *
4 - * Everything below lives inside an IIFE. Without it, the top-level `const` and
5 - * `class` declarations -- CONFIG, ITERATIONS, KEY_LENGTH, SecureUtils,
6 - * SecureEncryption -- sit in the global lexical environment of the page, and a
7 - * second script declaring any of those names does not merely overwrite them:
8 - * it throws "Identifier has already been declared" and one of the two scripts
9 - * stops dead. With names this general that is a matter of time, and the failure
10 - * would look like CryptX being broken for no reason.
11 - *
12 - * What the outside is meant to reach is assigned to `window` at the bottom,
13 - * deliberately and by name.
14 - */
15 -(function () {
16 -
17 -// Configuration constants
18 -// The ceiling PHP already enforces (SecureEncryption::MAX_ITERATIONS), repeated
19 -// here so that no path can hand an absurd number to PBKDF2 and leave a
20 -// visitor's browser tab grinding.
21 -const MAX_ROUNDS = 1000000;
22 -
23 -// parseInt, because wp_localize_script turns every value into a string on the
24 -// way into the page -- so cryptxConfig.iterations arrives as "10000", and a
25 -// string fails Number.isInteger. Without this the clamp below applied only to
26 -// the value read from a link and never to this one, which is the opposite of
27 -// what one would assume from reading it.
28 -const ITERATIONS = Math.min(
29 - parseInt(window.cryptxConfig?.iterations, 10) || 100000, // fallback to old value
30 - MAX_ROUNDS
31 -);
32 -const KEY_LENGTH = window.cryptxConfig?.keyLength || 32;
33 -const IV_LENGTH = window.cryptxConfig?.ivLength || 16;
34 -const SALT_LENGTH = window.cryptxConfig?.saltLength || 16;
35 -const CONFIG = {
36 - ALLOWED_PROTOCOLS: ['http:', 'https:', 'mailto:'],
37 - MAX_URL_LENGTH: 2048,
38 - ENCRYPTION_KEY_SIZE: 32,
39 - IV_SIZE: 16
40 -};
41 -
42 -/**
43 - * Utility functions for secure operations
44 - */
45 -class SecureUtils {
46 - static getSecureRandomBytes(length) {
47 - if (typeof crypto === 'undefined' || !crypto.getRandomValues) {
48 - throw new Error('Secure random number generation not available');
49 - }
50 - return crypto.getRandomValues(new Uint8Array(length));
1 +function DeCryptString( s )
2 +{
3 + var n = 0;
4 + var r = "mailto:";
5 + var z = 0;
6 + for( var i = 0; i < s.length/2; i++)
7 + {
8 + z = s.substr(i*2, 1);
9 + n = s.charCodeAt( i*2+1 );
10 + if( n >= 8364 )
11 + {
12 + n = 128;
13 + }
14 + r += String.fromCharCode( n - z );
51 15 }
52 -
53 - static arrayBufferToBase64(buffer) {
54 - const bytes = new Uint8Array(buffer);
55 - let binary = '';
56 - for (let i = 0; i < bytes.byteLength; i++) {
57 - binary += String.fromCharCode(bytes[i]);
58 - }
59 - return btoa(binary);
60 - }
61 -
62 - static base64ToArrayBuffer(base64) {
63 - const binary = atob(base64);
64 - const buffer = new ArrayBuffer(binary.length);
65 - const bytes = new Uint8Array(buffer);
66 - for (let i = 0; i < binary.length; i++) {
67 - bytes[i] = binary.charCodeAt(i);
68 - }
69 - return buffer;
70 - }
71 -
72 - static validateUrl(url) {
73 - if (typeof url !== 'string' || url.length === 0) {
74 - return null;
75 - }
76 -
77 - if (url.length > CONFIG.MAX_URL_LENGTH) {
78 - console.error('URL exceeds maximum length');
79 - return null;
80 - }
81 -
82 - try {
83 - const urlObj = new URL(url);
84 -
85 - if (!CONFIG.ALLOWED_PROTOCOLS.includes(urlObj.protocol)) {
86 - console.error('Protocol not allowed:', urlObj.protocol);
87 - return null;
88 - }
89 -
90 - if (urlObj.protocol === 'mailto:') {
91 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
92 - if (!emailRegex.test(urlObj.pathname)) {
93 - console.error('Invalid email format in mailto URL');
94 - return null;
95 - }
96 - }
97 -
98 - return url;
99 - } catch (error) {
100 - console.error('Invalid URL format:', error.message);
101 - return null;
102 - }
103 - }
104 -
105 - static escapeJavaScript(str) {
106 - if (typeof str !== 'string') {
107 - return '';
108 - }
109 - return str.replace(/\\/g, '\\\\')
110 - .replace(/'/g, "\\'")
111 - .replace(/"/g, '\\"')
112 - .replace(/\n/g, '\\n')
113 - .replace(/\r/g, '\\r')
114 - .replace(/\t/g, '\\t');
115 - }
16 + return r;
116 17 }
117 18
118 -/**
119 - * Legacy encryption class - Fixed to match original PHP algorithm
120 - */
121 -class LegacyEncryption {
122 - /**
123 - * Decrypts using the original CryptX algorithm (matches PHP version)
124 - * @param {string} encryptedString
125 - * @returns {string}
126 - */
127 - static originalDecrypt(encryptedString) {
128 - if (typeof encryptedString !== 'string' || encryptedString.length === 0) {
129 - throw new Error('Invalid encrypted string');
130 - }
131 -
132 - // Constants from original algorithm
133 - const UPPER_LIMIT = 8364;
134 - const DEFAULT_VALUE = 128;
135 -
136 - let charCode = 0;
137 - let decryptedString = "mailto:";
138 - let encryptionKey = 0;
139 -
140 - try {
141 - for (let i = 0; i < encryptedString.length; i += 2) {
142 - if (i + 1 >= encryptedString.length) {
143 - break;
144 - }
145 -
146 - // Get the salt (encryption key) from current position
147 - encryptionKey = parseInt(encryptedString.charAt(i), 10);
148 -
149 - // Handle invalid salt values
150 - if (isNaN(encryptionKey)) {
151 - encryptionKey = 0;
152 - }
153 -
154 - // Get the character code from next position
155 - charCode = encryptedString.charCodeAt(i + 1);
156 -
157 - // Apply the same logic as original
158 - if (charCode >= UPPER_LIMIT) {
159 - charCode = DEFAULT_VALUE;
160 - }
161 -
162 - // Decrypt by subtracting the salt
163 - const decryptedCharCode = charCode - encryptionKey;
164 -
165 - // Validate the result
166 - if (decryptedCharCode < 0 || decryptedCharCode > 1114111) {
167 - throw new Error('Invalid character code during decryption');
168 - }
169 -
170 - decryptedString += String.fromCharCode(decryptedCharCode);
171 - }
172 -
173 - return decryptedString;
174 - } catch (error) {
175 - throw new Error('Original decryption failed: ' + error.message);
176 - }
177 - }
178 -
179 - /**
180 - * Encrypts using the original CryptX algorithm (matches PHP version)
181 - * @param {string} inputString
182 - * @returns {string}
183 - */
184 - static originalEncrypt(inputString) {
185 - if (typeof inputString !== 'string' || inputString.length === 0) {
186 - throw new Error('Invalid input string');
187 - }
188 -
189 - // Remove "mailto:" prefix if present for encryption
190 - const cleanInput = inputString.replace(/^mailto:/, '');
191 - let crypt = '';
192 -
193 - // ASCII values blacklist (from PHP constant)
194 - const ASCII_VALUES_BLACKLIST = ['32', '34', '39', '60', '62', '63', '92', '94', '96', '127'];
195 -
196 - try {
197 - for (let i = 0; i < cleanInput.length; i++) {
198 - let salt, asciiValue;
199 - let attempts = 0;
200 - const maxAttempts = 20; // Prevent infinite loops
201 -
202 - do {
203 - if (attempts >= maxAttempts) {
204 - // Fallback to a safe salt if we can't find a valid one
205 - salt = 1;
206 - asciiValue = cleanInput.charCodeAt(i) + salt;
207 - break;
208 - }
209 -
210 - // Generate random number between 0 and 3 (matching PHP rand(0,3))
211 - const randomValues = SecureUtils.getSecureRandomBytes(1);
212 - salt = randomValues[0] % 4;
213 -
214 - // Get ASCII value and add salt
215 - asciiValue = cleanInput.charCodeAt(i) + salt;
216 -
217 - // Check if value exceeds limit (matching PHP logic)
218 - if (asciiValue >= 8364) {
219 - asciiValue = 128;
220 - }
221 -
222 - attempts++;
223 - } while (ASCII_VALUES_BLACKLIST.includes(asciiValue.toString()) && attempts < maxAttempts);
224 -
225 - // Append salt and character to result
226 - crypt += salt.toString() + String.fromCharCode(asciiValue);
227 - }
228 -
229 - return crypt;
230 - } catch (error) {
231 - throw new Error('Original encryption failed: ' + error.message);
232 - }
233 - }
234 -}
235 -
236 -/**
237 - * Modern encryption class using Web Crypto API - PHP Compatible
238 - */
239 -class SecureEncryption {
240 - /**
241 - * @param {string} password
242 - * @param {Uint8Array} salt
243 - * @param {number} [iterations] What the link itself says it was made with.
244 - * Left out only by links written before 4.2.0, which then fall back to
245 - * the configured value -- the behaviour that made changing the setting
246 - * kill every link already delivered.
247 - */
248 - static async deriveKey(password, salt, iterations) {
249 - // Clamped to the same ceiling PHP enforces. The value can only come
250 - // from the server today -- KSES lets neither class nor data-* through
251 - // for anyone without unfiltered_html, and anyone with it does not need
252 - // this route -- but a number that reaches PBKDF2 unchecked is worth one
253 - // line of arithmetic.
254 - const rounds = Number.isInteger(iterations) && iterations > 0
255 - ? Math.min(iterations, MAX_ROUNDS)
256 - : ITERATIONS;
257 - const encoder = new TextEncoder();
258 - const keyMaterial = await crypto.subtle.importKey(
259 - 'raw',
260 - encoder.encode(password),
261 - { name: 'PBKDF2' },
262 - false,
263 - ['deriveKey']
264 - );
265 -
266 - return crypto.subtle.deriveKey(
267 - {
268 - name: 'PBKDF2',
269 - salt: salt,
270 - iterations: rounds,
271 - hash: 'SHA-256'
272 - },
273 - keyMaterial,
274 - { name: 'AES-GCM', length: KEY_LENGTH * 8},
275 - false,
276 - ['encrypt', 'decrypt']
277 - );
278 - }
279 -
280 - static async encrypt(plaintext, password) {
281 - if (typeof plaintext !== 'string' || typeof password !== 'string') {
282 - throw new Error('Both plaintext and password must be strings');
283 - }
284 -
285 - const encoder = new TextEncoder();
286 - const salt = SecureUtils.getSecureRandomBytes(16);
287 - const iv = SecureUtils.getSecureRandomBytes(CONFIG.IV_SIZE);
288 -
289 - const key = await this.deriveKey(password, salt);
290 -
291 - const encrypted = await crypto.subtle.encrypt(
292 - { name: 'AES-GCM', iv: iv },
293 - key,
294 - encoder.encode(plaintext)
295 - );
296 -
297 - // Match PHP format: salt(16) + iv(16) + encrypted_data + tag(16)
298 - const encryptedArray = new Uint8Array(encrypted);
299 - const encryptedData = encryptedArray.slice(0, -16); // Remove tag from encrypted data
300 - const tag = encryptedArray.slice(-16); // Get the tag
301 -
302 - const combined = new Uint8Array(salt.length + iv.length + encryptedData.length + tag.length);
303 - combined.set(salt, 0);
304 - combined.set(iv, salt.length);
305 - combined.set(encryptedData, salt.length + iv.length);
306 - combined.set(tag, salt.length + iv.length + encryptedData.length);
307 -
308 - return SecureUtils.arrayBufferToBase64(combined.buffer);
309 - }
310 -
311 - /**
312 - * @param {string} encryptedData
313 - * @param {string} password
314 - * @param {number} [iterations] See deriveKey().
315 - */
316 - static async decrypt(encryptedData, password, iterations) {
317 - if (typeof encryptedData !== 'string' || typeof password !== 'string') {
318 - throw new Error('Both encryptedData and password must be strings');
319 - }
320 -
321 - try {
322 - const combined = SecureUtils.base64ToArrayBuffer(encryptedData);
323 - const totalLength = combined.byteLength;
324 -
325 - // PHP format: salt(16) + iv(16) + encrypted_data + tag(16)
326 - const saltLength = 16;
327 - const ivLength = 16;
328 - const tagLength = 16;
329 - const encryptedDataLength = totalLength - saltLength - ivLength - tagLength;
330 -
331 - if (totalLength < saltLength + ivLength + tagLength) {
332 - throw new Error('Encrypted data too short');
333 - }
334 -
335 - const salt = combined.slice(0, saltLength);
336 - const iv = combined.slice(saltLength, saltLength + ivLength);
337 - const encryptedDataOnly = combined.slice(saltLength + ivLength, saltLength + ivLength + encryptedDataLength);
338 - const tag = combined.slice(-tagLength); // Last 16 bytes
339 -
340 - const key = await this.deriveKey(password, new Uint8Array(salt), iterations);
341 -
342 - // Reconstruct the encrypted data with tag for Web Crypto API
343 - const encryptedWithTag = new Uint8Array(encryptedDataOnly.byteLength + tag.byteLength);
344 - encryptedWithTag.set(new Uint8Array(encryptedDataOnly), 0);
345 - encryptedWithTag.set(new Uint8Array(tag), encryptedDataOnly.byteLength);
346 -
347 - const decrypted = await crypto.subtle.decrypt(
348 - { name: 'AES-GCM', iv: new Uint8Array(iv) },
349 - key,
350 - encryptedWithTag
351 - );
352 -
353 - const decoder = new TextDecoder();
354 - return decoder.decode(decrypted);
355 - } catch (error) {
356 - throw new Error('Decryption failed: ' + error.message);
357 - }
358 - }
359 -}
360 -
361 -/**
362 - * Main CryptX functions with backward compatibility
363 - */
364 -
365 -/**
366 - * Securely decrypts and validates a URL before navigation
367 - * @param {string} encryptedUrl
368 - * @param {string} password
369 - */
370 -async function secureDecryptAndNavigate(encryptedUrl, password = 'default_key', iterations) {
371 - if (typeof encryptedUrl !== 'string' || encryptedUrl.length === 0) {
372 - console.error('Invalid encrypted URL provided');
373 - return;
374 - }
375 -
376 - try {
377 - let decryptedUrl;
378 -
379 - // Try modern decryption first, then fall back to original algorithm
380 - try {
381 - decryptedUrl = await SecureEncryption.decrypt(encryptedUrl, password, iterations);
382 - } catch (modernError) {
383 - console.warn('Modern decryption failed, trying original algorithm');
384 - decryptedUrl = LegacyEncryption.originalDecrypt(encryptedUrl);
385 - }
386 -
387 - const validatedUrl = SecureUtils.validateUrl(decryptedUrl);
388 - if (!validatedUrl) {
389 - console.error('Invalid or unsafe URL detected');
390 - return;
391 - }
392 -
393 - window.location.href = validatedUrl;
394 -
395 - } catch (error) {
396 - console.error('Error during URL decryption and navigation:', error.message);
397 - }
398 -}
399 -
400 -/**
401 - * Legacy function for backward compatibility - using original algorithm
402 - * @param {string} encryptedString
403 - * @returns {string|null}
404 - */
405 -function DeCryptString(encryptedString) {
406 - try {
407 - return LegacyEncryption.originalDecrypt(encryptedString);
408 - } catch (error) {
409 - console.error('Legacy decryption failed:', error.message);
410 - return null;
411 - }
412 -}
413 -
414 -/**
415 - * Legacy function for backward compatibility - secured
416 - * @param {string} encryptedUrl
417 - */
418 -function DeCryptX(encryptedUrl) {
419 - const decryptedUrl = DeCryptString(encryptedUrl);
420 - if (!decryptedUrl) {
421 - console.error('Failed to decrypt URL');
422 - return;
423 - }
424 -
425 - const validatedUrl = SecureUtils.validateUrl(decryptedUrl);
426 - if (!validatedUrl) {
427 - console.error('Invalid or unsafe URL detected');
428 - return;
429 - }
430 -
431 - window.location.href = validatedUrl;
432 -}
433 -
434 -/**
435 - * Generates encrypted email link with proper security
436 - * @param {string} emailAddress
437 - * @param {string} password
438 - * @returns {Promise<string>}
439 - */
440 -async function generateSecureEmailLink(emailAddress, password = 'default_key') {
441 - if (typeof emailAddress !== 'string' || emailAddress.length === 0) {
442 - throw new Error('Valid email address required');
443 - }
444 -
445 - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
446 - if (!emailRegex.test(emailAddress)) {
447 - throw new Error('Invalid email format');
448 - }
449 -
450 - const mailtoUrl = `mailto:${emailAddress}`;
451 - const encryptedData = await SecureEncryption.encrypt(mailtoUrl, password);
452 - const escapedData = SecureUtils.escapeJavaScript(encryptedData);
453 -
454 - // The iteration count goes in as well, for the same reason the PHP side
455 - // puts it in data-cxi: encrypt() used whatever ITERATIONS says right now,
456 - // and without recording that, a later change to the setting would leave
457 - // this link unopenable. Nothing in the plugin calls this function -- it is
458 - // here for anyone building links themselves -- which is exactly why it
459 - // should not be the one place that still breaks.
460 - return `javascript:secureDecryptAndNavigate('${escapedData}', '${SecureUtils.escapeJavaScript(password)}', ${ITERATIONS})`;
461 -}
462 -
463 -/**
464 - * Legacy function for backward compatibility - using original algorithm
465 - * @param {string} emailAddress
466 - * @returns {string}
467 - */
468 -function generateDeCryptXHandler(emailAddress) {
469 - if (typeof emailAddress !== 'string' || emailAddress.length === 0) {
470 - console.error('Valid email address required');
471 - return 'javascript:void(0)';
472 - }
473 -
474 - try {
475 - const encrypted = LegacyEncryption.originalEncrypt(emailAddress);
476 - const escaped = SecureUtils.escapeJavaScript(encrypted);
477 - return `javascript:DeCryptX('${escaped}')`;
478 - } catch (error) {
479 - console.error('Error generating handler:', error.message);
480 - return 'javascript:void(0)';
481 - }
482 -}
483 -
484 -/**
485 - * Legacy function - matches original PHP generateHashFromString
486 - * @param {string} inputString
487 - * @returns {string}
488 - */
489 -function generateHashFromString(inputString) {
490 - try {
491 - return LegacyEncryption.originalEncrypt(inputString);
492 - } catch (error) {
493 - console.error('Error generating hash:', error.message);
494 - return '';
495 - }
496 -}
497 -
498 -/**
499 - * CSP-safe link handling
500 - *
501 - * Markup produced by the PHP side (no javascript: URI, therefore no
502 - * 'unsafe-inline' needed in the Content-Security-Policy):
503 - *
504 - * <a href="#" class="cryptx-link" data-cx="BASE64" data-cxk="PASSWORD" data-cxm="secure" data-cxi="10000">…</a>
505 - * <a href="#" class="cryptx-link" data-cx="0i2p2h…" data-cxm="legacy">…</a>
506 - *
507 - * A single delegated listener on `document` covers links that are added later
508 - * (widgets, AJAX, block editor preview). The legacy javascript: entry points
509 - * above stay untouched for pages that were cached before this version.
510 - */
511 -
512 -const CRYPTX_LINK_CLASS = 'cryptx-link';
513 -const CRYPTX_ATTR_PAYLOAD = 'data-cx';
514 -const CRYPTX_ATTR_KEY = 'data-cxk';
515 -const CRYPTX_ATTR_MODE = 'data-cxm';
516 -const CRYPTX_ATTR_ITERATIONS = 'data-cxi';
517 -const CRYPTX_MAX_DELEGATION_DEPTH = 50;
518 -
519 -let cryptxLinkHandlerAttached = false;
520 -
521 -/**
522 - * True only when the Web Crypto API is usable (secure context, modern browser)
523 - * @returns {boolean}
524 - */
525 -function cryptxHasSubtleCrypto() {
526 - return typeof crypto !== 'undefined' &&
527 - !!crypto &&
528 - !!crypto.subtle &&
529 - typeof crypto.subtle.importKey === 'function';
530 -}
531 -
532 -/**
533 - * @param {*} element
534 - * @returns {boolean}
535 - */
536 -function isCryptxLink(element) {
537 - if (!element || typeof element.getAttribute !== 'function') {
538 - return false;
539 - }
540 -
541 - if (element.classList && typeof element.classList.contains === 'function') {
542 - return element.classList.contains(CRYPTX_LINK_CLASS);
543 - }
544 -
545 - if (typeof element.className === 'string') {
546 - return (' ' + element.className + ' ').indexOf(' ' + CRYPTX_LINK_CLASS + ' ') !== -1;
547 - }
548 -
549 - return false;
550 -}
551 -
552 -/**
553 - * Walks up from the event target to the CryptX link (clicks may land on a
554 - * child element, e.g. an <img> or <span> inside the anchor).
555 - * @param {*} startNode
556 - * @returns {*|null}
557 - */
558 -function findCryptxLink(startNode) {
559 - let node = startNode;
560 - let depth = 0;
561 -
562 - while (node && depth < CRYPTX_MAX_DELEGATION_DEPTH) {
563 - if (isCryptxLink(node)) {
564 - return node;
565 - }
566 - node = node.parentElement || node.parentNode || null;
567 - depth++;
568 - }
569 -
570 - return null;
571 -}
572 -
573 -/**
574 - * Decrypts a data-cx payload according to data-cxm.
575 - * Missing mode behaves like "secure" with a fallback to "legacy",
576 - * exactly like secureDecryptAndNavigate() does.
577 - * @param {string} payload
578 - * @param {string|null} password
579 - * @param {string|null} mode
580 - * @param {string|number|null} [iterations] What data-cxi says. Links written
581 - * before 4.2.0 do not carry it and fall back to the configured value -- which
582 - * is why changing that value used to break every link already delivered.
583 - * @returns {Promise<string>}
584 - */
585 -async function cryptxDecryptPayload(payload, password, mode, iterations) {
586 - if (typeof payload !== 'string' || payload.length === 0) {
587 - throw new Error('Missing or invalid data-cx payload');
588 - }
589 -
590 - const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
591 -
592 - if (normalizedMode === 'legacy') {
593 - return LegacyEncryption.originalDecrypt(payload);
594 - }
595 -
596 - if (!cryptxHasSubtleCrypto()) {
597 - if (normalizedMode === 'secure') {
598 - throw new Error('Web Crypto API (crypto.subtle) is not available in this context');
599 - }
600 - return LegacyEncryption.originalDecrypt(payload);
601 - }
602 -
603 - const key = typeof password === 'string' && password.length > 0 ? password : 'default_key';
604 - const rounds = parseInt(iterations, 10);
605 -
606 - try {
607 - return await SecureEncryption.decrypt(payload, key, rounds);
608 - } catch (secureError) {
609 - if (normalizedMode === 'secure') {
610 - throw secureError;
611 - }
612 - console.warn('CryptX: modern decryption failed, trying original algorithm');
613 - return LegacyEncryption.originalDecrypt(payload);
614 - }
615 -}
616 -
617 -/**
618 - * Delegated click handler. Never navigates without SecureUtils.validateUrl().
619 - * @param {Object} event
620 - * @returns {Promise<void>}
621 - */
622 -async function handleCryptxLinkClick(event) {
623 - if (!event) {
624 - return;
625 - }
626 -
627 - const link = findCryptxLink(event.target);
628 - if (!link) {
629 - return;
630 - }
631 -
632 - // Suppress the "#" jump before anything asynchronous happens.
633 - if (typeof event.preventDefault === 'function') {
634 - event.preventDefault();
635 - }
636 -
637 - const payload = link.getAttribute(CRYPTX_ATTR_PAYLOAD);
638 - const password = link.getAttribute(CRYPTX_ATTR_KEY);
639 - const mode = link.getAttribute(CRYPTX_ATTR_MODE);
640 - const iterations = link.getAttribute(CRYPTX_ATTR_ITERATIONS);
641 -
642 - try {
643 - const decryptedUrl = await cryptxDecryptPayload(payload, password, mode, iterations);
644 -
645 - const validatedUrl = SecureUtils.validateUrl(decryptedUrl);
646 - if (!validatedUrl) {
647 - console.error('CryptX: invalid or unsafe URL detected, navigation aborted');
648 - return;
649 - }
650 -
651 - window.location.href = validatedUrl;
652 - } catch (error) {
653 - console.error('CryptX: could not resolve link target:', error && error.message ? error.message : error);
654 - }
655 -}
656 -
657 -/**
658 - * Attaches the single delegated listener. Idempotent.
659 - * @param {Object} [targetDocument]
660 - * @returns {boolean} true when the listener was attached by this call
661 - */
662 -function initCryptxLinkHandler(targetDocument) {
663 - const doc = targetDocument || (typeof document !== 'undefined' ? document : null);
664 -
665 - if (!doc || typeof doc.addEventListener !== 'function') {
666 - return false;
667 - }
668 -
669 - if (cryptxLinkHandlerAttached) {
670 - return false;
671 - }
672 -
673 - doc.addEventListener('click', handleCryptxLinkClick, false);
674 - cryptxLinkHandlerAttached = true;
675 -
676 - return true;
677 -}
678 -
679 -// Attach immediately - delegation on `document` needs no finished DOM, so this
680 -// works for a <head> include as well as for a footer include, where
681 -// DOMContentLoaded may already have fired and would never come again.
682 -if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
683 - initCryptxLinkHandler(document);
684 -
685 - if (document.readyState === 'loading') {
686 - // Safety net for exotic environments that replace `document` while parsing.
687 - document.addEventListener('DOMContentLoaded', function () {
688 - initCryptxLinkHandler(document);
689 - });
690 - }
691 -}
692 -
693 -// Keep everything reachable by name, also after minification.
694 -//
695 -// The three the plugin itself depends on are secureDecryptAndNavigate and
696 -// DeCryptX, which appear in the generated "javascript:" links, and
697 -// generateDeCryptXHandler, which the help tab documents for use in a theme.
698 -// The rest is kept because it has been exported for years; new code should use
699 -// the window.CryptX namespace, and the bare names will go with the next major
700 -// release, together with the deprecated encryptx().
701 -if (typeof window !== 'undefined') {
702 - window.CryptX = {
703 - secureDecryptAndNavigate,
704 - DeCryptX,
705 - DeCryptString,
706 - generateSecureEmailLink,
707 - generateDeCryptXHandler,
708 - generateHashFromString,
709 - handleCryptxLinkClick,
710 - initCryptxLinkHandler,
711 - SecureUtils,
712 - LegacyEncryption,
713 - SecureEncryption
714 - };
715 -
716 - window.secureDecryptAndNavigate = secureDecryptAndNavigate;
717 - window.DeCryptX = DeCryptX;
718 - window.DeCryptString = DeCryptString;
719 - window.generateSecureEmailLink = generateSecureEmailLink;
720 - window.generateDeCryptXHandler = generateDeCryptXHandler;
721 - window.generateHashFromString = generateHashFromString;
722 - window.handleCryptxLinkClick = handleCryptxLinkClick;
723 - window.initCryptxLinkHandler = initCryptxLinkHandler;
724 - window.SecureUtils = SecureUtils;
725 - window.LegacyEncryption = LegacyEncryption;
726 - window.SecureEncryption = SecureEncryption;
727 -}
728 -
729 -// Export functions for module usage
730 -if (typeof module !== 'undefined' && module.exports) {
731 - module.exports = {
732 - secureDecryptAndNavigate,
733 - generateSecureEmailLink,
734 - DeCryptX,
735 - DeCryptString,
736 - generateDeCryptXHandler,
737 - generateHashFromString,
738 - handleCryptxLinkClick,
739 - initCryptxLinkHandler,
740 - cryptxDecryptPayload,
741 - SecureEncryption,
742 - LegacyEncryption,
743 - SecureUtils
744 - };
745 -}
746 -})();
19 +function DeCryptX( s )
20 +{
21 + location.href=DeCryptString( s );
22 +}