-
Notifications
You must be signed in to change notification settings - Fork 4
/
onion.py
299 lines (245 loc) · 8.81 KB
/
onion.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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
import os
import re
import warnings
from abc import ABC
from abc import abstractmethod
from base64 import b32encode
from base64 import b64encode
from hashlib import sha1
from hashlib import sha3_256
from hashlib import sha512
from typing import BinaryIO
from Crypto.PublicKey import RSA
from Crypto.Random import get_random_bytes
from .ed25519 import Ed25519
__all__ = [
"OnionV2",
"OnionV3",
"EmptyDirException",
"NonEmptyDirException",
]
class EmptyDirException(Exception):
pass
class NonEmptyDirException(Exception):
pass
class Onion(ABC):
"""
Interface to implement hidden services keys managment
"""
_priv = None
_pub = None
_hidden_service_path = None
_priv_key_filename = None
_pub_key_filename = None
_host_filename = None
_version = None
def __init__(
self, private_key: bytes = None, hidden_service_path: str = None
):
if self._version == 2:
warnings.warn(
"Onion addresses version 2 are not supported anymore by tor",
UserWarning
)
if hidden_service_path:
try:
self.load_hidden_service(hidden_service_path)
except EmptyDirException:
pass
self._hidden_service_path = hidden_service_path
if private_key:
self.set_private_key(private_key)
if not self._priv:
self.gen_new_private_key()
@abstractmethod
def gen_new_private_key(self) -> None:
"Generate new private key"
...
def set_private_key_from_file(self, file: BinaryIO):
"Load private key from file"
self.set_private_key(file.read())
@abstractmethod
def set_private_key(self, key: bytes) -> None:
"Add private key"
...
@abstractmethod
def _save_keypair(self, key) -> None:
"Generate pub key from priv key and save both in instance"
...
def load_hidden_service(self, path: str) -> None:
if not os.path.isdir(path):
raise Exception(
"{path} should be an existing directory".format(path=path)
)
if self._priv_key_filename not in os.listdir(path):
raise EmptyDirException(
"{key} file not found in {path}".format(
key=self._priv_key_filename, path=path
)
)
with open(os.path.join(path, self._priv_key_filename), "rb") as f:
self.set_private_key_from_file(f)
def write_hidden_service(
self, path: str = None, force: bool = False
) -> None:
path = path or self._hidden_service_path
if not path:
raise Exception("Missing hidden service path")
if not os.path.exists(path):
raise Exception(
"{path} should be an existing directory".format(path=path)
)
if (
os.path.exists(os.path.join(path, self._host_filename))
or os.path.exists(os.path.join(path, self._priv_key_filename))
) and not force:
raise NonEmptyDirException(
"Use force=True for non empty hidden service directory"
)
with open(os.path.join(path, self._priv_key_filename), "wb") as f:
f.write(self._get_private_key_has_native())
with open(os.path.join(path, self._host_filename), "w") as f:
f.write(self.onion_hostname)
def get_available_private_key_formats(self) -> list:
"Get private key export availables formats"
r = re.compile(r"_get_private_key_has_([a-z]+)")
formats = []
for method in dir(self):
m = r.match(method)
if m:
formats.append(m[1])
return formats
def get_private_key(self, format: str = "native"):
"Get the private key as specified format"
method = "_get_private_key_has_{format}".format(format=format)
if not hasattr(self, method) and not callable(getattr(self, method)):
raise NotImplementedError("Method {method} if not implemented")
return getattr(self, method)()
@abstractmethod
def _get_private_key_has_native(self) -> bytes:
"Get private key like in tor native format"
...
@abstractmethod
def get_public_key(self) -> bytes:
"Compute public key"
if not self._priv:
raise Exception("No private key has been set")
@abstractmethod
def get_onion_str(self) -> str:
"Compute onion address string"
...
@property
def onion_hostname(self) -> str:
return "{onion}.onion".format(onion=self.get_onion_str())
@property
def version(self) -> str:
return str(self._version)
class OnionV2(Onion):
"""
Tor onion address v2 implement
"""
_priv_key_filename = "private_key"
_host_filename = "hostname"
_version = 2
def gen_new_private_key(self) -> None:
"Generate new 1024 bits RSA key for hidden service"
self._save_keypair(RSA.generate(bits=1024))
def _save_keypair(self, key: RSA.RsaKey) -> None:
self._priv = key.exportKey("PEM")
self._pub = key.publickey().exportKey("DER")
def set_private_key(self, key: bytes) -> None:
"Add private key"
if not key.startswith(b"-----BEGIN RSA PRIVATE KEY-----"):
raise Exception(
"Private key does not seems to be a valid RSA PEM key"
)
self._save_keypair(RSA.importKey(key.strip()))
def _get_private_key_has_native(self) -> bytes:
"Get RSA private key like in PEM"
return self._get_private_key_has_pem()
def _get_private_key_has_pem(self) -> bytes:
"Get RSA private key like in PEM"
return RSA.importKey(self._priv).exportKey("PEM")
def get_public_key(self) -> bytes:
"Compute public key"
super().get_public_key()
return self._pub
def get_onion_str(self) -> str:
"Compute onion address string"
return b32encode(sha1(self._pub[22:]).digest()[:10]).decode().lower()
def serialize(self):
return {
self._host_filename: self.onion_hostname,
self._priv_key_filename: self.get_private_key().decode(),
}
class OnionV3(Onion):
"""
Tor onion address v3 implement
"""
_header_priv = b"== ed25519v1-secret: type0 ==\x00\x00\x00"
_header_pub = b"== ed25519v1-public: type0 ==\x00\x00\x00"
_priv_key_filename = "hs_ed25519_secret_key"
_pub_key_filename = "hs_ed25519_public_key"
_host_filename = "hostname"
_version = 3
def _save_keypair(self, key: bytes) -> None:
self._priv = key
self._pub = Ed25519().public_key_from_hash(key)
def gen_new_private_key(self) -> None:
"Generate new tor ed25519 512 bits key"
random = get_random_bytes(32)
key = bytearray(sha512(random).digest())
key[0] &= 248
key[31] &= 63
key[31] |= 64
self._save_keypair(bytes(key))
def set_private_key(self, key: bytes) -> None:
"Add private key"
if not key.startswith(self._header_priv):
raise Exception(
"Private key does not seems to be a valid ed25519 tor key"
)
parsed_key = key[32:]
if len(parsed_key) != 64:
raise Exception(
"Private key does not seem to have the good lenght"
)
self._save_keypair(parsed_key)
def set_private_key_from_file(self, file: BinaryIO):
"Load private key from file"
self.set_private_key(file.read())
def _get_private_key_has_native(self) -> bytes:
"Get RSA private key like in PEM"
return self._header_priv + self._priv
def get_public_key(self) -> bytes:
"Compute public key"
super().get_public_key()
return self._header_pub + self._pub
def write_hidden_service(
self, path: str = None, force: bool = False
) -> None:
path = path or self._hidden_service_path
super().write_hidden_service(path, force)
with open(os.path.join(path, self._pub_key_filename), "wb") as f:
f.write(self.get_public_key())
def get_onion_str(self) -> str:
"Compute onion address string"
version_byte = b"\x03"
def checksum(pubkey):
checksum_str = ".onion checksum".encode("ascii")
return sha3_256(checksum_str + self._pub + version_byte).digest()[
:2
]
return (
b32encode(self._pub + checksum(self._pub) + version_byte)
.decode()
.lower()
)
def serialize(self):
return {
self._host_filename: self.onion_hostname,
self._priv_key_filename: b64encode(
self.get_private_key()
).decode(),
self._pub_key_filename: b64encode(self.get_public_key()).decode(),
}