DeepQuery Secure Core (page-world only)

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

当前为 2025-09-04 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/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 {}
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址