DeepQuery Secure Core (page-world only)

深度查询(Shadow DOM/iframe)核心执行器(页面上下文)。仅接受带签名消息,页面代码无法直接调用。

Tính đến 04-09-2025. Xem phiên bản mới nhất.

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyforks.org/scripts/548365/1654734/DeepQuery%20Secure%20Core%20%28page-world%20only%29.js

// ==UserScript==
// @name         DeepQuery Secure Core (page-world only)
// @namespace    dq.secure.v2
// @version      2.0.0
// @description  深度查询(Shadow DOM/iframe)核心执行器(页面上下文)。仅接受带签名消息,页面代码无法直接调用。
// @author       you
// @match        http://*/*
// @match        https://*/*
// @include      about:blank
// @run-at       document-start
// @grant        none
// @inject-into  page
// ==/UserScript==
(function () {
  'use strict';

  /************** 配置 **************/
  // 通道名(随便换成很长的随机串,增加混淆)
  const CHANNEL = '__DQ_SECURE_V2__';
  // HMAC 的密钥以“片段+重组”方式藏在闭包里,页面拿不到;你可以把片段改成你自己的随机串
  // 强烈建议自行换成至少 32 字节以上随机 base64 的若干段
  const KEY_PARTS = [
    'dW5hcmFuZG9tLWtleS1zZWVkLQ', // 示例片段(请务必替换)
    'tZXBsZWFzZS1yZXBsYWNlLW1l', // 示例片段(请务必替换)
    'LXdpdGgtYS1wcm9wZXItb25l'   // 示例片段(请务必替换)
  ];
  // 允许的时间偏差(ms)与 nonce 存活(ms)
  const MAX_SKEW = 60_000;      // 60s
  const NONCE_TTL = 10 * 60_000; // 10min

  // --- 工具:base64 与 HMAC ---
  const te = new TextEncoder();
  const td = new TextDecoder();
  function b64ToU8(b64) {
    // 补 '='
    const pad = b64.length % 4 ? (4 - b64.length % 4) : 0;
    const s = b64 + '='.repeat(pad);
    const bin = atob(s.replace(/-/g, '+').replace(/_/g, '/'));
    const u8 = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
    return u8;
  }
  function u8eq(a, b) {
    if (a.byteLength !== b.byteLength) return false;
    let v = 0;
    for (let i = 0; i < a.byteLength; i++) v |= (a[i] ^ b[i]);
    return v === 0;
  }
  async function sha256U8(u8) {
    const buf = await crypto.subtle.digest('SHA-256', u8);
    return new Uint8Array(buf);
  }
  async function hmacSignRaw(key, u8) {
    const k = await crypto.subtle.importKey(
      'raw', key, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']
    );
    const sig = await crypto.subtle.sign('HMAC', k, u8);
    return new Uint8Array(sig);
  }
  const KEY_U8 = (() => {
    // 简单拼接再 sha256,避免直接把明文 key 出现在脚本文本中
    const joined = KEY_PARTS.join('');
    const u8 = b64ToU8(joined);
    return u8;
  })();

  // --- 防重放 ---
  const seen = new Map(); // nonce -> ts
  function sweep() {
    const now = Date.now();
    for (const [n, t] of seen) if (now - t > NONCE_TTL) seen.delete(n);
  }
  setInterval(sweep, 30_000);

  async function verifySig(msg) {
    // msg: {id, ts, nonce, spec, sigB64}
    if (!msg || typeof msg !== 'object') return false;
    const { id, ts, nonce, spec, sigB64 } = msg;
    if (!id || typeof ts !== 'number' || !nonce || !sigB64) return false;
    const skew = Math.abs(Date.now() - ts);
    if (skew > MAX_SKEW) return false;
    if (seen.has(nonce)) return false; // 重放
    // 绑定消息体,防止被改内容重放
    const specJson = JSON.stringify(spec || {});
    const payload = te.encode(id + '\n' + ts + '\n' + nonce + '\n');
    const bodyHash = await sha256U8(te.encode(specJson));
    const toSign = new Uint8Array(payload.length + bodyHash.length);
    toSign.set(payload, 0);
    toSign.set(bodyHash, payload.length);
    const expect = await hmacSignRaw(KEY_U8, toSign);
    const got = b64ToU8(sigB64);
    const ok = u8eq(expect, got);
    if (ok) {
      seen.set(nonce, ts);
    }
    return ok;
  }

  /************** 查询核心(与早先版本一致) **************/
  const SHADOW_KEY = Symbol.for('__dq_shadow__');
  const isTop = (window === window.top);

  function safeQueryAll(root, sel) {
    try { return Array.from(root.querySelectorAll(sel)); } catch { return []; }
  }
  function firstByIndex(list, idx) {
    if (!list || !list.length) return null;
    return typeof idx === 'number' ? (list[idx] || null) : list[0];
  }
  (function hookAttachShadowEarly() {
    try {
      const orig = Element.prototype.attachShadow;
      if (!orig || orig.__dq_hooked__) return;
      Object.defineProperty(Element.prototype, 'attachShadow', {
        configurable: true, enumerable: false, writable: true,
        value: function (init) {
          const root = orig.call(this, init);
          try {
            Object.defineProperty(this, SHADOW_KEY, {
              configurable: true, enumerable: false, writable: false, value: root
            });
          } catch {}
          return root;
        }
      });
      Element.prototype.attachShadow.__dq_hooked__ = true;
    } catch {}
  })();
  function ensureOpenShadowReference(host) {
    try {
      if (!host) return;
      if (host[SHADOW_KEY]) return;
      if (host.shadowRoot) {
        Object.defineProperty(host, SHADOW_KEY, {
          configurable: true, enumerable: false, writable: false, value: host.shadowRoot
        });
      }
    } catch {}
  }
  function parseChain(chain) {
    if (typeof chain === 'string') {
      const parts = chain.split('>>>').map(s => s.trim()).filter(Boolean);
      const steps = [];
      parts.forEach((seg, i) => {
        if (i === 0) steps.push({ find: seg });
        else steps.push({ shadow: true }, { find: seg });
      });
      return steps;
    }
    return (Array.isArray(chain) ? chain : []);
  }
  function toPlainRect(r) {
    return r ? { x: r.x, y: r.y, width: r.width, height: r.height, top: r.top, left: r.left, right: r.right, bottom: r.bottom } : null;
  }
  function pickInfo(el, pick = {}) {
    if (!el) return { ok: false, error: 'ELEMENT_NOT_FOUND' };
    const res = { ok: true, tag: el.tagName, exists: true };
    if (pick.attr) res.attr = el.getAttribute(pick.attr);
    if (pick.prop) try { res.prop = el[pick.prop]; } catch { res.prop = undefined; }
    if (pick.text) res.text = el.textContent;
    if (pick.html) res.html = el.innerHTML;
    if (pick.outerHTML) res.outerHTML = el.outerHTML;
    if (pick.value) res.value = (el.value !== undefined ? el.value : undefined);
    if (pick.rect) res.rect = toPlainRect(el.getBoundingClientRect ? el.getBoundingClientRect() : null);
    if (pick.styles && Array.isArray(pick.styles) && pick.styles.length) {
      const cs = getComputedStyle(el);
      const map = {};
      pick.styles.forEach(k => { map[k] = cs.getPropertyValue(k); });
      res.styles = map;
    }
    if (pick.dataset) {
      const dst = {};
      (Array.isArray(pick.dataset) ? pick.dataset : [pick.dataset]).forEach(k => { dst[k] = el.dataset ? el.dataset[k] : undefined; });
      res.dataset = dst;
    }
    return res;
  }
  function deepQueryOnce(rootLike, chainSteps) {
    let root = (rootLike instanceof ShadowRoot || rootLike instanceof Document) ? rootLike : document;
    let current = root;
    let lastEl = null;
    for (const step of chainSteps) {
      if (step.shadow) {
        if (!lastEl) return null;
        ensureOpenShadowReference(lastEl);
        const sr = lastEl.shadowRoot || lastEl[SHADOW_KEY];
        if (!sr) return null;
        current = sr;
        continue;
      }
      if (step.find) {
        const idx = (typeof step.index === 'number') ? step.index : null;
        const list = safeQueryAll(current, step.find);
        lastEl = firstByIndex(list, idx);
        if (!lastEl) return null;
        continue;
      }
      return null;
    }
    return lastEl;
  }
  async function waitForDeepQuery(rootLike, chainSteps, timeout = 0) {
    const start = Date.now();
    const tryOnce = () => deepQueryOnce(rootLike, chainSteps);
    let el = tryOnce();
    if (el || !timeout) return el;
    return new Promise((resolve) => {
      const limit = start + timeout;
      const observer = new MutationObserver(() => {
        el = tryOnce();
        if (el) { observer.disconnect(); resolve(el); }
        else if (Date.now() > limit) { observer.disconnect(); resolve(null); }
      });
      observer.observe(document, { childList: true, subtree: true, attributes: true });
      const t = setTimeout(() => { try { observer.disconnect(); } catch {} resolve(null); }, timeout);
      const tick = () => {
        el = tryOnce();
        if (el) { clearTimeout(t); try { observer.disconnect(); } catch {} resolve(el); }
        else if (Date.now() <= limit) requestAnimationFrame(tick);
      };
      requestAnimationFrame(tick);
    });
  }

  // frame 选择
  function findTargetFrames(step) {
    const cfg = (typeof step === 'string') ? { selector: step } : (step || {});
    const sel = cfg.selector || 'iframe';
    const all = safeQueryAll(document, sel).filter(n => n && n.tagName === 'IFRAME');
    if (!all.length) return [];
    if (typeof cfg.index === 'number') {
      const one = all[cfg.index];
      return (one && one.contentWindow) ? [one.contentWindow] : [];
    }
    return all.map(el => el.contentWindow).filter(Boolean);
  }

  /************** 消息收发(带签名) **************/
  const pending = new Map(); // id -> resolver
  function rid() {
    return (Date.now().toString(36) + Math.random().toString(36).slice(2, 10)).toUpperCase();
  }
  function sendMessage(targetWin, payload) {
    try { targetWin.postMessage({ [CHANNEL]: payload }, '*'); } catch {}
  }

  async function forwardOrExec(msg, sourceWin) {
    const { id, spec = {} } = msg;
    const framePath = Array.isArray(spec.framePath) ? spec.framePath : [];
    if (framePath.length > 0) {
      const [step, ...rest] = framePath;
      const targets = findTargetFrames(step);
      if (!targets.length) {
        sendMessage(sourceWin || window.parent, { cmd: 'RESP', id, res: { ok: false, error: 'FRAME_NOT_FOUND' } });
        return;
      }
      // fan-out:向匹配目标逐一转发,取第一个成功的结果
      const subResults = await Promise.all(targets.map(t => new Promise(async (resolve) => {
        const subId = rid();
        pending.set(subId, { resolve });
        // 用相同的 spec 但缩短 framePath;**重新签名**
        const subMsg = {
          cmd: 'REQ',
          id: subId,
          ts: Date.now(),
          nonce: rid() + Math.random().toString(36).slice(2),
          spec: { ...spec, framePath: rest }
        };
        // 签名
        const payload = te.encode(subMsg.id + '\n' + subMsg.ts + '\n' + subMsg.nonce + '\n');
        const bodyHash = await sha256U8(te.encode(JSON.stringify(subMsg.spec)));
        const toSign = new Uint8Array(payload.length + bodyHash.length);
        toSign.set(payload, 0); toSign.set(bodyHash, payload.length);
        const sigU8 = await hmacSignRaw(KEY_U8, toSign);
        const sigB64 = btoa(String.fromCharCode(...sigU8));
        sendMessage(t, { ...subMsg, sigB64 });
        // 超时保护
        setTimeout(() => {
          if (pending.has(subId)) {
            pending.delete(subId);
            resolve({ ok: false, error: 'TIMEOUT_FORWARD' });
          }
        }, typeof spec.timeout === 'number' ? Math.max(200, spec.timeout) : 5000);
      })));
      const ok = subResults.find(r => r && r.ok);
      const res = ok || subResults[0] || { ok: false, error: 'NO_RESULT' };
      sendMessage(sourceWin || window.parent, { cmd: 'RESP', id, res });
      return;
    }

    // 在本帧执行
    try {
      const chain = parseChain(spec.chain);
      const el = await waitForDeepQuery(document, chain, typeof spec.timeout === 'number' ? spec.timeout : 0);
      const res = pickInfo(el, spec.pick || {});
      sendMessage(sourceWin || window.parent, { cmd: 'RESP', id, res });
    } catch (err) {
      sendMessage(sourceWin || window.parent, { cmd: 'RESP', id, res: { ok: false, error: 'EXEC_ERROR', message: String(err && err.message || err) } });
    }
  }

  window.addEventListener('message', async (e) => {
    const msg = e.data && e.data[CHANNEL];
    if (!msg) return;

    // 响应回传(promise 归还)
    if (msg.cmd === 'RESP' && msg.id && pending.has(msg.id)) {
      const { resolve } = pending.get(msg.id);
      pending.delete(msg.id);
      resolve(msg.res);
      return;
    }

    // 进入点:必须验签
    if (msg.cmd === 'REQ') {
      const ok = await verifySig(msg);
      if (!ok) {
        sendMessage(e.source || window.parent, { cmd: 'RESP', id: msg.id, res: { ok: false, error: 'UNAUTHORIZED' } });
        return;
      }
      // 验签通过,执行/转发
      await forwardOrExec(msg, e.source || window.parent);
    }
  }, false);

  if (isTop) {
    try { console.debug('[DeepQuery Secure Core] ready. Listening on', CHANNEL); } catch {}
  }
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。