Skip to content

Commit

Permalink
Merge pull request #1506 from guardicore/mongo_key_encryption
Browse files Browse the repository at this point in the history
Mongo key encryption
  • Loading branch information
VakarisZ authored Oct 4, 2021
2 parents 2adf5a7 + ddff2f0 commit af99482
Show file tree
Hide file tree
Showing 37 changed files with 280 additions and 175 deletions.
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
2 changes: 0 additions & 2 deletions monkey/monkey_island/cc/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
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

Expand Down Expand Up @@ -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.
Expand Down
23 changes: 9 additions & 14 deletions monkey/monkey_island/cc/resources/auth/auth.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import json
import logging
from functools import wraps

Expand All @@ -9,8 +8,12 @@
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__)

Expand Down Expand Up @@ -38,28 +41,20 @@ def post(self):
"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
Expand Down
32 changes: 32 additions & 0 deletions monkey/monkey_island/cc/resources/auth/credential_utils.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 0 additions & 12 deletions monkey/monkey_island/cc/resources/auth/password_utils.py

This file was deleted.

22 changes: 8 additions & 14 deletions monkey/monkey_island/cc/resources/auth/registration.py
Original file line number Diff line number Diff line change
@@ -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__)
Expand All @@ -19,21 +21,13 @@ def get(self):
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)
4 changes: 2 additions & 2 deletions monkey/monkey_island/cc/resources/configuration_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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


Expand All @@ -21,7 +21,7 @@ def post(self):
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}
4 changes: 2 additions & 2 deletions monkey/monkey_island/cc/resources/configuration_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from monkey_island.cc.server_utils.encryption import (
InvalidCiphertextError,
InvalidCredentialsError,
PasswordBasedEncryptor,
PasswordBasedStringEncryptor,
is_encrypted,
)
from monkey_island.cc.services.config import ConfigService
Expand Down Expand Up @@ -72,7 +72,7 @@ def _get_plaintext_config_from_request(request_contents: dict) -> dict:
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):
Expand Down
2 changes: 0 additions & 2 deletions monkey/monkey_island/cc/server_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,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 +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)


Expand Down
23 changes: 14 additions & 9 deletions monkey/monkey_island/cc/server_utils/encryption/__init__.py
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
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)

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 _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 _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 _get_random_bytes() -> bytes:
return Random.new().read(_BLOCK_SIZE)

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 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):
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,22 @@ def encrypt(results: dict) -> dict:
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:
logger.error(
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
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]
Empty file.
Loading

0 comments on commit af99482

Please sign in to comment.