Merge pull request #2083 from guardicore/1965-encrypt-credentials

1965 encrypt credentials
This commit is contained in:
Mike Salvatore 2022-07-13 06:07:50 -04:00 committed by GitHub
commit 07a8d6194c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 118 additions and 52 deletions

View File

@ -1,10 +1,11 @@
from typing import Sequence
from typing import Any, Dict, Mapping, Sequence
from pymongo import MongoClient
from common.credentials import Credentials
from monkey_island.cc.repository import RemovalError, RetrievalError, StorageError
from monkey_island.cc.repository.i_credentials_repository import ICredentialsRepository
from monkey_island.cc.server_utils.encryption import ILockableEncryptor
class MongoCredentialsRepository(ICredentialsRepository):
@ -12,18 +13,15 @@ class MongoCredentialsRepository(ICredentialsRepository):
Store credentials in a mongo database that can be used to propagate around the network.
"""
def __init__(self, mongo: MongoClient):
def __init__(self, mongo: MongoClient, repository_encryptor: ILockableEncryptor):
self._mongo = mongo
self._repository_encryptor = repository_encryptor
def get_configured_credentials(self) -> Sequence[Credentials]:
return MongoCredentialsRepository._get_credentials_from_collection(
self._mongo.db.configured_credentials
)
return self._get_credentials_from_collection(self._mongo.db.configured_credentials)
def get_stolen_credentials(self) -> Sequence[Credentials]:
return MongoCredentialsRepository._get_credentials_from_collection(
self._mongo.db.stolen_credentials
)
return self._get_credentials_from_collection(self._mongo.db.stolen_credentials)
def get_all_credentials(self) -> Sequence[Credentials]:
configured_credentials = self.get_configured_credentials()
@ -33,14 +31,10 @@ class MongoCredentialsRepository(ICredentialsRepository):
def save_configured_credentials(self, credentials: Sequence[Credentials]):
# TODO: Fix deduplication of Credentials in mongo
MongoCredentialsRepository._save_credentials_to_collection(
credentials, self._mongo.db.configured_credentials
)
self._save_credentials_to_collection(credentials, self._mongo.db.configured_credentials)
def save_stolen_credentials(self, credentials: Sequence[Credentials]):
MongoCredentialsRepository._save_credentials_to_collection(
credentials, self._mongo.db.stolen_credentials
)
self._save_credentials_to_collection(credentials, self._mongo.db.stolen_credentials)
def remove_configured_credentials(self):
MongoCredentialsRepository._remove_credentials_fom_collection(
@ -56,27 +50,61 @@ class MongoCredentialsRepository(ICredentialsRepository):
self.remove_configured_credentials()
self.remove_stolen_credentials()
@staticmethod
def _get_credentials_from_collection(collection) -> Sequence[Credentials]:
def _get_credentials_from_collection(self, collection) -> Sequence[Credentials]:
try:
collection_result = []
list_collection_result = list(collection.find({}))
for c in list_collection_result:
del c["_id"]
collection_result.append(Credentials.from_mapping(c))
for encrypted_credentials in list_collection_result:
del encrypted_credentials["_id"]
plaintext_credentials = self._decrypt_credentials_mapping(encrypted_credentials)
collection_result.append(Credentials.from_mapping(plaintext_credentials))
return collection_result
except Exception as err:
raise RetrievalError(err)
@staticmethod
def _save_credentials_to_collection(credentials: Sequence[Credentials], collection):
def _save_credentials_to_collection(self, credentials: Sequence[Credentials], collection):
try:
for c in credentials:
collection.insert_one(Credentials.to_mapping(c))
encrypted_credentials = self._encrypt_credentials_mapping(Credentials.to_mapping(c))
collection.insert_one(encrypted_credentials)
except Exception as err:
raise StorageError(err)
# NOTE: The encryption/decryption is complicated and also full of mostly duplicated code. Rather
# than spend the effort to improve them now, we can revisit them when we resolve #2072.
# Resolving #2072 will make it easier to simplify these methods and remove duplication.
#
# If possible, implement the encryption/decryption as a decorator so it can be reused with
# different ICredentialsRepository implementations
def _encrypt_credentials_mapping(self, mapping: Mapping[str, Any]) -> Mapping[str, Any]:
encrypted_mapping: Dict[str, Any] = {}
for secret_or_identity, credentials_components in mapping.items():
encrypted_mapping[secret_or_identity] = []
for component in credentials_components:
encrypted_component = {}
for key, value in component.items():
encrypted_component[key] = self._repository_encryptor.encrypt(value.encode())
encrypted_mapping[secret_or_identity].append(encrypted_component)
return encrypted_mapping
def _decrypt_credentials_mapping(self, mapping: Mapping[str, Any]) -> Mapping[str, Any]:
encrypted_mapping: Dict[str, Any] = {}
for secret_or_identity, credentials_components in mapping.items():
encrypted_mapping[secret_or_identity] = []
for component in credentials_components:
encrypted_component = {}
for key, value in component.items():
encrypted_component[key] = self._repository_encryptor.decrypt(value).decode()
encrypted_mapping[secret_or_identity].append(encrypted_component)
return encrypted_mapping
@staticmethod
def _remove_credentials_fom_collection(collection):
try:

View File

@ -60,12 +60,12 @@ class RepositoryEncryptor(ILockableEncryptor):
def encrypt(self, plaintext: bytes) -> bytes:
if self._key_based_encryptor is None:
raise LockedKeyError("Cannot encrypt while the encryptor is locked)")
raise LockedKeyError("Cannot encrypt while the encryptor is locked")
return self._key_based_encryptor.encrypt(plaintext)
def decrypt(self, ciphertext: bytes) -> bytes:
if self._key_based_encryptor is None:
raise LockedKeyError("Cannot decrypt while the encryptor is locked)")
raise LockedKeyError("Cannot decrypt while the encryptor is locked")
return self._key_based_encryptor.decrypt(ciphertext)

View File

@ -1,8 +1,11 @@
from unittest.mock import MagicMock
import mongomock
import pytest
from common.credentials import Credentials, LMHash, NTHash, Password, SSHKeypair, Username
from monkey_island.cc.repository import MongoCredentialsRepository
from monkey_island.cc.server_utils.encryption import ILockableEncryptor
USER1 = "test_user_1"
USER2 = "test_user_2"
@ -36,80 +39,115 @@ STOLEN_CREDENTIALS = [CREDENTIALS_OBJECT_2]
CREDENTIALS_LIST = [CREDENTIALS_OBJECT_1, CREDENTIALS_OBJECT_2]
@pytest.fixture
def mongo_repository():
mongo = mongomock.MongoClient()
def reverse(data: bytes) -> bytes:
return bytes(reversed(data))
return MongoCredentialsRepository(mongo)
@pytest.fixture
def repository_encryptor():
repository_encryptor = MagicMock(spec=ILockableEncryptor)
repository_encryptor.encrypt = MagicMock(side_effect=reverse)
repository_encryptor.decrypt = MagicMock(side_effect=reverse)
return repository_encryptor
@pytest.fixture
def mongo_client():
return mongomock.MongoClient()
@pytest.fixture
def mongo_repository(mongo_client, repository_encryptor):
return MongoCredentialsRepository(mongo_client, repository_encryptor)
def test_mongo_repository_get_configured(mongo_repository):
actual_configured_credentials = mongo_repository.get_configured_credentials()
assert actual_configured_credentials == []
def test_mongo_repository_get_stolen(mongo_repository):
actual_stolen_credentials = mongo_repository.get_stolen_credentials()
assert actual_stolen_credentials == []
def test_mongo_repository_get_all(mongo_repository):
actual_credentials = mongo_repository.get_all_credentials()
assert actual_credentials == []
def test_mongo_repository_configured(mongo_repository):
mongo_repository.save_configured_credentials(CREDENTIALS_LIST)
actual_configured_credentials = mongo_repository.get_configured_credentials()
assert actual_configured_credentials == CREDENTIALS_LIST
mongo_repository.remove_configured_credentials()
actual_configured_credentials = mongo_repository.get_configured_credentials()
assert actual_configured_credentials == []
def test_mongo_repository_stolen(mongo_repository):
mongo_repository.save_configured_credentials(CONFIGURED_CREDENTIALS)
mongo_repository.save_stolen_credentials(STOLEN_CREDENTIALS)
actual_stolen_credentials = mongo_repository.get_stolen_credentials()
assert actual_stolen_credentials == STOLEN_CREDENTIALS
assert sorted(actual_stolen_credentials) == sorted(STOLEN_CREDENTIALS)
mongo_repository.remove_stolen_credentials()
actual_stolen_credentials = mongo_repository.get_stolen_credentials()
assert actual_stolen_credentials == []
def test_mongo_repository_all(mongo_repository):
mongo_repository.save_configured_credentials(CONFIGURED_CREDENTIALS)
mongo_repository.save_stolen_credentials(STOLEN_CREDENTIALS)
actual_credentials = mongo_repository.get_all_credentials()
assert actual_credentials == CREDENTIALS_LIST
mongo_repository.remove_all_credentials()
actual_credentials = mongo_repository.get_all_credentials()
actual_stolen_credentials = mongo_repository.get_stolen_credentials()
actual_configured_credentials = mongo_repository.get_configured_credentials()
assert mongo_repository.get_all_credentials() == []
assert mongo_repository.get_stolen_credentials() == []
assert mongo_repository.get_configured_credentials() == []
assert actual_credentials == []
assert actual_stolen_credentials == []
assert actual_configured_credentials == []
# NOTE: The following tests are complicated, but they work. Rather than spend the effort to improve
# them now, we can revisit them when we resolve #2072. Resolving #2072 will make it easier to
# simplify these tests.
def test_configured_secrets_encrypted(mongo_repository, mongo_client):
mongo_repository.save_configured_credentials([CREDENTIALS_OBJECT_2])
check_if_stored_credentials_encrypted(mongo_client, CREDENTIALS_OBJECT_2)
def test_stolen_secrets_encrypted(mongo_repository, mongo_client):
mongo_repository.save_stolen_credentials([CREDENTIALS_OBJECT_2])
check_if_stored_credentials_encrypted(mongo_client, CREDENTIALS_OBJECT_2)
def check_if_stored_credentials_encrypted(mongo_client, original_credentials):
raw_credentials = get_all_credentials_in_mongo(mongo_client)
original_credentials_mapping = Credentials.to_mapping(original_credentials)
for rc in raw_credentials:
for identity_or_secret, credentials_components in rc.items():
for component in credentials_components:
for key, value in component.items():
assert (
original_credentials_mapping[identity_or_secret][0].get(key, None) != value
)
def get_all_credentials_in_mongo(mongo_client):
encrypted_credentials = []
# Loop through all databases and collections and search for credentials. We don't want the tests
# to assume anything about the internal workings of the repository.
for db in mongo_client.list_database_names():
for collection in mongo_client[db].list_collection_names():
mongo_credentials = mongo_client[db][collection].find({})
for mc in mongo_credentials:
del mc["_id"]
encrypted_credentials.append(mc)
return encrypted_credentials