DeepQuery Secure Client (GM storage key)

DeepQuery 客户端(仅沙箱内可见)。密钥存放到 GM 存储,不写在代码里;负责签名并与 Core 通信。

Version vom 04.09.2025. Aktuellste Version

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @require https://update.greasyforks.org/scripts/548365/1654742/DeepQuery%20Secure%20Client%20%28GM%20storage%20key%29.js

// ==UserScript==
// @name         DeepQuery Secure Client (GM storage key)
// @namespace    dq.secure.v2.client
// @version      2.1.0
// @description  DeepQuery 客户端(仅沙箱内可见)。密钥存放到 GM 存储,不写在代码里;负责签名并与 Core 通信。
// @author       you
// @match        http://*/*
// @match        https://*/*
// @include      about:blank
// @run-at       document-start
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// ==/UserScript==
(function () {
  'use strict';

  /******** 基本配置(无需放密钥) ********/
  const CHANNEL = '__DQ_SECURE_V2__';                 // 必须与 Core 保持一致
  const KEY_STORE_NAME = 'DQ_SECURE_V2_KEY_B64';      // GM 存储的键名
  const EXPOSE_TOP_PROXY = false; // 设为 true 可零改动复用 top.DeepQuery(仅对本沙箱可见)

  /******** 小工具 ********/
  const te = new TextEncoder();
  function u8FromB64(b64) {
    if (!b64 || typeof b64 !== 'string') return null;
    const s = b64.replace(/-/g, '+').replace(/_/g, '/');
    const pad = s.length % 4 ? 4 - (s.length % 4) : 0;
    const bin = atob(s + '='.repeat(pad));
    const u8 = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
    return u8;
  }
  function b64FromU8(u8) {
    let s = '';
    for (let i = 0; i < u8.length; i++) s += String.fromCharCode(u8[i]);
    return btoa(s).replace(/=+$/,'');
  }
  const hasGM = (typeof GM !== 'undefined' && GM) || {};
  const gmGet = (k, d=null) =>
    (hasGM.getValue ? hasGM.getValue(k, d) :
      (typeof GM_getValue === 'function' ? Promise.resolve(GM_getValue(k, d)) : Promise.resolve(d)));
  const gmSet = (k, v) =>
    (hasGM.setValue ? hasGM.setValue(k, v) :
      (typeof GM_setValue === 'function' ? Promise.resolve(GM_setValue(k, v)) : Promise.resolve()));
  const gmReg = (name, fn) =>
    (hasGM.registerMenuCommand ? GM.registerMenuCommand(name, fn) :
      (typeof GM_registerMenuCommand === 'function' ? GM_registerMenuCommand(name, fn) : null));
  const gmClip = (text) =>
    (typeof GM_setClipboard === 'function' ? GM_setClipboard(text) : navigator.clipboard?.writeText?.(text));

  /******** HMAC ********/
  async function sha256U8(u8) {
    const buf = await crypto.subtle.digest('SHA-256', u8);
    return new Uint8Array(buf);
  }
  async function hmacSignRaw(keyU8, bytes) {
    const k = await crypto.subtle.importKey('raw', keyU8, { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
    const sig = await crypto.subtle.sign('HMAC', k, bytes);
    return new Uint8Array(sig);
  }

  /******** 读取/管理密钥(来自 GM 存储) ********/
  let KEY_U8 = null;
  let keyLoadPromise = null;

  async function ensureKeyLoaded() {
    if (KEY_U8) return KEY_U8;
    if (!keyLoadPromise) {
      keyLoadPromise = (async () => {
        const b64 = (await gmGet(KEY_STORE_NAME, '')).trim();
        if (!b64) throw new Error('[DeepQuery Client] KEY_MISSING: 请先在脚本菜单里设置密钥');
        const u8 = u8FromB64(b64);
        if (!u8 || u8.length < 16) throw new Error('[DeepQuery Client] KEY_INVALID: GM 存储中的密钥格式不正确');
        KEY_U8 = u8;
        return KEY_U8;
      })();
    }
    return keyLoadPromise;
  }

  // 菜单:设置/生成/查看密钥
  gmReg('Set DeepQuery Key…', async () => {
    const b64 = prompt('粘贴与你的 Core 相同的密钥(base64):\n(提示:不要泄露给网页脚本)');
    if (!b64) return;
    const u8 = u8FromB64(b64.trim());
    if (!u8 || u8.length < 16) { alert('密钥格式不正确(必须是 base64,建议 >= 32 字节)'); return; }
    await gmSet(KEY_STORE_NAME, b64.trim());
    KEY_U8 = u8;
    keyLoadPromise = Promise.resolve(u8);
    alert('已保存密钥到 GM 存储。');
  });

  gmReg('Generate Random Key', async () => {
    const u8 = new Uint8Array(32);
    crypto.getRandomValues(u8);
    const b64 = b64FromU8(u8);
    await gmSet(KEY_STORE_NAME, b64);
    KEY_U8 = u8;
    keyLoadPromise = Promise.resolve(u8);
    gmClip && gmClip(b64);
    alert('已生成随机密钥并保存到 GM 存储;密钥已复制到剪贴板。\n⚠️ 请同步更新 Core 脚本中的密钥为相同值!');
  });

  gmReg('Show Current Key (base64)', async () => {
    const b64 = await gmGet(KEY_STORE_NAME, '');
    if (!b64) { alert('尚未设置密钥'); return; }
    gmClip && gmClip(b64);
    alert('已复制当前密钥到剪贴板。');
  });

  /******** 与 Core 通信 ********/
  const pending = new Map();
  function rid() {
    return (Date.now().toString(36) + Math.random().toString(36).slice(2, 10)).toUpperCase();
  }
  function send(payload) {
    // 只与顶层 Core 打交道(Core 会做逐级转发)
    window.top.postMessage({ [CHANNEL]: payload }, '*');
  }
  window.addEventListener('message', (e) => {
    const msg = e.data && e.data[CHANNEL];
    if (!msg || msg.cmd !== 'RESP' || !msg.id) return;
    const hit = pending.get(msg.id);
    if (!hit) return;
    pending.delete(msg.id);
    hit.resolve(msg.res);
  }, false);

  async function request(spec) {
    const key = await ensureKeyLoaded(); // 确保已加载 GM 密钥
    const id = rid();
    const ts = Date.now();
    const nonce = rid() + Math.random().toString(36).slice(2);
    const header = te.encode(id + '\n' + ts + '\n' + nonce + '\n');
    const bodyHash = await sha256U8(te.encode(JSON.stringify(spec || {})));
    const toSign = new Uint8Array(header.length + bodyHash.length);
    toSign.set(header, 0); toSign.set(bodyHash, header.length);
    const sigU8 = await hmacSignRaw(key, toSign);
    const sigB64 = b64FromU8(sigU8);

    const timeout = typeof spec?.timeout === 'number' ? Math.max(200, spec.timeout + 500) : 6000;

    return new Promise((resolve) => {
      pending.set(id, { resolve });
      send({ cmd: 'REQ', id, ts, nonce, sigB64, spec });
      setTimeout(() => {
        if (pending.has(id)) {
          pending.delete(id);
          resolve({ ok: false, error: 'TIMEOUT' });
        }
      }, timeout);
    });
  }

  /******** 暴露 API(与旧版一致) ********/
  const DeepQuery = {
    async get(spec = {}) { return request(spec); },
    async attr({ framePath, chain, name, timeout }) { return request({ framePath, chain, timeout, pick: { attr: name } }); },
    async prop({ framePath, chain, name, timeout }) { return request({ framePath, chain, timeout, pick: { prop: name } }); },
    async text({ framePath, chain, timeout }) { return request({ framePath, chain, timeout, pick: { text: true } }); },
    async html({ framePath, chain, timeout }) { return request({ framePath, chain, timeout, pick: { html: true } }); },
    async rect({ framePath, chain, timeout }) { return request({ framePath, chain, timeout, pick: { rect: true } }); },
    version: '2.1.0-client'
  };

  // 方式 A(推荐):直接用 DeepQuery
  try { window.DeepQuery = DeepQuery; } catch {}

  // 方式 B(零改动):在当前沙箱里暴露 top.DeepQuery 代理(页面不可见)
  if (EXPOSE_TOP_PROXY) {
    try {
      Object.defineProperty(window.top, 'DeepQuery', {
        configurable: true,
        get() { return DeepQuery; }
      });
    } catch {}
  }

  // 尝试提前加载密钥(不影响后续调用)
  ensureKeyLoaded().catch(() => {
    // 首次未设置密钥时静默,调用时会抛清晰错误
  });
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。