PluginProbe
CryptX / 4.2.1
CryptX v4.2.1
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.2.1, at js/cryptx.js

757 lines 22.9 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 * 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));
51 }
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 }
116 }
117
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 /**
520 * True only when the Web Crypto API is usable (secure context, modern browser)
521 * @returns {boolean}
522 */
523 function cryptxHasSubtleCrypto() {
524 return typeof crypto !== 'undefined' &&
525 !!crypto &&
526 !!crypto.subtle &&
527 typeof crypto.subtle.importKey === 'function';
528 }
529
530 /**
531 * @param {*} element
532 * @returns {boolean}
533 */
534 function isCryptxLink(element) {
535 if (!element || typeof element.getAttribute !== 'function') {
536 return false;
537 }
538
539 if (element.classList && typeof element.classList.contains === 'function') {
540 return element.classList.contains(CRYPTX_LINK_CLASS);
541 }
542
543 if (typeof element.className === 'string') {
544 return (' ' + element.className + ' ').indexOf(' ' + CRYPTX_LINK_CLASS + ' ') !== -1;
545 }
546
547 return false;
548 }
549
550 /**
551 * Walks up from the event target to the CryptX link (clicks may land on a
552 * child element, e.g. an <img> or <span> inside the anchor).
553 * @param {*} startNode
554 * @returns {*|null}
555 */
556 function findCryptxLink(startNode) {
557 let node = startNode;
558 let depth = 0;
559
560 while (node && depth < CRYPTX_MAX_DELEGATION_DEPTH) {
561 if (isCryptxLink(node)) {
562 return node;
563 }
564 node = node.parentElement || node.parentNode || null;
565 depth++;
566 }
567
568 return null;
569 }
570
571 /**
572 * Decrypts a data-cx payload according to data-cxm.
573 * Missing mode behaves like "secure" with a fallback to "legacy",
574 * exactly like secureDecryptAndNavigate() does.
575 * @param {string} payload
576 * @param {string|null} password
577 * @param {string|null} mode
578 * @param {string|number|null} [iterations] What data-cxi says. Links written
579 * before 4.2.0 do not carry it and fall back to the configured value -- which
580 * is why changing that value used to break every link already delivered.
581 * @returns {Promise<string>}
582 */
583 async function cryptxDecryptPayload(payload, password, mode, iterations) {
584 if (typeof payload !== 'string' || payload.length === 0) {
585 throw new Error('Missing or invalid data-cx payload');
586 }
587
588 const normalizedMode = typeof mode === 'string' ? mode.trim().toLowerCase() : '';
589
590 if (normalizedMode === 'legacy') {
591 return LegacyEncryption.originalDecrypt(payload);
592 }
593
594 if (!cryptxHasSubtleCrypto()) {
595 if (normalizedMode === 'secure') {
596 throw new Error('Web Crypto API (crypto.subtle) is not available in this context');
597 }
598 return LegacyEncryption.originalDecrypt(payload);
599 }
600
601 const key = typeof password === 'string' && password.length > 0 ? password : 'default_key';
602 const rounds = parseInt(iterations, 10);
603
604 try {
605 return await SecureEncryption.decrypt(payload, key, rounds);
606 } catch (secureError) {
607 if (normalizedMode === 'secure') {
608 throw secureError;
609 }
610 console.warn('CryptX: modern decryption failed, trying original algorithm');
611 return LegacyEncryption.originalDecrypt(payload);
612 }
613 }
614
615 /**
616 * Delegated click handler. Never navigates without SecureUtils.validateUrl().
617 * @param {Object} event
618 * @returns {Promise<void>}
619 */
620 async function handleCryptxLinkClick(event) {
621 if (!event) {
622 return;
623 }
624
625 const link = findCryptxLink(event.target);
626 if (!link) {
627 return;
628 }
629
630 // Suppress the "#" jump before anything asynchronous happens.
631 if (typeof event.preventDefault === 'function') {
632 event.preventDefault();
633 }
634
635 const payload = link.getAttribute(CRYPTX_ATTR_PAYLOAD);
636 const password = link.getAttribute(CRYPTX_ATTR_KEY);
637 const mode = link.getAttribute(CRYPTX_ATTR_MODE);
638 const iterations = link.getAttribute(CRYPTX_ATTR_ITERATIONS);
639
640 try {
641 const decryptedUrl = await cryptxDecryptPayload(payload, password, mode, iterations);
642
643 const validatedUrl = SecureUtils.validateUrl(decryptedUrl);
644 if (!validatedUrl) {
645 console.error('CryptX: invalid or unsafe URL detected, navigation aborted');
646 return;
647 }
648
649 window.location.href = validatedUrl;
650 } catch (error) {
651 console.error('CryptX: could not resolve link target:', error && error.message ? error.message : error);
652 }
653 }
654
655 /**
656 * Attaches the single delegated listener. Idempotent.
657 *
658 * The "attached" flag lives on the document itself (an expando property),
659 * not in a closure variable. Two things went wrong with a closure variable:
660 * first, any plugin or loader that runs cryptx.js a second time on the same
661 * document -- @swup/scripts-plugin does, Turbo/Hotwire does, any AJAX loader
662 * that brings footer markup along does -- gets a fresh closure and therefore
663 * a second `click` listener on `document`; measured: one click fired
664 * `mailto:` twice. Second, initCryptxLinkHandler(otherDocument) is exported
665 * for exactly this use but never worked, because the closure flag was
666 * already true from the main document and nothing was attached to the one
667 * passed in. Marking the document, not the module, fixes both with the same
668 * few lines.
669 * @param {Object} [targetDocument]
670 * @returns {boolean} true when the listener was attached by this call
671 */
672 function initCryptxLinkHandler(targetDocument) {
673 const doc = targetDocument || (typeof document !== 'undefined' ? document : null);
674
675 if (!doc || typeof doc.addEventListener !== 'function') {
676 return false;
677 }
678
679 if (doc.__cryptxLinkHandlerAttached) {
680 return false;
681 }
682
683 doc.addEventListener('click', handleCryptxLinkClick, false);
684 doc.__cryptxLinkHandlerAttached = true;
685
686 return true;
687 }
688
689 // Attach immediately - delegation on `document` needs no finished DOM, so this
690 // works for a <head> include as well as for a footer include, where
691 // DOMContentLoaded may already have fired and would never come again.
692 if (typeof document !== 'undefined' && typeof document.addEventListener === 'function') {
693 initCryptxLinkHandler(document);
694
695 if (document.readyState === 'loading') {
696 // Safety net for exotic environments that replace `document` while parsing.
697 document.addEventListener('DOMContentLoaded', function () {
698 initCryptxLinkHandler(document);
699 });
700 }
701 }
702
703 // Keep everything reachable by name, also after minification.
704 //
705 // The three the plugin itself depends on are secureDecryptAndNavigate and
706 // DeCryptX, which appear in the generated "javascript:" links, and
707 // generateDeCryptXHandler, which the help tab documents for use in a theme.
708 // The rest is kept because it has been exported for years; new code should use
709 // the window.CryptX namespace, and the bare names will go with the next major
710 // release, together with the deprecated encryptx().
711 if (typeof window !== 'undefined') {
712 window.CryptX = {
713 secureDecryptAndNavigate,
714 DeCryptX,
715 DeCryptString,
716 generateSecureEmailLink,
717 generateDeCryptXHandler,
718 generateHashFromString,
719 handleCryptxLinkClick,
720 initCryptxLinkHandler,
721 SecureUtils,
722 LegacyEncryption,
723 SecureEncryption
724 };
725
726 window.secureDecryptAndNavigate = secureDecryptAndNavigate;
727 window.DeCryptX = DeCryptX;
728 window.DeCryptString = DeCryptString;
729 window.generateSecureEmailLink = generateSecureEmailLink;
730 window.generateDeCryptXHandler = generateDeCryptXHandler;
731 window.generateHashFromString = generateHashFromString;
732 window.handleCryptxLinkClick = handleCryptxLinkClick;
733 window.initCryptxLinkHandler = initCryptxLinkHandler;
734 window.SecureUtils = SecureUtils;
735 window.LegacyEncryption = LegacyEncryption;
736 window.SecureEncryption = SecureEncryption;
737 }
738
739 // Export functions for module usage
740 if (typeof module !== 'undefined' && module.exports) {
741 module.exports = {
742 secureDecryptAndNavigate,
743 generateSecureEmailLink,
744 DeCryptX,
745 DeCryptString,
746 generateDeCryptXHandler,
747 generateHashFromString,
748 handleCryptxLinkClick,
749 initCryptxLinkHandler,
750 cryptxDecryptPayload,
751 SecureEncryption,
752 LegacyEncryption,
753 SecureUtils
754 };
755 }
756 })();
757