diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..53617bf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,14 @@ +version: '3' + +networks: + redis: + external: false + +services: + db: + image: redis + restart: always + ports: + - "127.0.0.1:6379:6379" + networks: + - redis diff --git a/requirements.txt b/requirements.txt index 4619f3d..764db5e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,6 @@ musicbrainzngs flask flask-restful gunicorn -flask-jwt-extended sentry-sdk[flask] py-healthcheck flask-redis diff --git a/src/aes_encrypt.py b/src/aes_encrypt.py index 73a576a..c65d7a1 100644 --- a/src/aes_encrypt.py +++ b/src/aes_encrypt.py @@ -10,48 +10,48 @@ __module_name__ = "aes_encrypt" __version__text__ = "1" import base64 -import json +import pickle from Crypto.Cipher import AES from fred import flaskred -from schemas import UserSchema class AESCrypto: - def __init__(self, encoded_secret_key: str, padding_character: bytes = 'a'.encode('UFT-8')): + def __init__(self, encoded_secret_key: str, padding_character: bytes = '{'.encode('ascii')): self.padding_character = padding_character self.encoded_secret_key = encoded_secret_key - def encrypt_message(self, private_msg) -> bytes: + def encrypt_message(self, private_msg: str) -> tuple: secret_key = base64.b64decode(self.encoded_secret_key) cipher = AES.new(secret_key, AES.MODE_EAX) - padded_private_msg = private_msg + (self.padding_character.decode('UFT-8') * ((16 - len(private_msg)) % 16)) - encrypted_msg = cipher.encrypt(padded_private_msg) - encoded_encrypted_msg = base64.b64encode(encrypted_msg) - return encoded_encrypted_msg + ciphertext, tag = cipher.encrypt_and_digest(private_msg.encode('UTF-8')) + return cipher.nonce, ciphertext, tag - def decrypt_message(self, encoded_encrypted_msg) -> str: + def decrypt_message(self, nonce: bytes, encoded_encrypted_msg: bytes, tag: bytes) -> str: secret_key = base64.b64decode(self.encoded_secret_key) - encrypted_msg = base64.b64decode(encoded_encrypted_msg) - cipher = AES.new(secret_key, AES.MODE_EAX) - decrypted_msg = cipher.decrypt(encrypted_msg) - unpadded_private_msg = decrypted_msg.rstrip(self.padding_character) - return unpadded_private_msg.decode('UTF-8') + cipher = AES.new(secret_key, AES.MODE_EAX, nonce) + msg = cipher.decrypt_and_verify(encoded_encrypted_msg, tag).decode('UTF-8') + return msg class EncryptedUserRedis: + def __init__(self, encoded_secret_key: str): self.aes = AESCrypto(encoded_secret_key) - self.userschema = UserSchema(many=False) - def store(self, user: UserSchema) -> None: - plaindict = self.userschema.dump(user) - plaindict['password'] = self.aes.encrypt_message(user['password']) - flaskred.set(user['name'], json.dumps(plaindict).encode('UTF-8')) + def store(self, user: dict) -> None: + nonce, ciphertext, tag = self.aes.encrypt_message(user['password']) + user['nonce'] = nonce + user['ciphertext'] = ciphertext + user['tag'] = tag + user.pop('password', None) + flaskred.set(user['name'], pickle.dumps(user)) - def load(self, username: str) -> UserSchema: - encryptedstr = flaskred.get(username).decode('UTF-8') - encrypteddict = json.loads(encryptedstr) - user = UserSchema(name=encrypteddict['name'], password=self.aes.decrypt_message(encrypteddict['password'])) - return user + def load(self, username: str) -> dict: + encrypteddict = pickle.loads(flaskred.get(username)) + + plaindict = {"name": encrypteddict['name'], + "password": self.aes.decrypt_message(encrypteddict['nonce'], encrypteddict['ciphertext'], + encrypteddict['tag'])} + return plaindict diff --git a/src/app.py b/src/app.py index f37bca1..b635342 100644 --- a/src/app.py +++ b/src/app.py @@ -1,19 +1,17 @@ #!/usr/bin/env python3 -import os + import logging from flask import Flask from flask_restful import Api import sentry_sdk from sentry_sdk.integrations.flask import FlaskIntegration -from sentry_sdk.integrations.sqlalchemy import SqlalchemyIntegration from healthcheck import HealthCheck -from jwtman import jwtman from marshm import ma from fred import flaskred -from config import SENTRY_DSN, JWT_SECRET_KEY, RELEASEMODE, RELEASE_ID, PORT, DEBUG, REDIS_URL +from config import SENTRY_DSN, RELEASEMODE, RELEASE_ID, PORT, DEBUG, REDIS_HOST from errorhandlers import register_all_error_handlers -from resources import LoginApi +from resources import LoginApi, LogoffApi, MeApi """ Main Flask RESTful API @@ -27,7 +25,7 @@ __version__text__ = "1" if SENTRY_DSN: sentry_sdk.init( dsn=SENTRY_DSN, - integrations=[FlaskIntegration(), SqlalchemyIntegration()], + integrations=[FlaskIntegration()], traces_sample_rate=1.0, send_default_pii=True, release=RELEASE_ID, @@ -36,13 +34,13 @@ if SENTRY_DSN: ) app = Flask(__name__) -app.config['JWT_SECRET_KEY'] = JWT_SECRET_KEY -app.config['REDIS_URL'] = REDIS_URL +app.config['JWT_BLACKLIST_ENABLED'] = True +app.config['JWT_BLACKLIST_TOKEN_CHECKS'] = ['access', 'refresh'] +app.config['REDIS_URL'] = f"redis://{REDIS_HOST}:6379/0" api = Api(app) health = HealthCheck() ma.init_app(app) flaskred.init_app(app) -jwtman.init_app(app) formatter = logging.Formatter( fmt="%(asctime)s - %(levelname)s - %(module)s - %(message)s" @@ -58,18 +56,17 @@ logger.addHandler(handler) api.add_resource(LogoffApi, '/api/auth/logoff') api.add_resource(LoginApi, '/api/auth/login') api.add_resource(MeApi, '/api/auth/me') -api.add_resource(ListsApi, '/api/lists') -api.add_resource(SingleListApi, '/api/lists/') -api.add_resource(TrackApi, '/api/lists//') +# api.add_resource(ListsApi, '/api/lists') +# api.add_resource(SingleListApi, '/api/lists/') +# api.add_resource(TrackApi, '/api/lists//') app.add_url_rule("/healthz", "healthcheck", view_func=lambda: health.run()) register_all_error_handlers(app) - if __name__ == "__main__": app.run( debug=bool(DEBUG), host="0.0.0.0", port=int(PORT), - ) \ No newline at end of file + ) diff --git a/src/config.py b/src/config.py index 131ced6..26ca8f3 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import os -from uuid import uuid4 """ Configuration @@ -20,8 +19,6 @@ SENTRY_DSN = os.environ.get("SENTRY_DSN") RELEASE_ID = os.environ.get("RELEASE_ID", "test") RELEASEMODE = os.environ.get("ONSPOT_RELEASEMODE", "dev") -JWT_SECRET_KEY = os.getenv("ONSPOT_JWT_SECRET_KEY", str(uuid4())) - -REDIS_URL = os.getenv("ONSPOT_REDIS_URL") +REDIS_HOST = os.getenv("ONSPOT_REDIS_HOST") ENCODED_SECRET_KEY = os.getenv("ONSPOT_ENCODED_SECRET_KEY") diff --git a/src/jwtman.py b/src/jwtman.py deleted file mode 100644 index ab4904b..0000000 --- a/src/jwtman.py +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env python3 - -from flask_jwt_extended import JWTManager - -""" -JWTManager -""" - -__author__ = '@tormakris' -__copyright__ = "Copyright 2020, onSpot Team" -__module_name__ = "jwtman" -__version__text__ = "1" - -jwtman = JWTManager() diff --git a/src/resources.py b/src/resources.py index 82ef029..f654b6c 100644 --- a/src/resources.py +++ b/src/resources.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 -import datetime -from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity +import uuid + from flask_restful import Resource -from flask import request, current_app, abort +from flask import request, current_app, abort, request +from fred import flaskred from config import ENCODED_SECRET_KEY from schemas import UserSchema, ListSchema, TrackSchema from aes_encrypt import EncryptedUserRedis @@ -38,8 +39,32 @@ class LoginApi(Resource): current_app.logger.warning(e) abort(417, INVALID_JSON_SCHEMA_MSG) - self.encryptor.store(userobj) + self.encryptor.store(body) - expires = datetime.timedelta(days=7) - access_token = create_access_token(identity=str(userobj['name']), expires_delta=expires) - return {'token': access_token}, 200 + token = str(uuid.uuid4()) + + flaskred.set(token, userobj['name'].encode('UTF-8')) + + return { + 'token': token + }, 200 + + +class LogoffApi(Resource): + """ + See: https://swagger.kmlabz.com/?urls.primaryName=onSpot%20Backend#/backend/logoff + """ + + def delelete(self): + flaskred.delete(flaskred.get(request.headers.get('Authorization')).decode('UTF-8')) + flaskred.delete(request.headers.get('Authorization')) + return 204 + + +class MeApi(Resource): + """ + See: https://swagger.kmlabz.com/?urls.primaryName=onSpot%20Backend#/backend/currentUser + """ + + def get(self): + return {"name": flaskred.get(request.headers.get('Authorization')).decode('UTF-8')}, 200