PluginProbe
CryptX / 4.0.1
CryptX v4.0.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
← All changes | js/cryptx.js +9 -310 4.2.14.0.1 View file →
@@ -1,38 +1,9 @@
1 1 /**
2 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 3 */
15 -(function () {
16 4
17 5 // 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 6 const CONFIG = {
36 7 ALLOWED_PROTOCOLS: ['http:', 'https:', 'mailto:'],
37 8 MAX_URL_LENGTH: 2048,
38 9 ENCRYPTION_KEY_SIZE: 32,
@@ -236,25 +207,9 @@
236 207 /**
237 208 * Modern encryption class using Web Crypto API - PHP Compatible
238 209 */
239 210 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;
211 + static async deriveKey(password, salt) {
257 212 const encoder = new TextEncoder();
258 213 const keyMaterial = await crypto.subtle.importKey(
259 214 'raw',
260 215 encoder.encode(password),
@@ -266,13 +221,13 @@
266 221 return crypto.subtle.deriveKey(
267 222 {
268 223 name: 'PBKDF2',
269 224 salt: salt,
270 - iterations: rounds,
225 + iterations: 100000,
271 226 hash: 'SHA-256'
272 227 },
273 228 keyMaterial,
274 - { name: 'AES-GCM', length: KEY_LENGTH * 8},
229 + { name: 'AES-GCM', length: 256 },
275 230 false,
276 231 ['encrypt', 'decrypt']
277 232 );
278 233 }
@@ -307,14 +262,9 @@
307 262
308 263 return SecureUtils.arrayBufferToBase64(combined.buffer);
309 264 }
310 265
311 - /**
312 - * @param {string} encryptedData
313 - * @param {string} password
314 - * @param {number} [iterations] See deriveKey().
315 - */
316 - static async decrypt(encryptedData, password, iterations) {
266 + static async decrypt(encryptedData, password) {
317 267 if (typeof encryptedData !== 'string' || typeof password !== 'string') {
318 268 throw new Error('Both encryptedData and password must be strings');
319 269 }
320 270
@@ -336,9 +286,9 @@
336 286 const iv = combined.slice(saltLength, saltLength + ivLength);
337 287 const encryptedDataOnly = combined.slice(saltLength + ivLength, saltLength + ivLength + encryptedDataLength);
338 288 const tag = combined.slice(-tagLength); // Last 16 bytes
339 289
340 - const key = await this.deriveKey(password, new Uint8Array(salt), iterations);
290 + const key = await this.deriveKey(password, new Uint8Array(salt));
341 291
342 292 // Reconstruct the encrypted data with tag for Web Crypto API
343 293 const encryptedWithTag = new Uint8Array(encryptedDataOnly.byteLength + tag.byteLength);
344 294 encryptedWithTag.set(new Uint8Array(encryptedDataOnly), 0);
@@ -366,9 +316,9 @@
366 316 * Securely decrypts and validates a URL before navigation
367 317 * @param {string} encryptedUrl
368 318 * @param {string} password
369 319 */
370 -async function secureDecryptAndNavigate(encryptedUrl, password = 'default_key', iterations) {
320 +async function secureDecryptAndNavigate(encryptedUrl, password = 'default_key') {
371 321 if (typeof encryptedUrl !== 'string' || encryptedUrl.length === 0) {
372 322 console.error('Invalid encrypted URL provided');
373 323 return;
374 324 }
@@ -377,9 +327,9 @@
377 327 let decryptedUrl;
378 328
379 329 // Try modern decryption first, then fall back to original algorithm
380 330 try {
381 - decryptedUrl = await SecureEncryption.decrypt(encryptedUrl, password, iterations);
331 + decryptedUrl = await SecureEncryption.decrypt(encryptedUrl, password);
382 332 } catch (modernError) {
383 333 console.warn('Modern decryption failed, trying original algorithm');
384 334 decryptedUrl = LegacyEncryption.originalDecrypt(encryptedUrl);
385 335 }
@@ -450,15 +400,9 @@
450 400 const mailtoUrl = `mailto:${emailAddress}`;
451 401 const encryptedData = await SecureEncryption.encrypt(mailtoUrl, password);
452 402 const escapedData = SecureUtils.escapeJavaScript(encryptedData);
453 403
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})`;
404 + return `javascript:secureDecryptAndNavigate('${escapedData}', '${SecureUtils.escapeJavaScript(password)}')`;
461 405 }
462 406
463 407 /**
464 408 * Legacy function for backward compatibility - using original algorithm
@@ -494,249 +438,8 @@
494 438 return '';
495 439 }
496 440 }
497 441
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 442 // Export functions for module usage
740 443 if (typeof module !== 'undefined' && module.exports) {
741 444 module.exports = {
742 445 secureDecryptAndNavigate,
@@ -744,13 +447,9 @@
744 447 DeCryptX,
745 448 DeCryptString,
746 449 generateDeCryptXHandler,
747 450 generateHashFromString,
748 - handleCryptxLinkClick,
749 - initCryptxLinkHandler,
750 - cryptxDecryptPayload,
751 451 SecureEncryption,
752 452 LegacyEncryption,
753 453 SecureUtils
754 454 };
755 -}
756 -})();
455 +}