forked from p34709852/monkey
Island: Separate password and key encryption
This commit is contained in:
parent
67b23c42bf
commit
803d1c910f
|
@ -1,7 +1,7 @@
|
|||
from typing import List
|
||||
|
||||
from monkey_island.cc.models.utils.field_encryptors.i_field_encryptor import IFieldEncryptor
|
||||
from monkey_island.cc.server_utils.encryptor import get_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor
|
||||
|
||||
|
||||
class StringListEncryptor(IFieldEncryptor):
|
||||
|
|
|
@ -5,7 +5,7 @@ from flask import request
|
|||
|
||||
from monkey_island.cc.resources.auth.auth import jwt_required
|
||||
from monkey_island.cc.services.config import ConfigService
|
||||
from monkey_island.cc.services.utils.encryption import encrypt_string
|
||||
from monkey_island.cc.services.utils.password_encryption import PasswordBasedEncryptor
|
||||
|
||||
|
||||
class ConfigurationExport(flask_restful.Resource):
|
||||
|
@ -20,6 +20,8 @@ class ConfigurationExport(flask_restful.Resource):
|
|||
if should_encrypt:
|
||||
password = data["password"]
|
||||
plaintext_config = json.dumps(plaintext_config)
|
||||
config_export = encrypt_string(plaintext_config, password)
|
||||
|
||||
pb_encryptor = PasswordBasedEncryptor(password)
|
||||
config_export = pb_encryptor.encrypt(plaintext_config)
|
||||
|
||||
return {"config_export": config_export, "encrypted": should_encrypt}
|
||||
|
|
|
@ -9,10 +9,10 @@ from flask import request
|
|||
from common.utils.exceptions import InvalidConfigurationError
|
||||
from monkey_island.cc.resources.auth.auth import jwt_required
|
||||
from monkey_island.cc.services.config import ConfigService
|
||||
from monkey_island.cc.services.utils.encryption import (
|
||||
from monkey_island.cc.services.utils.password_encryption import (
|
||||
InvalidCiphertextError,
|
||||
InvalidCredentialsError,
|
||||
decrypt_ciphertext,
|
||||
PasswordBasedEncryptor,
|
||||
is_encrypted,
|
||||
)
|
||||
|
||||
|
@ -72,7 +72,8 @@ class ConfigurationImport(flask_restful.Resource):
|
|||
try:
|
||||
config = request_contents["config"]
|
||||
if ConfigurationImport.is_config_encrypted(request_contents["config"]):
|
||||
config = decrypt_ciphertext(config, request_contents["password"])
|
||||
pb_encryptor = PasswordBasedEncryptor(request_contents["password"])
|
||||
config = pb_encryptor.decrypt(config)
|
||||
return json.loads(config)
|
||||
except (JSONDecodeError, InvalidCiphertextError):
|
||||
logger.exception(
|
||||
|
|
|
@ -27,8 +27,8 @@ from monkey_island.cc.server_utils.consts import ( # noqa: E402
|
|||
GEVENT_EXCEPTION_LOG,
|
||||
MONGO_CONNECTION_TIMEOUT,
|
||||
)
|
||||
from monkey_island.cc.server_utils.encryptor import initialize_encryptor # noqa: E402
|
||||
from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402
|
||||
from monkey_island.cc.server_utils.key_encryptor import initialize_encryptor # 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
|
||||
from monkey_island.cc.services.utils.network_utils import local_ip_addresses # noqa: E402
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import base64
|
||||
import os
|
||||
|
||||
# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but
|
||||
# is maintained.
|
||||
from Crypto import Random # noqa: DUO133 # nosec: B413
|
||||
from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413
|
||||
|
||||
from monkey_island.cc.server_utils.file_utils import open_new_securely_permissioned_file
|
||||
from monkey_island.cc.services.utils.key_encryption import KeyBasedEncryptor
|
||||
|
||||
_encryptor = None
|
||||
|
||||
|
||||
class Encryptor:
|
||||
class DataStoreEncryptor:
|
||||
_BLOCK_SIZE = 32
|
||||
_PASSWORD_FILENAME = "mongo_key.bin"
|
||||
|
||||
|
@ -32,30 +31,19 @@ class Encryptor:
|
|||
with open(password_file, "rb") as f:
|
||||
self._cipher_key = f.read()
|
||||
|
||||
def _pad(self, message):
|
||||
return message + (self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)) * chr(
|
||||
self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)
|
||||
)
|
||||
|
||||
def _unpad(self, message: str):
|
||||
return message[0 : -ord(message[len(message) - 1])]
|
||||
|
||||
def enc(self, message: str):
|
||||
cipher_iv = Random.new().read(AES.block_size)
|
||||
cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv)
|
||||
return base64.b64encode(cipher_iv + cipher.encrypt(self._pad(message).encode())).decode()
|
||||
key_encryptor = KeyBasedEncryptor(self._cipher_key)
|
||||
return key_encryptor.encrypt(message)
|
||||
|
||||
def dec(self, enc_message):
|
||||
enc_message = base64.b64decode(enc_message)
|
||||
cipher_iv = enc_message[0 : AES.block_size]
|
||||
cipher = AES.new(self._cipher_key, AES.MODE_CBC, cipher_iv)
|
||||
return self._unpad(cipher.decrypt(enc_message[AES.block_size :]).decode())
|
||||
def dec(self, enc_message: str):
|
||||
key_encryptor = KeyBasedEncryptor(self._cipher_key)
|
||||
return key_encryptor.decrypt(enc_message)
|
||||
|
||||
|
||||
def initialize_encryptor(password_file_dir):
|
||||
global _encryptor
|
||||
|
||||
_encryptor = Encryptor(password_file_dir)
|
||||
_encryptor = DataStoreEncryptor(password_file_dir)
|
||||
|
||||
|
||||
def get_encryptor():
|
|
@ -1,4 +1,4 @@
|
|||
from monkey_island.cc.server_utils.encryptor import get_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor
|
||||
|
||||
|
||||
def parse_creds(attempt):
|
||||
|
|
|
@ -19,7 +19,7 @@ from common.config_value_paths import (
|
|||
USER_LIST_PATH,
|
||||
)
|
||||
from monkey_island.cc.database import mongo
|
||||
from monkey_island.cc.server_utils.encryptor import get_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor
|
||||
from monkey_island.cc.services.config_manipulator import update_config_per_mode
|
||||
from monkey_island.cc.services.config_schema.config_schema import SCHEMA
|
||||
from monkey_island.cc.services.mode.island_mode_service import ModeNotSetError, get_mode
|
||||
|
|
|
@ -3,7 +3,7 @@ import copy
|
|||
import dateutil
|
||||
|
||||
from monkey_island.cc.models import Monkey
|
||||
from monkey_island.cc.server_utils.encryptor import get_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor
|
||||
from monkey_island.cc.services.config import ConfigService
|
||||
from monkey_island.cc.services.edge.displayed_edge import EdgeService
|
||||
from monkey_island.cc.services.node import NodeService
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import logging
|
||||
|
||||
from monkey_island.cc.server_utils.encryptor import get_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor
|
||||
from monkey_island.cc.services.config import ConfigService
|
||||
from monkey_island.cc.services.node import NodeService
|
||||
from monkey_island.cc.services.telemetry.processing.system_info_collectors.system_info_telemetry_dispatcher import ( # noqa: E501
|
||||
|
|
|
@ -1,59 +0,0 @@
|
|||
import base64
|
||||
import io
|
||||
import logging
|
||||
|
||||
import pyAesCrypt
|
||||
|
||||
BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def encrypt_string(plaintext: str, password: str) -> str:
|
||||
plaintext_stream = io.BytesIO(plaintext.encode())
|
||||
ciphertext_stream = io.BytesIO()
|
||||
|
||||
pyAesCrypt.encryptStream(plaintext_stream, ciphertext_stream, password, BUFFER_SIZE)
|
||||
|
||||
ciphertext_b64 = base64.b64encode(ciphertext_stream.getvalue())
|
||||
logger.info("String encrypted.")
|
||||
|
||||
return ciphertext_b64.decode()
|
||||
|
||||
|
||||
def decrypt_ciphertext(ciphertext: str, password: str) -> str:
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
ciphertext_stream = io.BytesIO(ciphertext)
|
||||
plaintext_stream = io.BytesIO()
|
||||
|
||||
ciphertext_stream_len = len(ciphertext_stream.getvalue())
|
||||
|
||||
try:
|
||||
pyAesCrypt.decryptStream(
|
||||
ciphertext_stream,
|
||||
plaintext_stream,
|
||||
password,
|
||||
BUFFER_SIZE,
|
||||
ciphertext_stream_len,
|
||||
)
|
||||
except ValueError as ex:
|
||||
if str(ex).startswith("Wrong password"):
|
||||
logger.info("Wrong password provided for decryption.")
|
||||
raise InvalidCredentialsError
|
||||
else:
|
||||
logger.info("The corrupt ciphertext provided.")
|
||||
raise InvalidCiphertextError
|
||||
return plaintext_stream.getvalue().decode("utf-8")
|
||||
|
||||
|
||||
def is_encrypted(ciphertext: str) -> bool:
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
return ciphertext.startswith(b"AES")
|
||||
|
||||
|
||||
class InvalidCredentialsError(Exception):
|
||||
""" Raised when password for decryption is invalid """
|
||||
|
||||
|
||||
class InvalidCiphertextError(Exception):
|
||||
""" Raised when ciphertext is corrupted """
|
|
@ -0,0 +1,20 @@
|
|||
from abc import ABC, abstractmethod
|
||||
from typing import Any
|
||||
|
||||
|
||||
class IEncryptor(ABC):
|
||||
@abstractmethod
|
||||
def encrypt(self, plaintext: Any) -> Any:
|
||||
"""Encrypts data and returns the ciphertext.
|
||||
:param plaintext: Data that will be encrypted
|
||||
:return: Ciphertext generated by encrypting value
|
||||
:rtype: Any
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def decrypt(self, ciphertext: Any):
|
||||
"""Decrypts data and returns the plaintext.
|
||||
:param ciphertext: Ciphertext that will be decrypted
|
||||
:return: Plaintext generated by decrypting value
|
||||
:rtype: Any
|
||||
"""
|
|
@ -0,0 +1,38 @@
|
|||
import base64
|
||||
import logging
|
||||
|
||||
# PyCrypto is deprecated, but we use pycryptodome, which uses the exact same imports but
|
||||
# is maintained.
|
||||
from Crypto import Random # noqa: DUO133 # nosec: B413
|
||||
from Crypto.Cipher import AES # noqa: DUO133 # nosec: B413
|
||||
|
||||
from monkey_island.cc.services.utils.i_encryptor import IEncryptor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KeyBasedEncryptor(IEncryptor):
|
||||
|
||||
_BLOCK_SIZE = 32
|
||||
|
||||
def __init__(self, key: bytes):
|
||||
self._key = key
|
||||
|
||||
def encrypt(self, plaintext: str) -> str:
|
||||
cipher_iv = Random.new().read(AES.block_size)
|
||||
cipher = AES.new(self._key, AES.MODE_CBC, cipher_iv)
|
||||
return base64.b64encode(cipher_iv + cipher.encrypt(self._pad(plaintext).encode())).decode()
|
||||
|
||||
def decrypt(self, ciphertext: str):
|
||||
enc_message = base64.b64decode(ciphertext)
|
||||
cipher_iv = enc_message[0 : AES.block_size]
|
||||
cipher = AES.new(self._key, AES.MODE_CBC, cipher_iv)
|
||||
return self._unpad(cipher.decrypt(enc_message[AES.block_size :]).decode())
|
||||
|
||||
def _pad(self, message):
|
||||
return message + (self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)) * chr(
|
||||
self._BLOCK_SIZE - (len(message) % self._BLOCK_SIZE)
|
||||
)
|
||||
|
||||
def _unpad(self, message: str):
|
||||
return message[0 : -ord(message[len(message) - 1])]
|
|
@ -0,0 +1,67 @@
|
|||
import base64
|
||||
import io
|
||||
import logging
|
||||
|
||||
import pyAesCrypt
|
||||
|
||||
from monkey_island.cc.services.utils.i_encryptor import IEncryptor
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PasswordBasedEncryptor(IEncryptor):
|
||||
|
||||
_BUFFER_SIZE = pyAesCrypt.crypto.bufferSizeDef
|
||||
|
||||
def __init__(self, password: str):
|
||||
self.password = password
|
||||
|
||||
def encrypt(self, plaintext: str) -> str:
|
||||
plaintext_stream = io.BytesIO(plaintext.encode())
|
||||
ciphertext_stream = io.BytesIO()
|
||||
|
||||
pyAesCrypt.encryptStream(
|
||||
plaintext_stream, ciphertext_stream, self.password, self._BUFFER_SIZE
|
||||
)
|
||||
|
||||
ciphertext_b64 = base64.b64encode(ciphertext_stream.getvalue())
|
||||
logger.info("String encrypted.")
|
||||
|
||||
return ciphertext_b64.decode()
|
||||
|
||||
def decrypt(self, ciphertext: str):
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
ciphertext_stream = io.BytesIO(ciphertext)
|
||||
plaintext_stream = io.BytesIO()
|
||||
|
||||
ciphertext_stream_len = len(ciphertext_stream.getvalue())
|
||||
|
||||
try:
|
||||
pyAesCrypt.decryptStream(
|
||||
ciphertext_stream,
|
||||
plaintext_stream,
|
||||
self.password,
|
||||
self._BUFFER_SIZE,
|
||||
ciphertext_stream_len,
|
||||
)
|
||||
except ValueError as ex:
|
||||
if str(ex).startswith("Wrong password"):
|
||||
logger.info("Wrong password provided for decryption.")
|
||||
raise InvalidCredentialsError
|
||||
else:
|
||||
logger.info("The corrupt ciphertext provided.")
|
||||
raise InvalidCiphertextError
|
||||
return plaintext_stream.getvalue().decode("utf-8")
|
||||
|
||||
|
||||
class InvalidCredentialsError(Exception):
|
||||
""" Raised when password for decryption is invalid """
|
||||
|
||||
|
||||
class InvalidCiphertextError(Exception):
|
||||
""" Raised when ciphertext is corrupted """
|
||||
|
||||
|
||||
def is_encrypted(ciphertext: str) -> bool:
|
||||
ciphertext = base64.b64decode(ciphertext)
|
||||
return ciphertext.startswith(b"AES")
|
|
@ -5,7 +5,7 @@ from ScoutSuite.providers.base.authentication_strategy import AuthenticationExce
|
|||
from common.cloud.scoutsuite_consts import CloudProviders
|
||||
from common.config_value_paths import AWS_KEYS_PATH
|
||||
from common.utils.exceptions import InvalidAWSKeys
|
||||
from monkey_island.cc.server_utils.encryptor import get_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor
|
||||
from monkey_island.cc.services.config import ConfigService
|
||||
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import pytest
|
||||
|
||||
from monkey_island.cc.models.utils.field_encryptors.string_list_encryptor import StringListEncryptor
|
||||
from monkey_island.cc.server_utils.encryptor import initialize_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import initialize_encryptor
|
||||
|
||||
MOCK_STRING_LIST = ["test_1", "test_2"]
|
||||
EMPTY_LIST = []
|
||||
|
|
|
@ -6,7 +6,7 @@ from tests.unit_tests.monkey_island.cc.services.utils.test_encryption import PAS
|
|||
|
||||
from common.utils.exceptions import InvalidConfigurationError
|
||||
from monkey_island.cc.resources.configuration_import import ConfigurationImport
|
||||
from monkey_island.cc.services.utils.encryption import encrypt_string
|
||||
from monkey_island.cc.services.utils.password_encryption import PasswordBasedEncryptor
|
||||
|
||||
|
||||
def test_is_config_encrypted__json(monkey_config_json):
|
||||
|
@ -15,7 +15,8 @@ def test_is_config_encrypted__json(monkey_config_json):
|
|||
|
||||
@pytest.mark.slow
|
||||
def test_is_config_encrypted__ciphertext(monkey_config_json):
|
||||
encrypted_config = encrypt_string(monkey_config_json, PASSWORD)
|
||||
pb_encryptor = PasswordBasedEncryptor(PASSWORD)
|
||||
encrypted_config = pb_encryptor.encrypt(monkey_config_json)
|
||||
assert ConfigurationImport.is_config_encrypted(encrypted_config)
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import os
|
||||
|
||||
from monkey_island.cc.server_utils.encryptor import get_encryptor, initialize_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor, initialize_encryptor
|
||||
|
||||
PASSWORD_FILENAME = "mongo_key.bin"
|
||||
|
|
@ -4,10 +4,9 @@ from tests.unit_tests.monkey_island.cc.services.utils.ciphertexts_for_encryption
|
|||
VALID_CIPHER_TEXT,
|
||||
)
|
||||
|
||||
from monkey_island.cc.services.utils.encryption import (
|
||||
from monkey_island.cc.services.utils.password_encryption import (
|
||||
InvalidCredentialsError,
|
||||
decrypt_ciphertext,
|
||||
encrypt_string,
|
||||
PasswordBasedEncryptor,
|
||||
)
|
||||
|
||||
MONKEY_CONFIGS_DIR_PATH = "monkey_configs"
|
||||
|
@ -18,23 +17,27 @@ INCORRECT_PASSWORD = "goodbye321"
|
|||
|
||||
@pytest.mark.slow
|
||||
def test_encrypt_decrypt_string(monkey_config_json):
|
||||
encrypted_config = encrypt_string(monkey_config_json, PASSWORD)
|
||||
assert decrypt_ciphertext(encrypted_config, PASSWORD) == monkey_config_json
|
||||
pb_encryptor = PasswordBasedEncryptor(PASSWORD)
|
||||
encrypted_config = pb_encryptor.encrypt(monkey_config_json)
|
||||
assert pb_encryptor.decrypt(encrypted_config) == monkey_config_json
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_decrypt_string__wrong_password(monkey_config_json):
|
||||
pb_encryptor = PasswordBasedEncryptor(INCORRECT_PASSWORD)
|
||||
with pytest.raises(InvalidCredentialsError):
|
||||
decrypt_ciphertext(VALID_CIPHER_TEXT, INCORRECT_PASSWORD)
|
||||
pb_encryptor.decrypt(VALID_CIPHER_TEXT)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_decrypt_string__malformed_corrupted():
|
||||
pb_encryptor = PasswordBasedEncryptor(PASSWORD)
|
||||
with pytest.raises(ValueError):
|
||||
decrypt_ciphertext(MALFORMED_CIPHER_TEXT_CORRUPTED, PASSWORD)
|
||||
pb_encryptor.decrypt(MALFORMED_CIPHER_TEXT_CORRUPTED)
|
||||
|
||||
|
||||
@pytest.mark.slow
|
||||
def test_decrypt_string__no_password(monkey_config_json):
|
||||
pb_encryptor = PasswordBasedEncryptor("")
|
||||
with pytest.raises(InvalidCredentialsError):
|
||||
decrypt_ciphertext(VALID_CIPHER_TEXT, "")
|
||||
pb_encryptor.decrypt(VALID_CIPHER_TEXT)
|
||||
|
|
|
@ -5,7 +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.encryptor import get_encryptor, initialize_encryptor
|
||||
from monkey_island.cc.server_utils.key_encryptor import get_encryptor, initialize_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,
|
||||
|
|
Loading…
Reference in New Issue