oicq-icalingua-plus-plus 1.26.24 → 1.26.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -57,9 +57,14 @@ export interface ConfBot {
57
57
  /** 使用NT上线包,部分消息暂未支持 */
58
58
  useNT?: boolean,
59
59
 
60
+ /** 强制使用旧版WT登录,即使当前APK支持NT登录 */
61
+ forceWt?: boolean,
62
+
60
63
  /** 指定APK信息,覆盖默认的协议参数 */
61
64
  apk_info?: {
62
65
  id?: string,
66
+ nt?: boolean,
67
+ platform?: string,
63
68
  name?: string,
64
69
  version?: string,
65
70
  ver?: string,
@@ -517,6 +522,24 @@ export interface DeviceEventData extends CommonSystemEventData {
517
522
  url: string, //验证地址
518
523
  phone: string, //密保手机
519
524
  }
525
+ export interface LoginAuthDevice {
526
+ guid: string,
527
+ qimei: string,
528
+ qimei36: string,
529
+ subappid: string,
530
+ platform: string,
531
+ brand: string,
532
+ model: string,
533
+ bssid: string,
534
+ devInfo: string,
535
+ sysVersion: string,
536
+ }
537
+ export interface LoginAuthEventData extends CommonSystemEventData {
538
+ system_type: "login",
539
+ sub_type: "auth", //登录频繁时的身份验证
540
+ url: string, //验证地址
541
+ device: LoginAuthDevice,
542
+ }
520
543
  export interface LoginErrorEventData extends CommonSystemEventData {
521
544
  system_type: "login",
522
545
  sub_type: "error", //登录遇到错误
@@ -805,8 +828,8 @@ export type GroupNoticeEventData = GroupRecallEventData | GroupSettingEventData
805
828
  MemberIncreaseEventData | MemberDecreaseEventData | GroupPokeEventData | GroupSignEventData |
806
829
  GroupEssenceEventData; //11
807
830
 
808
- export type SystemEventData = DeviceEventData | SliderEventData | LoginErrorEventData | QrcodeEventData |
809
- OfflineEventData | OnlineEventData; //6(4+2)
831
+ export type SystemEventData = DeviceEventData | LoginAuthEventData | SliderEventData | LoginErrorEventData | QrcodeEventData |
832
+ OfflineEventData | OnlineEventData; //7(5+2)
810
833
  export type RequestEventData = FriendAddEventData | GroupAddEventData | GroupInviteEventData; //3
811
834
  export type MessageEventData = PrivateMessageEventData | GroupMessageEventData | DiscussMessageEventData; //3
812
835
  export type NoticeEventData = FriendNoticeEventData | GroupNoticeEventData; //2
@@ -921,9 +944,11 @@ export interface EventMap {
921
944
  "system.login.slider": (this: Client, data: SliderEventData) => void;
922
945
  /**设备锁验证事件 */
923
946
  "system.login.device": (this: Client, data: DeviceEventData) => void;
947
+ /**登录频繁时的身份验证事件 */
948
+ "system.login.auth": (this: Client, data: LoginAuthEventData) => void;
924
949
  /**登录遇到错误事件 */
925
950
  "system.login.error": (this: Client, data: LoginErrorEventData) => void;
926
- "system.login": (this: Client, data: DeviceEventData | LoginErrorEventData | SliderEventData | QrcodeEventData) => void;
951
+ "system.login": (this: Client, data: DeviceEventData | LoginAuthEventData | LoginErrorEventData | SliderEventData | QrcodeEventData) => void;
927
952
  /**上线事件 */
928
953
  "system.online": (this: Client, data: OnlineEventData) => void;
929
954
 
package/lib/client.js CHANGED
@@ -128,6 +128,28 @@ class Client extends EventEmitter {
128
128
  "SQQzoneSvc.shuoshuo",
129
129
  "trpc.group_pro.msgproxy.sendmsg",
130
130
  "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLoginUnusualDevice",
131
+ "trpc.login.ecdh.EcdhService.SsoQRLoginGenQr",
132
+ "trpc.login.ecdh.EcdhService.SsoNTLoginPasswordLogin",
133
+ "trpc.login.ecdh.EcdhService.SsoNTLoginGetSaltList",
134
+ "trpc.login.ecdh.EcdhService.SsoNTLoginCheckA1List",
135
+ "trpc.login.ecdh.EcdhService.SsoNTLoginAuthNewDevice",
136
+ "trpc.login.ecdh.EcdhService.SsoNTLoginRefreshTicket",
137
+ "trpc.login.ecdh.EcdhService.SsoNTLoginOptimusLogin",
138
+ "trpc.login.ecdh.EcdhService.SsoNTLoginCheckGateWayCode",
139
+ "trpc.login.ecdh.EcdhService.SsoNTLoginCheckThirdCode",
140
+ "trpc.login.ecdh.EcdhService.SsoNTLoginGetSms",
141
+ "trpc.login.ecdh.EcdhService.SsoNTLoginCheckSms",
142
+ "trpc.login.ecdh.EcdhService.SsoOIDB0x916a",
143
+ "trpc.login.ecdh.EcdhService.SsoOIDB0x916b",
144
+ "trpc.login.ecdh.EcdhService.SsoOIDB0x916c",
145
+ "trpc.login.ecdh.EcdhService.SsoOIDB0x916d",
146
+ "trpc.login.ecdh.EcdhService.SsoNTLoginRapidLogin",
147
+ "trpc.login.ecdh.EcdhService.SsoNTLoginRefreshA2",
148
+ "trpc.login.ecdh.EcdhService.SsoKeyExchange",
149
+ "trpc.login.ecdh.EcdhService.SsoQRLoginAuthQr",
150
+ "trpc.login.ecdh.EcdhService.SsoQRLoginRejectQr",
151
+ "trpc.login.ecdh.EcdhService.SsoQRLoginCancleQr",
152
+ "trpc.login.ecdh.EcdhService.SsoNTLoginEasyLogin",
131
153
  "trpc.o3.ecdh_access.EcdhAccess.SsoEstablishShareKey",
132
154
  "trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Access",
133
155
  "trpc.o3.ecdh_access.EcdhAccess.SsoSecureA2Establish",
@@ -264,6 +286,7 @@ class Client extends EventEmitter {
264
286
  ffmpeg_path: "ffmpeg",
265
287
  ffprobe_path: "ffprobe",
266
288
  apk_info: {},
289
+ forceWt: false,
267
290
  ...config
268
291
  };
269
292
  this.config = config;
package/lib/device.js CHANGED
@@ -142,6 +142,8 @@ function getDeviceInfo(filepath, uin) {
142
142
 
143
143
  const DEFAULT_ANDROID_APK_INFO = {//android phone
144
144
  id: "com.tencent.mobileqq",
145
+ nt: false,
146
+ platform: "Android",
145
147
  name: "A8.9.50.f5a7d351",
146
148
  version: "8.9.50.10650",
147
149
  ver: "8.9.50",
@@ -168,6 +170,8 @@ const apk = {
168
170
  //android watch
169
171
  3: {
170
172
  id: "com.tencent.qqlite",
173
+ nt: false,
174
+ platform: "Android",
171
175
  name: "A9.0.1",
172
176
  version: "9.0.1",
173
177
  ver: "9.0.1",
@@ -185,6 +189,8 @@ const apk = {
185
189
  //mac
186
190
  4: {
187
191
  id: "com.tencent.qq",
192
+ nt: false,
193
+ platform: "Mac",
188
194
  name: "A6.8.2.21241",
189
195
  version: "6.8.2.21241",
190
196
  ver: "6.8.2",
@@ -226,6 +232,8 @@ const apk = {
226
232
  //TIM 3.5.1
227
233
  10: {
228
234
  id: "com.tencent.tim",
235
+ nt: false,
236
+ platform: "Android",
229
237
  name: "A3.5.1.db08e878",
230
238
  version: "3.5.1.3168",
231
239
  ver: "3.5.1",
@@ -568,6 +576,7 @@ const apk = {
568
576
  //android phone 9.2.80
569
577
  61: {
570
578
  ...DEFAULT_ANDROID_APK_INFO,
579
+ nt: true,
571
580
  name: "A9.2.80.90316290",
572
581
  version: "9.2.80.35125",
573
582
  ver: "9.2.80",
@@ -697,6 +706,7 @@ apk[5].subid = 537155074;
697
706
  apk[5].sign = apk[4].sign;
698
707
  apk[5].name = "A8.9.50.611";
699
708
  apk[5].version = "8.9.50.611";
709
+ apk[5].platform = "iPad";
700
710
 
701
711
  //ipad 8.9.33
702
712
  apk[9] = { ...apk[7] };
@@ -704,9 +714,12 @@ apk[9].subid = 537151363;
704
714
  apk[9].sign = apk[4].sign;
705
715
  apk[9].name = "A8.9.33.614";
706
716
  apk[9].version = "8.9.33.614";
717
+ apk[9].platform = "iPad";
707
718
 
708
719
  apk[30001] = {
709
720
  id: "com.tencent.qqlite",
721
+ nt: false,
722
+ platform: "Android",
710
723
  name: "A2.0.8",
711
724
  version: "2.0.8",
712
725
  ver: "2.0.8",
@@ -722,6 +735,8 @@ apk[30001] = {
722
735
 
723
736
  apk[30002] = {
724
737
  id: "com.tencent.qqlite",
738
+ nt: false,
739
+ platform: "Android",
725
740
  name: "A2.0.5",
726
741
  version: "2.0.5",
727
742
  ver: "2.0.5",
@@ -737,6 +752,8 @@ apk[30002] = {
737
752
 
738
753
  apk[30003] = {
739
754
  id: "com.tencent.qqlite",
755
+ nt: false,
756
+ platform: "Android",
740
757
  name: "A9.0.3",
741
758
  version: "9.0.3",
742
759
  ver: "9.0.3",
@@ -411,6 +411,21 @@ const map = {
411
411
  490: "/能送我吗",
412
412
  491: "/自信学霸",
413
413
  492: "/让我细品",
414
+ 493: "/又是早八",
415
+ 494: "/举杯邀月",
416
+ 495: "/兔来",
417
+ 496: "/阴晴圆缺",
418
+ 497: "/休假了",
419
+ 498: "/中!",
420
+ 499: "/观月C位",
421
+ 500: "/秋秋赏月",
422
+ 501: "/秋秋之约",
423
+ 502: "/秋秋泛舟",
424
+ 503: "/秋愿达成",
425
+ 504: "/同你玩",
426
+ 505: "/送花花",
427
+ 506: "/捡到宝了",
428
+ 507: "/被发现了",
414
429
  };
415
430
 
416
431
  const FACE_OLD_BUF = Buffer.from([0x00, 0x01, 0x00, 0x04, 0x52, 0xCC, 0xF5, 0xD0]);
@@ -0,0 +1,19 @@
1
+ "use strict";
2
+ const { createECDH } = require("crypto");
3
+
4
+ const NTQQ_PUBLIC_KEY = Buffer.from("049D1423332735980EDABE7E9EA451B3395B6F35250DB8FC56F25889F628CBAE3E8E73077914071EEEBC108F4E0170057792BB17AA303AF652313D17C1AC815E79", "hex");
5
+
6
+ class Ecdh {
7
+ constructor() {
8
+ const ecdh = createECDH("prime256v1");
9
+ this.public_key = ecdh.generateKeys();
10
+ this.nt_share_key = ecdh.computeSecret(NTQQ_PUBLIC_KEY);
11
+ this._ecdh = ecdh;
12
+ }
13
+
14
+ ntExchange(bobPublic) {
15
+ return this._ecdh.computeSecret(bobPublic);
16
+ }
17
+ }
18
+
19
+ module.exports = Ecdh;
@@ -0,0 +1,691 @@
1
+ "use strict";
2
+
3
+ const {
4
+ randomBytes,
5
+ createCipheriv,
6
+ createDecipheriv,
7
+ createHash,
8
+ } = require("crypto");
9
+ const { WtLogin } = require("../wtlogin/wt");
10
+ const Writer = require("../wtlogin/writer");
11
+ const Ecdh = require("./ecdh");
12
+ const pb = require("../algo/pb");
13
+ const tea = require("../algo/tea");
14
+ const { md5, timestamp, BUF0, BUF4 } = require("../common");
15
+ const { TimeoutError } = require("../exception");
16
+
17
+ const NT_LOGIN_CMD_PREFIX = "trpc.login.ecdh.EcdhService.";
18
+ const NT_LOGIN_COMMANDS = Object.freeze({
19
+ KEY_EXCHANGE: NT_LOGIN_CMD_PREFIX + "SsoKeyExchange",
20
+ PASSWORD_LOGIN: NT_LOGIN_CMD_PREFIX + "SsoNTLoginPasswordLogin",
21
+ PASSWORD_LOGIN_UNUSUAL_DEVICE: NT_LOGIN_CMD_PREFIX + "SsoNTLoginPasswordLoginUnusualDevice",
22
+ EASY_LOGIN: NT_LOGIN_CMD_PREFIX + "SsoNTLoginEasyLogin",
23
+ REFRESH_TICKET: NT_LOGIN_CMD_PREFIX + "SsoNTLoginRefreshTicket",
24
+ REFRESH_A2: NT_LOGIN_CMD_PREFIX + "SsoNTLoginRefreshA2",
25
+ });
26
+
27
+ const NT_LOGIN_PROOF_KEY = Buffer.from(
28
+ "e2733bf403149913cbf80c7a95168bd4ca6935ee53cd39764beebe2e007e3aee",
29
+ "hex",
30
+ );
31
+
32
+ const NTLoginErrorCode = {
33
+ AccountNotUin: 140022018,
34
+ AccountOrPasswordError: 140022013,
35
+ BlackAccount: 150022021,
36
+ CookieExpired: 150022039,
37
+ CookieUsedExceedUpperLimit: 150022040,
38
+ Default: 140022000,
39
+ ExpireTicket: 140022014,
40
+ Frozen: 140022005,
41
+ IllegalTicket: 140022016,
42
+ IllegalAccount: 150022030,
43
+ InterfaceUnavailable: 150022031,
44
+ InvalidCookie: 140022012,
45
+ InvalidParameter: 140022001,
46
+ KickedTicket: 140022015,
47
+ NeedUpdate: 140022004,
48
+ NewDevice: 140022010,
49
+ ProofWater: 140022008,
50
+ Strict: 140022007,
51
+ Success: 0,
52
+ UnusualDevice: 140022011,
53
+ };
54
+
55
+ const NT_LOGIN_PLATFORM_ID = {
56
+ Android: 2,
57
+ Mac: 5,
58
+ iPad: 6,
59
+ };
60
+
61
+ /**
62
+ * @param {any} value
63
+ * @returns {Buffer}
64
+ */
65
+ function toBuffer(value) {
66
+ if (Buffer.isBuffer(value)) return value;
67
+ if (value instanceof Uint8Array) return Buffer.from(value);
68
+ if (value && typeof value.toBuffer === "function") {
69
+ const buffer = value.toBuffer();
70
+ return Buffer.isBuffer(buffer) ? buffer : Buffer.from(buffer);
71
+ }
72
+ return BUF0;
73
+ }
74
+
75
+ /**
76
+ * @param {any} value
77
+ * @returns {string}
78
+ */
79
+ function toString(value) {
80
+ return value === undefined || value === null ? "" : String(value);
81
+ }
82
+
83
+ /**
84
+ * @param {any} value
85
+ * @returns {number}
86
+ */
87
+ function toNumber(value) {
88
+ if (typeof value === "bigint") return Number(value);
89
+ return Number(value || 0);
90
+ }
91
+
92
+ /**
93
+ * @param {any} value
94
+ * @returns {any}
95
+ */
96
+ function first(value) {
97
+ return Array.isArray(value) ? value[0] : value;
98
+ }
99
+
100
+ /**
101
+ * @param {import("../wtlogin/wt").WtLogin} wt
102
+ * @returns {number}
103
+ */
104
+ function getNTLoginPlatform(wt) {
105
+ return NT_LOGIN_PLATFORM_ID[wt.apk.platform] || 2;
106
+ }
107
+
108
+ /**
109
+ * @param {import("../wtlogin/wt").WtLogin} wt
110
+ * @returns {string}
111
+ */
112
+ function getNTLoginPlatformName(wt) {
113
+ return String(wt.apk.platform || "Android").toUpperCase();
114
+ }
115
+
116
+ /**
117
+ * @param {Buffer} data
118
+ * @param {Buffer} key
119
+ * @returns {Buffer}
120
+ */
121
+ function aesGcmEncrypt(data, key) {
122
+ const iv = randomBytes(12);
123
+ const cipher = createCipheriv("aes-256-gcm", toBuffer(key), iv);
124
+ const encrypted = Buffer.concat([cipher.update(data), cipher.final()]);
125
+ return Buffer.concat([iv, encrypted, cipher.getAuthTag()]);
126
+ }
127
+
128
+ /**
129
+ * @param {Buffer} data
130
+ * @param {Buffer} key
131
+ * @returns {Buffer}
132
+ */
133
+ function aesGcmDecrypt(data, key) {
134
+ if (!Buffer.isBuffer(data) || data.length < 28)
135
+ throw new Error("invalid AES-GCM data");
136
+ const iv = data.subarray(0, 12);
137
+ const tag = data.subarray(-16);
138
+ const decipher = createDecipheriv("aes-256-gcm", toBuffer(key), iv);
139
+ decipher.setAuthTag(tag);
140
+ return Buffer.concat([
141
+ decipher.update(data.subarray(12, -16)),
142
+ decipher.final(),
143
+ ]);
144
+ }
145
+
146
+ /**
147
+ * @param {Buffer} data
148
+ * @returns {Buffer}
149
+ */
150
+ function sha256(data) {
151
+ return createHash("sha256").update(data).digest();
152
+ }
153
+
154
+ /**
155
+ * NT 0x106 is intentionally kept here. The normal TLV packer must retain
156
+ * the original OICQ behavior, while NT uses sso_ver=0 and subid=1.
157
+ * @param {import("../wtlogin/wt").WtLogin} wt
158
+ * @param {Buffer} md5pass
159
+ * @returns {Buffer}
160
+ */
161
+ function buildNTPasswordA1(wt, md5pass) {
162
+ md5pass = toBuffer(md5pass);
163
+ if (!md5pass.length) return toBuffer(wt.t106);
164
+ const body = new Writer()
165
+ .writeU16(4)
166
+ .writeBytes(randomBytes(4))
167
+ .writeU32(0)
168
+ .writeU32(wt.apk.appid)
169
+ .writeU32(0)
170
+ .writeU64(wt.uin)
171
+ .write32((Date.now() / 1000) & 0xffffffff)
172
+ .writeBytes(BUF4)
173
+ .writeU8(1)
174
+ .writeBytes(md5pass)
175
+ .writeBytes(wt.device.tgtgt)
176
+ .writeU32(0)
177
+ .writeU8(1)
178
+ .writeBytes(wt.device.guid)
179
+ .writeU32(1)
180
+ .writeU32(1)
181
+ .writeTlv(String(wt.uin))
182
+ .writeU16(0)
183
+ .read();
184
+ const uin = Buffer.alloc(4);
185
+ uin.writeUInt32BE(wt.uin);
186
+ const key = md5(Buffer.concat([md5pass, BUF4, uin]));
187
+ return tea.encrypt(body, key);
188
+ }
189
+
190
+ /**
191
+ * @param {import("../wtlogin/wt").WtLogin} wt
192
+ * @param {boolean} passwordLogin
193
+ * @returns {object}
194
+ */
195
+ function buildNTLoginHead(wt, passwordLogin) {
196
+ const head = {
197
+ 1: {
198
+ 1: String(wt.uin),
199
+ },
200
+ 2: {
201
+ 1: getNTLoginPlatformName(wt),
202
+ 2: wt.device.model,
203
+ 3: getNTLoginPlatform(wt),
204
+ 4: wt.device.guid,
205
+ },
206
+ 3: {
207
+ 1: passwordLogin ? "" : wt.apk.ver,
208
+ 2: wt.apk.appid,
209
+ 3: wt.apk.id,
210
+ 5: wt.apk.qua,
211
+ },
212
+ };
213
+ if (passwordLogin) head[7] = { 1: 1 };
214
+ if (wt.nt_login_cookie) head[5] = { 1: wt.nt_login_cookie };
215
+ return head;
216
+ }
217
+
218
+ /**
219
+ * @param {import("../wtlogin/wt").WtLogin} wt
220
+ * @param {object} proto
221
+ * @returns {Promise<Buffer>}
222
+ */
223
+ async function buildNTServicePacket(wt, proto) {
224
+ if (!await wt.keyExchange()) throw new Error("NT key exchange failed");
225
+ const inner = wt.apk.appid === 16
226
+ ? { 2: proto, 3: { 1: 0, 2: String(wt.uin) } }
227
+ : proto;
228
+ const encrypted = aesGcmEncrypt(pb.encode(inner), wt.nt_session.key);
229
+ return Buffer.from(pb.encode({
230
+ 1: wt.nt_session.ticket,
231
+ [wt.apk.appid === 16 ? 5 : 3]: encrypted,
232
+ 4: 1,
233
+ }));
234
+ }
235
+
236
+ /**
237
+ * 构造 SsoKeyExchange 包。
238
+ * @param {import("../wtlogin/wt").WtLogin} wt
239
+ * @param {number} seq
240
+ * @returns {Promise<Buffer>}
241
+ */
242
+ async function buildSsoKeyExchangePacket(wt, seq) {
243
+ if (!wt.nt_ecdh) wt.nt_ecdh = new Ecdh();
244
+ const ecdh = wt.nt_ecdh;
245
+ const plain1 = pb.encode({
246
+ 1: "",
247
+ 2: wt.device.guid,
248
+ });
249
+ const gcmCalc1 = aesGcmEncrypt(plain1, ecdh.nt_share_key);
250
+ const ts = Date.now();
251
+ const plain2 = new Writer()
252
+ .writeBytes(ecdh.public_key)
253
+ .writeU32(1)
254
+ .writeBytes(gcmCalc1)
255
+ .writeU64(ts)
256
+ .read();
257
+ const gcmCalc2 = aesGcmEncrypt(sha256(plain2), NT_LOGIN_PROOF_KEY);
258
+ const body = pb.encode({
259
+ 1: ecdh.public_key,
260
+ 2: 1,
261
+ 3: gcmCalc1,
262
+ 4: ts,
263
+ 5: gcmCalc2,
264
+ });
265
+ return wt._buildLoginPacket(NT_LOGIN_COMMANDS.KEY_EXCHANGE, body, 2, seq, 0);
266
+ }
267
+
268
+ /**
269
+ * @param {import("../wtlogin/wt").WtLogin} wt
270
+ * @returns {object|undefined}
271
+ */
272
+ function buildNTCaptcha(wt) {
273
+ const captcha = wt.nt_captcha;
274
+ if (!captcha) return undefined;
275
+ const match = /(?:^|[?&])sid=([^&]*)/.exec(String(captcha.url || ""));
276
+ return {
277
+ 1: captcha.ticket,
278
+ 2: captcha.randStr,
279
+ 3: match ? match[1] : "",
280
+ };
281
+ }
282
+
283
+ /**
284
+ * 构造 SsoNTLoginPasswordLogin 包。
285
+ * @param {import("../wtlogin/wt").WtLogin} wt
286
+ * @returns {Promise<Buffer>}
287
+ */
288
+ async function buildSsoNTLoginPasswordLoginPacket(wt) {
289
+ const proto = {
290
+ 1: buildNTLoginHead(wt, true),
291
+ 2: {
292
+ 1: toBuffer(wt.t106),
293
+ 2: buildNTCaptcha(wt),
294
+ 5: { 1: 1 },
295
+ },
296
+ };
297
+ const packet = await buildNTServicePacket(wt, proto);
298
+ wt.nt_captcha = null;
299
+ return packet;
300
+ }
301
+
302
+ /**
303
+ * 构造 SsoNTLoginPasswordLoginUnusualDevice 包。
304
+ * @param {import("../wtlogin/wt").WtLogin} wt
305
+ * @returns {Promise<Buffer>}
306
+ */
307
+ async function buildSsoNTLoginPasswordLoginUnusualDevicePacket(wt) {
308
+ const proto = {
309
+ 1: buildNTLoginHead(wt, true),
310
+ 2: {
311
+ 1: toBuffer(wt.t106),
312
+ 2: wt.nt_unusual_device_check_sig,
313
+ },
314
+ };
315
+ const packet = await buildNTServicePacket(wt, proto);
316
+ wt.nt_unusual_device_check_sig = null;
317
+ return packet;
318
+ }
319
+
320
+ /**
321
+ * 构造 SsoNTLoginEasyLogin 包。
322
+ * @param {import("../wtlogin/wt").WtLogin} wt
323
+ * @returns {Promise<Buffer>}
324
+ */
325
+ async function buildSsoNTLoginEasyLoginPacket(wt) {
326
+ return buildNTServicePacket(wt, {
327
+ 1: buildNTLoginHead(wt, false),
328
+ 2: {
329
+ 1: toBuffer(wt.t106),
330
+ 2: wt.sig.srm_token,
331
+ },
332
+ });
333
+ }
334
+
335
+ /**
336
+ * 构造 SsoNTLoginRefreshTicket 包。
337
+ * @param {import("../wtlogin/wt").WtLogin} wt
338
+ * @returns {Promise<Buffer>}
339
+ */
340
+ async function buildSsoNTLoginRefreshTicketPacket(wt) {
341
+ return buildNTServicePacket(wt, {
342
+ 1: buildNTLoginHead(wt, false),
343
+ 2: {
344
+ 1: toBuffer(wt.t106),
345
+ 2: wt.sig.srm_token,
346
+ },
347
+ });
348
+ }
349
+
350
+ /**
351
+ * 构造 SsoNTLoginRefreshA2 包。
352
+ * @param {import("../wtlogin/wt").WtLogin} wt
353
+ * @returns {Promise<Buffer>}
354
+ */
355
+ async function buildSsoNTLoginRefreshA2Packet(wt) {
356
+ return buildNTServicePacket(wt, {
357
+ 1: buildNTLoginHead(wt, false),
358
+ 2: {
359
+ 1: wt.sig.tgt,
360
+ 2: wt.sig.d2,
361
+ 3: wt.sig.d2key,
362
+ },
363
+ });
364
+ }
365
+
366
+ WtLogin.prototype.buildSsoKeyExchangePacket = function (seq) {
367
+ return buildSsoKeyExchangePacket(this, seq);
368
+ };
369
+ WtLogin.prototype.buildSsoNTLoginPasswordLoginPacket = function () {
370
+ return buildSsoNTLoginPasswordLoginPacket(this);
371
+ };
372
+ WtLogin.prototype.buildSsoNTLoginPasswordLoginUnusualDevicePacket = function () {
373
+ return buildSsoNTLoginPasswordLoginUnusualDevicePacket(this);
374
+ };
375
+ WtLogin.prototype.buildSsoNTLoginEasyLoginPacket = function () {
376
+ return buildSsoNTLoginEasyLoginPacket(this);
377
+ };
378
+ WtLogin.prototype.buildSsoNTLoginRefreshTicketPacket = function () {
379
+ return buildSsoNTLoginRefreshTicketPacket(this);
380
+ };
381
+ WtLogin.prototype.buildSsoNTLoginRefreshA2Packet = function () {
382
+ return buildSsoNTLoginRefreshA2Packet(this);
383
+ };
384
+ /**
385
+ * @param {import("../wtlogin/wt").WtLogin} wt
386
+ * @returns {boolean}
387
+ */
388
+ function hasNTSession(wt) {
389
+ const session = wt.nt_session;
390
+ return !!session && Buffer.isBuffer(session.key) && session.key.length === 32 &&
391
+ Buffer.isBuffer(session.ticket) && session.ticket.length > 0 &&
392
+ session.expire_time - timestamp() >= 60;
393
+ }
394
+
395
+ /**
396
+ * @param {import("../wtlogin/wt").WtLogin} wt
397
+ * @returns {Promise<boolean>}
398
+ */
399
+ async function performNTKeyExchange(wt) {
400
+ wt.nt_session = null;
401
+ if (!wt.nt_ecdh) wt.nt_ecdh = new Ecdh();
402
+ const seq = wt.c.nextSeq();
403
+ const packet = await wt.buildSsoKeyExchangePacket(seq);
404
+ const response = pb.decode(await wt.c.send(packet, 6, seq));
405
+ const encrypted = toBuffer(response?.[1]);
406
+ const serverPublicKey = toBuffer(response?.[3]);
407
+ if (!encrypted.length || !serverPublicKey.length) return false;
408
+ const shareKey = wt.nt_ecdh.ntExchange(serverPublicKey);
409
+ const session = pb.decode(aesGcmDecrypt(encrypted, shareKey));
410
+ const key = toBuffer(session?.[1]);
411
+ const ticket = toBuffer(session?.[2]);
412
+ const expire = toNumber(session?.[3]);
413
+ if (key.length !== 32 || !ticket.length || !expire) return false;
414
+ wt.nt_session = {
415
+ key,
416
+ ticket,
417
+ expire_time: timestamp() + expire,
418
+ };
419
+ wt.logger.debug(`NT key exchange succeeded, session: ${expire}s`);
420
+ return true;
421
+ }
422
+
423
+ WtLogin.prototype.keyExchange = function () {
424
+ if (hasNTSession(this)) return Promise.resolve(true);
425
+ if (!this.ntKeyExchangePromise) {
426
+ this.ntKeyExchangePromise = performNTKeyExchange(this).finally(() => {
427
+ this.ntKeyExchangePromise = null;
428
+ });
429
+ }
430
+ return this.ntKeyExchangePromise;
431
+ };
432
+
433
+ /**
434
+ * @param {import("../wtlogin/wt").WtLogin} wt
435
+ * @param {Buffer} payload
436
+ * @returns {Buffer}
437
+ */
438
+ function decryptNTLoginPacket(wt, payload) {
439
+ if (!wt.nt_session) return BUF0;
440
+ const response = pb.decode(payload);
441
+ const encrypted = toBuffer(response?.[wt.apk.appid === 16 ? 5 : 3]);
442
+ if (!encrypted.length) throw new Error("invalid NT login response");
443
+ const decrypted = aesGcmDecrypt(encrypted, wt.nt_session.key);
444
+ if (wt.apk.appid !== 16) return decrypted;
445
+ const inner = pb.decode(decrypted);
446
+ return toBuffer(inner?.[2]);
447
+ }
448
+
449
+ /**
450
+ * @param {import("../wtlogin/wt").WtLogin} wt
451
+ * @param {string} cmd
452
+ * @param {Buffer} body
453
+ * @param {number} timeout
454
+ * @returns {Promise<void>}
455
+ */
456
+ async function sendNTLoginPacket(wt, cmd, body, timeout = 5) {
457
+ try {
458
+ const seq = wt.c.nextSeq();
459
+ const packet = await wt._buildLoginPacket(cmd, body, 2, seq);
460
+ const response = await wt.c.send(packet, timeout, seq);
461
+ await decodeNTLoginResponse(wt, cmd, decryptNTLoginPacket(wt, response));
462
+ } catch (e) {
463
+ wt.logger.error(e.message || e);
464
+ if (wt.token_flag && e instanceof TimeoutError === false)
465
+ await wt.deleteToken();
466
+ wt.c.emit("internal.network", "服务器繁忙");
467
+ }
468
+ }
469
+
470
+ /**
471
+ * @param {import("../wtlogin/wt").WtLogin} wt
472
+ * @param {string} cmd
473
+ * @param {() => Promise<Buffer>} builder
474
+ * @returns {Promise<void>}
475
+ */
476
+ async function sendNTLoginWithBuilder(wt, cmd, builder) {
477
+ let body;
478
+ try {
479
+ body = await builder.call(wt);
480
+ } catch (e) {
481
+ const message = e.message || "NT login packet build failed";
482
+ wt.logger.error(message);
483
+ wt.c.em("system.login.error", { code: -1, message });
484
+ return;
485
+ }
486
+ return sendNTLoginPacket(wt, cmd, body, 5);
487
+ }
488
+
489
+ WtLogin.prototype.ntPasswordLogin = function (md5pass) {
490
+ this.nt_ecdh = new Ecdh();
491
+ this.nt_session = null;
492
+ this.t106 = buildNTPasswordA1(this, md5pass);
493
+ if (this.nt_unusual_device_check_sig) {
494
+ return sendNTLoginWithBuilder(
495
+ this,
496
+ NT_LOGIN_COMMANDS.PASSWORD_LOGIN_UNUSUAL_DEVICE,
497
+ this.buildSsoNTLoginPasswordLoginUnusualDevicePacket,
498
+ );
499
+ }
500
+ return sendNTLoginWithBuilder(
501
+ this,
502
+ NT_LOGIN_COMMANDS.PASSWORD_LOGIN,
503
+ this.buildSsoNTLoginPasswordLoginPacket,
504
+ );
505
+ };
506
+
507
+ WtLogin.prototype.ntHasCaptcha = function () {
508
+ return !!this.nt_captcha;
509
+ };
510
+
511
+ WtLogin.prototype.ntSubmitCaptcha = function (ticket, randStr = "") {
512
+ if (!this.nt_captcha || !this.t106?.length) return;
513
+ this.nt_captcha.ticket = ticket;
514
+ this.nt_captcha.randStr = randStr;
515
+ return sendNTLoginWithBuilder(
516
+ this,
517
+ NT_LOGIN_COMMANDS.PASSWORD_LOGIN,
518
+ this.buildSsoNTLoginPasswordLoginPacket,
519
+ );
520
+ };
521
+
522
+ /**
523
+ * NT返回的token字段直接落到现有WtLogin字段:
524
+ * a1 -> t106,a1_key -> t10c,a2 -> sig.tgt,a2_key -> sig.tgt_key,
525
+ * d2 -> sig.d2,d2_key -> sig.d2key,no_pic_sig -> sig.srm_token。
526
+ * @param {import("../wtlogin/wt").WtLogin} wt
527
+ * @param {any} payload
528
+ * @returns {boolean}
529
+ */
530
+ function parseNTToken(wt, payload) {
531
+ const token = payload?.[1];
532
+ if (!token || typeof token !== "object") return false;
533
+ const a1 = toBuffer(token[3]);
534
+ const a1Key = toBuffer(token[14]);
535
+ const a2 = toBuffer(token[4]);
536
+ const a2Key = toBuffer(token[17]);
537
+ const d2 = toBuffer(token[5]);
538
+ const d2key = toBuffer(token[6]);
539
+ const noPicSig = toBuffer(token[13]);
540
+ if (a1.length) wt.readT106(a1);
541
+ if (a1Key.length) wt.readT10C(a1Key);
542
+ if (a2.length) wt.sig.tgt = a2;
543
+ if (a2Key.length) wt.sig.tgt_key = a2Key;
544
+ if (d2.length) wt.sig.d2 = d2;
545
+ if (d2key.length) wt.sig.d2key = d2key;
546
+ if (noPicSig.length) wt.sig.srm_token = noPicSig;
547
+
548
+ const uid = first(payload?.[4]?.[2]) || first(payload?.[2]?.[2]);
549
+ if (uid) wt.uid = toString(uid);
550
+ const user = payload?.[4]?.[3];
551
+ const nickname = user?.[1] ? toString(user[1]) : wt.c.nickname;
552
+ wt.nickname = nickname;
553
+ if (a1Key.length) {
554
+ wt.device.tgtgt = a1Key;
555
+ wt.t10c = a1Key;
556
+ } else if (d2key.length) {
557
+ wt.device.tgtgt = md5(d2key);
558
+ }
559
+ return true;
560
+ }
561
+
562
+ /**
563
+ * @param {any} errorInfo
564
+ * @returns {string}
565
+ */
566
+ function getNTErrorMessage(errorInfo) {
567
+ if (!errorInfo) return "NT 登录失败";
568
+ const title = errorInfo[2] ? toString(errorInfo[2]) : "登录失败";
569
+ const content = errorInfo[3] ? toString(errorInfo[3]) : "未知错误";
570
+ return `[${title}]${content}`;
571
+ }
572
+
573
+ /**
574
+ * 解码 NT 登录返回并转换为现有 OICQ 登录事件。
575
+ * @param {import("../wtlogin/wt").WtLogin} wt
576
+ * @param {string} cmd
577
+ * @param {Buffer} payload
578
+ * @returns {Promise<any>}
579
+ */
580
+ async function decodeNTLoginResponse(wt, cmd, payload) {
581
+ wt.NTLoginResponse = payload;
582
+ const proto = pb.decode(payload);
583
+ if (!proto) throw new Error("invalid NT login response");
584
+ const errorInfo = proto[1]?.[4];
585
+ const code = errorInfo ? toNumber(errorInfo[1]) : 0;
586
+ wt.nt_login_cookie = proto[1]?.[5]?.[1]
587
+ ? toString(proto[1][5][1])
588
+ : null;
589
+
590
+ if (code === NTLoginErrorCode.Success) {
591
+ if (!parseNTToken(wt, proto[2]))
592
+ throw new Error("NT login response has no token");
593
+ if (wt.apk.appid === 16) {
594
+ wt.token_flag = true;
595
+ return wt.tokenLogin(wt.sig.d2);
596
+ }
597
+ return wt.c.emit("internal.login");
598
+ }
599
+
600
+ if (code === NTLoginErrorCode.Strict) {
601
+ const message = getNTErrorMessage(errorInfo);
602
+ let jump;
603
+ if (errorInfo?.[7]?.length && errorInfo[7][0]?.[2]) {
604
+ jump = {
605
+ word: toString(errorInfo[7][0][1]),
606
+ url: toString(errorInfo[7][0][2]),
607
+ };
608
+ } else if (errorInfo?.[5]) {
609
+ jump = {
610
+ word: toString(errorInfo[4]),
611
+ url: toString(errorInfo[5]),
612
+ };
613
+ }
614
+ if (jump?.url) {
615
+ wt.logger.mark(message);
616
+ wt.logger.mark("访问URL完成验证后调用login()可直接登录(需要提交设备信息)。");
617
+ wt.logger.mark(`${jump.word}: ${jump.url}`);
618
+ return wt.c.em("system.login.auth", {
619
+ url: jump.url,
620
+ device: wt.buildLoginAuthDevice(),
621
+ });
622
+ }
623
+ wt.logger.error(message);
624
+ return wt.c.em("system.login.error", { code: 237, message });
625
+ }
626
+
627
+ if (code === NTLoginErrorCode.ProofWater) {
628
+ const url = errorInfo?.[5]
629
+ ? toString(errorInfo[5])
630
+ : proto[2]?.[2]?.[3]
631
+ ? toString(proto[2][2][3])
632
+ : "";
633
+ if (url) {
634
+ wt.nt_captcha = { url };
635
+ wt.logger.mark(`收到滑动验证码,请访问以下地址完成滑动,并从网络响应中取出ticket和randstr参数以英文逗号拼接输入:${url}`);
636
+ return wt.c.em("system.login.slider", { url });
637
+ }
638
+ wt.logger.error("[登陆失败]未知格式的验证码。");
639
+ return wt.c.em("system.login.error", {
640
+ code: 2,
641
+ message: "[登陆失败]未知格式的验证码。",
642
+ });
643
+ }
644
+
645
+ if (code === NTLoginErrorCode.NewDevice) {
646
+ const url = errorInfo?.[5] ? toString(errorInfo[5]) : "";
647
+ wt.logger.mark("登录保护验证URL:" + url);
648
+ return wt.c.em("system.login.device", {
649
+ url,
650
+ phone: "",
651
+ });
652
+ }
653
+
654
+ if (code === NTLoginErrorCode.UnusualDevice) {
655
+ const detail = proto[2]?.[3];
656
+ if (detail?.[2]) {
657
+ wt.nt_unusual_device_check_sig = toBuffer(detail[2]);
658
+ }
659
+ const url = detail?.[3] ? toString(detail[3]) : "";
660
+ wt.logger.warn("[登陆失败]当前设备环境异常,请完成设备验证后重新登录");
661
+ if (url) wt.logger.mark("登录保护验证URL:" + url);
662
+ return wt.c.em("system.login.device", {
663
+ url,
664
+ phone: "",
665
+ });
666
+ }
667
+
668
+ const message = getNTErrorMessage(errorInfo);
669
+ if (code === NTLoginErrorCode.NeedUpdate) {
670
+ wt.logger.warn(message + "(错误码:" + code + ")");
671
+ return wt.c.em("system.login.error", { code, message });
672
+ }
673
+
674
+ if (cmd === NT_LOGIN_COMMANDS.EASY_LOGIN || cmd === NT_LOGIN_COMMANDS.REFRESH_TICKET) {
675
+ await wt.deleteToken();
676
+ return wt.passwordLogin();
677
+ }
678
+ return wt.c.em("system.login.error", { code, message });
679
+ }
680
+
681
+ module.exports = {
682
+ NT_LOGIN_CMD_PREFIX,
683
+ NT_LOGIN_COMMANDS,
684
+ NTLoginErrorCode,
685
+ buildSsoKeyExchangePacket,
686
+ buildSsoNTLoginPasswordLoginPacket,
687
+ buildSsoNTLoginPasswordLoginUnusualDevicePacket,
688
+ buildSsoNTLoginEasyLoginPacket,
689
+ buildSsoNTLoginRefreshTicketPacket,
690
+ buildSsoNTLoginRefreshA2Packet,
691
+ };
package/lib/ref.d.ts CHANGED
@@ -44,6 +44,8 @@ export interface Device {
44
44
 
45
45
  export interface ApkInfo {
46
46
  id: string,
47
+ nt: boolean,
48
+ platform: string,
47
49
  name: string,
48
50
  version: string,
49
51
  ver: string,
@@ -89,6 +89,15 @@ WtLogin.prototype.passwordLogin = async function () {
89
89
  } catch {
90
90
  this.token_flag = false;
91
91
  }
92
+ if (this.apk.nt && !this.c.config.forceWt && this.password_md5) {
93
+ try {
94
+ return await this.ntPasswordLogin(this.password_md5);
95
+ } catch (e) {
96
+ const message = e.message || "NT login failed";
97
+ this.logger.error(message);
98
+ return this.c.em("system.login.error", { code: -1, message });
99
+ }
100
+ }
92
101
  if (!this.password_md5)
93
102
  return this.fetchQrcode().catch((e) => {
94
103
  const message = e.message || "[qsign][HeadSign] 未知错误";
@@ -155,12 +164,15 @@ WtLogin.prototype.passwordLogin = async function () {
155
164
  }
156
165
 
157
166
  WtLogin.prototype.sliderLogin = async function (ticket) {
167
+ const [captchaTicket, randStr = ""] = String(ticket).trim().split(",", 2);
168
+ if (this.ntHasCaptcha()) {
169
+ return this.ntSubmitCaptcha(captchaTicket, randStr);
170
+ }
158
171
  if (!this.t104)
159
172
  return this.logger.warn("未收到滑动验证码或已过期,你不能调用sliderLogin函数。");
160
- ticket = String(ticket).trim();
161
173
  const t = this.tlvPacker;
162
174
  const tlvs = [
163
- t(0x193, ticket),
175
+ t(0x193, captchaTicket),
164
176
  t(0x8),
165
177
  t(0x104),
166
178
  t(0x116),
package/lib/wtlogin/wt.js CHANGED
@@ -57,6 +57,26 @@ class WtLogin {
57
57
  this.c = c;
58
58
  }
59
59
 
60
+ /**
61
+ * 构造身份验证事件中的设备信息。
62
+ * @returns {object}
63
+ */
64
+ buildLoginAuthDevice() {
65
+ const qimei = this.device.qimei36 || this.device.qimei16 || this.device.android_id;
66
+ return {
67
+ guid: this.device.guid.toString("hex"),
68
+ qimei,
69
+ qimei36: qimei,
70
+ subappid: String(this.apk.subid),
71
+ platform: this.apk.platform,
72
+ brand: this.device.brand,
73
+ model: this.device.model,
74
+ bssid: "",
75
+ devInfo: `${this.device.brand} ${this.device.model}`,
76
+ sysVersion: String(this.device.version.sdk),
77
+ };
78
+ }
79
+
60
80
  async sendLogin(cmd, body) {
61
81
  const seq = this.c.nextSeq();
62
82
  let pkt;
@@ -283,6 +303,29 @@ class WtLogin {
283
303
  this.decodeT119(t[0x119], this.token_flag);
284
304
  return this.c.emit("internal.login");
285
305
  }
306
+
307
+ if (type === 237) {
308
+ const proto = pb.decode(t[0x543] || BUF0);
309
+ const info = proto?.[60];
310
+ if (info) {
311
+ const title = String(info[7] || "登录失败");
312
+ const content = String(info[8] || "未知错误,错误码:237");
313
+ const message = `[${title}]${content}`;
314
+ if (info[6] && info[6][3]) {
315
+ const jump = {
316
+ word: String(info[6][1]),
317
+ url: String(info[6][3]),
318
+ };
319
+ this.logger.mark(message);
320
+ this.logger.mark("访问URL完成验证后调用login()可直接登录(需要提交设备信息)。");
321
+ this.logger.mark(`${jump.word}: ${jump.url}`);
322
+ return this.c.em("system.login.auth", {
323
+ url: jump.url,
324
+ device: this.buildLoginAuthDevice(),
325
+ });
326
+ }
327
+ }
328
+ }
286
329
 
287
330
  if (this.token_flag) {
288
331
  this.logger.mark("token失效,重新login..");
@@ -575,9 +618,10 @@ class WtLogin {
575
618
  * @param {Buffer} body
576
619
  * @param {0|1|2} type 0心跳 1上线 2登录
577
620
  * @param {number} seq
621
+ * @param {number} uin
578
622
  * @returns {Promise<Buffer>}
579
623
  */
580
- async _buildLoginPacket(cmd, body, type, seq = 0) {
624
+ async _buildLoginPacket(cmd, body, type, seq = 0, uin = this.uin) {
581
625
  seq = seq ? seq : this.c.nextSeq();
582
626
  const ntRegCmds = [
583
627
  "trpc.msg.register_proxy.RegisterProxy.SsoInfoSync",
@@ -614,7 +658,7 @@ class WtLogin {
614
658
  .writeU8(type)
615
659
  .writeWithLength(this.sig.d2)
616
660
  .writeU8(0)
617
- .writeWithLength(String(this.uin))
661
+ .writeWithLength(String(uin))
618
662
  .writeBytes(sso)
619
663
  .read();
620
664
  return new Writer().writeWithLength(body).read();
@@ -717,5 +761,6 @@ module.exports = {
717
761
  WtLogin, readTlv, BUF_UNKNOWN,
718
762
  };
719
763
 
764
+ require("../ntlogin");
720
765
  require("./login-password");
721
766
  require("./login-qrcode");
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "oicq-icalingua-plus-plus",
3
3
  "app_name": "oicq-icalingua-plus-plus",
4
- "version": "1.26.24",
5
- "app_version": "1.26.24",
6
- "upday": "2026/09/03",
4
+ "version": "1.26.26",
5
+ "app_version": "1.26.26",
6
+ "upday": "2026/09/23",
7
7
  "protocol_version": "v11",
8
8
  "description": "oicq(https://github.com/takayama-lily/oicq) v1 for Icalingua++",
9
9
  "main": "index.js",