forked from khyperia/weechat-discord
-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy pathfind_token.py
138 lines (115 loc) · 4.46 KB
/
find_token.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#!/usr/bin/env python3
import subprocess
import sys
import string
import platform
from base64 import b64decode
import urllib.request
import json
from functools import cache
from datetime import datetime
from collections import namedtuple
from typing import Optional, Iterator, List
import re
ParsedToken = namedtuple("ParsedToken", ["raw", "userid", "created", "hmac"])
DB_FILTER = ["chrome", "vivaldi", "discord"]
_urlsafe_decode_translation = str.maketrans("-_", "+/")
def round_down(num, divisor):
return num - (num % divisor)
def urlsafe_b64decode(s: str):
s = s.translate(_urlsafe_decode_translation)
return b64decode(s, validate=True)
@cache
def id2username(id: str) -> str:
try:
resp = urllib.request.urlopen(
"https://terminal-discord.vercel.app/api/lookup-user?id={}".format(id)
)
data = json.load(resp)
return data.get("username") or "Unknown"
except:
return "Unknown"
def parseIdPart(id_part: str) -> str:
return urlsafe_b64decode(id_part).decode()
# This doesn't return the correct value anymore, discord changed something
def parseTimePart(time_part: str) -> datetime:
if len(time_part) < 6:
raise Exception("Time part too short")
padded_time_part = time_part + "=" * (
(round_down(len(time_part), 4) + 4) - len(time_part)
)
decoded = urlsafe_b64decode(padded_time_part)
timestamp = sum((item * 256 ** idx for idx, item in enumerate(reversed(decoded))))
if timestamp < 1293840000:
timestamp += 1293840000
return datetime.fromtimestamp(timestamp)
def parseToken(token: str) -> ParsedToken:
parts = token.split(".")
return ParsedToken(
raw=token,
userid=parseIdPart(parts[0]),
created=parseTimePart(parts[1]),
hmac=parts[2],
)
def run_command(cmd: str) -> List[str]:
output = subprocess.Popen(
[cmd], shell=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL
)
return output.communicate()[0].decode().splitlines()
def main():
skip_username_lookup = "--no-lookup" in sys.argv
print("Searching for Discord localstorage databases...")
# First, we search for .ldb files, these are the leveldb files used by chromium to store localstorage data,
# which contains the discord token.
# Try and use ripgrep, because it's much faster, otherwise, fallback to `find`.
try:
subprocess.check_output(["rg", "--version"])
results = run_command("rg ~/ --hidden --files -g '*.ldb' -g '*.log' -g 'data.sqlite'")
except FileNotFoundError:
results = run_command("find ~/ -name '*.ldb' -or -name '*.log' -or -name 'data.sqlite'")
if len(results) == 0:
print("No databases found.")
sys.exit(1)
# Only search for tokens in local starage directories belonging known Chromium browsers or discord
discord_databases = list(
filter(
lambda x: any([db in x.lower() for db in DB_FILTER])
and ("Local Storage" in x or "ls/" in x),
results,
)
)
# Then collect strings that look like discord tokens.
token_candidates = {}
token_re = re.compile(rb'([a-z0-9_-]{23,28}\.[a-z0-9_-]{6,7}\.[a-z0-9_-]{27})', flags=re.IGNORECASE)
for database in discord_databases:
for line in open(database, 'rb'):
for result in token_re.finditer(line):
try:
token_candidates[parseToken(result.group(0).decode())] = database
except:
continue
token_candidates = list(token_candidates.items())
if len(token_candidates) == 0:
print("No Discord tokens found")
return
print("Possible Discord tokens found (sorted newest to oldest):\n")
token_candidates = sorted(token_candidates, key=lambda t: t[0].created, reverse=True)
for [token, db] in token_candidates:
if "discord/Local Storage/" in db:
source = "Discord App"
elif "ivaldi" in db: # case insensitive hack
source = "Vivaldi"
elif "Local Storage/" in db:
source = "Chrome"
elif "ls/data.sqlite" in db:
source = "Firefox"
if skip_username_lookup:
print("{} created: {}, source: {}".format(token.raw, token.created, source))
else:
print(
"@{}: {} created: {}, source: {}".format(
id2username(token.userid), token.raw, token.created, source
)
)
if __name__ == "__main__":
main()