From 3ec26bcef84d593f1a5f8651878273a1ecec9018 Mon Sep 17 00:00:00 2001 From: VakarisZ Date: Mon, 4 Oct 2021 12:03:30 +0300 Subject: [PATCH] Refactor data store encryptor to IEncryptor interface, move data store encryptor creation related code to data_store_encryptor.py, move the reponsibility to initialize data store encryptor to AuthenticationService --- .../monkey_island/cc/resources/auth/auth.py | 8 +- .../cc/resources/auth/registration.py | 8 +- monkey/monkey_island/cc/server_setup.py | 3 - .../encryption/data_store_encryptor.py | 62 ++++++++++----- .../mimikatz_results_encryptor.py | 8 +- .../field_encryptors/string_list_encryptor.py | 4 +- .../encryption/encryptor_factory.py | 75 ------------------- .../technique_report_tools.py | 4 +- .../cc/services/authentication.py | 35 +++++++++ monkey/monkey_island/cc/services/config.py | 26 +++---- .../monkey_island/cc/services/initialize.py | 2 + .../services/telemetry/processing/exploit.py | 2 +- .../telemetry/processing/system_info.py | 2 +- .../scoutsuite/scoutsuite_auth_service.py | 2 +- 14 files changed, 111 insertions(+), 130 deletions(-) delete mode 100644 monkey/monkey_island/cc/server_utils/encryption/encryptor_factory.py create mode 100644 monkey/monkey_island/cc/services/authentication.py diff --git a/monkey/monkey_island/cc/resources/auth/auth.py b/monkey/monkey_island/cc/resources/auth/auth.py index 9a693e80d..92a372a99 100644 --- a/monkey/monkey_island/cc/resources/auth/auth.py +++ b/monkey/monkey_island/cc/resources/auth/auth.py @@ -13,10 +13,7 @@ from monkey_island.cc.resources.auth.credential_utils import ( get_username_password_from_request, password_matches_hash, ) -from monkey_island.cc.server_utils.encryption import ( - get_datastore_encryptor, - initialize_datastore_encryptor, -) +from monkey_island.cc.services.authentication import AuthenticationService logger = logging.getLogger(__name__) @@ -47,8 +44,7 @@ class Authenticate(flask_restful.Resource): username, password = get_username_password_from_request(request) if _credentials_match_registered_user(username, password): - if not get_datastore_encryptor(): - initialize_datastore_encryptor(username, password) + AuthenticationService.ensure_datastore_encryptor(username, password) access_token = _create_access_token(username) return make_response({"access_token": access_token, "error": ""}, 200) else: diff --git a/monkey/monkey_island/cc/resources/auth/registration.py b/monkey/monkey_island/cc/resources/auth/registration.py index 82dbcfe3a..670fa4d19 100644 --- a/monkey/monkey_island/cc/resources/auth/registration.py +++ b/monkey/monkey_island/cc/resources/auth/registration.py @@ -9,10 +9,7 @@ from monkey_island.cc.resources.auth.credential_utils import ( get_user_credentials_from_request, get_username_password_from_request, ) -from monkey_island.cc.server_utils.encryption import ( - initialize_datastore_encryptor, - remove_old_datastore_key, -) +from monkey_island.cc.services.authentication import AuthenticationService from monkey_island.cc.setup.mongo.database_initializer import reset_database logger = logging.getLogger(__name__) @@ -28,9 +25,8 @@ class Registration(flask_restful.Resource): try: env_singleton.env.try_add_user(credentials) - remove_old_datastore_key() username, password = get_username_password_from_request(request) - initialize_datastore_encryptor(username, password) + AuthenticationService.reset_datastore_encryptor(username, password) reset_database() return make_response({"error": ""}, 200) except (InvalidRegistrationCredentialsError, RegistrationNotNeededError) as e: diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 82c1b3a58..fdb94b67f 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -11,8 +11,6 @@ from gevent.pywsgi import WSGIServer # Add the monkey_island directory to the path, to make sure imports that don't start with # "monkey_island." work. -from monkey_island.cc.server_utils.encryption import initialize_encryptor_factory - MONKEY_ISLAND_DIR_BASE_PATH = str(Path(__file__).parent.parent) if str(MONKEY_ISLAND_DIR_BASE_PATH) not in sys.path: sys.path.insert(0, MONKEY_ISLAND_DIR_BASE_PATH) @@ -88,7 +86,6 @@ 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_factory(config_options.data_dir) initialize_services(config_options.data_dir) 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 index 949102c84..fb38e95a8 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/data_store_encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryption/data_store_encryptor.py @@ -1,29 +1,57 @@ -from __future__ import annotations - -# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but -# is maintained. +import os from typing import Union -from monkey_island.cc.server_utils.encryption import KeyBasedEncryptor +from Crypto import Random # noqa: DUO133 # nosec: B413 -_encryptor: Union[None, DataStoreEncryptor] = None +from monkey_island.cc.server_utils.encryption import IEncryptor, KeyBasedEncryptor +from monkey_island.cc.server_utils.encryption.encryptors.password_based_bytes_encryption import ( + PasswordBasedBytesEncryptor, +) +from monkey_island.cc.server_utils.file_utils import open_new_securely_permissioned_file + +_KEY_FILENAME = "mongo_key.bin" +_BLOCK_SIZE = 32 + +_encryptor: Union[None, IEncryptor] = None -class DataStoreEncryptor: - def __init__(self, key_based_encryptor: KeyBasedEncryptor): - self._key_based_encryptor = key_based_encryptor - - def enc(self, message: str): - return self._key_based_encryptor.encrypt(message) - - def dec(self, enc_message: str): - return self._key_based_encryptor.decrypt(enc_message) +def _load_existing_key(key_file_path: str, secret: str): + with open(key_file_path, "rb") as f: + encrypted_key = f.read() + cipher_key = PasswordBasedBytesEncryptor(secret).decrypt(encrypted_key) + return KeyBasedEncryptor(cipher_key) -def initialize_datastore_encryptor(key_based_encryptor: KeyBasedEncryptor): +def _create_new_key(key_file_path: str, secret: str): + cipher_key = _get_random_bytes() + encrypted_key = PasswordBasedBytesEncryptor(secret).encrypt(cipher_key) + with open_new_securely_permissioned_file(key_file_path, "wb") as f: + f.write(encrypted_key) + return KeyBasedEncryptor(cipher_key) + + +def _get_random_bytes() -> bytes: + return Random.new().read(_BLOCK_SIZE) + + +def remove_old_datastore_key(key_file_dir: str): + key_file_path = _get_key_file_path(key_file_dir) + if os.path.isfile(key_file_path): + os.remove(key_file_path) + + +def initialize_datastore_encryptor(key_file_dir: str, secret: str): global _encryptor - _encryptor = DataStoreEncryptor(key_based_encryptor) + key_file_path = _get_key_file_path(key_file_dir) + if os.path.exists(key_file_path): + _encryptor = _load_existing_key(key_file_path, secret) + else: + _encryptor = _create_new_key(key_file_path, secret) + + +def _get_key_file_path(key_file_dir: str): + return os.path.join(key_file_dir, _KEY_FILENAME) def get_datastore_encryptor(): diff --git a/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/mimikatz_results_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/mimikatz_results_encryptor.py index 6261f5147..ff2ee314e 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/mimikatz_results_encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/mimikatz_results_encryptor.py @@ -17,7 +17,7 @@ class MimikatzResultsEncryptor(IFieldEncryptor): for _, credentials in results.items(): for secret_type in MimikatzResultsEncryptor.secret_types: try: - credentials[secret_type] = get_datastore_encryptor().enc( + credentials[secret_type] = get_datastore_encryptor().encrypt( credentials[secret_type] ) except ValueError as e: @@ -25,12 +25,14 @@ class MimikatzResultsEncryptor(IFieldEncryptor): f"Failed encrypting sensitive field for " f"user {credentials['username']}! Error: {e}" ) - credentials[secret_type] = get_datastore_encryptor().enc("") + credentials[secret_type] = get_datastore_encryptor().encrypt("") return results @staticmethod def decrypt(results: dict) -> dict: for _, credentials in results.items(): for secret_type in MimikatzResultsEncryptor.secret_types: - credentials[secret_type] = get_datastore_encryptor().dec(credentials[secret_type]) + credentials[secret_type] = get_datastore_encryptor().decrypt( + credentials[secret_type] + ) return results diff --git a/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/string_list_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/string_list_encryptor.py index 46eef09cb..04374c462 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/string_list_encryptor.py +++ b/monkey/monkey_island/cc/server_utils/encryption/dict_encryption/field_encryptors/string_list_encryptor.py @@ -9,8 +9,8 @@ from monkey_island.cc.server_utils.encryption.dict_encryption.field_encryptors i class StringListEncryptor(IFieldEncryptor): @staticmethod def encrypt(value: List[str]): - return [get_datastore_encryptor().enc(string) for string in value] + return [get_datastore_encryptor().encrypt(string) for string in value] @staticmethod def decrypt(value: List[str]): - return [get_datastore_encryptor().dec(string) for string in value] + return [get_datastore_encryptor().decrypt(string) for string in value] diff --git a/monkey/monkey_island/cc/server_utils/encryption/encryptor_factory.py b/monkey/monkey_island/cc/server_utils/encryption/encryptor_factory.py deleted file mode 100644 index 0ae3e70a6..000000000 --- a/monkey/monkey_island/cc/server_utils/encryption/encryptor_factory.py +++ /dev/null @@ -1,75 +0,0 @@ -import os - -from Crypto import Random - -from monkey_island.cc.server_utils.encryption import ( - KeyBasedEncryptor, - initialize_datastore_encryptor, -) -from monkey_island.cc.server_utils.encryption.password_based_bytes_encryption import ( - PasswordBasedBytesEncryptor, -) -from monkey_island.cc.server_utils.file_utils import open_new_securely_permissioned_file - -_KEY_FILENAME = "mongo_key.bin" -_BLOCK_SIZE = 32 - - -class EncryptorFactory: - def __init__(self): - self.key_file_path = None - self.secret = None - - def set_key_file_path(self, key_file_path: str): - self.key_file_path = key_file_path - - def set_secret(self, username: str, password: str): - self.secret = _get_secret_from_credentials(username, password) - - def initialize_encryptor(self): - if os.path.exists(self.key_file_path): - key_based_encryptor = _load_existing_key(self.key_file_path, self.secret) - else: - key_based_encryptor = _create_new_key(self.key_file_path, self.secret) - initialize_datastore_encryptor(key_based_encryptor) - - -class KeyPathNotSpecifiedError(Exception): - pass - - -def _load_existing_key(key_file_path: str, secret: str): - with open(key_file_path, "rb") as f: - encrypted_key = f.read() - cipher_key = PasswordBasedBytesEncryptor(secret).decrypt(encrypted_key) - return KeyBasedEncryptor(cipher_key) - - -def _create_new_key(key_file_path: str, secret: str): - cipher_key = _get_random_bytes() - encrypted_key = PasswordBasedBytesEncryptor(secret).encrypt(cipher_key) - with open_new_securely_permissioned_file(key_file_path, "wb") as f: - f.write(encrypted_key) - return KeyBasedEncryptor(cipher_key) - - -def _get_random_bytes() -> bytes: - return Random.new().read(_BLOCK_SIZE) - - -def _get_secret_from_credentials(username: str, password: str) -> str: - return f"{username}:{password}" - - -def remove_old_datastore_key(): - if not _factory.key_file_path: - raise KeyPathNotSpecifiedError - if os.path.isfile(_factory.key_file_path): - os.remove(_factory.key_file_path) - - -def get_encryptor_factory(): - return _factory - - -_factory = EncryptorFactory() 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 16884678b..5bb61bc14 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 @@ -29,7 +29,7 @@ def censor_password(password, plain_chars=3, secret_chars=5): """ if not password: return "" - password = get_datastore_encryptor().dec(password) + password = get_datastore_encryptor().decrypt(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_datastore_encryptor().dec(hash_) + hash_ = get_datastore_encryptor().decrypt(hash_) return hash_[0:plain_chars] + " ..." diff --git a/monkey/monkey_island/cc/services/authentication.py b/monkey/monkey_island/cc/services/authentication.py new file mode 100644 index 000000000..ac5400bfd --- /dev/null +++ b/monkey/monkey_island/cc/services/authentication.py @@ -0,0 +1,35 @@ +from monkey_island.cc.server_utils.encryption import ( + get_datastore_encryptor, + initialize_datastore_encryptor, +) +from monkey_island.cc.server_utils.encryption.data_store_encryptor import remove_old_datastore_key + + +class AuthenticationService: + KEY_FILE_DIRECTORY = None + + # TODO: A number of these services should be instance objects instead of + # static/singleton hybrids. At the moment, this requires invasive refactoring that's + # not a priority. + @classmethod + def initialize(cls, key_file_directory): + cls.KEY_FILE_DIRECTORY = key_file_directory + + @staticmethod + def ensure_datastore_encryptor(username: str, password: str): + if not get_datastore_encryptor(): + AuthenticationService._init_encryptor_from_credentials(username, password) + + @staticmethod + def reset_datastore_encryptor(username: str, password: str): + remove_old_datastore_key(AuthenticationService.KEY_FILE_DIRECTORY) + AuthenticationService._init_encryptor_from_credentials(username, password) + + @staticmethod + def _init_encryptor_from_credentials(username: str, password: str): + secret = AuthenticationService._get_secret_from_credentials(username, password) + initialize_datastore_encryptor(AuthenticationService.KEY_FILE_DIRECTORY, secret) + + @staticmethod + def _get_secret_from_credentials(username: str, password: str) -> str: + return f"{username}:{password}" diff --git a/monkey/monkey_island/cc/services/config.py b/monkey/monkey_island/cc/services/config.py index 973ca104a..6ddcd896f 100644 --- a/monkey/monkey_island/cc/services/config.py +++ b/monkey/monkey_island/cc/services/config.py @@ -90,9 +90,9 @@ class ConfigService: if should_decrypt: if config_key_as_arr in ENCRYPTED_CONFIG_VALUES: if isinstance(config, str): - config = get_datastore_encryptor().dec(config) + config = get_datastore_encryptor().decrypt(config) elif isinstance(config, list): - config = [get_datastore_encryptor().dec(x) for x in config] + config = [get_datastore_encryptor().decrypt(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_datastore_encryptor().enc(item_value) + item_value = get_datastore_encryptor().encrypt(item_value) mongo.db.config.update( {"name": "newconfig"}, {"$addToSet": {item_key: item_value}}, upsert=False ) @@ -350,10 +350,10 @@ class ConfigService: ] else: flat_config[key] = [ - get_datastore_encryptor().dec(item) for item in flat_config[key] + get_datastore_encryptor().decrypt(item) for item in flat_config[key] ] else: - flat_config[key] = get_datastore_encryptor().dec(flat_config[key]) + flat_config[key] = get_datastore_encryptor().decrypt(flat_config[key]) return flat_config @staticmethod @@ -379,25 +379,25 @@ class ConfigService: ) else: config_arr[i] = ( - get_datastore_encryptor().dec(config_arr[i]) + get_datastore_encryptor().decrypt(config_arr[i]) if is_decrypt - else get_datastore_encryptor().enc(config_arr[i]) + else get_datastore_encryptor().encrypt(config_arr[i]) ) else: parent_config_arr[config_arr_as_array[-1]] = ( - get_datastore_encryptor().dec(config_arr) + get_datastore_encryptor().decrypt(config_arr) if is_decrypt - else get_datastore_encryptor().enc(config_arr) + else get_datastore_encryptor().encrypt(config_arr) ) @staticmethod def decrypt_ssh_key_pair(pair, encrypt=False): if encrypt: - pair["public_key"] = get_datastore_encryptor().enc(pair["public_key"]) - pair["private_key"] = get_datastore_encryptor().enc(pair["private_key"]) + pair["public_key"] = get_datastore_encryptor().encrypt(pair["public_key"]) + pair["private_key"] = get_datastore_encryptor().encrypt(pair["private_key"]) else: - pair["public_key"] = get_datastore_encryptor().dec(pair["public_key"]) - pair["private_key"] = get_datastore_encryptor().dec(pair["private_key"]) + pair["public_key"] = get_datastore_encryptor().decrypt(pair["public_key"]) + pair["private_key"] = get_datastore_encryptor().decrypt(pair["private_key"]) return pair @staticmethod diff --git a/monkey/monkey_island/cc/services/initialize.py b/monkey/monkey_island/cc/services/initialize.py index 6ff0d2706..b6e37bbc7 100644 --- a/monkey/monkey_island/cc/services/initialize.py +++ b/monkey/monkey_island/cc/services/initialize.py @@ -1,3 +1,4 @@ +from monkey_island.cc.services.authentication import AuthenticationService from monkey_island.cc.services.post_breach_files import PostBreachFilesService from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService @@ -5,3 +6,4 @@ from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService def initialize_services(data_dir): PostBreachFilesService.initialize(data_dir) LocalMonkeyRunService.initialize(data_dir) + AuthenticationService.initialize(key_file_directory=data_dir) diff --git a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py index 7c156930a..e302be5f5 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/exploit.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/exploit.py @@ -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_datastore_encryptor().enc(credential) + attempts[i][field] = get_datastore_encryptor().encrypt(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 ba72e822b..7d7f404ce 100644 --- a/monkey/monkey_island/cc/services/telemetry/processing/system_info.py +++ b/monkey/monkey_island/cc/services/telemetry/processing/system_info.py @@ -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_datastore_encryptor().enc(ssh_info[idx][field]) + ssh_info[idx][field] = get_datastore_encryptor().encrypt(ssh_info[idx][field]) def process_credential_info(telemetry_json): 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 89aa002fa..b54b3252c 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 @@ -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_datastore_encryptor().enc(key_value) + encrypted_key = get_datastore_encryptor().encrypt(key_value) ConfigService.set_config_value(path_to_keys + [key_type], encrypted_key)