diff --git a/monkey/monkey_island/cc/models/utils/field_encryptors/string_list_encryptor.py b/monkey/monkey_island/cc/models/utils/field_encryptors/string_list_encryptor.py index 63801cf69..91464c1fa 100644 --- a/monkey/monkey_island/cc/models/utils/field_encryptors/string_list_encryptor.py +++ b/monkey/monkey_island/cc/models/utils/field_encryptors/string_list_encryptor.py @@ -1,7 +1,7 @@ from typing import List from monkey_island.cc.models.utils.field_encryptors.i_field_encryptor import IFieldEncryptor -from monkey_island.cc.server_utils.encryptor import get_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor class StringListEncryptor(IFieldEncryptor): diff --git a/monkey/monkey_island/cc/resources/configuration_export.py b/monkey/monkey_island/cc/resources/configuration_export.py index 4a7aeec24..c9565011b 100644 --- a/monkey/monkey_island/cc/resources/configuration_export.py +++ b/monkey/monkey_island/cc/resources/configuration_export.py @@ -5,7 +5,7 @@ from flask import request from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.utils.encryption import encrypt_string +from monkey_island.cc.services.utils.password_encryption import PasswordBasedEncryptor class ConfigurationExport(flask_restful.Resource): @@ -20,6 +20,8 @@ class ConfigurationExport(flask_restful.Resource): if should_encrypt: password = data["password"] plaintext_config = json.dumps(plaintext_config) - config_export = encrypt_string(plaintext_config, password) + + pb_encryptor = PasswordBasedEncryptor(password) + config_export = pb_encryptor.encrypt(plaintext_config) return {"config_export": config_export, "encrypted": should_encrypt} diff --git a/monkey/monkey_island/cc/resources/configuration_import.py b/monkey/monkey_island/cc/resources/configuration_import.py index efa1d79a7..99b43f3ba 100644 --- a/monkey/monkey_island/cc/resources/configuration_import.py +++ b/monkey/monkey_island/cc/resources/configuration_import.py @@ -9,10 +9,10 @@ from flask import request from common.utils.exceptions import InvalidConfigurationError from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.utils.encryption import ( +from monkey_island.cc.services.utils.password_encryption import ( InvalidCiphertextError, InvalidCredentialsError, - decrypt_ciphertext, + PasswordBasedEncryptor, is_encrypted, ) @@ -72,7 +72,8 @@ class ConfigurationImport(flask_restful.Resource): try: config = request_contents["config"] if ConfigurationImport.is_config_encrypted(request_contents["config"]): - config = decrypt_ciphertext(config, request_contents["password"]) + pb_encryptor = PasswordBasedEncryptor(request_contents["password"]) + config = pb_encryptor.decrypt(config) return json.loads(config) except (JSONDecodeError, InvalidCiphertextError): logger.exception( diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 35879a1d4..5ed167126 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -27,8 +27,8 @@ from monkey_island.cc.server_utils.consts import ( # noqa: E402 GEVENT_EXCEPTION_LOG, MONGO_CONNECTION_TIMEOUT, ) -from monkey_island.cc.server_utils.encryptor import initialize_encryptor # noqa: E402 from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402 +from monkey_island.cc.server_utils.key_encryptor import initialize_encryptor # noqa: E402 from monkey_island.cc.services.initialize import initialize_services # noqa: E402 from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list # noqa: E402 from monkey_island.cc.services.utils.network_utils import local_ip_addresses # noqa: E402 diff --git a/monkey/monkey_island/cc/server_utils/encryptor.py b/monkey/monkey_island/cc/server_utils/key_encryptor.py similarity index 54% rename from monkey/monkey_island/cc/server_utils/encryptor.py rename to monkey/monkey_island/cc/server_utils/key_encryptor.py index ab9bc617a..e41cf56f4 100644 --- a/monkey/monkey_island/cc/server_utils/encryptor.py +++ b/monkey/monkey_island/cc/server_utils/key_encryptor.py @@ -1,17 +1,16 @@ -import base64 import os # PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but # is maintained. from Crypto import Random # noqa: DUO133 # nosec: B413 -from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 from monkey_island.cc.server_utils.file_utils import open_new_securely_permissioned_file +from monkey_island.cc.services.utils.key_encryption import KeyBasedEncryptor _encryptor = None -class Encryptor: +class DataStoreEncryptor: _BLOCK_SIZE = 32 _PASSWORD_FILENAME = "mongo_key.bin" @@ -32,30 +31,19 @@ class Encryptor: with open(password_file, "rb") as f: self._cipher_key = f.read() - def _pad(self, message): - return message + (self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)) * chr( - self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE) - ) - - def _unpad(self, message: str): - return message[0 : -ord(message[len(message) - 1])] - def enc(self, message: str): - cipher_iv = Random.new().read(AES.block_size) - cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv) - return base64.b64encode(cipher_iv + cipher.encrypt(self._pad(message).encode())).decode() + key_encryptor = KeyBasedEncryptor(self._cipher_key) + return key_encryptor.encrypt(message) - def dec(self, enc_message): - enc_message = base64.b64decode(enc_message) - cipher_iv = enc_message[0 : AES.block_size] - cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv) - return self._unpad(cipher.decrypt(enc_message[AES.block_size :]).decode()) + def dec(self, enc_message: str): + key_encryptor = KeyBasedEncryptor(self._cipher_key) + return key_encryptor.decrypt(enc_message) def initialize_encryptor(password_file_dir): global _encryptor - _encryptor = Encryptor(password_file_dir) + _encryptor = DataStoreEncryptor(password_file_dir) def get_encryptor(): diff --git a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py index 0a9a1045b..6a431f35a 100644 --- a/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py +++ b/monkey/monkey_island/cc/services/attack/technique_reports/technique_report_tools.py @@ -1,4 +1,4 @@ -from monkey_island.cc.server_utils.encryptor import get_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor def parse_creds(attempt): diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index ba4083286..26e3ab971 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -19,7 +19,7 @@ from common.config_value_paths import ( USER_LIST_PATH, ) from monkey_island.cc.database import mongo -from monkey_island.cc.server_utils.encryptor import get_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor from monkey_island.cc.services.config_manipulator import update_config_per_mode from monkey_island.cc.services.config_schema.config_schema import SCHEMA from monkey_island.cc.services.mode.island_mode_service import ModeNotSetError, get_mode diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 7fa5654c5..0a0ccce15 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -3,7 +3,7 @@ import copy import dateutil from monkey_island.cc.models import Monkey -from monkey_island.cc.server_utils.encryptor import get_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.edge.displayed_edge import EdgeService from monkey_island.cc.services.node import NodeService diff --git a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py index 73a81e332..1bcc61ecd 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -1,6 +1,6 @@ import logging -from monkey_island.cc.server_utils.encryptor import get_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.node import NodeService from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501 diff --git a/monkey/monkey_island/cc/services/utils/encryption.py b/monkey/monkey_island/cc/services/utils/encryption.py deleted file mode 100644 index ae4af2257..000000000 --- a/monkey/monkey_island/cc/services/utils/encryption.py +++ /dev/null @@ -1,59 +0,0 @@ -import base64 -import io -import logging - -import pyAesCrypt - -BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef - -logger = logging.getLogger(__name__) - - -def encrypt_string(plaintext: str, password: str) -> str: - plaintext_stream = io.BytesIO(plaintext.encode()) - ciphertext_stream = io.BytesIO() - - pyAesCrypt.encryptStream(plaintext_stream, ciphertext_stream, password, BUFFER_SIZE) - - ciphertext_b64 = base64.b64encode(ciphertext_stream.getvalue()) - logger.info("String encrypted.") - - return ciphertext_b64.decode() - - -def decrypt_ciphertext(ciphertext: str, password: str) -> str: - ciphertext = base64.b64decode(ciphertext) - ciphertext_stream = io.BytesIO(ciphertext) - plaintext_stream = io.BytesIO() - - ciphertext_stream_len = len(ciphertext_stream.getvalue()) - - try: - pyAesCrypt.decryptStream( - ciphertext_stream, - plaintext_stream, - password, - BUFFER_SIZE, - ciphertext_stream_len, - ) - except ValueError as ex: - if str(ex).startswith("Wrong password"): - logger.info("Wrong password provided for decryption.") - raise InvalidCredentialsError - else: - logger.info("The corrupt ciphertext provided.") - raise InvalidCiphertextError - return plaintext_stream.getvalue().decode("utf-8") - - -def is_encrypted(ciphertext: str) -> bool: - ciphertext = base64.b64decode(ciphertext) - return ciphertext.startswith(b"AES") - - -class InvalidCredentialsError(Exception): - """ Raised when password for decryption is invalid """ - - -class InvalidCiphertextError(Exception): - """ Raised when ciphertext is corrupted """ diff --git a/monkey/monkey_island/cc/services/utils/i_encryptor.py b/monkey/monkey_island/cc/services/utils/i_encryptor.py new file mode 100644 index 000000000..d83198b7b --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/i_encryptor.py @@ -0,0 +1,20 @@ +from abc import ABC, abstractmethod +from typing import Any + + +class IEncryptor(ABC): + @abstractmethod + def encrypt(self, plaintext: Any) -> Any: + """Encrypts data and returns the ciphertext. + :param plaintext: Data that will be encrypted + :return: Ciphertext generated by encrypting value + :rtype: Any + """ + + @abstractmethod + def decrypt(self, ciphertext: Any): + """Decrypts data and returns the plaintext. + :param ciphertext: Ciphertext that will be decrypted + :return: Plaintext generated by decrypting value + :rtype: Any + """ diff --git a/monkey/monkey_island/cc/services/utils/key_encryption.py b/monkey/monkey_island/cc/services/utils/key_encryption.py new file mode 100644 index 000000000..cb8366da8 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/key_encryption.py @@ -0,0 +1,38 @@ +import base64 +import logging + +# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but +# is maintained. +from Crypto import Random # noqa: DUO133 # nosec: B413 +from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413 + +from monkey_island.cc.services.utils.i_encryptor import IEncryptor + +logger = logging.getLogger(__name__) + + +class KeyBasedEncryptor(IEncryptor): + + _BLOCK_SIZE = 32 + + def __init__(self, key: bytes): + self._key = key + + def encrypt(self, plaintext: str) -> str: + cipher_iv = Random.new().read(AES.block_size) + cipher = AES.new(self._key, AES.MODE_CBC, cipher_iv) + return base64.b64encode(cipher_iv + cipher.encrypt(self._pad(plaintext).encode())).decode() + + def decrypt(self, ciphertext: str): + enc_message = base64.b64decode(ciphertext) + cipher_iv = enc_message[0 : AES.block_size] + cipher = AES.new(self._key, AES.MODE_CBC, cipher_iv) + return self._unpad(cipher.decrypt(enc_message[AES.block_size :]).decode()) + + def _pad(self, message): + return message + (self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)) * chr( + self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE) + ) + + def _unpad(self, message: str): + return message[0 : -ord(message[len(message) - 1])] diff --git a/monkey/monkey_island/cc/services/utils/password_encryption.py b/monkey/monkey_island/cc/services/utils/password_encryption.py new file mode 100644 index 000000000..1854722e8 --- /dev/null +++ b/monkey/monkey_island/cc/services/utils/password_encryption.py @@ -0,0 +1,67 @@ +import base64 +import io +import logging + +import pyAesCrypt + +from monkey_island.cc.services.utils.i_encryptor import IEncryptor + +logger = logging.getLogger(__name__) + + +class PasswordBasedEncryptor(IEncryptor): + + _BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef + + def __init__(self, password: str): + self.password = password + + def encrypt(self, plaintext: str) -> str: + plaintext_stream = io.BytesIO(plaintext.encode()) + ciphertext_stream = io.BytesIO() + + pyAesCrypt.encryptStream( + plaintext_stream, ciphertext_stream, self.password, self._BUFFER_SIZE + ) + + ciphertext_b64 = base64.b64encode(ciphertext_stream.getvalue()) + logger.info("String encrypted.") + + return ciphertext_b64.decode() + + def decrypt(self, ciphertext: str): + ciphertext = base64.b64decode(ciphertext) + ciphertext_stream = io.BytesIO(ciphertext) + plaintext_stream = io.BytesIO() + + ciphertext_stream_len = len(ciphertext_stream.getvalue()) + + try: + pyAesCrypt.decryptStream( + ciphertext_stream, + plaintext_stream, + self.password, + self._BUFFER_SIZE, + ciphertext_stream_len, + ) + except ValueError as ex: + if str(ex).startswith("Wrong password"): + logger.info("Wrong password provided for decryption.") + raise InvalidCredentialsError + else: + logger.info("The corrupt ciphertext provided.") + raise InvalidCiphertextError + return plaintext_stream.getvalue().decode("utf-8") + + +class InvalidCredentialsError(Exception): + """ Raised when password for decryption is invalid """ + + +class InvalidCiphertextError(Exception): + """ Raised when ciphertext is corrupted """ + + +def is_encrypted(ciphertext: str) -> bool: + ciphertext = base64.b64decode(ciphertext) + return ciphertext.startswith(b"AES") diff --git a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index 36eae6271..0d423bd6a 100644 --- a/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py +++ b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py @@ -5,7 +5,7 @@ from ScoutSuite.providers.base.authentication_strategy import AuthenticationExce from common.cloud.scoutsuite_consts import CloudProviders from common.config_value_paths import AWS_KEYS_PATH from common.utils.exceptions import InvalidAWSKeys -from monkey_island.cc.server_utils.encryptor import get_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor from monkey_island.cc.services.config import ConfigService diff --git a/monkey/tests/unit_tests/monkey_island/cc/models/utils/field_encryptors/test_string_list_encryptor.py b/monkey/tests/unit_tests/monkey_island/cc/models/utils/field_encryptors/test_string_list_encryptor.py index 53b004401..fe8af8e0d 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/models/utils/field_encryptors/test_string_list_encryptor.py +++ b/monkey/tests/unit_tests/monkey_island/cc/models/utils/field_encryptors/test_string_list_encryptor.py @@ -1,7 +1,7 @@ import pytest from monkey_island.cc.models.utils.field_encryptors.string_list_encryptor import StringListEncryptor -from monkey_island.cc.server_utils.encryptor import initialize_encryptor +from monkey_island.cc.server_utils.key_encryptor import initialize_encryptor MOCK_STRING_LIST = ["test_1", "test_2"] EMPTY_LIST = [] diff --git a/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py b/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py index 989994cb6..45ef8daaf 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py +++ b/monkey/tests/unit_tests/monkey_island/cc/resources/test_configuration_import.py @@ -6,7 +6,7 @@ from tests.unit_tests.monkey_island.cc.services.utils.test_encryption import PAS from common.utils.exceptions import InvalidConfigurationError from monkey_island.cc.resources.configuration_import import ConfigurationImport -from monkey_island.cc.services.utils.encryption import encrypt_string +from monkey_island.cc.services.utils.password_encryption import PasswordBasedEncryptor def test_is_config_encrypted__json(monkey_config_json): @@ -15,7 +15,8 @@ def test_is_config_encrypted__json(monkey_config_json): @pytest.mark.slow def test_is_config_encrypted__ciphertext(monkey_config_json): - encrypted_config = encrypt_string(monkey_config_json, PASSWORD) + pb_encryptor = PasswordBasedEncryptor(PASSWORD) + encrypted_config = pb_encryptor.encrypt(monkey_config_json) assert ConfigurationImport.is_config_encrypted(encrypted_config) diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_key_encryptor.py similarity index 89% rename from monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py rename to monkey/tests/unit_tests/monkey_island/cc/server_utils/test_key_encryptor.py index 0ca724d44..f7097ec00 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_key_encryptor.py @@ -1,6 +1,6 @@ import os -from monkey_island.cc.server_utils.encryptor import get_encryptor, initialize_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor, initialize_encryptor PASSWORD_FILENAME = "mongo_key.bin" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_encryption.py index fd3191f50..029e8201f 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_encryption.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_encryption.py @@ -4,10 +4,9 @@ from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption VALID_CIPHER_TEXT, ) -from monkey_island.cc.services.utils.encryption import ( +from monkey_island.cc.services.utils.password_encryption import ( InvalidCredentialsError, - decrypt_ciphertext, - encrypt_string, + PasswordBasedEncryptor, ) MONKEY_CONFIGS_DIR_PATH = "monkey_configs" @@ -18,23 +17,27 @@ INCORRECT_PASSWORD = "goodbye321" @pytest.mark.slow def test_encrypt_decrypt_string(monkey_config_json): - encrypted_config = encrypt_string(monkey_config_json, PASSWORD) - assert decrypt_ciphertext(encrypted_config, PASSWORD) == monkey_config_json + pb_encryptor = PasswordBasedEncryptor(PASSWORD) + encrypted_config = pb_encryptor.encrypt(monkey_config_json) + assert pb_encryptor.decrypt(encrypted_config) == monkey_config_json @pytest.mark.slow def test_decrypt_string__wrong_password(monkey_config_json): + pb_encryptor = PasswordBasedEncryptor(INCORRECT_PASSWORD) with pytest.raises(InvalidCredentialsError): - decrypt_ciphertext(VALID_CIPHER_TEXT, INCORRECT_PASSWORD) + pb_encryptor.decrypt(VALID_CIPHER_TEXT) @pytest.mark.slow def test_decrypt_string__malformed_corrupted(): + pb_encryptor = PasswordBasedEncryptor(PASSWORD) with pytest.raises(ValueError): - decrypt_ciphertext(MALFORMED_CIPHER_TEXT_CORRUPTED, PASSWORD) + pb_encryptor.decrypt(MALFORMED_CIPHER_TEXT_CORRUPTED) @pytest.mark.slow def test_decrypt_string__no_password(monkey_config_json): + pb_encryptor = PasswordBasedEncryptor("") with pytest.raises(InvalidCredentialsError): - decrypt_ciphertext(VALID_CIPHER_TEXT, "") + pb_encryptor.decrypt(VALID_CIPHER_TEXT) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py index faea76f4f..af47d51b5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/zero_trust/scoutsuite/test_scoutsuite_auth_service.py @@ -5,7 +5,7 @@ import pytest from common.config_value_paths import AWS_KEYS_PATH from monkey_island.cc.database import mongo -from monkey_island.cc.server_utils.encryptor import get_encryptor, initialize_encryptor +from monkey_island.cc.server_utils.key_encryptor import get_encryptor, initialize_encryptor from monkey_island.cc.services.config import ConfigService from monkey_island.cc.services.zero_trust.scoutsuite.scoutsuite_auth_service import ( is_aws_keys_setup,