Bilibili App Auth

哔哩哔哩 App 端鉴权

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.greasyfork.org/scripts/566236/1754598/Bilibili%20App%20Auth.js

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name               Bilibili App Auth
// @name:zh-CN         哔哩哔哩 App 扫码鉴权库
// @namespace          https://gab.moe/
// @version            1.0.0
// @author             GabrielxD
// @description:zh-CN  为用户脚本提供 B 站 App 端扫码登录和 API 签名能力的依赖库
// @license            MIT
// @grant              GM_xmlhttpRequest
// ==/UserScript==

"use strict";

function createBilibiliAppAuth(
  deps = {
    GM_xmlhttpRequest: null,
  },
) {
  const logger = (() => {
    const { name: scriptName, version: scriptVersion } = GM_info.script;
    const nameStyle = `padding: 2px 10px; border-radius: 4px 0 0 4px; color: #fff; background: #2394F1; font-weight: bold;`;
    const versionStyle = `padding: 2px 10px; border-radius: 0 4px 4px 0; color: #fff; background: #FA7298; font-weight: bold;`;

    return new Proxy(console, {
      get(target, prop, receiver) {
        const value = Reflect.get(target, prop, receiver);
        if (typeof value !== "function") {
          return value;
        }

        return value.bind(
          target,
          `%c${scriptName}%cv${scriptVersion}`,
          nameStyle,
          versionStyle,
        );
      },
    });
  })();

  const missingDeps = Object.entries(deps)
    .filter(([_, value]) => value === null)
    .map(([key]) => key);

  if (missingDeps.length > 0) {
    logger.warn(
      `${missingDeps.join(
        ", ",
      )} are required, please pass them to the function`,
    );
    return null;
  }

  const { GM_xmlhttpRequest } = deps;

  // Migrated from https://github.com/blueimp/JavaScript-MD5
  const md5 = (() => {
    function safeAdd(x, y) {
      var lsw = (x & 0xffff) + (y & 0xffff);
      var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
      return (msw << 16) | (lsw & 0xffff);
    }
    function bitRotateLeft(num, cnt) {
      return (num << cnt) | (num >>> (32 - cnt));
    }
    function md5cmn(q, a, b, x, s, t) {
      return safeAdd(
        bitRotateLeft(safeAdd(safeAdd(a, q), safeAdd(x, t)), s),
        b,
      );
    }
    function md5ff(a, b, c, d, x, s, t) {
      return md5cmn((b & c) | (~b & d), a, b, x, s, t);
    }
    function md5gg(a, b, c, d, x, s, t) {
      return md5cmn((b & d) | (c & ~d), a, b, x, s, t);
    }
    function md5hh(a, b, c, d, x, s, t) {
      return md5cmn(b ^ c ^ d, a, b, x, s, t);
    }
    function md5ii(a, b, c, d, x, s, t) {
      return md5cmn(c ^ (b | ~d), a, b, x, s, t);
    }
    function binlMD5(x, len) {
      /* append padding */
      x[len >> 5] |= 0x80 << len % 32;
      x[(((len + 64) >>> 9) << 4) + 14] = len;

      var i;
      var olda;
      var oldb;
      var oldc;
      var oldd;
      var a = 1732584193;
      var b = -271733879;
      var c = -1732584194;
      var d = 271733878;

      for (i = 0; i < x.length; i += 16) {
        olda = a;
        oldb = b;
        oldc = c;
        oldd = d;

        a = md5ff(a, b, c, d, x[i], 7, -680876936);
        d = md5ff(d, a, b, c, x[i + 1], 12, -389564586);
        c = md5ff(c, d, a, b, x[i + 2], 17, 606105819);
        b = md5ff(b, c, d, a, x[i + 3], 22, -1044525330);
        a = md5ff(a, b, c, d, x[i + 4], 7, -176418897);
        d = md5ff(d, a, b, c, x[i + 5], 12, 1200080426);
        c = md5ff(c, d, a, b, x[i + 6], 17, -1473231341);
        b = md5ff(b, c, d, a, x[i + 7], 22, -45705983);
        a = md5ff(a, b, c, d, x[i + 8], 7, 1770035416);
        d = md5ff(d, a, b, c, x[i + 9], 12, -1958414417);
        c = md5ff(c, d, a, b, x[i + 10], 17, -42063);
        b = md5ff(b, c, d, a, x[i + 11], 22, -1990404162);
        a = md5ff(a, b, c, d, x[i + 12], 7, 1804603682);
        d = md5ff(d, a, b, c, x[i + 13], 12, -40341101);
        c = md5ff(c, d, a, b, x[i + 14], 17, -1502002290);
        b = md5ff(b, c, d, a, x[i + 15], 22, 1236535329);

        a = md5gg(a, b, c, d, x[i + 1], 5, -165796510);
        d = md5gg(d, a, b, c, x[i + 6], 9, -1069501632);
        c = md5gg(c, d, a, b, x[i + 11], 14, 643717713);
        b = md5gg(b, c, d, a, x[i], 20, -373897302);
        a = md5gg(a, b, c, d, x[i + 5], 5, -701558691);
        d = md5gg(d, a, b, c, x[i + 10], 9, 38016083);
        c = md5gg(c, d, a, b, x[i + 15], 14, -660478335);
        b = md5gg(b, c, d, a, x[i + 4], 20, -405537848);
        a = md5gg(a, b, c, d, x[i + 9], 5, 568446438);
        d = md5gg(d, a, b, c, x[i + 14], 9, -1019803690);
        c = md5gg(c, d, a, b, x[i + 3], 14, -187363961);
        b = md5gg(b, c, d, a, x[i + 8], 20, 1163531501);
        a = md5gg(a, b, c, d, x[i + 13], 5, -1444681467);
        d = md5gg(d, a, b, c, x[i + 2], 9, -51403784);
        c = md5gg(c, d, a, b, x[i + 7], 14, 1735328473);
        b = md5gg(b, c, d, a, x[i + 12], 20, -1926607734);

        a = md5hh(a, b, c, d, x[i + 5], 4, -378558);
        d = md5hh(d, a, b, c, x[i + 8], 11, -2022574463);
        c = md5hh(c, d, a, b, x[i + 11], 16, 1839030562);
        b = md5hh(b, c, d, a, x[i + 14], 23, -35309556);
        a = md5hh(a, b, c, d, x[i + 1], 4, -1530992060);
        d = md5hh(d, a, b, c, x[i + 4], 11, 1272893353);
        c = md5hh(c, d, a, b, x[i + 7], 16, -155497632);
        b = md5hh(b, c, d, a, x[i + 10], 23, -1094730640);
        a = md5hh(a, b, c, d, x[i + 13], 4, 681279174);
        d = md5hh(d, a, b, c, x[i], 11, -358537222);
        c = md5hh(c, d, a, b, x[i + 3], 16, -722521979);
        b = md5hh(b, c, d, a, x[i + 6], 23, 76029189);
        a = md5hh(a, b, c, d, x[i + 9], 4, -640364487);
        d = md5hh(d, a, b, c, x[i + 12], 11, -421815835);
        c = md5hh(c, d, a, b, x[i + 15], 16, 530742520);
        b = md5hh(b, c, d, a, x[i + 2], 23, -995338651);

        a = md5ii(a, b, c, d, x[i], 6, -198630844);
        d = md5ii(d, a, b, c, x[i + 7], 10, 1126891415);
        c = md5ii(c, d, a, b, x[i + 14], 15, -1416354905);
        b = md5ii(b, c, d, a, x[i + 5], 21, -57434055);
        a = md5ii(a, b, c, d, x[i + 12], 6, 1700485571);
        d = md5ii(d, a, b, c, x[i + 3], 10, -1894986606);
        c = md5ii(c, d, a, b, x[i + 10], 15, -1051523);
        b = md5ii(b, c, d, a, x[i + 1], 21, -2054922799);
        a = md5ii(a, b, c, d, x[i + 8], 6, 1873313359);
        d = md5ii(d, a, b, c, x[i + 15], 10, -30611744);
        c = md5ii(c, d, a, b, x[i + 6], 15, -1560198380);
        b = md5ii(b, c, d, a, x[i + 13], 21, 1309151649);
        a = md5ii(a, b, c, d, x[i + 4], 6, -145523070);
        d = md5ii(d, a, b, c, x[i + 11], 10, -1120210379);
        c = md5ii(c, d, a, b, x[i + 2], 15, 718787259);
        b = md5ii(b, c, d, a, x[i + 9], 21, -343485551);

        a = safeAdd(a, olda);
        b = safeAdd(b, oldb);
        c = safeAdd(c, oldc);
        d = safeAdd(d, oldd);
      }
      return [a, b, c, d];
    }
    function binl2rstr(input) {
      var i;
      var output = "";
      var length32 = input.length * 32;
      for (i = 0; i < length32; i += 8) {
        output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);
      }
      return output;
    }
    function rstr2binl(input) {
      var i;
      var output = [];
      output[(input.length >> 2) - 1] = undefined;
      for (i = 0; i < output.length; i += 1) {
        output[i] = 0;
      }
      var length8 = input.length * 8;
      for (i = 0; i < length8; i += 8) {
        output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
      }
      return output;
    }
    function rstrMD5(s) {
      return binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
    }
    function rstrHMACMD5(key, data) {
      var i;
      var bkey = rstr2binl(key);
      var ipad = [];
      var opad = [];
      var hash;
      ipad[15] = opad[15] = undefined;
      if (bkey.length > 16) {
        bkey = binlMD5(bkey, key.length * 8);
      }
      for (i = 0; i < 16; i += 1) {
        ipad[i] = bkey[i] ^ 0x36363636;
        opad[i] = bkey[i] ^ 0x5c5c5c5c;
      }
      hash = binlMD5(ipad.concat(rstr2binl(data)), 512 + data.length * 8);
      return binl2rstr(binlMD5(opad.concat(hash), 512 + 128));
    }
    function rstr2hex(input) {
      var hexTab = "0123456789abcdef";
      var output = "";
      var x;
      var i;
      for (i = 0; i < input.length; i += 1) {
        x = input.charCodeAt(i);
        output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f);
      }
      return output;
    }
    function str2rstrUTF8(input) {
      return unescape(encodeURIComponent(input));
    }
    function rawMD5(s) {
      return rstrMD5(str2rstrUTF8(s));
    }
    function hexMD5(s) {
      return rstr2hex(rawMD5(s));
    }
    function rawHMACMD5(k, d) {
      return rstrHMACMD5(str2rstrUTF8(k), str2rstrUTF8(d));
    }
    function hexHMACMD5(k, d) {
      return rstr2hex(rawHMACMD5(k, d));
    }
    return function md5(string, key, raw) {
      if (!key) {
        if (!raw) {
          return hexMD5(string);
        }
        return rawMD5(string);
      }
      if (!raw) {
        return hexHMACMD5(key, string);
      }
      return rawHMACMD5(key, string);
    };
  })();

  const isObject = value => value !== null && typeof value === "object";

  const getCurrentUnixTS = () => Math.floor(Date.now() / 1000);

  /** 部分可用的 APP 预设 (appkey / appsec) */
  const APP_PRESETS = {
    /** 粉版 (Android, 7.X 及更新版本获取用户信息) */
    android: {
      appkey: "783bbb7264451d82",
      appsec: "2653583c8873dea268ab9386918b1d65",
    },
    /** AndroidBiliThings */
    android_things: {
      appkey: "8d23902c1688a798",
      appsec: "710f0212e62bd499b8d3ac6e1db9302a",
    },
    /** 第三方授权 */
    third_party: {
      appkey: "27eb53fc9058f8c3",
      appsec: "c2ed53a74eeefe3cf99fbd01d8c9c375",
    },
    /** 云视听小电视(TV版) */
    tv: {
      appkey: "4409e2ce8ffd12b8",
      appsec: "59b43e04ad6965f34319062b478f83dd",
    },
    /** HD 版 */
    hd: {
      appkey: "dfca71928277209b",
      appsec: "b5475a8825547a4fc26c7d518eaaa02e",
    },
  };

  /**
   * 哔哩哔哩 App 端扫码登录
   *
   * 事件:
   * - `start`     — 登录流程开始(获取到二维码后触发)
   * - `scan`      — 二维码状态轮询中(等待扫码 / 已扫码未确认), `event.detail` 为轮询响应
   * - `completed` — 登录成功, `event.detail` 为登录响应数据
   * - `error`     — 登录失败 / 二维码过期, `event.detail` 为错误响应
   * - `end`       — 登录流程结束(无论成功或失败), `event.detail` 为最终响应
   */
  class BilibiliAppAuth extends EventTarget {
    USER_AGENT =
      "Mozilla/5.0 BiliDroid/8.43.0 ([email protected]) os/android model/android mobi_app/android build/8430300 channel/master innerVer/8430300 osVer/15 network/2";
    #appkey;
    #appsec;
    #timer = null;
    /** 轮询间隔(ms) */
    interval = 2000;

    /**
     * @param {string | { appkey: string; appsec: string }} app
     *   预设名称(如 "tv", "android", "hd" 等)或自定义 { appkey, appsec }
     */
    constructor(app) {
      super();
      if (typeof app === "string") {
        const preset = APP_PRESETS[app];
        if (!preset) {
          logger.error(
            `未知的 APP 预设: "${app}",可用预设: ${Object.keys(
              APP_PRESETS,
            ).join(", ")}`,
          );
          return;
        }
        this.#appkey = preset.appkey;
        this.#appsec = preset.appsec;
      } else {
        if (!app?.appkey || !app?.appsec) {
          logger.error("自定义配置必须提供 appkey 和 appsec");
          return;
        }
        this.#appkey = app.appkey;
        this.#appsec = app.appsec;
      }
    }

    static request = (() => {
      const _request = (url, options = {}) => {
        const {
          method = "GET",
          headers,
          data = null,
          params = null,
          timeout = 30_000,
          responseType,
          cookie,
          anonymous,
        } = options;

        let query = params;
        if (isObject(params) && !(params instanceof URLSearchParams)) {
          query = new URLSearchParams(params);
        }
        const fullURL = query
          ? `${url}${url.includes("?") ? "&" : "?"}${query}`
          : url;

        let ctrl;
        const promise = new Promise((resolve, reject) => {
          GM_xmlhttpRequest({
            method,
            url: fullURL,
            headers,
            data,
            timeout,
            responseType,
            cookie,
            anonymous,
            onload(res) {
              if (res.status >= 200 && res.status < 300) {
                let result = res.response ?? res.responseText;
                if (!responseType && typeof result === "string") {
                  try {
                    result = JSON.parse(result);
                  } catch {}
                }
                resolve({
                  body: result,
                  status: res.status,
                  headers: res.responseHeaders,
                  raw: res,
                });
              } else {
                reject({
                  body: res.responseText,
                  status: res.status,
                  statusText: res.statusText,
                  raw: res,
                });
              }
            },
            onerror: res =>
              reject({ status: 0, msg: "Network Error", raw: res }),
            ontimeout: res => reject({ status: 0, msg: "Timeout", raw: res }),
          });
        });

        promise.abort = () => ctrl?.abort();
        return promise;
      };
      _request.get = (url, params, opts) =>
        _request(url, { ...opts, method: "GET", params });
      _request.post = (url, data, opts) =>
        _request(url, { ...opts, method: "POST", data });

      return _request;
    })();

    /**
     * 为请求参数进行 APP 签名
     * @param {Record<string, any> | URLSearchParams} params
     * @returns {URLSearchParams} 签名后的 params
     */
    signParams(params) {
      if (!(params instanceof URLSearchParams)) {
        params = new URLSearchParams(params);
      }
      params.set("appkey", this.#appkey);
      params.sort();
      params.set("sign", md5(`${params.toString()}${this.#appsec}`));
      return params;
    }

    /**
     * 获取登录二维码 URL 及密钥
     */
    async getAuthCode() {
      const signedParams = this.signParams({
        local_id: 0,
        ts: getCurrentUnixTS(),
        mobi_app: "android",
      });

      const resp = await BilibiliAppAuth.request.post(
        "https://passport.bilibili.com/x/passport-tv-login/qrcode/auth_code",
        signedParams,
        {
          responseType: "json",
          anonymous: true,
          headers: {
            "User-Agent": this.USER_AGENT,
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
          },
        },
      );

      if (resp.body.code !== 0) {
        logger.error("getAuthCode error:", resp.body);
        return null;
      }

      return resp.body.data;
    }

    /**
     * 查询登录二维码状态
     * @param {string} authCode
     */
    async poll(authCode) {
      const signedParams = this.signParams({
        auth_code: authCode,
        local_id: 0,
        ts: getCurrentUnixTS(),
      });

      const resp = await BilibiliAppAuth.request.post(
        "https://passport.bilibili.com/x/passport-tv-login/qrcode/poll",
        signedParams,
        {
          responseType: "json",
          anonymous: true,
          headers: {
            "User-Agent": this.USER_AGENT,
            "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
          },
        },
      );

      return resp.body;
    }

    /**
     * 开始扫码登录流程
     * @returns {Promise<string | null>} 二维码 URL,失败返回 null
     */
    async login() {
      const data = await this.getAuthCode();
      if (!data) {
        this.dispatchEvent(
          new CustomEvent("error", {
            detail: { code: -1, message: "获取二维码失败", data: null },
          }),
        );
        return null;
      }

      const authCode = data.auth_code;
      this.dispatchEvent(new Event("start"));

      let count = 0;
      this.#timer = setInterval(async () => {
        try {
          const resp = await this.poll(authCode);

          if (resp.code === 0) {
            this.dispatchEvent(new CustomEvent("completed", { detail: resp }));
            this.#finish(resp);
          } else if (resp.code === 86039 || resp.code === 86090) {
            // 86039: 尚未扫码, 86090: 已扫码未确认
            this.dispatchEvent(new CustomEvent("scan", { detail: resp }));
          } else {
            // 86038: 二维码已失效 / 其他错误
            this.dispatchEvent(new CustomEvent("error", { detail: resp }));
            this.#finish(resp);
          }

          count++;
          if (count > 180_000 / this.interval) {
            const timeout = {
              code: 86038,
              message: "二维码已失效",
              ttl: 1,
              data: null,
            };
            this.dispatchEvent(new CustomEvent("error", { detail: timeout }));
            this.#finish(timeout);
          }
        } catch (err) {
          logger.error("poll error:", err);
          this.dispatchEvent(
            new CustomEvent("error", {
              detail: { code: -1, message: "轮询请求异常", data: err },
            }),
          );
          this.#finish({
            code: -1,
            message: "轮询请求异常",
            data: err,
          });
        }
      }, this.interval);

      return data.url;
    }

    /**
     * 结束轮询并触发 end 事件
     * @param {any} detail
     */
    #finish(detail) {
      clearInterval(this.#timer);
      this.#timer = null;
      this.dispatchEvent(new CustomEvent("end", { detail }));
    }

    /**
     * 中断登录流程
     */
    interrupt() {
      if (this.#timer !== null) {
        clearInterval(this.#timer);
        this.#timer = null;
        this.dispatchEvent(
          new CustomEvent("end", {
            detail: { code: -1, message: "用户中断", data: null },
          }),
        );
      }
    }
  }

  return BilibiliAppAuth;
}