UNPKG

oicq

Version:
622 lines (592 loc) 19.8 kB
/** * login相关处理和api */ "use strict"; const fs = require("fs"); const path = require("path"); const Readable = require("stream").Readable; const tea = require("../algo/tea"); const jce = require("../algo/jce"); const pb = require("../algo/pb"); const ecdh = require("./ecdh"); const Writer = require("./writer"); const tlv = require("./tlv"); const { timestamp, md5, BUF16, NOOP } = require("../common"); /** * @this {import("../ref").Client} * @param {Buffer} body * @returns {Buffer} */ function buildOICQPacket(body, emp = false) { if (emp) { body = new Writer() .writeTlv(this.sig.sig_key) .writeBytes(tea.encrypt(body, this.sig.ticket_key)) .read(); } else { body = new Writer() .writeU8(0x02) .writeU8(0x01) .writeBytes(this.random_key) .writeU16(0x131) .writeU16(0x01) .writeTlv(ecdh.public_key) .writeBytes(tea.encrypt(body, ecdh.share_key)) .read(); } return new Writer() .writeU8(0x02) .writeU16(29 + body.length) // 1 + 27 + body.length + 1 .writeU16(8001) // protocol ver .writeU16(0x810) // command id .writeU16(1) // const .writeU32(this.uin) .writeU8(3) // const .writeU8(emp ? 69 : 0x87) // encrypt type 7:0 69:emp 0x87:4 .writeU8(0) // const .writeU32(2) // const .writeU32(0) // app client ver .writeU32(0) // const .writeBytes(body) .writeU8(0x03) .read(); } /** * @this {import("../ref").Client} * @param {string} cmd * @param {Buffer} body * @param {0|1|2} type * @returns {Buffer} */ function buildLoginPacket(cmd, body, type) { this.logger.trace(`send:${cmd} seq:${this.seq_id}`); let sso = new Writer().writeU32(this.seq_id) .writeU32(this.apk.subid) .writeU32(this.apk.subid) .writeBytes(Buffer.from([0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00])) // unknown .writeWithLength(this.sig.tgt) .writeWithLength(cmd) .writeWithLength(this.session_id) .writeWithLength(this.device.imei) .writeU32(4) .writeU16(this.ksid.length + 2) .writeBytes(this.ksid) .writeU32(4) .read(); sso = new Writer().writeWithLength(sso).writeWithLength(body).read(); if (type === 1) sso = tea.encrypt(sso, this.sig.d2key); else if (type === 2) sso = tea.encrypt(sso, BUF16); body = new Writer() .writeU32(0x0A) .writeU8(type) .writeWithLength(this.sig.d2) .writeU8(0) .writeWithLength(String(this.uin)) .writeBytes(sso) .read(); return new Writer().writeWithLength(body).read(); } //login req---------------------------------------------------------------------------------------- /** * @this {import("../ref").Client} */ async function passwordLogin() { this.logining = true; try { this.t106 = await fs.promises.readFile(path.join(this.dir, "t106")); const token = await fs.promises.readFile(path.join(this.dir, "token")); const d2key = token.slice(0, 16); const d2 = token.slice(16, 80); const ticket = token.slice(80, 96); const sig = token.slice(96, 144); const srm = token.slice(144, 200); const tgt = token.slice(200, 272); this.sig.device_token = token.slice(272); if (d2key.length && d2.length && ticket.length && sig.length && srm.length && tgt.length) { this.sig.ticket_key = ticket; this.sig.sig_key = sig; this.sig.srm_token = srm; this.sig.tgt = tgt; this.device.tgtgt = md5(d2key); return tokenLogin.call(this, d2); } } catch { } this.nextSeq(); const t = tlv.getPacker(this); let body = new Writer() .writeU16(9) .writeU16(24) .writeBytes(t(0x18)) .writeBytes(t(0x1)) .writeBytes(t(0x106)) .writeBytes(t(0x116)) .writeBytes(t(0x100)) .writeBytes(t(0x107)) .writeBytes(t(0x108)) .writeBytes(t(0x142)) .writeBytes(t(0x144)) .writeBytes(t(0x145)) .writeBytes(t(0x147)) .writeBytes(t(0x154)) .writeBytes(t(0x141)) .writeBytes(t(0x8)) .writeBytes(t(0x511)) .writeBytes(t(0x187)) .writeBytes(t(0x188)) .writeBytes(t(0x194)) .writeBytes(t(0x191)) .writeBytes(t(0x202)) .writeBytes(t(0x177)) .writeBytes(t(0x516)) .writeBytes(t(0x521)) .writeBytes(t(0x525)) .read(); const pkt = buildLoginPacket.call(this, "wtlogin.login", buildOICQPacket.call(this, body), 2); try { var blob = await this.send(pkt); decodeLoginResponse.call(this, blob); } catch (e) { this.logger.debug(e); return this.emit("internal.network", "服务器繁忙(password)"); } } /** * @this {import("../ref").Client} * @param {string} ticket */ async function sliderLogin(ticket) { this.logining = true; ticket = String(ticket).trim(); this.nextSeq(); const t = tlv.getPacker(this); const body = new Writer() .writeU16(2) .writeU16(4) .writeBytes(t(0x193, ticket)) .writeBytes(t(0x8)) .writeBytes(t(0x104)) .writeBytes(t(0x116)) .read(); const pkt = buildLoginPacket.call(this, "wtlogin.login", buildOICQPacket.call(this, body), 2); try { var blob = await this.send(pkt); decodeLoginResponse.call(this, blob); } catch (e) { this.logger.debug(e); return this.emit("internal.network", "服务器繁忙(slider)"); } } /** * @this {import("../ref").Client} */ async function deviceLogin() { this.nextSeq(); const t = tlv.getPacker(this); const body = new Writer() .writeU16(20) .writeU16(4) .writeBytes(t(0x8)) .writeBytes(t(0x104)) .writeBytes(t(0x116)) .writeBytes(t(0x401)) .read(); const pkt = buildLoginPacket.call(this, "wtlogin.login", buildOICQPacket.call(this, body), 2); try { var blob = await this.send(pkt); decodeLoginResponse.call(this, blob); } catch (e) { this.logger.debug(e); return this.emit("internal.network", "服务器繁忙(device)"); } } /** * @this {import("../ref").Client} */ async function sendSMS() { this.nextSeq(); const t = tlv.getPacker(this); const body = new Writer() .writeU16(8) .writeU16(6) .writeBytes(t(0x8)) .writeBytes(t(0x104)) .writeBytes(t(0x116)) .writeBytes(t(0x174)) .writeBytes(t(0x17a)) .writeBytes(t(0x197)) .read(); const pkt = buildLoginPacket.call(this, "wtlogin.login", buildOICQPacket.call(this, body), 2); try { await this.send(pkt); this.logger.mark(`已向手机 ${this.phone} 发送短信验证码,请查看并输入。`); } catch (e) { this.logger.debug(e); return this.emit("internal.network", "服务器繁忙(sms)"); } } /** * @this {import("../ref").Client} * @param {string} code */ async function smsLogin(code) { code = String(code).trim(); if (Buffer.byteLength(code) !== 6) code = "123456"; this.nextSeq(); const t = tlv.getPacker(this); const body = new Writer() .writeU16(7) .writeU16(7) .writeBytes(t(0x8)) .writeBytes(t(0x104)) .writeBytes(t(0x116)) .writeBytes(t(0x174)) .writeBytes(t(0x17c, code)) .writeBytes(t(0x401)) .writeBytes(t(0x198)) .read(); const pkt = buildLoginPacket.call(this, "wtlogin.login", buildOICQPacket.call(this, body), 2); try { var blob = await this.send(pkt); decodeLoginResponse.call(this, blob); } catch (e) { this.logger.debug(e); return this.emit("internal.network", "服务器繁忙(submit)"); } } /** * @this {import("../ref").Client} */ async function heartbeat() { this.nextSeq(); const pkt = buildLoginPacket.call(this, "Heartbeat.Alive", Buffer.alloc(0), 0); this.send(pkt).catch(NOOP); } /** * @this {import("../ref").Client} */ async function exchangeEMP() { if (!this.isOnline() || timestamp() - this.sig.emp_time < 14400) return; const t = tlv.getPacker(this); const body = new Writer() .writeU16(15) .writeU16(20) .writeBytes(t(0x18)) .writeBytes(t(0x1)) .writeU16(0x106) .writeU16(this.t106.length) .writeBytes(this.t106) .writeBytes(t(0x116)) .writeBytes(t(0x100, 1)) .writeBytes(t(0x107)) .writeBytes(t(0x144)) .writeBytes(t(0x142)) .writeBytes(t(0x145)) .writeBytes(t(0x16a)) // .writeBytes(t(0x154)) .writeBytes(t(0x141)) .writeBytes(t(0x8)) .writeBytes(t(0x511)) .writeBytes(t(0x147)) .writeBytes(t(0x177)) .writeBytes(t(0x187)) .writeBytes(t(0x188)) .writeBytes(t(0x194)) .writeBytes(t(0x202)) .writeBytes(t(0x516)) .read(); const pkt = buildOICQPacket.call(this, body, true); try { let blob = await this.sendUni("wtlogin.exchange_emp", pkt); blob = tea.decrypt(blob.slice(16, blob.length - 1), this.sig.ticket_key); const stream = Readable.from(blob, { objectMode: false }); stream.read(5); const t = readTlv(stream, 2); if (t[0x119]) { decodeT119.call(this, t[0x119]); } else { fs.unlink(path.join(this.dir, "token"), NOOP); this.sig.emp_time = 0xffffffff; this.logger.warn("刷新cookies失败,可能是由于你切换过登录协议所导致。如果你需要使用依赖cookies的功能建议立即重新登录。"); } } catch (e) { this.logger.warn("刷新cookies失败。"); this.logger.debug(e); } } /** * @this {import("../ref").Client} */ async function tokenLogin(d2) { this.nextSeq(); const t = tlv.getPacker(this); const body = new Writer() .writeU16(11) .writeU16(16) .writeBytes(t(0x100)) .writeBytes(t(0x10a)) .writeBytes(t(0x116)) .writeBytes(t(0x144)) .writeBytes(t(0x143, d2)) .writeBytes(t(0x142)) .writeBytes(t(0x154)) .writeBytes(t(0x18)) .writeBytes(t(0x141)) .writeBytes(t(0x8)) .writeBytes(t(0x147)) .writeBytes(t(0x177)) .writeBytes(t(0x187)) .writeBytes(t(0x188)) .writeBytes(t(0x202)) .writeBytes(t(0x511)) .read(); const pkt = buildLoginPacket.call(this, "wtlogin.exchange_emp", buildOICQPacket.call(this, body), 2); try { let blob = await this.send(pkt); decodeLoginResponse.call(this, blob, true); } catch (e) { await fs.promises.unlink(path.join(this.dir, "token")); this.logger.debug(e); return this.emit("internal.network", "服务器繁忙(token)"); } } //---------------------------------------------------------------------------------------------- /** * @this {import("../ref").Client} */ async function register(logout = false) { this.nextSeq(); const pb_buf = pb.encode({ 1: [{ 1: 46, 2: timestamp() }, { 1: 283, 2: 0 }] }); const SvcReqRegister = jce.encodeStruct([ this.uin, logout ? 0 : 7, 0, "", logout ? 21 : 11, 0, 0, 0, 0, 0, logout ? 44 : 0, this.device.version.sdk, 1, "", 0, null, this.device.guid, 2052, 0, this.device.model, this.device.model, this.device.version.release, 1, 0, 0, null, 0, 0, "", 0, this.device.brand, this.device.brand, "", pb_buf, 0, null, 0, null, 1000, 98 ]); const extra = { service: "PushService", method: "SvcReqRegister", }; const body = jce.encodeWrapper({ SvcReqRegister }, extra); const pkt = buildLoginPacket.call(this, "StatSvc.register", body, 1); try { const blob = await this.send(pkt); const rsp = jce.decode(blob); const result = rsp[9] ? true : false; if (!result && !logout) fs.unlink(path.join(this.dir, "token"), NOOP); return result; } catch { return false; } } //decode tlv---------------------------------------------------------------------------------------------- /** * @param {Readable} stream * @param {number} size * @returns {{[k: number]: Buffer}} */ function readTlv(stream, size) { const t = {}; var k; while (true) { if (stream.readableLength < size) break; if (size === 1) k = stream.read(1).readUInt8(); else if (size === 2) k = stream.read(2).readUInt16BE(); else if (size === 4) k = stream.read(4).readInt32BE(); if (k === 255) break; t[k] = stream.read(stream.read(2).readUInt16BE()); } return t; } /** * @this {import("../ref").Client} * @param {Buffer} data */ function decodeT119(data, token = false) { const reader = Readable.from(tea.decrypt(data, this.device.tgtgt), { objectMode: false }); reader.read(2); const t = readTlv(reader, 2); readT106.call(this, token ? this.t106 : t[0x106]); readT11A.call(this, t[0x11a]); readT512.call(this, t[0x512]); this.sig = { srm_token: t[0x16a] ? t[0x16a] : this.sig.srm_token, tgt: t[0x10a] ? t[0x10a] : this.sig.tgt, tgt_key: t[0x10d] ? t[0x10d] : this.sig.tgt_key, st_key: t[0x10e] ? t[0x10e] : this.sig.st_key, st_web_sig: t[0x103] ? t[0x103] : this.sig.st_web_sig, skey: t[0x120] ? t[0x120] : this.sig.skey, d2: t[0x143] ? t[0x143] : this.sig.d2, d2key: t[0x305] ? t[0x305] : this.sig.d2key, sig_key: t[0x133] ? t[0x133] : this.sig.sig_key, ticket_key: t[0x134] ? t[0x134] : this.sig.ticket_key, device_token: t[0x322] ? t[0x322] : this.sig.device_token, emp_time: token ? 0 : timestamp(), }; fs.writeFile( path.join(this.dir, "token"), Buffer.concat([ this.sig.d2key, this.sig.d2, this.sig.ticket_key, this.sig.sig_key, this.sig.srm_token, this.sig.tgt, this.sig.device_token, ]), { mode: 0o600 }, NOOP ); } function readT106(data) { if (!data) return; this.t106 = data; fs.writeFile( path.join(this.dir, "t106"), data, { mode: 0o600 }, NOOP ); const buf = Buffer.alloc(4); buf.writeUInt32BE(this.uin); const key = md5(Buffer.concat([ this.password_md5, Buffer.alloc(4), buf ])); data = tea.decrypt(Buffer.concat([data]), key); this.device.tgtgt = data.slice(51, 67); } function readT11A(data) { if (!data) return; const stream = Readable.from(data, { objectMode: false }); stream.read(2); this.age = stream.read(1).readUInt8(); this.sex = ["unknown", "male", "female"][stream.read(1).readUInt8()]; this.nickname = stream.read(stream.read(1).readUInt8() & 0xff); this.nickname = this.nickname ? String(this.nickname) : ""; } function readT512(data) { if (!data) return; const stream = Readable.from(data, { objectMode: false }); let len = stream.read(2).readUInt16BE(); while (len-- > 0) { const domain = String(stream.read(stream.read(2).readUInt16BE())); const pskey = stream.read(stream.read(2).readUInt16BE()); const pt4token = stream.read(stream.read(2).readUInt16BE()); this.cookies[domain] = pskey; } } //login rsp---------------------------------------------------------------------------------------------- /** * 0 success * 1 wrong password * 2 captcha * 3 ?? * 6,8,9 其他错误 * 7 安全风险 * 15,16 你的用户身份已失效,为保证帐号安全,请你重新登录。 * 40 frozen * 139 ?????? * 160 短信验证解锁设备 * 162 短信验证失败 * 163 短信验证码输入错误 * 167 请使用QQ一键登录 * 192 ?????? * 204 need unlock device * 235 当前版本过低 * 237 环境异常 * 239 异地登陆短信验证 * * @this {import("../ref").Client} */ function decodeLoginResponse(blob, token = false) { blob = tea.decrypt(blob.slice(16, blob.length - 1), ecdh.share_key); const stream = Readable.from(blob, { objectMode: false }); stream.read(2); const type = stream.read(1).readUInt8(); stream.read(2); const t = readTlv(stream, 2); if (type === 204) { this.t104 = t[0x104]; this.t402 = t[0x402]; this.t403 = t[0x403]; this.logger.mark("login..."); return deviceLogin.call(this); } this.logining = false; if (type === 0) { this.t104 = undefined; this.t174 = undefined; this.phone = undefined; decodeT119.call(this, t[0x119], token); return this.emit("internal.login"); } if (token) { this.logining = true; this.logger.mark("token失效,重新login.."); return fs.unlink(path.join(this.dir, "token"), passwordLogin.bind(this)); } if (type === 2) { this.t104 = t[0x104]; if (t[0x192]) { const url = String(t[0x192]); this.logger.mark(`收到滑动验证码,请访问以下地址完成滑动,并从网络响应中取出ticket输入:${url}`); return this.em("system.login.slider", { url }); } const message = "[登陆失败]未知格式的验证码。"; this.logger.error(message); return this.em("system.login.error", { code: 2, message }); } if (type === 160) { const url = String(t[0x204]).replace("verify", "qrcode"); this.logger.mark("需要扫码完成设备锁验证,验证地址:" + url); let phone; if (t[0x174] && t[0x178]) { this.t104 = t[0x104]; this.t174 = t[0x174]; phone = String(t[0x178]).substr(t[0x178].indexOf("\x0b") + 1, 11); this.phone = phone; } this.em("system.login.device", { url, phone }); return; } if (t[0x149]) { const stream = Readable.from(t[0x149], { objectMode: false }); stream.read(2); const title = stream.read(stream.read(2).readUInt16BE()).toString(); const content = stream.read(stream.read(2).readUInt16BE()).toString(); const message = `[${title}]${content}`; this.logger.error(message + "(错误码:" + type + ")"); return this.em("system.login.error", { code: type, message }); } if (t[0x146]) { const stream = Readable.from(t[0x146], { objectMode: false }); const version = stream.read(4); const title = stream.read(stream.read(2).readUInt16BE()).toString(); const content = stream.read(stream.read(2).readUInt16BE()).toString(); const message = `[${title}]${content}`; this.logger.error(message + "(错误码:" + type + ")"); return this.em("system.login.error", { code: type, message }); } this.logger.error("[登陆失败]未知错误,错误码:" + type); this.em("system.login.error", { code: type, message: "[登陆失败]未知错误。" }); } module.exports = { passwordLogin, sliderLogin, heartbeat, register, exchangeEMP, sendSMS, smsLogin };