diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 56881bb17..482a45ef8 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -1032,6 +1032,22 @@ six = ">=1.3.0" [package.extras] docs = ["sphinx"] +[[package]] +name = "flask-simple-crypt" +version = "0.3.3" +description = "Flask extension based on simple-crypt that allows simple, secure encryption and decryption for Python." +category = "main" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Flask-Simple-Crypt-0.3.3.tar.gz", hash = "sha256:0d4033b6c9a03ac85d10f0fd213914390217dc53b2d41d153fa050fee9723594"}, + {file = "Flask_Simple_Crypt-0.3.3-py3-none-any.whl", hash = "sha256:08c3fcad955ac148bb885b1de4798c1cfce8512452072beee414bacf1552e8ef"}, +] + +[package.dependencies] +Flask = "*" +pycryptodome = "*" + [[package]] name = "flask-sqlalchemy" version = "3.0.2" @@ -1980,6 +1996,49 @@ files = [ {file = "pycodestyle-2.8.0.tar.gz", hash = "sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"}, ] +[[package]] +name = "pycryptodome" +version = "3.17" +description = "Cryptographic library for Python" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +files = [ + {file = "pycryptodome-3.17-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:2c5631204ebcc7ae33d11c43037b2dafe25e2ab9c1de6448eb6502ac69c19a56"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:04779cc588ad8f13c80a060b0b1c9d1c203d051d8a43879117fe6b8aaf1cd3fa"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:f812d58c5af06d939b2baccdda614a3ffd80531a26e5faca2c9f8b1770b2b7af"}, + {file = "pycryptodome-3.17-cp27-cp27m-manylinux2014_aarch64.whl", hash = "sha256:9453b4e21e752df8737fdffac619e93c9f0ec55ead9a45df782055eb95ef37d9"}, + {file = "pycryptodome-3.17-cp27-cp27m-musllinux_1_1_aarch64.whl", hash = "sha256:121d61663267f73692e8bde5ec0d23c9146465a0d75cad75c34f75c752527b01"}, + {file = "pycryptodome-3.17-cp27-cp27m-win32.whl", hash = "sha256:ba2d4fcb844c6ba5df4bbfee9352ad5352c5ae939ac450e06cdceff653280450"}, + {file = "pycryptodome-3.17-cp27-cp27m-win_amd64.whl", hash = "sha256:87e2ca3aa557781447428c4b6c8c937f10ff215202ab40ece5c13a82555c10d6"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:f44c0d28716d950135ff21505f2c764498eda9d8806b7c78764165848aa419bc"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:5a790bc045003d89d42e3b9cb3cc938c8561a57a88aaa5691512e8540d1ae79c"}, + {file = "pycryptodome-3.17-cp27-cp27mu-manylinux2014_aarch64.whl", hash = "sha256:d086d46774e27b280e4cece8ab3d87299cf0d39063f00f1e9290d096adc5662a"}, + {file = "pycryptodome-3.17-cp27-cp27mu-musllinux_1_1_aarch64.whl", hash = "sha256:5587803d5b66dfd99e7caa31ed91fba0fdee3661c5d93684028ad6653fce725f"}, + {file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_universal2.whl", hash = "sha256:e7debd9c439e7b84f53be3cf4ba8b75b3d0b6e6015212355d6daf44ac672e210"}, + {file = "pycryptodome-3.17-cp35-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ca1ceb6303be1282148f04ac21cebeebdb4152590842159877778f9cf1634f09"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:dc22cc00f804485a3c2a7e2010d9f14a705555f67020eb083e833cabd5bd82e4"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80ea8333b6a5f2d9e856ff2293dba2e3e661197f90bf0f4d5a82a0a6bc83a626"}, + {file = "pycryptodome-3.17-cp35-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c133f6721fba313722a018392a91e3c69d3706ae723484841752559e71d69dc6"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:333306eaea01fde50a73c4619e25631e56c4c61bd0fb0a2346479e67e3d3a820"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_i686.whl", hash = "sha256:1a30f51b990994491cec2d7d237924e5b6bd0d445da9337d77de384ad7f254f9"}, + {file = "pycryptodome-3.17-cp35-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:909e36a43fe4a8a3163e9c7fc103867825d14a2ecb852a63d3905250b308a4e5"}, + {file = "pycryptodome-3.17-cp35-abi3-win32.whl", hash = "sha256:a3228728a3808bc9f18c1797ec1179a0efb5068c817b2ffcf6bcd012494dffb2"}, + {file = "pycryptodome-3.17-cp35-abi3-win_amd64.whl", hash = "sha256:9ec565e89a6b400eca814f28d78a9ef3f15aea1df74d95b28b7720739b28f37f"}, + {file = "pycryptodome-3.17-pp27-pypy_73-macosx_10_9_x86_64.whl", hash = "sha256:e1819b67bcf6ca48341e9b03c2e45b1c891fa8eb1a8458482d14c2805c9616f2"}, + {file = "pycryptodome-3.17-pp27-pypy_73-manylinux2010_x86_64.whl", hash = "sha256:f8e550caf52472ae9126953415e4fc554ab53049a5691c45b8816895c632e4d7"}, + {file = "pycryptodome-3.17-pp27-pypy_73-win32.whl", hash = "sha256:afbcdb0eda20a0e1d44e3a1ad6d4ec3c959210f4b48cabc0e387a282f4c7deb8"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a74f45aee8c5cc4d533e585e0e596e9f78521e1543a302870a27b0ae2106381e"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:38bbd6717eac084408b4094174c0805bdbaba1f57fc250fd0309ae5ec9ed7e09"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f68d6c8ea2974a571cacb7014dbaada21063a0375318d88ac1f9300bc81e93c3"}, + {file = "pycryptodome-3.17-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:8198f2b04c39d817b206ebe0db25a6653bb5f463c2319d6f6d9a80d012ac1e37"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:3a232474cd89d3f51e4295abe248a8b95d0332d153bf46444e415409070aae1e"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4992ec965606054e8326e83db1c8654f0549cdb26fce1898dc1a20bc7684ec1c"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53068e33c74f3b93a8158dacaa5d0f82d254a81b1002e0cd342be89fcb3433eb"}, + {file = "pycryptodome-3.17-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:74794a2e2896cd0cf56fdc9db61ef755fa812b4a4900fa46c49045663a92b8d0"}, + {file = "pycryptodome-3.17.tar.gz", hash = "sha256:bce2e2d8e82fcf972005652371a3e8731956a0c1fbb719cc897943b3695ad91b"}, +] + [[package]] name = "pydocstyle" version = "6.1.1" @@ -3567,4 +3626,4 @@ testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools" [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.12" -content-hash = "966e10cff1c2553a407e0119d04fe01e7cdf40c1155086e551deb2c9df1914d6" +content-hash = "2fd5138221eabec441b601bb3769be478bed42099e72e20f7b8aaa1c1a888909" diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index 0cbb37c58..4f47921bb 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -72,6 +72,7 @@ dateparser = "^1.1.2" types-dateparser = "^1.1.4.1" flask-jwt-extended = "^4.4.4" pylint = "^2.15.10" +flask-simple-crypt = "^0.3.3" [tool.poetry.dev-dependencies] diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py index 0fc3d0496..ed0b5c05e 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/__init__.py @@ -13,6 +13,7 @@ from apscheduler.schedulers.base import BaseScheduler # type: ignore from flask.json.provider import DefaultJSONProvider from flask_cors import CORS # type: ignore from flask_mail import Mail # type: ignore +from flask_simple_crypt import SimpleCrypt # type: ignore from werkzeug.exceptions import NotFound import spiffworkflow_backend.load_database_models # noqa: F401 @@ -133,6 +134,11 @@ def create_app() -> flask.app.Flask: configure_sentry(app) + cipher = SimpleCrypt() + app.config["FSC_EXPANSION_COUNT"] = 2048 + cipher.init_app(app) + app.config["CIPHER"] = cipher + app.before_request(verify_token) app.before_request(AuthorizationService.check_for_permission) app.after_request(set_new_access_token_in_cookie) diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py b/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py index 61c8f6dad..13df219a4 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/routes/service_tasks_controller.py @@ -43,7 +43,7 @@ def authentication_callback( """Authentication_callback.""" verify_token(request.args.get("token"), force_run=True) response = request.args["response"] - SecretService().update_secret( + SecretService.update_secret( f"{service}/{auth_method}", response, g.user.id, create_if_not_exists=True ) return redirect( diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py index f34be93d6..f7c20e86c 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/secret_service.py @@ -1,41 +1,38 @@ """Secret_service.""" from typing import Optional +from flask import current_app + from spiffworkflow_backend.exceptions.api_error import ApiError from spiffworkflow_backend.models.db import db from spiffworkflow_backend.models.secret_model import SecretModel -# from cryptography.fernet import Fernet -# -# -# class EncryptionService: -# key = Fernet.generate_key() # this is your "password" -# cipher_suite = Fernet(key) -# encoded_text = cipher_suite.encrypt(b"Hello stackoverflow!") -# decoded_text = cipher_suite.decrypt(encoded_text) - class SecretService: """SecretService.""" - # def encrypt_key(self, plain_key: str) -> str: - # """Encrypt_key.""" - # # flask_secret = current_app.secret_key - # # print("encrypt_key") - # ... + CIPHER_ENCODING = "ascii" - # def decrypt_key(self, encrypted_key: str) -> str: - # """Decrypt key.""" - # ... + @classmethod + def _encrypt(cls, value: str) -> str: + encrypted_bytes: bytes = current_app.config["CIPHER"].encrypt(value) + return encrypted_bytes.decode(cls.CIPHER_ENCODING) - @staticmethod + @classmethod + def _decrypt(cls, value: str) -> str: + bytes_to_decrypt = bytes(value, cls.CIPHER_ENCODING) + decrypted_bytes: bytes = current_app.config["CIPHER"].decrypt(bytes_to_decrypt) + return decrypted_bytes.decode(cls.CIPHER_ENCODING) + + @classmethod def add_secret( + cls, key: str, value: str, user_id: int, ) -> SecretModel: """Add_secret.""" - # encrypted_key = self.encrypt_key(key) + value = cls._encrypt(value) secret_model = SecretModel(key=key, value=value, user_id=user_id) db.session.add(secret_model) try: @@ -62,8 +59,9 @@ class SecretService: message=f"Unable to locate a secret with the name: {key}. ", ) - @staticmethod + @classmethod def update_secret( + cls, key: str, value: str, user_id: Optional[int] = None, @@ -72,6 +70,7 @@ class SecretService: """Does this pass pre commit?""" secret_model = SecretModel.query.filter(SecretModel.key == key).first() if secret_model: + value = cls._encrypt(value) secret_model.value = value db.session.add(secret_model) try: diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py index 5931989bc..2701e97e5 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/service_task_service.py @@ -31,8 +31,8 @@ class ServiceTaskDelegate: secret_prefix = "secret:" # noqa: S105 if value.startswith(secret_prefix): key = value.removeprefix(secret_prefix) - secret = SecretService().get_secret(key) - return secret.value + secret = SecretService.get_secret(key) + return SecretService._decrypt(secret.value) file_prefix = "file:" if value.startswith(file_prefix): @@ -136,7 +136,7 @@ class ServiceTaskDelegate: secret_key = parsed_response["auth"] refreshed_token_set = json.dumps(parsed_response["refreshed_token_set"]) user_id = g.user.id if UserService.has_user() else None - SecretService().update_secret(secret_key, refreshed_token_set, user_id) + SecretService.update_secret(secret_key, refreshed_token_set, user_id) return json.dumps(parsed_response["api_response"]) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py index f7f5b5621..8d11fa493 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_secret_service.py @@ -72,7 +72,7 @@ class TestSecretService(SecretServiceTestHelpers): assert test_secret is not None assert test_secret.key == self.test_key - assert test_secret.value == self.test_value + assert SecretService._decrypt(test_secret.value) == self.test_value assert test_secret.user_id == with_super_admin_user.id def test_add_secret_duplicate_key_fails( @@ -98,7 +98,7 @@ class TestSecretService(SecretServiceTestHelpers): secret = SecretService().get_secret(self.test_key) assert secret is not None - assert secret.value == self.test_value + assert SecretService._decrypt(secret.value) == self.test_value def test_get_secret_bad_key_fails( self, @@ -123,13 +123,15 @@ class TestSecretService(SecretServiceTestHelpers): self.add_test_secret(with_super_admin_user) secret = SecretService.get_secret(self.test_key) assert secret - assert secret.value == self.test_value + assert SecretService._decrypt(secret.value) == self.test_value SecretService.update_secret( self.test_key, "new_secret_value", with_super_admin_user.id ) new_secret = SecretService.get_secret(self.test_key) assert new_secret - assert new_secret.value == "new_secret_value" # noqa: S105 + assert ( + SecretService._decrypt(new_secret.value) == "new_secret_value" + ) # noqa: S105 def test_update_secret_bad_secret_fails( self, @@ -205,7 +207,7 @@ class TestSecretServiceApi(SecretServiceTestHelpers): for key in ["key", "value", "user_id"]: assert key in secret.keys() assert secret["key"] == self.test_key - assert secret["value"] == self.test_value + assert SecretService._decrypt(secret["value"]) == self.test_value assert secret["user_id"] == with_super_admin_user.id def test_get_secret( @@ -224,7 +226,7 @@ class TestSecretServiceApi(SecretServiceTestHelpers): assert secret_response assert secret_response.status_code == 200 assert secret_response.json - assert secret_response.json["value"] == self.test_value + assert SecretService._decrypt(secret_response.json["value"]) == self.test_value def test_update_secret( self, @@ -237,7 +239,7 @@ class TestSecretServiceApi(SecretServiceTestHelpers): self.add_test_secret(with_super_admin_user) secret: Optional[SecretModel] = SecretService.get_secret(self.test_key) assert secret - assert secret.value == self.test_value + assert SecretService._decrypt(secret.value) == self.test_value secret_model = SecretModel( key=self.test_key, value="new_secret_value", @@ -254,7 +256,7 @@ class TestSecretServiceApi(SecretServiceTestHelpers): secret_model = SecretModel.query.filter( SecretModel.key == self.test_key ).first() - assert secret_model.value == "new_secret_value" + assert SecretService._decrypt(secret_model.value) == "new_secret_value" def test_delete_secret( self, @@ -267,7 +269,7 @@ class TestSecretServiceApi(SecretServiceTestHelpers): self.add_test_secret(with_super_admin_user) secret = SecretService.get_secret(self.test_key) assert secret - assert secret.value == self.test_value + assert SecretService._decrypt(secret.value) == self.test_value secret_response = client.delete( f"/v1.0/secrets/{self.test_key}", headers=self.logged_in_headers(with_super_admin_user),