diff --git a/CHANGELOG.md b/CHANGELOG.md index 69408a7fc..0aec3d277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ Changelog](https://keepachangelog.com/en/1.0.0/). - Generate a random password when creating a new user for CommunicateAsNewUser PBA. #1434 - Credentials gathered from victim machines are no longer stored plaintext in the database. #1454 +- Encrypt the database key with user's credentials. #1463 ## [1.11.0] - 2021-08-13 diff --git a/monkey/monkey_island/cc/app.py b/monkey/monkey_island/cc/app.py index 0bc20852f..5f877d318 100644 --- a/monkey/monkey_island/cc/app.py +++ b/monkey/monkey_island/cc/app.py @@ -54,7 +54,6 @@ from monkey_island.cc.resources.zero_trust.scoutsuite_auth.scoutsuite_auth impor from monkey_island.cc.resources.zero_trust.zero_trust_report import ZeroTrustReport from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.custom_json_encoder import CustomJSONEncoder -from monkey_island.cc.services.database import Database from monkey_island.cc.services.remote_run_aws import RemoteRunAwsService from monkey_island.cc.services.representations import output_json @@ -108,7 +107,6 @@ def init_app_services(app): with app.app_context(): database.init() - Database.init_db() # If running on AWS, this will initialize the instance data, which is used "later" in the # execution of the island. diff --git a/monkey/monkey_island/cc/resources/auth/auth.py b/monkey/monkey_island/cc/resources/auth/auth.py index 064395eaf..92a372a99 100644 --- a/monkey/monkey_island/cc/resources/auth/auth.py +++ b/monkey/monkey_island/cc/resources/auth/auth.py @@ -1,4 +1,3 @@ -import json import logging from functools import wraps @@ -9,8 +8,12 @@ from flask_jwt_extended.exceptions import JWTExtendedException from jwt import PyJWTError import monkey_island.cc.environment.environment_singleton as env_singleton -import monkey_island.cc.resources.auth.password_utils as password_utils import monkey_island.cc.resources.auth.user_store as user_store +from monkey_island.cc.resources.auth.credential_utils import ( + get_username_password_from_request, + password_matches_hash, +) +from monkey_island.cc.services.authentication import AuthenticationService logger = logging.getLogger(__name__) @@ -38,28 +41,20 @@ class Authenticate(flask_restful.Resource): "password": "my_password" } """ - (username, password) = _get_credentials_from_request(request) + username, password = get_username_password_from_request(request) if _credentials_match_registered_user(username, password): + AuthenticationService.ensure_datastore_encryptor(username, password) access_token = _create_access_token(username) return make_response({"access_token": access_token, "error": ""}, 200) else: return make_response({"error": "Invalid credentials"}, 401) -def _get_credentials_from_request(request): - credentials = json.loads(request.data) - - username = credentials["username"] - password = credentials["password"] - - return (username, password) - - -def _credentials_match_registered_user(username, password): +def _credentials_match_registered_user(username: str, password: str) -> bool: user = user_store.UserStore.username_table.get(username, None) - if user and password_utils.password_matches_hash(password, user.secret): + if user and password_matches_hash(password, user.secret): return True return False diff --git a/monkey/monkey_island/cc/resources/auth/credential_utils.py b/monkey/monkey_island/cc/resources/auth/credential_utils.py new file mode 100644 index 000000000..a0823d42b --- /dev/null +++ b/monkey/monkey_island/cc/resources/auth/credential_utils.py @@ -0,0 +1,32 @@ +import json +from typing import Tuple + +import bcrypt +from flask import Request, request + +from monkey_island.cc.environment.user_creds import UserCreds + + +def hash_password(plaintext_password): + salt = bcrypt.gensalt() + password_hash = bcrypt.hashpw(plaintext_password.encode("utf-8"), salt) + + return password_hash.decode() + + +def password_matches_hash(plaintext_password, password_hash): + return bcrypt.checkpw(plaintext_password.encode("utf-8"), password_hash.encode("utf-8")) + + +def get_user_credentials_from_request(_request) -> UserCreds: + username, password = get_username_password_from_request(_request) + password_hash = hash_password(password) + + return UserCreds(username, password_hash) + + +def get_username_password_from_request(_request: Request) -> Tuple[str, str]: + cred_dict = json.loads(request.data) + username = cred_dict.get("username", "") + password = cred_dict.get("password", "") + return username, password diff --git a/monkey/monkey_island/cc/resources/auth/password_utils.py b/monkey/monkey_island/cc/resources/auth/password_utils.py deleted file mode 100644 index f470fd882..000000000 --- a/monkey/monkey_island/cc/resources/auth/password_utils.py +++ /dev/null @@ -1,12 +0,0 @@ -import bcrypt - - -def hash_password(plaintext_password): - salt = bcrypt.gensalt() - password_hash = bcrypt.hashpw(plaintext_password.encode("utf-8"), salt) - - return password_hash.decode() - - -def password_matches_hash(plaintext_password, password_hash): - return bcrypt.checkpw(plaintext_password.encode("utf-8"), password_hash.encode("utf-8")) diff --git a/monkey/monkey_island/cc/resources/auth/registration.py b/monkey/monkey_island/cc/resources/auth/registration.py index 12c17d6e5..670fa4d19 100644 --- a/monkey/monkey_island/cc/resources/auth/registration.py +++ b/monkey/monkey_island/cc/resources/auth/registration.py @@ -1,13 +1,15 @@ -import json import logging import flask_restful from flask import make_response, request import monkey_island.cc.environment.environment_singleton as env_singleton -import monkey_island.cc.resources.auth.password_utils as password_utils from common.utils.exceptions import InvalidRegistrationCredentialsError, RegistrationNotNeededError -from monkey_island.cc.environment.user_creds import UserCreds +from monkey_island.cc.resources.auth.credential_utils import ( + get_user_credentials_from_request, + get_username_password_from_request, +) +from monkey_island.cc.services.authentication import AuthenticationService from monkey_island.cc.setup.mongo.database_initializer import reset_database logger = logging.getLogger(__name__) @@ -19,21 +21,13 @@ class Registration(flask_restful.Resource): return {"needs_registration": is_registration_needed} def post(self): - credentials = _get_user_credentials_from_request(request) + credentials = get_user_credentials_from_request(request) try: env_singleton.env.try_add_user(credentials) + username, password = get_username_password_from_request(request) + AuthenticationService.reset_datastore_encryptor(username, password) reset_database() return make_response({"error": ""}, 200) except (InvalidRegistrationCredentialsError, RegistrationNotNeededError) as e: return make_response({"error": str(e)}, 400) - - -def _get_user_credentials_from_request(request): - cred_dict = json.loads(request.data) - - username = cred_dict.get("user", "") - password = cred_dict.get("password", "") - password_hash = password_utils.hash_password(password) - - return UserCreds(username, password_hash) diff --git a/monkey/monkey_island/cc/resources/configuration_export.py b/monkey/monkey_island/cc/resources/configuration_export.py index c550acc7d..111cfa177 100644 --- a/monkey/monkey_island/cc/resources/configuration_export.py +++ b/monkey/monkey_island/cc/resources/configuration_export.py @@ -4,7 +4,7 @@ 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.server_utils.encryption import PasswordBasedStringEncryptor from monkey_island.cc.services.config import ConfigService @@ -21,7 +21,7 @@ class ConfigurationExport(flask_restful.Resource): password = data["password"] plaintext_config = json.dumps(plaintext_config) - pb_encryptor = PasswordBasedEncryptor(password) + pb_encryptor = PasswordBasedStringEncryptor(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 6c0575e94..3a66a2ed0 100644 --- a/monkey/monkey_island/cc/resources/configuration_import.py +++ b/monkey/monkey_island/cc/resources/configuration_import.py @@ -11,7 +11,7 @@ from monkey_island.cc.resources.auth.auth import jwt_required from monkey_island.cc.server_utils.encryption import ( InvalidCiphertextError, InvalidCredentialsError, - PasswordBasedEncryptor, + PasswordBasedStringEncryptor, is_encrypted, ) from monkey_island.cc.services.config import ConfigService @@ -72,7 +72,7 @@ class ConfigurationImport(flask_restful.Resource): try: config = request_contents["config"] if ConfigurationImport.is_config_encrypted(request_contents["config"]): - pb_encryptor = PasswordBasedEncryptor(request_contents["password"]) + pb_encryptor = PasswordBasedStringEncryptor(request_contents["password"]) config = pb_encryptor.decrypt(config) return json.loads(config) except (JSONDecodeError, InvalidCiphertextError): diff --git a/monkey/monkey_island/cc/server_setup.py b/monkey/monkey_island/cc/server_setup.py index 69ab0437a..fdb94b67f 100644 --- a/monkey/monkey_island/cc/server_setup.py +++ b/monkey/monkey_island/cc/server_setup.py @@ -27,7 +27,6 @@ from monkey_island.cc.server_utils.consts import ( # noqa: E402 GEVENT_EXCEPTION_LOG, MONGO_CONNECTION_TIMEOUT, ) -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 @@ -87,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_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 index 7d806139c..109423634 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/__init__.py +++ b/monkey/monkey_island/cc/server_utils/encryption/__init__.py @@ -1,15 +1,20 @@ -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, +from monkey_island.cc.server_utils.encryption.encryptors.i_encryptor import IEncryptor +from monkey_island.cc.server_utils.encryption.encryptors.key_based_encryptor import ( + KeyBasedEncryptor, +) +from monkey_island.cc.server_utils.encryption.encryptors.password_based_string_encryptior import ( + PasswordBasedStringEncryptor, is_encrypted, ) -from monkey_island.cc.server_utils.encryption.data_store_encryptor import ( - DataStoreEncryptor, - get_datastore_encryptor, +from monkey_island.cc.server_utils.encryption.encryptors.password_based_bytes_encryption import ( + PasswordBasedBytesEncryptor, + InvalidCredentialsError, + InvalidCiphertextError, +) +from .data_store_encryptor import ( initialize_datastore_encryptor, + get_datastore_encryptor, + remove_old_datastore_key, ) from .dict_encryption.dict_encryptor import ( SensitiveField, 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 215703c02..f7add80f3 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,50 +1,59 @@ import os +from typing import Union -# 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.encryption import ( + IEncryptor, + KeyBasedEncryptor, + PasswordBasedBytesEncryptor, +) from monkey_island.cc.server_utils.file_utils import open_new_securely_permissioned_file -_encryptor = None +_KEY_FILENAME = "mongo_key.bin" +_BLOCK_SIZE = 32 + +_encryptor: Union[None, IEncryptor] = 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 _load_existing_key(key_file_path: str, secret: str) -> KeyBasedEncryptor: + 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_file_dir): +def _create_new_key(key_file_path: str, secret: str) -> KeyBasedEncryptor: + 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_file_dir) + 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_datastore_encryptor(): +def _get_key_file_path(key_file_dir: str) -> str: + return os.path.join(key_file_dir, _KEY_FILENAME) + + +def get_datastore_encryptor() -> IEncryptor: return _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/encryptors/__init__.py b/monkey/monkey_island/cc/server_utils/encryption/encryptors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/monkey/monkey_island/cc/server_utils/encryption/i_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/encryptors/i_encryptor.py similarity index 100% rename from monkey/monkey_island/cc/server_utils/encryption/i_encryptor.py rename to monkey/monkey_island/cc/server_utils/encryption/encryptors/i_encryptor.py diff --git a/monkey/monkey_island/cc/server_utils/encryption/key_based_encryptor.py b/monkey/monkey_island/cc/server_utils/encryption/encryptors/key_based_encryptor.py similarity index 100% rename from monkey/monkey_island/cc/server_utils/encryption/key_based_encryptor.py rename to monkey/monkey_island/cc/server_utils/encryption/encryptors/key_based_encryptor.py diff --git a/monkey/monkey_island/cc/server_utils/encryption/password_based_encryption.py b/monkey/monkey_island/cc/server_utils/encryption/encryptors/password_based_bytes_encryption.py similarity index 66% rename from monkey/monkey_island/cc/server_utils/encryption/password_based_encryption.py rename to monkey/monkey_island/cc/server_utils/encryption/encryptors/password_based_bytes_encryption.py index 20708ce31..2e7c05819 100644 --- a/monkey/monkey_island/cc/server_utils/encryption/password_based_encryption.py +++ b/monkey/monkey_island/cc/server_utils/encryption/encryptors/password_based_bytes_encryption.py @@ -1,4 +1,3 @@ -import base64 import io import logging @@ -17,36 +16,30 @@ logger = logging.getLogger(__name__) # Note: password != key -class PasswordBasedEncryptor(IEncryptor): +class PasswordBasedBytesEncryptor(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()) + def encrypt(self, plaintext: bytes) -> bytes: ciphertext_stream = io.BytesIO() pyAesCrypt.encryptStream( - plaintext_stream, ciphertext_stream, self.password, self._BUFFER_SIZE + io.BytesIO(plaintext), ciphertext_stream, self.password, self._BUFFER_SIZE ) - ciphertext_b64 = base64.b64encode(ciphertext_stream.getvalue()) - logger.info("String encrypted.") + return ciphertext_stream.getvalue() - return ciphertext_b64.decode() - - def decrypt(self, ciphertext: str): - ciphertext = base64.b64decode(ciphertext) - ciphertext_stream = io.BytesIO(ciphertext) + def decrypt(self, ciphertext: bytes) -> bytes: plaintext_stream = io.BytesIO() - ciphertext_stream_len = len(ciphertext_stream.getvalue()) + ciphertext_stream_len = len(ciphertext) try: pyAesCrypt.decryptStream( - ciphertext_stream, + io.BytesIO(ciphertext), plaintext_stream, self.password, self._BUFFER_SIZE, @@ -59,7 +52,7 @@ class PasswordBasedEncryptor(IEncryptor): else: logger.info("The corrupt ciphertext provided.") raise InvalidCiphertextError - return plaintext_stream.getvalue().decode("utf-8") + return plaintext_stream.getvalue() class InvalidCredentialsError(Exception): @@ -68,8 +61,3 @@ class InvalidCredentialsError(Exception): 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/encryption/encryptors/password_based_string_encryptior.py b/monkey/monkey_island/cc/server_utils/encryption/encryptors/password_based_string_encryptior.py new file mode 100644 index 000000000..9f99f735b --- /dev/null +++ b/monkey/monkey_island/cc/server_utils/encryption/encryptors/password_based_string_encryptior.py @@ -0,0 +1,35 @@ +import base64 +import logging + +import pyAesCrypt + +from monkey_island.cc.server_utils.encryption import IEncryptor +from monkey_island.cc.server_utils.encryption.encryptors.password_based_bytes_encryption import ( + PasswordBasedBytesEncryptor, +) + +logger = logging.getLogger(__name__) + + +class PasswordBasedStringEncryptor(IEncryptor): + + _BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef + + def __init__(self, password: str): + self.password = password + + def encrypt(self, plaintext: str) -> str: + ciphertext = PasswordBasedBytesEncryptor(self.password).encrypt(plaintext.encode()) + + return base64.b64encode(ciphertext).decode() + + def decrypt(self, ciphertext: str) -> str: + ciphertext = base64.b64decode(ciphertext) + + plaintext_stream = PasswordBasedBytesEncryptor(self.password).decrypt(ciphertext) + return plaintext_stream.decode() + + +def is_encrypted(ciphertext: str) -> bool: + ciphertext = base64.b64decode(ciphertext) + return ciphertext.startswith(b"AES") 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..9d3d3baa7 --- /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, + 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/database.py b/monkey/monkey_island/cc/services/database.py index afd4ecc02..fb46cd726 100644 --- a/monkey/monkey_island/cc/services/database.py +++ b/monkey/monkey_island/cc/services/database.py @@ -33,11 +33,6 @@ class Database(object): mongo.db[collection_name].drop() logger.info("Dropped collection {}".format(collection_name)) - @staticmethod - def init_db(): - if not mongo.db.collection_names(): - Database.reset_db() - @staticmethod def is_mitigations_missing() -> bool: return bool(AttackMitigations.COLLECTION_NAME not in mongo.db.list_collection_names()) 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) diff --git a/monkey/monkey_island/cc/ui/src/services/AuthService.js b/monkey/monkey_island/cc/ui/src/services/AuthService.js index 11cf37044..7838a8563 100644 --- a/monkey/monkey_island/cc/ui/src/services/AuthService.js +++ b/monkey/monkey_island/cc/ui/src/services/AuthService.js @@ -46,7 +46,7 @@ export default class AuthService { return this._authFetch(this.REGISTRATION_API_ENDPOINT, { method: 'POST', body: JSON.stringify({ - 'user': username, + 'username': username, 'password': password }) }).then(res => { diff --git a/monkey/tests/data_for_tests/mongo_key.bin b/monkey/tests/data_for_tests/mongo_key.bin index 6b8091efb..7b49bd4dc 100644 Binary files a/monkey/tests/data_for_tests/mongo_key.bin and b/monkey/tests/data_for_tests/mongo_key.bin differ diff --git a/monkey/tests/unit_tests/monkey_island/cc/conftest.py b/monkey/tests/unit_tests/monkey_island/cc/conftest.py index 9cca0caab..7d9642201 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/conftest.py +++ b/monkey/tests/unit_tests/monkey_island/cc/conftest.py @@ -11,6 +11,7 @@ from tests.unit_tests.monkey_island.cc.server_utils.encryption.test_password_bas ) from monkey_island.cc.server_utils.encryption import initialize_datastore_encryptor +from monkey_island.cc.services.authentication import AuthenticationService @pytest.fixture @@ -27,6 +28,11 @@ def monkey_config_json(monkey_config): return json.dumps(monkey_config) +MOCK_USERNAME = "m0nk3y_u53r" +MOCK_PASSWORD = "3cr3t_p455w0rd" + + @pytest.fixture def uses_encryptor(data_for_tests_dir): - initialize_datastore_encryptor(data_for_tests_dir) + secret = AuthenticationService._get_secret_from_credentials(MOCK_USERNAME, MOCK_PASSWORD) + initialize_datastore_encryptor(data_for_tests_dir, secret) 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 d02ad5bbb..7ea21849b 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,6 +1,3 @@ -import pytest - -from monkey_island.cc.server_utils.encryption import initialize_datastore_encryptor from monkey_island.cc.server_utils.encryption.dict_encryption.field_encryptors import ( StringListEncryptor, ) @@ -9,11 +6,6 @@ MOCK_STRING_LIST = ["test_1", "test_2"] EMPTY_LIST = [] -@pytest.fixture -def uses_encryptor(data_for_tests_dir): - initialize_datastore_encryptor(data_for_tests_dir) - - def test_encryption_and_decryption(uses_encryptor): encrypted_list = StringListEncryptor.encrypt(MOCK_STRING_LIST) assert not encrypted_list == MOCK_STRING_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 fb397f234..bf7ccff80 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 @@ -8,7 +8,7 @@ from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption from common.utils.exceptions import InvalidConfigurationError from monkey_island.cc.resources.configuration_import import ConfigurationImport -from monkey_island.cc.server_utils.encryption import PasswordBasedEncryptor +from monkey_island.cc.server_utils.encryption import PasswordBasedStringEncryptor def test_is_config_encrypted__json(monkey_config_json): @@ -17,7 +17,7 @@ def test_is_config_encrypted__json(monkey_config_json): @pytest.mark.slow def test_is_config_encrypted__ciphertext(monkey_config_json): - pb_encryptor = PasswordBasedEncryptor(PASSWORD) + pb_encryptor = PasswordBasedStringEncryptor(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 index bb005fbf7..7c379af1c 100644 --- 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 @@ -1,35 +1,61 @@ -import os +import pytest from monkey_island.cc.server_utils.encryption import ( + data_store_encryptor, get_datastore_encryptor, initialize_datastore_encryptor, + remove_old_datastore_key, ) -PASSWORD_FILENAME = "mongo_key.bin" - PLAINTEXT = "Hello, Monkey!" -CYPHERTEXT = "vKgvD6SjRyIh1dh2AM/rnTa0NI/vjfwnbZLbMocWtE4e42WJmSUz2ordtbQrH1Fq" +MOCK_SECRET = "53CR31" -def test_aes_cbc_encryption(data_for_tests_dir): - initialize_datastore_encryptor(data_for_tests_dir) +@pytest.mark.usefixtures("uses_encryptor") +def test_encryption(data_for_tests_dir): + encrypted_data = get_datastore_encryptor().encrypt(PLAINTEXT) + assert encrypted_data != PLAINTEXT - assert get_datastore_encryptor().enc(PLAINTEXT) != PLAINTEXT + decrypted_data = get_datastore_encryptor().decrypt(encrypted_data) + assert decrypted_data == PLAINTEXT -def test_aes_cbc_decryption(data_for_tests_dir): - initialize_datastore_encryptor(data_for_tests_dir) - - assert get_datastore_encryptor().dec(CYPHERTEXT) == PLAINTEXT +@pytest.fixture +def cleanup_encryptor(): + yield + data_store_encryptor._encryptor = None -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 +@pytest.mark.usefixtures("cleanup_encryptor") +@pytest.fixture +def initialized_encryptor_dir(tmpdir): + initialize_datastore_encryptor(tmpdir, MOCK_SECRET) + return tmpdir -def test_create_new_password_file(tmpdir): - initialize_datastore_encryptor(tmpdir) +def test_key_creation(initialized_encryptor_dir): + assert (initialized_encryptor_dir / data_store_encryptor._KEY_FILENAME).isfile() - assert os.path.isfile(os.path.join(tmpdir, PASSWORD_FILENAME)) + +def test_key_removal(initialized_encryptor_dir): + remove_old_datastore_key(initialized_encryptor_dir) + assert not (initialized_encryptor_dir / data_store_encryptor._KEY_FILENAME).isfile() + + +def test_key_removal__no_key(tmpdir): + assert not (tmpdir / data_store_encryptor._KEY_FILENAME).isfile() + # Make sure no error thrown when we try to remove an non-existing key + remove_old_datastore_key(tmpdir) + data_store_encryptor._factory = None + + +@pytest.mark.usefixtures("cleanup_encryptor") +def test_key_file_encryption(tmpdir, monkeypatch): + monkeypatch.setattr(data_store_encryptor, "_get_random_bytes", lambda: PLAINTEXT.encode()) + initialize_datastore_encryptor(tmpdir, MOCK_SECRET) + key_file_path = data_store_encryptor._get_key_file_path(tmpdir) + key_file_contents = open(key_file_path, "rb").read() + assert not key_file_contents == PLAINTEXT.encode() + + key_based_encryptor = data_store_encryptor._load_existing_key(key_file_path, MOCK_SECRET) + assert key_based_encryptor._key == PLAINTEXT.encode() diff --git a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py index d00609481..a231f3219 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py +++ b/monkey/tests/unit_tests/monkey_island/cc/server_utils/encryption/test_password_based_encryption.py @@ -4,7 +4,10 @@ from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption VALID_CIPHER_TEXT, ) -from monkey_island.cc.server_utils.encryption import InvalidCredentialsError, PasswordBasedEncryptor +from monkey_island.cc.server_utils.encryption import ( + InvalidCredentialsError, + PasswordBasedStringEncryptor, +) MONKEY_CONFIGS_DIR_PATH = "monkey_configs" STANDARD_PLAINTEXT_MONKEY_CONFIG_FILENAME = "monkey_config_standard.json" @@ -14,27 +17,27 @@ INCORRECT_PASSWORD = "goodbye321" @pytest.mark.slow def test_encrypt_decrypt_string(monkey_config_json): - pb_encryptor = PasswordBasedEncryptor(PASSWORD) + pb_encryptor = PasswordBasedStringEncryptor(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) + pb_encryptor = PasswordBasedStringEncryptor(INCORRECT_PASSWORD) with pytest.raises(InvalidCredentialsError): pb_encryptor.decrypt(VALID_CIPHER_TEXT) @pytest.mark.slow def test_decrypt_string__malformed_corrupted(): - pb_encryptor = PasswordBasedEncryptor(PASSWORD) + pb_encryptor = PasswordBasedStringEncryptor(PASSWORD) with pytest.raises(ValueError): pb_encryptor.decrypt(MALFORMED_CIPHER_TEXT_CORRUPTED) @pytest.mark.slow def test_decrypt_string__no_password(monkey_config_json): - pb_encryptor = PasswordBasedEncryptor("") + pb_encryptor = PasswordBasedStringEncryptor("") with pytest.raises(InvalidCredentialsError): pb_encryptor.decrypt(VALID_CIPHER_TEXT) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py index 6829965f2..a16299707 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/reporting/test_report.py @@ -156,7 +156,7 @@ def test_get_stolen_creds_exploit(fake_mongo): assert expected_stolen_creds_exploit == stolen_creds_exploit -@pytest.mark.usefixtures("uses_database") +@pytest.mark.usefixtures("uses_database", "uses_encryptor") def test_get_stolen_creds_system_info(fake_mongo): fake_mongo.db.monkey.insert_one(MONKEY_TELEM) save_telemetry(SYSTEM_INFO_TELEMETRY_TELEM) diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py index 799fc40e1..75b3152e5 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config.py @@ -18,12 +18,14 @@ def mock_port_in_env_singleton(monkeypatch, PORT): monkeypatch.setattr("monkey_island.cc.services.config.env_singleton", mock_singleton) +@pytest.mark.usefixtures("uses_encryptor") def test_set_server_ips_in_config_command_servers(config, IPS, PORT): ConfigService.set_server_ips_in_config(config) expected_config_command_servers = [f"{ip}:{PORT}" for ip in IPS] assert config["internal"]["island_server"]["command_servers"] == expected_config_command_servers +@pytest.mark.usefixtures("uses_encryptor") def test_set_server_ips_in_config_current_server(config, IPS, PORT): ConfigService.set_server_ips_in_config(config) expected_config_current_server = f"{IPS[0]}:{PORT}" diff --git a/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py b/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py index 12cd44c10..1935d6f79 100644 --- a/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py +++ b/monkey/tests/unit_tests/monkey_island/cc/services/test_config_manipulator.py @@ -1,7 +1,10 @@ +import pytest + from monkey_island.cc.services.config_manipulator import update_config_on_mode_set from monkey_island.cc.services.mode.mode_enum import IslandModeEnum +@pytest.mark.usefixtures("uses_encryptor") def test_update_config_on_mode_set_advanced(config, monkeypatch): monkeypatch.setattr("monkey_island.cc.services.config.ConfigService.get_config", lambda: config) monkeypatch.setattr( @@ -14,6 +17,7 @@ def test_update_config_on_mode_set_advanced(config, monkeypatch): assert manipulated_config == config +@pytest.mark.usefixtures("uses_encryptor") def test_update_config_on_mode_set_ransomware(config, monkeypatch): monkeypatch.setattr("monkey_island.cc.services.config.ConfigService.get_config", lambda: config) monkeypatch.setattr( 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 2e6c2fd50..974377915 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,10 +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.encryption import ( - get_datastore_encryptor, - initialize_datastore_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.zero_trust.scoutsuite.scoutsuite_auth_service import ( is_aws_keys_setup, @@ -19,7 +16,7 @@ class MockObject: pass -@pytest.mark.usefixtures("uses_database") +@pytest.mark.usefixtures("uses_database", "uses_encryptor") def test_is_aws_keys_setup(tmp_path): # Mock default configuration ConfigService.init_default_config() @@ -29,9 +26,7 @@ def test_is_aws_keys_setup(tmp_path): mongo.db.config.find_one = MagicMock(return_value=ConfigService.default_config) assert not is_aws_keys_setup() - # Make sure noone changed config path and broke this function - initialize_datastore_encryptor(tmp_path) - bogus_key_value = get_datastore_encryptor().enc("bogus_aws_key") + bogus_key_value = get_datastore_encryptor().encrypt("bogus_aws_key") dpath.util.set( ConfigService.default_config, AWS_KEYS_PATH + ["aws_secret_access_key"], bogus_key_value )