From d7f0be237fdd898186d91c87b483399439eba78c Mon Sep 17 00:00:00 2001 From: Christophe Mehay Date: Thu, 11 Apr 2019 23:10:06 +0200 Subject: [PATCH] Add main and serializer in Onion classes --- .gitignore | 1 + Pipfile | 1 + Pipfile.lock | 16 +++++++++- README.md | 41 +++++++++++++++++++++++++ pytor/__init__.py | 7 +++++ pytor/__main__.py | 77 +++++++++++++++++++++++++++++++++++++++++++++++ pytor/onion.py | 30 ++++++++++++++++-- setup.py | 12 ++++---- 8 files changed, 175 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 64d44b1..fd3c441 100644 --- a/.gitignore +++ b/.gitignore @@ -106,3 +106,4 @@ ENV/ # more key/ .vscode/* +test/* diff --git a/Pipfile b/Pipfile index 3f9b262..5241c03 100644 --- a/Pipfile +++ b/Pipfile @@ -16,6 +16,7 @@ autopep8 = "*" pycryptodome = "*" numpy = "*" pytor = {path = "."} +fire = "*" [requires] python_version = "3" diff --git a/Pipfile.lock b/Pipfile.lock index 4b8162d..7e7d465 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "98280bfddd663d905e9cac4c207929de6b28343792535c2b0076f75094081cd2" + "sha256": "533e3ebed51a842c59590b22114b83da41d244def46dcceaae1899291dec379e" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,13 @@ ] }, "default": { + "fire": { + "hashes": [ + "sha256:c299d16064ff81cbb649b65988300d4a28b71ecfb789d1fb74d99ea98ae4d2eb" + ], + "index": "pypi", + "version": "==0.1.3" + }, "numpy": { "hashes": [ "sha256:1980f8d84548d74921685f68096911585fee393975f53797614b34d4f409b6da", @@ -81,6 +88,13 @@ }, "pytor": { "path": "." + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" } }, "develop": { diff --git a/README.md b/README.md index e69de29..6994b16 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,41 @@ +# pytor + +`pytor` is a simple python librairy to create and manager tor hidden services in version 2 and 3. + +```sh +$ pytor new +hostname: +cljfodghi4w5frc6.onion +private_key: +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQD2Gza8HXzgDGo+YwyhjOdgD0GY7ti5en8YGXtcsBi/JIwHdKZo +iLC4e5pWzlmB2ACdTw93ASFTGpPFs7nRxk4NuXo1BnvTsqzzcrsd9HV6xuKO7BkS +aTEY3tgrSvB2nQtM1WR9FQoyxV+EWeE0Q9vrBVpEizO4kHqFXRanOJpJbwIDAQAB +AoGAA/axPXteGP+qMGIJAIsT6OSmAlAKdoZGCL3UUkxVwbJVfQNAcNuOuRHojPBa +2bAAZogw8BI5Fq0NZzg7TGkctazKbvrmIx6o22spx2MOQXEc7lj3R3CJ8B+F1moz +9lNxIhNmw4bHeL3Sw5XMTPnOhCy1OKmouWrrcOj7B59YKrkCQQD2pWkZih6Ijl0y +xG3vB8w22krpe0YOne94aXwdggkji6Cfne8YRNWU9y8FvxGZgwfXZKwGCOSOgq7r +0SP7vEoZAkEA/3CP8BGY1jThrLHLWNPKm5Vu1+YZClL4ibs4cdtxIs0J0l+dQcYW +LMSkQpOy1C/nIIYPJpq9x8sCXG2BsRgwxwJAR9NhqONVAvVaZKdZUEuYB71IJXgV +rboGe61UTI+Ks8Q8kV7/urSI8imNkwHSUT8cMHiLs/IxBOM/p0KvVOa/OQJAHlXY +0jLUysOW9XJb6t2kFxwFAODTonU+DOVOC796zR46h2BRhaknowNrWni96RMTSLqC +/BuuZBbI3f8nQsfTqwJBAMX/KjXO/MqcB8TAjyKWHNyVR4T8OJM5lgbk8IGLKE5/ +w96jWD0AEePqKKdWofLImi074zMSyMKuu6RFrkBSUuI= +-----END RSA PRIVATE KEY----- +``` + +``` +$ mkdir test +$ pytor new-hidden-service test --version 3 +FYI: Binary data is base64 encoded +path: +test +hostname: +byb3bkhwi2ccbrctsqkowckpvk3tok36geddzg4l2m6yn6mrw626nqid.onion +hs_ed25519_secret_key: +PT0gZWQyNTUxOXYxLXNlY3JldDogdHlwZTAgPT0AAAAwIFsWaVtOk8r3RvXnkZcmxwIaDmmOdV8D7KaVf6yBWjVUIUTPpOWNQ9+hEiPKUclJ1RpflZ9FSdPgSj0j0tE3 +hs_ed25519_public_key: +PT0gZWQyNTUxOXYxLXB1YmxpYzogdHlwZTAgPT0AAAAOA7Co9kaEIMRTlBTrCU+qtzcrfjEGPJuL0z2G+ZG3tQ== +``` + +(more doc soon, I'm tired right mow ~) diff --git a/pytor/__init__.py b/pytor/__init__.py index e69de29..c1c20bd 100644 --- a/pytor/__init__.py +++ b/pytor/__init__.py @@ -0,0 +1,7 @@ +from .onion import OnionV2 +from .onion import OnionV3 + +__all__ = [ + 'OnionV2', + 'OnionV3', +] diff --git a/pytor/__main__.py b/pytor/__main__.py index e69de29..ec3037a 100644 --- a/pytor/__main__.py +++ b/pytor/__main__.py @@ -0,0 +1,77 @@ +import json +import sys + +import fire +import yaml + +from .onion import NonEmptyDirException +from .onion import OnionV2 +from .onion import OnionV3 + + +class Format(object): + + def __init__(self, format: str): + attr = 'print_{format}'.format(format=format) + if not hasattr(self, attr): + raise Exception('Output format not valid') + self.method = getattr(self, attr) + print('FYI: Binary data is base64 encoded', file=sys.stderr) + + def print(self, data: dict): + self.method(data) + + def print_plain(self, data: dict): + for key, value in data.items(): + print("{key}:\n{value}".format(key=key, value=value)) + + def print_json(self, data: dict): + print(json.dumps(data, indent=4)) + + def print_yaml(self, data: dict): + print(yaml.dump(data)) + + +class Pytor(object): + + _obj = { + 2: OnionV2, + 3: OnionV3, + } + + def __init__(self, version: int = 2, format: str = 'plain'): + if version not in self._obj: + raise Exception('Onion version not valid') + self._version = version + self._print = Format(format).print + self._stderr: lambda x: print(x, file=sys.stderr) + + @property + def _cls(self): + return self._obj[self._version] + + def new(self): + obj = self._cls() + self._print(obj.serialize()) + + def new_hidden_service(self, path: str, force: bool = False): + obj = self._cls() + try: + obj.write_hidden_service(path=path, force=force) + except NonEmptyDirException: + s = input( + 'Dir {path} not empty, override? [Y/n]'.format(path=path) + ) + if not s or s.lower() == 'y': + obj.write_hidden_service(path=path, force=True) + else: + print('Canceled...') + self._print({'path': path, **obj.serialize()}) + + +def main(): + fire.Fire(Pytor) + + +if __name__ == '__main__': + main() diff --git a/pytor/onion.py b/pytor/onion.py index 2902202..e432ae2 100644 --- a/pytor/onion.py +++ b/pytor/onion.py @@ -3,6 +3,7 @@ import re 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 @@ -17,6 +18,8 @@ from .ed25519 import Ed25519 __all__ = [ 'OnionV2', 'OnionV3', + 'EmptyDirException', + 'NonEmptyDirException', ] @@ -24,6 +27,10 @@ class EmptyDirException(Exception): pass +class NonEmptyDirException(Exception): + pass + + class Onion(ABC): ''' Interface to implement hidden services keys managment @@ -92,10 +99,12 @@ class Onion(ABC): raise Exception( '{path} should be an existing directory'.format(path=path) ) - if os.path.exists( + 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 Exception( + )) 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: @@ -187,6 +196,12 @@ class OnionV2(Onion): '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): ''' @@ -261,3 +276,12 @@ class OnionV3(Onion): 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(), + } diff --git a/setup.py b/setup.py index c2a0693..47dbf69 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import setup setup( name='pytor', - version='0.1.1', + version='0.1.2', packages=find_packages(), @@ -33,11 +33,11 @@ setup( install_requires=['pycryptodome==3.8.1'], - # entry_points={ - # 'console_scripts': [ - # 'pytor = pytor:main', - # ], - # }, + entry_points={ + 'console_scripts': [ + 'pytor = pytor.__main__:main', + ], + }, license="WTFPL", )