Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mongo key encryption #1506

Merged
merged 17 commits into from
Oct 4, 2021
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
191fbea
Refactor password based encryptor into PasswordBasedStringEncryptor a…
VakarisZ Sep 30, 2021
fd1cb9d
Add a secret to datastore encryptor
VakarisZ Sep 30, 2021
4f17693
Split up the initialization of mongo_key into 2 parts: directory of m…
VakarisZ Sep 30, 2021
f97ec4e
Implement data store encryptor key removal on registration and unit t…
VakarisZ Oct 1, 2021
e280c4f
Move data store encryptor secret generation into the data store encry…
VakarisZ Oct 1, 2021
4cbed6d
Fix typos and rename files/classes related to data store encryptor. C…
VakarisZ Oct 1, 2021
ddae092
Refactor test_data_store_encryptor.py to use (path / to / file).isfil…
VakarisZ Oct 1, 2021
b2bbb62
Add CHANGELOG.md entry for #1463 (Encrypt the database key with user'…
VakarisZ Oct 1, 2021
da169dd
Refactor DataStoreEncryptor by splitting up initialization related me…
VakarisZ Oct 1, 2021
26ba02a
Refactor get_credentials_from_request to get_username_password_from_r…
VakarisZ Oct 1, 2021
9d6dc3b
Move all encryptor building related code to encryptor_factory.py from…
VakarisZ Oct 1, 2021
34d065c
Move encryptors into a separate folder
VakarisZ Oct 4, 2021
3ec26bc
Refactor data store encryptor to IEncryptor interface, move data stor…
VakarisZ Oct 4, 2021
ea6fe37
Fix scoutsuite unit test to use updated datastore encryptor interface
VakarisZ Oct 4, 2021
a2b09a9
Fix unit tests for data store encryptor
VakarisZ Oct 4, 2021
3b5dd6a
Remove database initialization during island startup
VakarisZ Oct 4, 2021
ddff2f0
Refactor a couple of imports into a shorter import statement
VakarisZ Oct 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 9 additions & 6 deletions monkey/monkey_island/cc/resources/auth/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import monkey_island.cc.environment.environment_singleton as env_singleton
import monkey_island.cc.resources.auth.user_store as user_store
from monkey_island.cc.resources.auth.credential_utils import (
get_creds_from_request,
get_secret_from_request,
get_username_password_from_request,
password_matches_hash,
)
from monkey_island.cc.server_utils.encryption.data_store_encryptor import setup_datastore_key
from monkey_island.cc.server_utils.encryption import (
get_datastore_encryptor,
initialize_datastore_encryptor,
)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -42,17 +44,18 @@ def post(self):
"password": "my_password"
}
"""
username, password = get_creds_from_request(request)
username, password = get_username_password_from_request(request)

if _credentials_match_registered_user(username, password):
setup_datastore_key(get_secret_from_request(request))
if not get_datastore_encryptor():
initialize_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 _credentials_match_registered_user(username: str, password: str):
def _credentials_match_registered_user(username: str, password: str) -> bool:
user = user_store.UserStore.username_table.get(username, None)

if user and password_matches_hash(password, user.secret):
Expand Down
9 changes: 2 additions & 7 deletions monkey/monkey_island/cc/resources/auth/credential_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,13 @@ def password_matches_hash(plaintext_password, password_hash):


def get_user_credentials_from_request(_request) -> UserCreds:
username, password = get_creds_from_request(_request)
username, password = get_username_password_from_request(_request)
password_hash = hash_password(password)

return UserCreds(username, password_hash)


def get_secret_from_request(_request) -> str:
username, password = get_creds_from_request(_request)
return f"{username}:{password}"


def get_creds_from_request(_request: Request) -> Tuple[str, str]:
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", "")
Expand Down
12 changes: 8 additions & 4 deletions monkey/monkey_island/cc/resources/auth/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@
import monkey_island.cc.environment.environment_singleton as env_singleton
from common.utils.exceptions import InvalidRegistrationCredentialsError, RegistrationNotNeededError
from monkey_island.cc.resources.auth.credential_utils import (
get_secret_from_request,
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.server_utils.encryption.data_store_encryptor import setup_datastore_key
from monkey_island.cc.setup.mongo.database_initializer import reset_database

logger = logging.getLogger(__name__)
Expand All @@ -21,12 +24,13 @@ def get(self):
return {"needs_registration": is_registration_needed}

def post(self):
# TODO delete the old key here, before creating new one
credentials = get_user_credentials_from_request(request)

try:
env_singleton.env.try_add_user(credentials)
setup_datastore_key(get_secret_from_request(request))
remove_old_datastore_key()
username, password = get_username_password_from_request(request)
initialize_datastore_encryptor(username, password)
reset_database()
return make_response({"error": ""}, 200)
except (InvalidRegistrationCredentialsError, RegistrationNotNeededError) as e:
Expand Down
5 changes: 3 additions & 2 deletions monkey/monkey_island/cc/server_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

# 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)
Expand All @@ -27,7 +29,6 @@
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
Expand Down Expand Up @@ -87,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_datastore_encryptor(config_options.data_dir)
initialize_encryptor_factory(config_options.data_dir)
initialize_services(config_options.data_dir)


Expand Down
15 changes: 9 additions & 6 deletions monkey/monkey_island/cc/server_utils/encryption/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
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_string_encryption import (
from monkey_island.cc.server_utils.encryption.password_based_string_encryptior import (
PasswordBasedStringEncryptor,
is_encrypted,
)
from .password_based_byte_encryption import InvalidCredentialsError, InvalidCiphertextError
from monkey_island.cc.server_utils.encryption.data_store_encryptor import (
DataStoreEncryptor,
get_datastore_encryptor,
initialize_datastore_encryptor,
from .encryptor_factory import (
FactoryNotInitializedError,
remove_old_datastore_key,
get_encryptor_factory,
get_secret_from_credentials,
initialize_encryptor_factory,
)
from .data_store_encryptor import initialize_datastore_encryptor, get_datastore_encryptor
from .password_based_bytes_encryption import InvalidCredentialsError, InvalidCiphertextError
from .dict_encryption.dict_encryptor import (
SensitiveField,
encrypt_dict,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from __future__ import annotations

import io
import os

# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but
Expand All @@ -9,9 +8,13 @@

from Crypto import Random # noqa: DUO133 # nosec: B413

from monkey_island.cc.server_utils.encryption import KeyBasedEncryptor
from monkey_island.cc.server_utils.encryption.password_based_byte_encryption import (
PasswordBasedByteEncryptor,
from monkey_island.cc.server_utils.encryption import FactoryNotInitializedError, KeyBasedEncryptor
from monkey_island.cc.server_utils.encryption.encryptor_factory import (
get_encryptor_factory,
get_secret_from_credentials,
)
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

Expand All @@ -20,61 +23,43 @@

class DataStoreEncryptor:
_BLOCK_SIZE = 32
_KEY_FILENAME = "mongo_key.bin"

def __init__(self, key_file_dir: str):
self.key_file_path = os.path.join(key_file_dir, self._KEY_FILENAME)
self._key_base_encryptor = None

def init_key(self, secret: str):
if os.path.exists(self.key_file_path):
self._load_existing_key(secret)
def __init__(self, key_file_path: str, secret: str):
if os.path.exists(key_file_path):
self._key_based_encryptor = DataStoreEncryptor._load_existing_key(key_file_path, secret)
else:
self._create_new_key(secret)
self._key_based_encryptor = DataStoreEncryptor._create_new_key(key_file_path, secret)

def _load_existing_key(self, secret: str):
with open(self.key_file_path, "rb") as f:
@staticmethod
def _load_existing_key(key_file_path: str, secret: str):
with open(key_file_path, "rb") as f:
encrypted_key = f.read()
cipher_key = (
PasswordBasedByteEncryptor(secret).decrypt(io.BytesIO(encrypted_key)).getvalue()
)
self._key_base_encryptor = KeyBasedEncryptor(cipher_key)

def _create_new_key(self, secret: str):
cipher_key = Random.new().read(self._BLOCK_SIZE)
encrypted_key = (
PasswordBasedByteEncryptor(secret).encrypt(io.BytesIO(cipher_key)).getvalue()
)
with open_new_securely_permissioned_file(self.key_file_path, "wb") as f:
cipher_key = PasswordBasedBytesEncryptor(secret).decrypt(encrypted_key)
return KeyBasedEncryptor(cipher_key)

@staticmethod
def _create_new_key(key_file_path: str, secret: str):
cipher_key = Random.new().read(DataStoreEncryptor._BLOCK_SIZE)
encrypted_key = PasswordBasedBytesEncryptor(secret).encrypt(cipher_key)
with open_new_securely_permissioned_file(key_file_path, "wb") as f:
f.write(encrypted_key)
self._key_base_encryptor = KeyBasedEncryptor(cipher_key)

def is_key_setup(self) -> bool:
return self._key_base_encryptor is not None
return KeyBasedEncryptor(cipher_key)

def enc(self, message: str):
return self._key_base_encryptor.encrypt(message)
return self._key_based_encryptor.encrypt(message)

def dec(self, enc_message: str):
return self._key_base_encryptor.decrypt(enc_message)
return self._key_based_encryptor.decrypt(enc_message)


def initialize_datastore_encryptor(key_file_dir: str):
def initialize_datastore_encryptor(username: str, password: str):
global _encryptor

_encryptor = DataStoreEncryptor(key_file_dir)


class EncryptorNotInitializedError(Exception):
pass


def setup_datastore_key(secret: str):
if _encryptor is None:
raise EncryptorNotInitializedError
else:
if not _encryptor.is_key_setup():
_encryptor.init_key(secret)
factory = get_encryptor_factory()
if not factory:
raise FactoryNotInitializedError
secret = get_secret_from_credentials(username, password)
_encryptor = DataStoreEncryptor(factory.key_file_path, secret)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Factories are responsible for constructing objects. We should have factory.set_key_file_path() and factory.set_secret() and then call factory.construct_data_store_encryptor() or similar.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this is not a factory, it's a class that handles initialization parameters for data store encryptor. I'm not sure what would be a better name, maybe DataStoreEncryptorInitializer? I don't see any benefits in making this an actual factory, since the output object will not be different depending on key file path and secret. What is more, the usage of such factory is less intuitive. The registration resource would have to factory.set_secret() then built_encryptor = factory.construct_data_store_encryptor() and finally set_encryptor(built_encryptor). Now it's only one call - initialize_datastore_encryptor(username, password).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can simplify this way down. I'm going to dig into it a bit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try to refactor and see how it looks



def get_datastore_encryptor():
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

import os
from ctypes import Union

_factory: Union[None, EncryptorFactory] = None


class EncryptorFactory:

_KEY_FILENAME = "mongo_key.bin"

def __init__(self, key_file_dir: str):
self.key_file_path = os.path.join(key_file_dir, self._KEY_FILENAME)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer pathlib



class FactoryNotInitializedError(Exception):
pass


def get_secret_from_credentials(username: str, password: str) -> str:
return f"{username}:{password}"


def remove_old_datastore_key():
if _factory is None:
raise FactoryNotInitializedError
if os.path.isfile(_factory.key_file_path):
os.remove(_factory.key_file_path)


def initialize_encryptor_factory(key_file_dir: str):
global _factory
_factory = EncryptorFactory(key_file_dir)


def get_encryptor_factory():
return _factory
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import io
import logging
from io import BytesIO

import pyAesCrypt

Expand All @@ -17,28 +16,30 @@
# Note: password != key


class PasswordBasedByteEncryptor(IEncryptor):
class PasswordBasedBytesEncryptor(IEncryptor):

_BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef

def __init__(self, password: str):
self.password = password

def encrypt(self, plaintext: BytesIO) -> BytesIO:
def encrypt(self, plaintext: bytes) -> bytes:
ciphertext_stream = io.BytesIO()

pyAesCrypt.encryptStream(plaintext, ciphertext_stream, self.password, self._BUFFER_SIZE)
pyAesCrypt.encryptStream(
io.BytesIO(plaintext), ciphertext_stream, self.password, self._BUFFER_SIZE
)

return ciphertext_stream
return ciphertext_stream.getvalue()

def decrypt(self, ciphertext: BytesIO) -> BytesIO:
def decrypt(self, ciphertext: bytes) -> bytes:
plaintext_stream = io.BytesIO()

ciphertext_stream_len = len(ciphertext.getvalue())
ciphertext_stream_len = len(ciphertext)

try:
pyAesCrypt.decryptStream(
ciphertext,
io.BytesIO(ciphertext),
plaintext_stream,
self.password,
self._BUFFER_SIZE,
Expand All @@ -51,7 +52,7 @@ def decrypt(self, ciphertext: BytesIO) -> BytesIO:
else:
logger.info("The corrupt ciphertext provided.")
raise InvalidCiphertextError
return plaintext_stream
return plaintext_stream.getvalue()


class InvalidCredentialsError(Exception):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import base64
import io
import logging

import pyAesCrypt

from monkey_island.cc.server_utils.encryption import IEncryptor
from monkey_island.cc.server_utils.encryption.password_based_byte_encryption import (
PasswordBasedByteEncryptor,
from monkey_island.cc.server_utils.encryption.password_based_bytes_encryption import (
PasswordBasedBytesEncryptor,
)

logger = logging.getLogger(__name__)
Expand All @@ -20,17 +19,15 @@ def __init__(self, password: str):
self.password = password

def encrypt(self, plaintext: str) -> str:
plaintext_stream = io.BytesIO(plaintext.encode())
ciphertext = PasswordBasedByteEncryptor(self.password).encrypt(plaintext_stream)
ciphertext = PasswordBasedBytesEncryptor(self.password).encrypt(plaintext.encode())

return base64.b64encode(ciphertext.getvalue()).decode()
return base64.b64encode(ciphertext).decode()

def decrypt(self, ciphertext: str) -> str:
ciphertext = base64.b64decode(ciphertext)
ciphertext_stream = io.BytesIO(ciphertext)

plaintext_stream = PasswordBasedByteEncryptor(self.password).decrypt(ciphertext_stream)
return plaintext_stream.getvalue().decode("utf-8")
plaintext_stream = PasswordBasedBytesEncryptor(self.password).decrypt(ciphertext)
return plaintext_stream.decode()


def is_encrypted(ciphertext: str) -> bool:
Expand Down
Binary file modified monkey/tests/data_for_tests/mongo_key.bin
Binary file not shown.
Loading