--[[ 阿越云验证 - Lua SDK 支持:卡密登录、心跳保活、公告获取、版本检查(含强制更新)、 卡密解绑、获取应用配置、应用账号登录 适用环境: - 安卓 GameGuardian / 触动精灵 / Auto.js 等 Lua 环境 - 标准 Lua 5.1+ (需自备 http / md5 / json 实现,见文件末端适配说明) @version 1.0.0 ]] local AyueVerify = {} AyueVerify.__index = AyueVerify -- ==================== 构造函数 ==================== --- 创建客户端 -- @param opts table {base_url, app_id, app_key, rc4_key, mi_type, sign, timeout} function AyueVerify.new(opts) opts = opts or {} local self = setmetatable({}, AyueVerify) local base = tostring(opts.base_url or "") base = base:gsub("/+$", "") self.baseUrl = base .. "/" self.appId = tonumber(opts.app_id) or 0 self.appKey = tostring(opts.app_key or "") self.rc4Key = tostring(opts.rc4_key or "") self.miType = tonumber(opts.mi_type) or 0 self.miTime = tonumber(opts.mi_time) or 10 self.signEnabled= opts.sign and true or false self.timeout = tonumber(opts.timeout) or 10000 self.lastError = "" self.lastCode = 0 return self end -- ==================== 业务接口 ==================== --- 卡密登录 -- @return table {ok, code, msg, data, expires_ts} function AyueVerify:login(kami, markcode) local r = self:_request("kmlogon", {kami = kami, markcode = markcode}) return self:_normalize(r, true) end --- 心跳保活 function AyueVerify:heartbeat(kami, markcode) local r = self:_request("heartbeat", {kami = kami, markcode = markcode}) return self:_normalize(r, true) end --- 退出心跳 function AyueVerify:heartbeatQuit(kami) return self:heartbeat(kami, "quit") end --- 获取公告 -- @return string 公告内容 function AyueVerify:notice() local r = self:_request("notice", {}) if tonumber(r.code) == 200 and type(r.msg) == "table" then return tostring(r.msg.app_gg or "") end return "" end --- 获取应用配置 -- @return table {ok, version, version_info, update_url, update_show, must_update} function AyueVerify:config(withPay) local extra = {} if withPay then extra.pay = 1 end local r = self:_request("ini", extra) local ok = (tonumber(r.code) == 200) local d = (ok and type(r.msg) == "table") and r.msg or {} return { ok = ok, code = self.lastCode, msg = self.lastError, version = tostring(d.version or ""), version_info = tostring(d.version_info or ""), update_url = tostring(d.app_update_url or ""), update_show = tostring(d.app_update_show or ""), must_update = (tostring(d.app_update_must or "n") == "y"), pay = d.pay, } end --- 检查更新 -- @param currentVersion 当前客户端版本 -- @return table {ok, has_update, must_update, latest, url, desc} function AyueVerify:checkUpdate(currentVersion) local cfg = self:config() if not cfg.ok then return {ok = false, has_update = false, must_update = false, latest = "", url = "", desc = "", msg = cfg.msg} end local hasUpdate = AyueVerify.versionCompare(cfg.version, currentVersion) > 0 return { ok = true, has_update = hasUpdate, must_update = (hasUpdate and cfg.must_update) or false, latest = cfg.version, url = cfg.update_url, desc = (cfg.update_show ~= "") and cfg.update_show or cfg.version_info, msg = "", } end --- 卡密解绑 function AyueVerify:unbind(kami, markcode) local r = self:_request("kmunmachine", {kami = kami, markcode = markcode}) return self:_normalize(r, false) end --- 获取外链文件 function AyueVerify:getFiles(fileId) local p = {} if tonumber(fileId) and tonumber(fileId) > 0 then p.id = tonumber(fileId) end local r = self:_request("getfile", p) local ok = (tonumber(r.code) == 200) return { ok = ok, files = (ok and type(r.msg) == "table") and r.msg or {}, msg = self.lastError, code = self.lastCode, } end --- 应用账号登录 function AyueVerify:userLogin(user, password) local r = self:_request("userlogon", {user = user, password = password}) return self:_normalize(r, false) end --- 应用账号注册 function AyueVerify:userRegister(user, password) local r = self:_request("userreg", {user = user, password = password}) return self:_normalize(r, false) end --- 应用账号心跳 function AyueVerify:userHeartbeat(user, markcode) local r = self:_request("userheartbeat", {user = user, markcode = markcode}) return self:_normalize(r, false) end -- ==================== 底层通信 ==================== function AyueVerify:_request(api, fields) self.lastError = "" self.lastCode = 0 local value = self:_randomStr(16) -- 复制一份,避免污染调用方表 local f = {} for k, v in pairs(fields or {}) do f[k] = v end f.value = value -- 关键:服务端 mi_time > 0 时强制要求 t 参数,缺失返回 108 if self.miTime > 0 and f.t == nil then f.t = os.time() end local url = self.baseUrl .. "api.php?api=" .. self:_urlEncode(api) .. "&app=" .. tostring(self.appId) local post = {} if self.miType == 0 then for k, v in pairs(f) do post[k] = v end else local plain = self:_buildQuery(f) post.data = self:_encrypt(plain) post.value = value end if self.signEnabled then local src = {} for k, v in pairs(f) do src[k] = v end if self.miType ~= 0 then src.data = post.data end post.sign = self:_makeSign(src) end local raw = self:_httpPost(url, post) if not raw or raw == "" then self.lastError = "网络请求失败" self.lastCode = -1 return {code = -1, msg = self.lastError} end -- 去除 UTF-8 BOM 与首尾空白 raw = tostring(raw):gsub("^\239\187\191", ""):match("^%s*(.-)%s*$") or raw local json = self:_jsonDecode(raw) if type(json) ~= "table" then self.lastError = "返回格式错误: " .. tostring(raw):sub(1, 120) self.lastCode = -1 return {code = -1, msg = self.lastError} end self.lastCode = tonumber(json.code) or -1 if self.lastCode ~= 200 then self.lastError = (type(json.msg) == "string") and json.msg or "" end return json end function AyueVerify:_normalize(r, wantExpires) local code = tonumber(r.code) or -1 local ok = (code == 200) local out = { ok = ok, code = code, msg = r.msg, data = (ok and type(r.msg) == "table") and r.msg or {}, } if wantExpires and ok and type(r.msg) == "table" and r.msg.expires_ts then out.expires_ts = tonumber(r.msg.expires_ts) or 0 end return out end function AyueVerify:_buildQuery(fields) local parts = {} for k, v in pairs(fields) do parts[#parts + 1] = tostring(k) .. "=" .. tostring(v) end return table.concat(parts, "&") end function AyueVerify:_makeSign(fields) local ignore = { sign = true, app = true, api = true, value = true, PHPSESSID = true, sec_defend = true, ["sidenav-state"] = true } local s = "" for k, v in pairs(fields) do if not ignore[k] then s = s .. tostring(k) .. "=" .. tostring(v) .. "&" end end s = s .. self.appKey return self:_md5(s) end function AyueVerify:_encrypt(plain) if self.miType == 1 then -- 关键:服务端 mi_rc4 会先做 UTF-8 -> GBK 转换。 -- Lua 无内置 GBK 支持,此处尝试使用环境提供的转换函数; -- 若环境不支持,则要求参数为纯 ASCII(卡密/设备码通常都是 ASCII)。 local d = self:_toGbk(plain) local k = self:_toGbk(self.rc4Key) return self:_rc4Hex(d, k) end return self:_base64Encode(plain) end --- UTF-8 转 GBK(尽力而为) function AyueVerify:_toGbk(s) if not s or s == "" then return s end -- 纯 ASCII 无需转换 local ascii = true for i = 1, #s do if s:byte(i) > 127 then ascii = false; break end end if ascii then return s end -- 环境提供时才转换 if type(gg) == "table" and gg.utf8ToGbk then local ok, r = pcall(gg.utf8ToGbk, s) if ok and r then return r end end if type(string.utf8ToGbk) == "function" then return string.utf8ToGbk(s) end -- 无法转换:原样返回(中文参数可能乱码,建议避免在参数中使用中文) return s end function AyueVerify:_rc4Hex(data, key) if not key or key == "" then return "" end local s = {} for i = 0, 255 do s[i] = i end local j = 0 local kl = #key for i = 0, 255 do j = (j + s[i] + string.byte(key, (i % kl) + 1)) % 256 s[i], s[j] = s[j], s[i] end local x, y = 0, 0 local out = {} for n = 1, #data do x = (x + 1) % 256 y = (y + s[x]) % 256 s[x], s[y] = s[y], s[x] local c = string.byte(data, n) out[#out + 1] = string.format("%02x", self:_bxor(c, s[(s[x] + s[y]) % 256])) end return table.concat(out) end function AyueVerify:_randomStr(n) local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" local t = {} for i = 1, n do local idx = math.random(1, #chars) t[#t + 1] = chars:sub(idx, idx) end return table.concat(t) end -- ==================== 环境适配层 ==================== -- 以下方法需要根据你的 Lua 运行环境替换实现。 -- 默认实现提供了常见环境(GameGuardian / 触动精灵 / 标准 Lua+luasocket)的分支。 --- HTTP POST(返回响应体字符串) function AyueVerify:_httpPost(url, data) -- 1) GameGuardian 环境 if type(gg) == "table" and gg.makeRequest then local body = {} for k, v in pairs(data) do body[#body + 1] = self:_urlEncode(k) .. "=" .. self:_urlEncode(tostring(v)) end local ok, resp = pcall(function() return gg.makeRequest(url, { ["Content-Type"] = "application/x-www-form-urlencoded", ["User-Agent"] = "AyueVerify-Lua-SDK/1.0.0", }, table.concat(body, "&")) end) if ok and type(resp) == "table" then return resp.content end self.lastError = "GameGuardian 请求失败" return nil end -- 2) 触动精灵 / 叉叉助手 if type(http) == "table" and http.post then local body = {} for k, v in pairs(data) do body[#body + 1] = self:_urlEncode(k) .. "=" .. self:_urlEncode(tostring(v)) end local ok, code, resp = pcall(function() return http.post(url, table.concat(body, "&"), 10) end) if ok and resp then return resp end self.lastError = "http.post 请求失败" return nil end -- 3) 标准 Lua + luasocket local okHttp, socketHttp = pcall(require, "socket.http") if okHttp and socketHttp then local body = {} for k, v in pairs(data) do body[#body + 1] = self:_urlEncode(k) .. "=" .. self:_urlEncode(tostring(v)) end local ltn12 = require("ltn12") local resp = {} local ok, code = socketHttp.request({ url = url, method = "POST", headers = { ["Content-Type"] = "application/x-www-form-urlencoded", ["Content-Length"] = tostring(#table.concat(body, "&")), ["User-Agent"] = "AyueVerify-Lua-SDK/1.0.0", }, source = ltn12.source.string(table.concat(body, "&")), sink = ltn12.sink.table(resp), }) if ok and code == 200 then return table.concat(resp) end self.lastError = "luasocket 请求失败" return nil end self.lastError = "当前环境未提供 HTTP 实现,请重写 AyueVerify:_httpPost" return nil end --- MD5(需要环境提供) function AyueVerify:_md5(s) -- GameGuardian if type(gg) == "table" and gg.md5 then return string.lower(gg.md5(s)) end -- 触动精灵 if type(md5) == "function" then local ok, r = pcall(md5, s) if ok and r then return string.lower(r) end end -- LuaCrypto local okC, crypto = pcall(require, "crypto") if okC and crypto and crypto.digest then return string.lower(crypto.digest("md5", s)) end -- 回退:纯 Lua MD5(较慢,仅兼容用) return self:_md5PureLua(s) end --- 纯 Lua MD5 实现(兼容无扩展环境) function AyueVerify:_md5PureLua(str) local function band(a, b) local r, bit = 0, 1 for i = 1, 32 do if a % 2 == 1 and b % 2 == 1 then r = r + bit end a, b, bit = math.floor(a / 2), math.floor(b / 2), bit * 2 end return r end local function bor(a, b) local r, bit = 0, 1 for i = 1, 32 do if a % 2 == 1 or b % 2 == 1 then r = r + bit end a, b, bit = math.floor(a / 2), math.floor(b / 2), bit * 2 end return r end local function bxor(a, b) local r, bit = 0, 1 for i = 1, 32 do if (a % 2) ~= (b % 2) then r = r + bit end a, b, bit = math.floor(a / 2), math.floor(b / 2), bit * 2 end return r end local function bnot(a) return 4294967295 - a end local function lshift(a, n) return (a * (2 ^ n)) % 4294967296 end local function rshift(a, n) return math.floor(a / (2 ^ n)) end local function rol(a, n) return bor(lshift(a, n), rshift(a, 32 - n)) end local msg = str local len = #msg msg = msg .. string.char(0x80) while #msg % 64 ~= 56 do msg = msg .. string.char(0) end local bitLen = len * 8 for i = 1, 8 do msg = msg .. string.char(bitLen % 256) bitLen = math.floor(bitLen / 256) end local a0, b0, c0, d0 = 0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476 local S = {7,12,17,22,7,12,17,22,7,12,17,22,7,12,17,22, 5,9,14,20,5,9,14,20,5,9,14,20,5,9,14,20, 4,11,16,23,4,11,16,23,4,11,16,23,4,11,16,23, 6,10,15,21,6,10,15,21,6,10,15,21,6,10,15,21} local K = {} for i = 1, 64 do K[i] = math.floor(math.abs(math.sin(i)) * 4294967296) % 4294967296 end for chunk = 1, #msg, 64 do local M = {} for i = 0, 15 do local b1, b2, b3, b4 = string.byte(msg, chunk + i * 4, chunk + i * 4 + 3) M[i] = b1 + b2 * 256 + b3 * 65536 + b4 * 16777216 end local A, B, C, D = a0, b0, c0, d0 for i = 0, 63 do local F, g if i < 16 then F = bor(band(B, C), band(bnot(B), D)); g = i elseif i < 32 then F = bor(band(D, B), band(bnot(D), C)); g = (5 * i + 1) % 16 elseif i < 48 then F = bxor(bxor(B, C), D); g = (3 * i + 5) % 16 else F = bxor(C, bor(B, bnot(D))); g = (7 * i) % 16 end F = (F + A + K[i + 1] + M[g]) % 4294967296 A, B, C, D = D, (B + rol(F, S[i + 1])) % 4294967296, B, C end a0 = (a0 + A) % 4294967296 b0 = (b0 + B) % 4294967296 c0 = (c0 + C) % 4294967296 d0 = (d0 + D) % 4294967296 end local function hexLE(n) local t = {} for i = 1, 4 do t[#t + 1] = string.format("%02x", n % 256) n = math.floor(n / 256) end return table.concat(t) end return hexLE(a0) .. hexLE(b0) .. hexLE(c0) .. hexLE(d0) end --- 位异或(供 RC4 使用) function AyueVerify:_bxor(a, b) local r, bit = 0, 1 for i = 1, 8 do if (a % 2) ~= (b % 2) then r = r + bit end a, b, bit = math.floor(a / 2), math.floor(b / 2), bit * 2 end return r end --- URL 编码 function AyueVerify:_urlEncode(s) if type(s) ~= "string" then s = tostring(s) end return (s:gsub("([^%w%-%._~])", function(c) return string.format("%%%02X", string.byte(c)) end)) end --- Base64 编码 function AyueVerify:_base64Encode(s) local b = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" return ((s:gsub('.', function(x) local r, byte = '', x:byte() for i = 8, 1, -1 do r = r .. (byte % 2 ^ i - byte % 2 ^ (i - 1) > 0 and '1' or '0') end return r end) .. '0000'):gsub('%d%d%d?%d?%d?%d?', function(x) if #x < 6 then return '' end local c = 0 for i = 1, 6 do c = c + (x:sub(i, i) == '1' and 2 ^ (6 - i) or 0) end return b:sub(c + 1, c + 1) end) .. ({ '', '==', '=' })[#s % 3 + 1]) end --- JSON 解析(极简) function AyueVerify:_jsonDecode(str) local pos = 1 local function skipWs() while pos <= #str and str:sub(pos, pos):match("[ \t\r\n]") do pos = pos + 1 end end local parseValue local function parseString() pos = pos + 1 local out = {} while pos <= #str do local c = str:sub(pos, pos) if c == '"' then pos = pos + 1; break end if c == '\\' then local n = str:sub(pos + 1, pos + 1) if n == 'n' then out[#out + 1] = '\n' elseif n == 'r' then out[#out + 1] = '\r' elseif n == 't' then out[#out + 1] = '\t' elseif n == 'u' then local hex = str:sub(pos + 2, pos + 5) local code = tonumber(hex, 16) or 63 -- UTF-8 编码 if code < 0x80 then out[#out + 1] = string.char(code) elseif code < 0x800 then out[#out + 1] = string.char(192 + math.floor(code / 64), 128 + code % 64) else out[#out + 1] = string.char(224 + math.floor(code / 4096), 128 + math.floor(code / 64) % 64, 128 + code % 64) end pos = pos + 4 else out[#out + 1] = n end pos = pos + 2 else out[#out + 1] = c pos = pos + 1 end end return table.concat(out) end parseValue = function() skipWs() local c = str:sub(pos, pos) if c == '{' then pos = pos + 1 local obj = {} skipWs() if str:sub(pos, pos) == '}' then pos = pos + 1; return obj end while true do skipWs() if str:sub(pos, pos) ~= '"' then break end local k = parseString() skipWs() if str:sub(pos, pos) == ':' then pos = pos + 1 end obj[k] = parseValue() skipWs() local d = str:sub(pos, pos) if d == ',' then pos = pos + 1 elseif d == '}' then pos = pos + 1; break else break end end return obj elseif c == '[' then pos = pos + 1 local arr = {} skipWs() if str:sub(pos, pos) == ']' then pos = pos + 1; return arr end while true do arr[#arr + 1] = parseValue() skipWs() local d = str:sub(pos, pos) if d == ',' then pos = pos + 1 elseif d == ']' then pos = pos + 1; break else break end end return arr elseif c == '"' then return parseString() elseif str:sub(pos, pos + 3) == 'true' then pos = pos + 4; return true elseif str:sub(pos, pos + 4) == 'false' then pos = pos + 5; return false elseif str:sub(pos, pos + 3) == 'null' then pos = pos + 4; return nil else local num = str:match('^-?%d+%.?%d*[eE]?[-+]?%d*', pos) if num and #num > 0 then pos = pos + #num; return tonumber(num) end pos = pos + 1 return nil end end local ok, v = pcall(parseValue) if ok then return v end return nil end -- ==================== 静态工具 ==================== --- 版本比较:v1 > v2 返回 1 function AyueVerify.versionCompare(v1, v2) local function split(s) local t = {} for p in tostring(s or ""):gmatch("[^%.%-%+]+") do t[#t + 1] = p end return t end local a, b = split(v1), split(v2) local n = math.max(#a, #b) for i = 1, n do local x = a[i] or "0" local y = b[i] or "0" local xi, yi = tonumber(x), tonumber(y) if xi and yi then if xi > yi then return 1 end if xi < yi then return -1 end else if x > y then return 1 end if x < y then return -1 end end end return 0 end --- 生成设备码(首次运行应持久化到本地文件) function AyueVerify.generateMarkcode() local seed if type(gg) == "table" and gg.getTargetInfo then local ok, info = pcall(gg.getTargetInfo) if ok and info then seed = tostring(info.packageName) .. "|" .. tostring(os.time()) end end if not seed then seed = tostring(os.time()) .. "|" .. tostring(math.random(100000, 999999)) end local md5 = "0123456789abcdef" local h = 0 for i = 1, #seed do h = (h * 31 + seed:byte(i)) % 4294967296 end return string.format("%08X%08X%08X", h, h * 7 % 4294967296, h * 13 % 4294967296) end return AyueVerify