diff --git a/.flake8 b/.flake8 index 97d903b8f..213c030a2 100644 --- a/.flake8 +++ b/.flake8 @@ -5,6 +5,7 @@ exclude = monkey/monkey_island/cc/ui,vulture_allowlist.py show-source = True max-complexity = 10 max-line-length = 100 +per-file-ignores = __init__.py:F401 ### ignore "whitespace before ':'", "line break before binary operator" for ### compatibility with black, and cyclomatic complexity (for now). 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..089155289 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,14 +1,14 @@ 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.encryption import get_datastore_encryptor class StringListEncryptor(IFieldEncryptor): @staticmethod def encrypt(value: List[str]): - return [get_encryptor().enc(string) for string in value] + return [get_datastore_encryptor().enc(string) for string in value] @staticmethod def decrypt(value: List[str]): - return [get_encryptor().dec(string) for string in value] + return [get_datastore_encryptor().dec(string) for string in value] diff --git a/monkey/monkey_island/cc/resources/configuration_export.py b/monkey/monkey_island/cc/resources/configuration_export.py index 4a7aeec24..c550acc7d 100644 --- a/monkey/monkey_island/cc/resources/configuration_export.py +++ b/monkey/monkey_island/cc/resources/configuration_export.py @@ -4,8 +4,8 @@ import flask_restful from flask import request from monkey_island.cc.resources.auth.auth import jwt_required +from monkey_island.cc.server_utils.encryption import PasswordBasedEncryptor from monkey_island.cc.services.config import ConfigService -from monkey_island.cc.services.utils.encryption import encrypt_string 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..6c0575e94 100644 --- a/monkey/monkey_island/cc/resources/configuration_import.py +++ b/monkey/monkey_island/cc/resources/configuration_import.py @@ -8,13 +8,13 @@ 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.server_utils.encryption import ( InvalidCiphertextError, InvalidCredentialsError, - decrypt_ciphertext, + PasswordBasedEncryptor, is_encrypted, ) +from monkey_island.cc.services.config import ConfigService logger = logging.getLogger(__name__) @@ -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..a4e4da485 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -27,7 +27,7 @@ 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.encryption import initialize_datastore_encryptor # noqa: E402 from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # 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 @@ -88,7 +88,7 @@ def _configure_logging(config_options): def _initialize_globals(config_options: IslandConfigOptions, server_config_path: str): env_singleton.initialize_from_file(server_config_path) - initialize_encryptor(config_options.data_dir) + initialize_datastore_encryptor(config_options.data_dir) initialize_services(config_options.data_dir) diff --git a/monkey/monkey_island/cc/server_utils/encryption/__init__.py b/monkey/monkey_island/cc/server_utils/encryption/__init__.py new file mode 100644 index 000000000..a41240be1 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/encryption/__init__.py @@ -0,0 +1,13 @@ +from monkey_island.cc.server_utils.encryption.i_encryptor import IEncryptor +from monkey_island.cc.server_utils.encryption.key_based_encryptor import KeyBasedEncryptor +from monkey_island.cc.server_utils.encryption.password_based_encryption import ( + InvalidCiphertextError, + InvalidCredentialsError, + PasswordBasedEncryptor, + is_encrypted, +) +from monkey_island.cc.server_utils.encryption.data_store_encryptor import ( + DataStoreEncryptor, + get_datastore_encryptor, + initialize_datastore_encryptor, +) diff --git a/monkey/monkey_island/cc/server_utils/encryption/data_store_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/data_store_encryptor.py new file mode 100644 index 000000000..215703c02 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/encryption/data_store_encryptor.py @@ -0,0 +1,50 @@ +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 monkey_island.cc.server_utils.encryption import KeyBasedEncryptor +from monkey_island.cc.server_utils.file_utils import open_new_securely_permissioned_file + +_encryptor = None + + +class DataStoreEncryptor: + _BLOCK_SIZE = 32 + _KEY_FILENAME = "mongo_key.bin" + + def __init__(self, key_file_dir): + key_file = os.path.join(key_file_dir, self._KEY_FILENAME) + + if os.path.exists(key_file): + self._load_existing_key(key_file) + else: + self._init_key(key_file) + + self._key_base_encryptor = KeyBasedEncryptor(self._cipher_key) + + def _init_key(self, password_file_path: str): + self._cipher_key = Random.new().read(self._BLOCK_SIZE) + with open_new_securely_permissioned_file(password_file_path, "wb") as f: + f.write(self._cipher_key) + + def _load_existing_key(self, key_file): + with open(key_file, "rb") as f: + self._cipher_key = f.read() + + def enc(self, message: str): + return self._key_base_encryptor.encrypt(message) + + def dec(self, enc_message: str): + return self._key_base_encryptor.decrypt(enc_message) + + +def initialize_datastore_encryptor(key_file_dir): + global _encryptor + + _encryptor = DataStoreEncryptor(key_file_dir) + + +def get_datastore_encryptor(): + return _encryptor diff --git a/monkey/monkey_island/cc/server_utils/encryption/i_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/i_encryptor.py new file mode 100644 index 000000000..d83198b7b --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/encryption/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/server_utils/encryption/key_based_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/key_based_encryptor.py new file mode 100644 index 000000000..b5fe92d96 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/encryption/key_based_encryptor.py @@ -0,0 +1,47 @@ +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.server_utils.encryption import IEncryptor + +logger = logging.getLogger(__name__) + +# KeyBasedEncryptor is an encryption method which use random key of specific length +# and AES block cipher to encrypt/decrypt the data. The key is more complex +# one and hard to remember than user provided one. This class provides more secure way of +# encryption compared to PasswordBasedEncryptor because of the random and complex key. +# We can merge the two into the one encryption method but then we lose the entropy +# of the key with whatever key derivation function we use. +# Note: password != key + + +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()) + + # TODO: Review and evaluate the security of the padding function + 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/server_utils/encryption/password_based_encryption.py b/monkey/monkey_island/cc/server_utils/encryption/password_based_encryption.py new file mode 100644 index 000000000..20708ce31 --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/encryption/password_based_encryption.py @@ -0,0 +1,75 @@ +import base64 +import io +import logging + +import pyAesCrypt + +from monkey_island.cc.server_utils.encryption import IEncryptor + +logger = logging.getLogger(__name__) + +# PasswordBasedEncryptor as implemented takes low-entropy, user provided password and it adds some +# entropy to it and encrypts/decrypts the data. This implementation uses AES256-CBC +# and it is less secure encryption then KeyBasedEncryptor. +# The security of it depends on what will the user provide as password. +# We can merge the two into the one encryption method but then we lose the entropy +# of the key with whatever key derivation function we use. +# Note: password != key + + +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/server_utils/encryptor.py b/monkey/monkey_island/cc/server_utils/encryptor.py deleted file mode 100644 index ab9bc617a..000000000 --- a/monkey/monkey_island/cc/server_utils/encryptor.py +++ /dev/null @@ -1,62 +0,0 @@ -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 - -_encryptor = None - - -class Encryptor: - _BLOCK_SIZE = 32 - _PASSWORD_FILENAME = "mongo_key.bin" - - def __init__(self, password_file_dir): - password_file = os.path.join(password_file_dir, self._PASSWORD_FILENAME) - - if os.path.exists(password_file): - self._load_existing_key(password_file) - else: - self._init_key(password_file) - - def _init_key(self, password_file_path: str): - self._cipher_key = Random.new().read(self._BLOCK_SIZE) - with open_new_securely_permissioned_file(password_file_path, "wb") as f: - f.write(self._cipher_key) - - def _load_existing_key(self, password_file): - 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() - - 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 initialize_encryptor(password_file_dir): - global _encryptor - - _encryptor = Encryptor(password_file_dir) - - -def get_encryptor(): - return _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..16884678b 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.encryption import get_datastore_encryptor def parse_creds(attempt): @@ -29,7 +29,7 @@ def censor_password(password, plain_chars=3, secret_chars=5): """ if not password: return "" - password = get_encryptor().dec(password) + password = get_datastore_encryptor().dec(password) return password[0:plain_chars] + "*" * secret_chars @@ -42,5 +42,5 @@ def censor_hash(hash_, plain_chars=5): """ if not hash_: return "" - hash_ = get_encryptor().dec(hash_) + hash_ = get_datastore_encryptor().dec(hash_) return hash_[0:plain_chars] + " ..." diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index ba4083286..973ca104a 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.encryption import get_datastore_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 @@ -90,9 +90,9 @@ class ConfigService: if should_decrypt: if config_key_as_arr in ENCRYPTED_CONFIG_VALUES: if isinstance(config, str): - config = get_encryptor().dec(config) + config = get_datastore_encryptor().dec(config) elif isinstance(config, list): - config = [get_encryptor().dec(x) for x in config] + config = [get_datastore_encryptor().dec(x) for x in config] return config @staticmethod @@ -130,7 +130,7 @@ class ConfigService: if item_value in items_from_config: return if should_encrypt: - item_value = get_encryptor().enc(item_value) + item_value = get_datastore_encryptor().enc(item_value) mongo.db.config.update( {"name": "newconfig"}, {"$addToSet": {item_key: item_value}}, upsert=False ) @@ -349,9 +349,11 @@ class ConfigService: ConfigService.decrypt_ssh_key_pair(item) for item in flat_config[key] ] else: - flat_config[key] = [get_encryptor().dec(item) for item in flat_config[key]] + flat_config[key] = [ + get_datastore_encryptor().dec(item) for item in flat_config[key] + ] else: - flat_config[key] = get_encryptor().dec(flat_config[key]) + flat_config[key] = get_datastore_encryptor().dec(flat_config[key]) return flat_config @staticmethod @@ -377,25 +379,25 @@ class ConfigService: ) else: config_arr[i] = ( - get_encryptor().dec(config_arr[i]) + get_datastore_encryptor().dec(config_arr[i]) if is_decrypt - else get_encryptor().enc(config_arr[i]) + else get_datastore_encryptor().enc(config_arr[i]) ) else: parent_config_arr[config_arr_as_array[-1]] = ( - get_encryptor().dec(config_arr) + get_datastore_encryptor().dec(config_arr) if is_decrypt - else get_encryptor().enc(config_arr) + else get_datastore_encryptor().enc(config_arr) ) @staticmethod def decrypt_ssh_key_pair(pair, encrypt=False): if encrypt: - pair["public_key"] = get_encryptor().enc(pair["public_key"]) - pair["private_key"] = get_encryptor().enc(pair["private_key"]) + pair["public_key"] = get_datastore_encryptor().enc(pair["public_key"]) + pair["private_key"] = get_datastore_encryptor().enc(pair["private_key"]) else: - pair["public_key"] = get_encryptor().dec(pair["public_key"]) - pair["private_key"] = get_encryptor().dec(pair["private_key"]) + pair["public_key"] = get_datastore_encryptor().dec(pair["public_key"]) + pair["private_key"] = get_datastore_encryptor().dec(pair["private_key"]) return pair @staticmethod diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 7fa5654c5..7c156930a 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.encryption import get_datastore_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 @@ -76,4 +76,4 @@ def encrypt_exploit_creds(telemetry_json): credential = attempts[i][field] if credential: # PowerShell exploiter's telem may have `None` here if len(credential) > 0: - attempts[i][field] = get_encryptor().enc(credential) + attempts[i][field] = get_datastore_encryptor().enc(credential) 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..ba72e822b 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.encryption import get_datastore_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 @@ -70,7 +70,7 @@ def encrypt_system_info_ssh_keys(ssh_info): for idx, user in enumerate(ssh_info): for field in ["public_key", "private_key", "known_hosts"]: if ssh_info[idx][field]: - ssh_info[idx][field] = get_encryptor().enc(ssh_info[idx][field]) + ssh_info[idx][field] = get_datastore_encryptor().enc(ssh_info[idx][field]) def process_credential_info(telemetry_json): 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/zero_trust/scoutsuite/scoutsuite_auth_service.py b/monkey/monkey_island/cc/services/zero_trust/scoutsuite/scoutsuite_auth_service.py index 36eae6271..89aa002fa 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.encryption import get_datastore_encryptor from monkey_island.cc.services.config import ConfigService @@ -41,7 +41,7 @@ def set_aws_keys(access_key_id: str, secret_access_key: str, session_token: str) def _set_aws_key(key_type: str, key_value: str): path_to_keys = AWS_KEYS_PATH - encrypted_key = get_encryptor().enc(key_value) + encrypted_key = get_datastore_encryptor().enc(key_value) ConfigService.set_config_value(path_to_keys + [key_type], encrypted_key) diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index c14524411..438ee3fef 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -5,7 +5,7 @@ import os import pytest from tests.unit_tests.monkey_island.cc.mongomock_fixtures import * # noqa: F401,F403,E402 -from tests.unit_tests.monkey_island.cc.services.utils.test_encryption import ( +from tests.unit_tests.monkey_island.cc.server_utils.encryption.test_password_based_encryption import ( # noqa: E501 MONKEY_CONFIGS_DIR_PATH, STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME, ) 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..a93397392 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.encryption import initialize_datastore_encryptor MOCK_STRING_LIST = ["test_1", "test_2"] EMPTY_LIST = [] @@ -9,7 +9,7 @@ EMPTY_LIST = [] @pytest.fixture def uses_encryptor(data_for_tests_dir): - initialize_encryptor(data_for_tests_dir) + initialize_datastore_encryptor(data_for_tests_dir) def test_encryption_and_decryption(uses_encryptor): 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..fb397f234 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 @@ -1,12 +1,14 @@ import pytest +from tests.unit_tests.monkey_island.cc.server_utils.encryption.test_password_based_encryption import ( # noqa: E501 + PASSWORD, +) from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption_test import ( MALFORMED_CIPHER_TEXT_CORRUPTED, ) -from tests.unit_tests.monkey_island.cc.services.utils.test_encryption import PASSWORD 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.server_utils.encryption import PasswordBasedEncryptor def test_is_config_encrypted__json(monkey_config_json): @@ -15,7 +17,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/encryption/test_data_store_encryptor.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_data_store_encryptor.py new file mode 100644 index 000000000..bb005fbf7 --- /dev/null +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_data_store_encryptor.py @@ -0,0 +1,35 @@ +import os + +from monkey_island.cc.server_utils.encryption import ( + get_datastore_encryptor, + initialize_datastore_encryptor, +) + +PASSWORD_FILENAME = "mongo_key.bin" + +PLAINTEXT = "Hello, Monkey!" +CYPHERTEXT = "vKgvD6SjRyIh1dh2AM/rnTa0NI/vjfwnbZLbMocWtE4e42WJmSUz2ordtbQrH1Fq" + + +def test_aes_cbc_encryption(data_for_tests_dir): + initialize_datastore_encryptor(data_for_tests_dir) + + assert get_datastore_encryptor().enc(PLAINTEXT) != PLAINTEXT + + +def test_aes_cbc_decryption(data_for_tests_dir): + initialize_datastore_encryptor(data_for_tests_dir) + + assert get_datastore_encryptor().dec(CYPHERTEXT) == PLAINTEXT + + +def test_aes_cbc_enc_dec(data_for_tests_dir): + initialize_datastore_encryptor(data_for_tests_dir) + + assert get_datastore_encryptor().dec(get_datastore_encryptor().enc(PLAINTEXT)) == PLAINTEXT + + +def test_create_new_password_file(tmpdir): + initialize_datastore_encryptor(tmpdir) + + assert os.path.isfile(os.path.join(tmpdir, PASSWORD_FILENAME)) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py similarity index 56% rename from monkey/tests/unit_tests/monkey_island/cc/services/utils/test_encryption.py rename to monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py index fd3191f50..d00609481 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/utils/test_encryption.py +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py @@ -4,11 +4,7 @@ from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption VALID_CIPHER_TEXT, ) -from monkey_island.cc.services.utils.encryption import ( - InvalidCredentialsError, - decrypt_ciphertext, - encrypt_string, -) +from monkey_island.cc.server_utils.encryption import InvalidCredentialsError, PasswordBasedEncryptor MONKEY_CONFIGS_DIR_PATH = "monkey_configs" STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME = "monkey_config_standard.json" @@ -18,23 +14,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/server_utils/test_encryptor.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py deleted file mode 100644 index 0ca724d44..000000000 --- a/monkey/tests/unit_tests/monkey_island/cc/server_utils/test_encryptor.py +++ /dev/null @@ -1,32 +0,0 @@ -import os - -from monkey_island.cc.server_utils.encryptor import get_encryptor, initialize_encryptor - -PASSWORD_FILENAME = "mongo_key.bin" - -PLAINTEXT = "Hello, Monkey!" -CYPHERTEXT = "vKgvD6SjRyIh1dh2AM/rnTa0NI/vjfwnbZLbMocWtE4e42WJmSUz2ordtbQrH1Fq" - - -def test_aes_cbc_encryption(data_for_tests_dir): - initialize_encryptor(data_for_tests_dir) - - assert get_encryptor().enc(PLAINTEXT) != PLAINTEXT - - -def test_aes_cbc_decryption(data_for_tests_dir): - initialize_encryptor(data_for_tests_dir) - - assert get_encryptor().dec(CYPHERTEXT) == PLAINTEXT - - -def test_aes_cbc_enc_dec(data_for_tests_dir): - initialize_encryptor(data_for_tests_dir) - - assert get_encryptor().dec(get_encryptor().enc(PLAINTEXT)) == PLAINTEXT - - -def test_create_new_password_file(tmpdir): - initialize_encryptor(tmpdir) - - assert os.path.isfile(os.path.join(tmpdir, PASSWORD_FILENAME)) 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..2e6c2fd50 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,10 @@ 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.encryption import ( + get_datastore_encryptor, + initialize_datastore_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, @@ -27,8 +30,8 @@ def test_is_aws_keys_setup(tmp_path): assert not is_aws_keys_setup() # Make sure noone changed config path and broke this function - initialize_encryptor(tmp_path) - bogus_key_value = get_encryptor().enc("bogus_aws_key") + initialize_datastore_encryptor(tmp_path) + bogus_key_value = get_datastore_encryptor().enc("bogus_aws_key") dpath.util.set( ConfigService.default_config, AWS_KEYS_PATH + ["aws_secret_access_key"], bogus_key_value ) diff --git a/pyproject.toml b/pyproject.toml index 05c8dfe81..88da84d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ include_trailing_comma = true force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true +skip_glob="**/__init__.py" [tool.pytest.ini_options] minversion = "6.0"