diff --git a/.gitignore b/.gitignore index c5a6848..dcf5b77 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ build/ dist/ *.pyc __pycache__ -maltego_trx.egg-info \ No newline at end of file +maltego_trx.egg-info +.pytest_cache diff --git a/demo/gunicorn/project.py b/demo/gunicorn/project.py index 09ab5f6..410e445 100644 --- a/demo/gunicorn/project.py +++ b/demo/gunicorn/project.py @@ -1,11 +1,10 @@ import sys import transforms -from maltego_trx.registry import register_transform_function, register_transform_classes -from maltego_trx.server import app, application +from maltego_trx.registry import register_transform_classes +from maltego_trx.server import app from maltego_trx.handler import handle_run -# register_transform_function(transform_func) register_transform_classes(transforms) handle_run(__name__, sys.argv, app) diff --git a/maltego_trx/__init__.py b/maltego_trx/__init__.py index 288108b..bf730e4 100644 --- a/maltego_trx/__init__.py +++ b/maltego_trx/__init__.py @@ -1 +1 @@ -VERSION = "1.3.6" +VERSION = "1.3.7" diff --git a/maltego_trx/oauth.py b/maltego_trx/oauth.py new file mode 100644 index 0000000..e0d3464 --- /dev/null +++ b/maltego_trx/oauth.py @@ -0,0 +1,156 @@ +"""Maltego OAuth Crypto Helper""" +import base64 +from Crypto import Random +from Crypto.Cipher import PKCS1_v1_5, AES +from Crypto.Hash import SHA +from Crypto.PublicKey import RSA +from requests.auth import AuthBase + + +class MaltegoOauth: + """ + A Crypto Helper for Maltego OAuth Secrets received from the Transform Distribution Server + The TDS will send back an encrypted combination of the following : + 1. Token + 2. Token Secret + 3. Refresh Token + 4. Expires In + + Contains 1 Methods: + 1. decrypt_secrets(private_key_path="pem file", ciphertext="request.getTransformSetting('name from TDS')") + """ + + @staticmethod + def _rsa_decrypt(private_key_path=None, ciphertext=None): + """ + RSA Decryption function, returns decrypted plaintext in b64 encoding + """ + dsize = SHA.digest_size + sentinel = Random.new().read(20 + dsize) + ciphertext = base64.b64decode(ciphertext) + private_key = RSA.import_key(open(private_key_path).read()) + cipher = PKCS1_v1_5.new(private_key) + plaintext = cipher.decrypt(ciphertext, sentinel).decode('utf8') + return plaintext + + @staticmethod + def _aes_decrypt(key=None, ciphertext=None): + """ + AES Decryption function, returns decrypted plaintext value + """ + unpad = lambda s: s[:-ord(s[len(s) - 1:])] + key = base64.b64decode(key) + ciphertext = base64.b64decode(ciphertext) + cipher = AES.new(key, AES.MODE_ECB) + plaintext = unpad(cipher.decrypt(ciphertext)).decode('utf8') + return plaintext + + @classmethod + def decrypt_secrets(cls, private_key_path=None, encoded_ciphertext=None): + """ + The TDS will send back an encrypted combination of the following : + 1. Token + 2. Token Secret + 3. Refresh Token + 4. Expires In + + This function decodes the combinations and decrypts as required and returns a dictionary with the following keys + {"token":"", + "token_secret": "", + "refresh_token": "", + "expires_in": ""} + """ + + encrypted_fields = encoded_ciphertext.split("$") + + if len(encrypted_fields) == 1: + token = cls._rsa_decrypt(private_key_path, encrypted_fields[0]) + token_fields = { + "token": token + } + + elif len(encrypted_fields) == 2: + token = cls._rsa_decrypt(private_key_path, encrypted_fields[0]) + token_secret = cls._rsa_decrypt(private_key_path, encrypted_fields[1]) + token_fields = { + "token": token, + "token_secret": token_secret + } + + elif len(encrypted_fields) == 3: + aes_key = cls._rsa_decrypt(private_key_path, encrypted_fields[2]) + token = cls._aes_decrypt(aes_key, encrypted_fields[0]) + token_secret = cls._aes_decrypt(aes_key, encrypted_fields[1]) + token_fields = { + "token": token, + "token_secret": token_secret + } + elif len(encrypted_fields) == 4: + token = cls._rsa_decrypt(private_key_path, encrypted_fields[0]) + token_secret = cls._rsa_decrypt(private_key_path, encrypted_fields[1]) + refresh_token = cls._rsa_decrypt(private_key_path, encrypted_fields[2]) + expires_in = cls._rsa_decrypt(private_key_path, encrypted_fields[3]) + token_fields = { + "token": token, + "token_secret": token_secret, + "refresh_token": refresh_token, + "expires_in": expires_in + } + elif len(encrypted_fields) == 5: + aes_key = cls._rsa_decrypt(private_key_path, encrypted_fields[4]) + token = cls._aes_decrypt(aes_key, encrypted_fields[0]) + token_secret = cls._aes_decrypt(aes_key, encrypted_fields[1]) + refresh_token = cls._aes_decrypt(aes_key, encrypted_fields[2]) + expires_in = cls._aes_decrypt(aes_key, encrypted_fields[3]) + token_fields = { + "token": token, + "token_secret": token_secret, + "refresh_token": refresh_token, + "expires_in": expires_in + } + else: + token_fields = { + "token": "", + "token_secret": "", + "refresh_token": "", + "expires_in": "" + } + + return token_fields + + +class OAuth2BearerToken(AuthBase): + """Implements OAuth2 Bearer access token authentication. + + Pass this object via the `auth` parameter to a request or a + session object in order to authenticate your requests. + + Example usage, once you have the `access_token`: + + class GreetPerson(DiscoverableTransform): + + @classmethod + def create_entities(cls, request, response): + person_name = request.Value + + private_key_path = "private_key.pem" + + encrypted_secrets = request.getTransformSetting('maltego.web.api.key.linkedin') + + token_fields = MaltegoCrypto.decrypt_secrets(private_key_path,encrypted_secrets) + + api_url = ("https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))") + auth = OAuth2BearerToken(token_fields['token']) + result = requests.get(api_url,auth=auth) + + response.addEntity(Phrase, result.text) + """ + + def __init__(self, access_token): + self.access_token = access_token + + def __call__(self, request): + request.headers['Authorization'] = 'Bearer {}'.format( + self.access_token + ) + return request diff --git a/setup.py b/setup.py index 2ac37d7..6092967 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,8 @@ license='MIT', install_requires=[ 'flask>=1', - 'six>=1' + 'six>=1', + 'pycryptodome>=3.9.7' ], packages=[ 'maltego_trx', diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test.py b/tests/test.py new file mode 100644 index 0000000..c03f577 --- /dev/null +++ b/tests/test.py @@ -0,0 +1,33 @@ +from maltego_trx.oauth import MaltegoOauth + +okta_ciphertoken = "pZ598ZEZ7EwpBQSOJSvCZJKkcWhtbX95K7Q0f0hwbk93O+xaUB4/NegK3r54PH1NReis/Jgt4UbGc5oCuU+R7UDM1icoDUmyCmV1U78iqjHElPdDBHlxPrl2zoXBwcU1iFbukDNy49Xghy3cwwqhmEXg/FKnYUlBQl6jdf06kE1pwfHiRgF5NrJISAD7eCmUybqiAVRDW4lbLeMGk/uSrrCpDQBxd2Em/sYKBzqO87pQRPLll23G9KNxyVinHQksGgc+dI0OPYv9EQcUs7g1JFraUKUeFjHAf/OyGGrp6WH84nGqG35afkH9xsDmySBb5DZWRjef8DzSd7oRagoGq3wKBHDh700mMKf/YkR8L9cK4l1w4yNh7EfIVlCWD5yWJUx4a1lN9CvJmFs7N9A903spVVGeP8avz50raWFb5g/4XhtIpei4ylVFi49dDeVjgb0BR0I4vuU7VDmcwFweAtSfclPb5Xfp98zI9yXnr8DB31gmzf3DIkVFIIFLxZe0gG+BSIEmC7/z0L7J+YvIqoGDq8jC0Ehe9IVXYfK1yluWAsbwj62rNAzkPVSe5l+FMQFrQRXpxZFBMXzphqRAhyYKfGNX/tocoMJoLFp8O3PBRC0kIHjTNaSX8KpftNYWreEdZx6wjk2n2eeOXM5nVeSOx4uoIifRe4NUV7VYzbGm3FSRc0k1HZIGHwn1WTvn95U3crvwFN86N3k41JU+0kzSpsLxy+Y63VfPxYTbRUET/wIOf0NTfG96QCPtVKkdVh9aTqnhaN0TpGoaZQEjUqpURxbj+8mT3g6MI4vOC9Qr6vw2wIkvzehr/yKxh6vsnfUUw8UJ1YtlZI7exPsHyFLUr2J3VvpDsaemRcn1KY/uHJZ3MMHK9GNciLA6Skw8SL38mw1tJJZ1h1bMMk1IYgBZLe2ZSXXyv7JaDPq/WJmHWDaIalmXE1cKMA56qMgYGdHxs3Pai5nwEtzIGVS1OcuMnBF6sOMMBTKSFAJmlcYo8NGwjKqe/yy1cVJZe7Ucv/jy/IS3AdE1my8cDya92jDsxifsaONQMm6YIs54SIA=$f7TU33wTTxYFtbVnwJeGGw==$c64l75qukq+980EkB1tQZKEicryt6HBB0IOjeMSRQmLOYCQpTcaXlPRJ3QdS+DfRocor7OL36wLAooRqz1IaN4QQ5+6Wx5reEXFVgKtaDX777I046DM5QU3Jf+iibSbm3mXYMw0z+OknbXyGLnc7ceSaPTJdP1LkCGQ75fLMXHCLWSpqwOKHOBhqyQGwrYlj2WxPzmOfAMaIkjKZIQWzoGjrYRzwkNCQbxykJcwD5TVuVDwAHyp84zcvW0WWoUZ+rrrooJwuJJQEdiTwLZsseqklXRNso4e5eFQwH49T9IDPHkKfVusu6rLiMNgFyc18rFR1d/BYLXBu7uzMAuvQ8wZdcOtJYx+JLJmOaPI65ymGNFTpwTHYnDBTXmpW0qX2dtEglAERw1nrdl94fwsa+I/iw3H7VIivqRF3pAchfvdNF75MIEm6XH2UXY/sZD7zjMdxxwKQiaEg2bpLd2mEtqsLc0mTq/zd03ZLEUnzXsXlp8brCfDgjoVsZAJH+ElQ6wr3dxzRMlDIxWWBYFcv8LnGVQk/Vciqtee780Yh/lgttv06kyXz81dIWjumz8TqV2eV9ZpP/YxTnNzAa91fleGNah/IKhMOV8PsGy2uOnHRiUL218p1T6+Cm9B2kmL1C/2MM3080NjJVWIfqYsyTGPbR+hURK0RB/oteG5QWPs=" +private_key_path = "test_private_key.pem" + + +def testOktaDecrypt(): + decrypted_linkedIn_token = MaltegoOauth.decrypt_secrets(private_key_path, okta_ciphertoken)['token'] + expected_token = "eyJraWQiOiJ4ZzEyMmNZMXQ0NU5GM2l5YlUzUF9tWGluYkxEXzVrRHFwMkQ0VEl2RGd3IiwiYWxnIjoiUlMyNTYifQ.eyJ2ZXIiOjEsImp0aSI6IkFULklldnVNcXNJNDFqMURldEdPU1N2Q2VoNlI5eUxEQ0EydFBJdl9FZ3hDU3ciLCJpc3MiOiJodHRwczovL2Rldi02MTcyNDUub2t0YS5jb20vb2F1dGgyL2RlZmF1bHQiLCJhdWQiOiJhcGk6Ly9kZWZhdWx0IiwiaWF0IjoxNTg5NzgzNjU0LCJleHAiOjE1ODk3ODcyNTQsImNpZCI6IjBvYTR1YTZ1YlJIeFRDTGg4NHg2IiwidWlkIjoiMDB1NHU0ZzA4SDFqYXQwVnA0eDYiLCJzY3AiOlsib3BlbmlkIl0sInN1YiI6InRtQG1hbHRlZ28uY29tIn0.dAZrIZ9NCIqIx3fr8_0EexYCzarj8CC4CvWpVgZQCvfhtV08tGnMdWN8yuzADYvJUSzDz_meqIMMUCaVOpYZ5vzepr3LZfT-Xn00KoaaRcHGLPowphvsEPkpimvJqgnRmWw0e0VTH5Pfg9eyl3o2UUUsDofM-RkJNjxB4Uf0D6IyZaCyl0s_KhcXGZuh7hDGoR76UcKaCqRXqmqnOZs_GoMPIoDS5NHIze0MOK6sgKr8uiLikIhdh481n8PyWzPX2XZLUNcjogqX280so6Ki24VBloyxt5VgAJ1gV-e9SDj-9QSdQohQNDDSHbgiO4s1TUlhFKIm5UQWUImdhSuv1w" + assert decrypted_linkedIn_token == expected_token + + +def testReadKey(): + private_key = open(private_key_path).read() + assert private_key.__sizeof__() == 3292 + + +def testCipherSplit(): + encrypted_fields = okta_ciphertoken.split("$") + assert len(encrypted_fields) == 3 + + +def testToken_Field_Creation(): + encrypted_fields = okta_ciphertoken.split("$") + aes_key = MaltegoOauth._rsa_decrypt(private_key_path, encrypted_fields[2]) + token = MaltegoOauth._aes_decrypt(aes_key, encrypted_fields[0]) + token_secret = MaltegoOauth._aes_decrypt(aes_key, encrypted_fields[1]) + token_fields = { + "token": token, + "token_secret": token_secret + } + decrypted_azure_token_dict = MaltegoOauth.decrypt_secrets(private_key_path, okta_ciphertoken) + assert token_fields == decrypted_azure_token_dict diff --git a/tests/test_private_key.pem b/tests/test_private_key.pem new file mode 100644 index 0000000..d3f357f --- /dev/null +++ b/tests/test_private_key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +e+tSC0NMYgwe0MkkVArhPdYdAbQS +-----END PRIVATE KEY----- \ No newline at end of file