atm-droid 1.0.1 → 1.0.3
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/package.json +1 -1
- package/src/api.js +109 -20
package/package.json
CHANGED
package/src/api.js
CHANGED
|
@@ -4,19 +4,35 @@ const { config, getDeviceId } = require('./config');
|
|
|
4
4
|
|
|
5
5
|
const getServerUrl = () => config.get('serverUrl');
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
function
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
7
|
+
// 解密静态字节(与 Rust 客户端一致)
|
|
8
|
+
function decryptStaticBytes(encrypted, keySeed) {
|
|
9
|
+
const result = [];
|
|
10
|
+
let key = keySeed;
|
|
11
|
+
|
|
12
|
+
for (let i = 0; i < encrypted.length; i++) {
|
|
13
|
+
const byte = encrypted[i];
|
|
14
|
+
let k1 = ((key * 0x1B) + i) & 0xff;
|
|
15
|
+
let k2 = ((k1 << 3) | (k1 >>> 5)) & 0xff; // rotate_left(3)
|
|
16
|
+
k2 = k2 ^ 0xA5;
|
|
17
|
+
const decrypted = byte ^ k2;
|
|
18
|
+
result.push(decrypted);
|
|
19
|
+
key = (((key + decrypted) & 0xff) * 0x3D) & 0xff;
|
|
18
20
|
}
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
return Buffer.from(result);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 加密的通信密钥(与 Rust 客户端一致)
|
|
26
|
+
const ENC_COMM_KEY = Buffer.from([
|
|
27
|
+
0x11, 0x18, 0xFC, 0x4A, 0xF9, 0xB8, 0x1A, 0x9C,
|
|
28
|
+
0x4F, 0xA4, 0xD1, 0x44, 0x47, 0xE5, 0x03, 0x57,
|
|
29
|
+
0xA3, 0x2B, 0x8E, 0x38, 0x3B, 0x99, 0x8F, 0x8C,
|
|
30
|
+
0x2B, 0x91, 0x60, 0xC1, 0x15, 0x07, 0x21, 0x53,
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
// 生成加密密钥(与客户端一致)
|
|
34
|
+
function getEncryptionKey() {
|
|
35
|
+
return decryptStaticBytes(ENC_COMM_KEY, 0x8D);
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
// 生成签名
|
|
@@ -28,6 +44,40 @@ function generateSignature(data, timestamp, deviceId) {
|
|
|
28
44
|
return hmac.digest('hex');
|
|
29
45
|
}
|
|
30
46
|
|
|
47
|
+
// 解密服务器返回的数据(返回字符串)
|
|
48
|
+
function decryptPayload(encryptedData, ivHex, tagHex) {
|
|
49
|
+
try {
|
|
50
|
+
const key = getEncryptionKey();
|
|
51
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
52
|
+
const tag = Buffer.from(tagHex, 'hex');
|
|
53
|
+
const encrypted = Buffer.from(encryptedData, 'hex');
|
|
54
|
+
|
|
55
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
56
|
+
decipher.setAuthTag(tag);
|
|
57
|
+
|
|
58
|
+
let decrypted = decipher.update(encrypted, null, 'utf8');
|
|
59
|
+
decrypted += decipher.final('utf8');
|
|
60
|
+
|
|
61
|
+
return decrypted;
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error('解密失败:', e.message);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 解密服务器返回的 JSON 数据
|
|
69
|
+
function decryptPayloadJson(encryptedData, ivHex, tagHex) {
|
|
70
|
+
const decrypted = decryptPayload(encryptedData, ivHex, tagHex);
|
|
71
|
+
if (decrypted) {
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(decrypted);
|
|
74
|
+
} catch (e) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
31
81
|
// 激活码登录
|
|
32
82
|
async function activate(code) {
|
|
33
83
|
const deviceId = getDeviceId();
|
|
@@ -51,19 +101,27 @@ async function getTokens() {
|
|
|
51
101
|
throw new Error('未登录');
|
|
52
102
|
}
|
|
53
103
|
|
|
104
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
105
|
+
const signature = generateSignature(sessionToken, timestamp, deviceId);
|
|
106
|
+
|
|
54
107
|
const response = await fetch(`${getServerUrl()}/api/v1/tokens`, {
|
|
55
108
|
headers: {
|
|
56
109
|
'Authorization': `Bearer ${sessionToken}`,
|
|
57
|
-
'X-Device-ID': deviceId
|
|
110
|
+
'X-Device-ID': deviceId,
|
|
111
|
+
'X-Timestamp': timestamp.toString(),
|
|
112
|
+
'X-Signature': signature
|
|
58
113
|
}
|
|
59
114
|
});
|
|
60
115
|
|
|
61
116
|
const result = await response.json();
|
|
62
117
|
|
|
63
118
|
// 如果返回加密数据,需要解密
|
|
64
|
-
if (result.success && result.data && result.iv) {
|
|
65
|
-
|
|
66
|
-
|
|
119
|
+
if (result.success && result.data && result.iv && result.tag) {
|
|
120
|
+
const decrypted = decryptPayloadJson(result.data, result.iv, result.tag);
|
|
121
|
+
if (decrypted) {
|
|
122
|
+
return { success: true, data: decrypted };
|
|
123
|
+
}
|
|
124
|
+
return { success: false, error: '解密失败' };
|
|
67
125
|
}
|
|
68
126
|
|
|
69
127
|
return result;
|
|
@@ -73,18 +131,40 @@ async function getTokens() {
|
|
|
73
131
|
async function activateToken(tokenId) {
|
|
74
132
|
const sessionToken = config.get('sessionToken');
|
|
75
133
|
const deviceId = getDeviceId();
|
|
134
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
135
|
+
const signature = generateSignature(sessionToken, timestamp, deviceId);
|
|
76
136
|
|
|
77
137
|
const response = await fetch(`${getServerUrl()}/api/v1/tokens/activate`, {
|
|
78
138
|
method: 'POST',
|
|
79
139
|
headers: {
|
|
80
140
|
'Content-Type': 'application/json',
|
|
81
141
|
'Authorization': `Bearer ${sessionToken}`,
|
|
82
|
-
'X-Device-ID': deviceId
|
|
142
|
+
'X-Device-ID': deviceId,
|
|
143
|
+
'X-Timestamp': timestamp.toString(),
|
|
144
|
+
'X-Signature': signature
|
|
83
145
|
},
|
|
84
146
|
body: JSON.stringify({ token_id: tokenId })
|
|
85
147
|
});
|
|
86
148
|
|
|
87
|
-
|
|
149
|
+
const result = await response.json();
|
|
150
|
+
|
|
151
|
+
// 解密 access_token 和 refresh_token
|
|
152
|
+
if (result.success && result.access_token && result.access_iv && result.access_tag) {
|
|
153
|
+
const accessToken = decryptPayload(result.access_token, result.access_iv, result.access_tag);
|
|
154
|
+
const refreshToken = decryptPayload(result.refresh_token, result.refresh_iv, result.refresh_tag);
|
|
155
|
+
|
|
156
|
+
if (accessToken && refreshToken) {
|
|
157
|
+
return {
|
|
158
|
+
success: true,
|
|
159
|
+
email: result.email,
|
|
160
|
+
access_token: accessToken,
|
|
161
|
+
refresh_token: refreshToken
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return { success: false, error: '解密失败' };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return result;
|
|
88
168
|
}
|
|
89
169
|
|
|
90
170
|
// 心跳
|
|
@@ -94,12 +174,17 @@ async function heartbeat() {
|
|
|
94
174
|
|
|
95
175
|
if (!sessionToken) return { valid: false };
|
|
96
176
|
|
|
177
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
178
|
+
const signature = generateSignature(sessionToken, timestamp, deviceId);
|
|
179
|
+
|
|
97
180
|
const response = await fetch(`${getServerUrl()}/api/v1/auth/heartbeat`, {
|
|
98
181
|
method: 'POST',
|
|
99
182
|
headers: {
|
|
100
183
|
'Content-Type': 'application/json',
|
|
101
184
|
'Authorization': `Bearer ${sessionToken}`,
|
|
102
|
-
'X-Device-ID': deviceId
|
|
185
|
+
'X-Device-ID': deviceId,
|
|
186
|
+
'X-Timestamp': timestamp.toString(),
|
|
187
|
+
'X-Signature': signature
|
|
103
188
|
}
|
|
104
189
|
});
|
|
105
190
|
|
|
@@ -110,13 +195,17 @@ async function heartbeat() {
|
|
|
110
195
|
async function unbind() {
|
|
111
196
|
const sessionToken = config.get('sessionToken');
|
|
112
197
|
const deviceId = getDeviceId();
|
|
198
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
199
|
+
const signature = generateSignature(sessionToken, timestamp, deviceId);
|
|
113
200
|
|
|
114
201
|
const response = await fetch(`${getServerUrl()}/api/v1/auth/unbind`, {
|
|
115
202
|
method: 'POST',
|
|
116
203
|
headers: {
|
|
117
204
|
'Content-Type': 'application/json',
|
|
118
205
|
'Authorization': `Bearer ${sessionToken}`,
|
|
119
|
-
'X-Device-ID': deviceId
|
|
206
|
+
'X-Device-ID': deviceId,
|
|
207
|
+
'X-Timestamp': timestamp.toString(),
|
|
208
|
+
'X-Signature': signature
|
|
120
209
|
}
|
|
121
210
|
});
|
|
122
211
|
|