Merge pull request #2081 from guardicore/1965-reset-repository-encryptor

1965 reset repository encryptor
This commit is contained in:
Mike Salvatore 2022-07-12 11:39:48 -04:00 committed by GitHub
commit 9bee01917e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 84 additions and 42 deletions

View File

@ -10,6 +10,7 @@ from common.utils.exceptions import (
from monkey_island.cc.models import UserCredentials from monkey_island.cc.models import UserCredentials
from monkey_island.cc.repository import IUserRepository from monkey_island.cc.repository import IUserRepository
from monkey_island.cc.server_utils.encryption import ( from monkey_island.cc.server_utils.encryption import (
ILockableEncryptor,
reset_datastore_encryptor, reset_datastore_encryptor,
unlock_datastore_encryptor, unlock_datastore_encryptor,
) )
@ -17,39 +18,52 @@ from monkey_island.cc.setup.mongo.database_initializer import reset_database
class AuthenticationService: class AuthenticationService:
def __init__(self, data_dir: Path, user_datastore: IUserRepository): def __init__(
self,
data_dir: Path,
user_repository: IUserRepository,
repository_encryptor: ILockableEncryptor,
):
self._data_dir = data_dir self._data_dir = data_dir
self._user_datastore = user_datastore self._user_repository = user_repository
self._repository_encryptor = repository_encryptor
def needs_registration(self) -> bool: def needs_registration(self) -> bool:
return not self._user_datastore.has_registered_users() return not self._user_repository.has_registered_users()
def register_new_user(self, username: str, password: str): def register_new_user(self, username: str, password: str):
if not username or not password: if not username or not password:
raise InvalidRegistrationCredentialsError("Username or password can not be empty.") raise InvalidRegistrationCredentialsError("Username or password can not be empty.")
credentials = UserCredentials(username, _hash_password(password)) credentials = UserCredentials(username, _hash_password(password))
self._user_datastore.add_user(credentials) self._user_repository.add_user(credentials)
self._reset_datastore_encryptor(username, password) self._reset_repository_encryptor(username, password)
reset_database() reset_database()
def authenticate(self, username: str, password: str): def authenticate(self, username: str, password: str):
try: try:
registered_user = self._user_datastore.get_user_credentials(username) registered_user = self._user_repository.get_user_credentials(username)
except UnknownUserError: except UnknownUserError:
raise IncorrectCredentialsError() raise IncorrectCredentialsError()
if not _credentials_match_registered_user(username, password, registered_user): if not _credentials_match_registered_user(username, password, registered_user):
raise IncorrectCredentialsError() raise IncorrectCredentialsError()
self._unlock_datastore_encryptor(username, password) self._unlock_repository_encryptor(username, password)
def _unlock_datastore_encryptor(self, username: str, password: str): def _unlock_repository_encryptor(self, username: str, password: str):
secret = _get_secret_from_credentials(username, password) secret = _get_secret_from_credentials(username, password)
self._repository_encryptor.unlock(secret.encode())
# Legacy datastore encryptor will be removed soon
unlock_datastore_encryptor(self._data_dir, secret) unlock_datastore_encryptor(self._data_dir, secret)
def _reset_datastore_encryptor(self, username: str, password: str): def _reset_repository_encryptor(self, username: str, password: str):
secret = _get_secret_from_credentials(username, password) secret = _get_secret_from_credentials(username, password)
self._repository_encryptor.reset_key()
self._repository_encryptor.unlock(secret.encode())
# Legacy datastore encryptor will be removed soon
reset_datastore_encryptor(self._data_dir, secret) reset_datastore_encryptor(self._data_dir, secret)

View File

@ -31,6 +31,7 @@ from monkey_island.cc.repository import (
RetrievalError, RetrievalError,
) )
from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH
from monkey_island.cc.server_utils.encryption import ILockableEncryptor, RepositoryEncryptor
from monkey_island.cc.services import AWSService, IslandModeService from monkey_island.cc.services import AWSService, IslandModeService
from monkey_island.cc.services.post_breach_files import PostBreachFilesService from monkey_island.cc.services.post_breach_files import PostBreachFilesService
from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService
@ -48,6 +49,7 @@ from .reporting.report import ReportService
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
AGENT_BINARIES_PATH = Path(MONKEY_ISLAND_ABS_PATH) / "cc" / "binaries" AGENT_BINARIES_PATH = Path(MONKEY_ISLAND_ABS_PATH) / "cc" / "binaries"
REPOSITORY_KEY_FILE_NAME = "repository_key.bin"
def initialize_services(data_dir: Path) -> DIContainer: def initialize_services(data_dir: Path) -> DIContainer:
@ -56,6 +58,9 @@ def initialize_services(data_dir: Path) -> DIContainer:
container.register_instance(AWSInstance, AWSInstance()) container.register_instance(AWSInstance, AWSInstance())
container.register_instance(MongoClient, MongoClient(MONGO_URL, serverSelectionTimeoutMS=100)) container.register_instance(MongoClient, MongoClient(MONGO_URL, serverSelectionTimeoutMS=100))
container.register_instance(
ILockableEncryptor, RepositoryEncryptor(data_dir / REPOSITORY_KEY_FILE_NAME)
)
_register_repositories(container, data_dir) _register_repositories(container, data_dir)
_register_services(container) _register_services(container)

View File

@ -10,6 +10,7 @@ from common.utils.exceptions import (
) )
from monkey_island.cc.models import UserCredentials from monkey_island.cc.models import UserCredentials
from monkey_island.cc.repository import IUserRepository from monkey_island.cc.repository import IUserRepository
from monkey_island.cc.server_utils.encryption import ILockableEncryptor
from monkey_island.cc.services import AuthenticationService, authentication_service from monkey_island.cc.services import AuthenticationService, authentication_service
USERNAME = "user1" USERNAME = "user1"
@ -48,6 +49,11 @@ def mock_unlock_datastore_encryptor():
return MagicMock() return MagicMock()
@pytest.fixture
def mock_repository_encryptor():
return MagicMock(spec=ILockableEncryptor)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def patch_datastore_utils( def patch_datastore_utils(
monkeypatch, monkeypatch,
@ -64,20 +70,20 @@ def patch_datastore_utils(
) )
def test_needs_registration__true(tmp_path): def test_needs_registration__true(tmp_path, mock_repository_encryptor):
has_registered_users = False has_registered_users = False
mock_user_datastore = MockUserDatastore(lambda: has_registered_users, None, None) mock_user_datastore = MockUserDatastore(lambda: has_registered_users, None, None)
a_s = AuthenticationService(tmp_path, mock_user_datastore) a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
assert a_s.needs_registration() assert a_s.needs_registration()
def test_needs_registration__false(tmp_path): def test_needs_registration__false(tmp_path, mock_repository_encryptor):
has_registered_users = True has_registered_users = True
mock_user_datastore = MockUserDatastore(lambda: has_registered_users, None, None) mock_user_datastore = MockUserDatastore(lambda: has_registered_users, None, None)
a_s = AuthenticationService(tmp_path, mock_user_datastore) a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
assert not a_s.needs_registration() assert not a_s.needs_registration()
@ -85,39 +91,45 @@ def test_needs_registration__false(tmp_path):
@pytest.mark.slow @pytest.mark.slow
@pytest.mark.parametrize("error", [InvalidRegistrationCredentialsError, AlreadyRegisteredError]) @pytest.mark.parametrize("error", [InvalidRegistrationCredentialsError, AlreadyRegisteredError])
def test_register_new_user__fails( def test_register_new_user__fails(
tmp_path, mock_reset_datastore_encryptor, mock_reset_database, error tmp_path, mock_reset_datastore_encryptor, mock_reset_database, mock_repository_encryptor, error
): ):
mock_user_datastore = MockUserDatastore(lambda: True, MagicMock(side_effect=error), None) mock_user_datastore = MockUserDatastore(lambda: True, MagicMock(side_effect=error), None)
a_s = AuthenticationService(tmp_path, mock_user_datastore) a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
with pytest.raises(error): with pytest.raises(error):
a_s.register_new_user(USERNAME, PASSWORD) a_s.register_new_user(USERNAME, PASSWORD)
mock_reset_datastore_encryptor.assert_not_called() mock_reset_datastore_encryptor.assert_not_called()
mock_repository_encryptor.reset_key().assert_not_called()
mock_repository_encryptor.unlock.assert_not_called()
mock_reset_database.assert_not_called() mock_reset_database.assert_not_called()
def test_register_new_user__empty_password_fails( def test_register_new_user__empty_password_fails(
tmp_path, mock_reset_datastore_encryptor, mock_reset_database tmp_path, mock_reset_datastore_encryptor, mock_reset_database, mock_repository_encryptor
): ):
mock_user_datastore = MockUserDatastore(lambda: False, None, None) mock_user_datastore = MockUserDatastore(lambda: False, None, None)
a_s = AuthenticationService(tmp_path, mock_user_datastore) a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
with pytest.raises(InvalidRegistrationCredentialsError): with pytest.raises(InvalidRegistrationCredentialsError):
a_s.register_new_user(USERNAME, "") a_s.register_new_user(USERNAME, "")
mock_reset_datastore_encryptor.assert_not_called() mock_reset_datastore_encryptor.assert_not_called()
mock_repository_encryptor.reset_key().assert_not_called()
mock_repository_encryptor.unlock.assert_not_called()
mock_reset_database.assert_not_called() mock_reset_database.assert_not_called()
@pytest.mark.slow @pytest.mark.slow
def test_register_new_user(tmp_path, mock_reset_datastore_encryptor, mock_reset_database): def test_register_new_user(
tmp_path, mock_reset_datastore_encryptor, mock_reset_database, mock_repository_encryptor
):
mock_add_user = MagicMock() mock_add_user = MagicMock()
mock_user_datastore = MockUserDatastore(lambda: False, mock_add_user, None) mock_user_datastore = MockUserDatastore(lambda: False, mock_add_user, None)
a_s = AuthenticationService(tmp_path, mock_user_datastore) a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
a_s.register_new_user(USERNAME, PASSWORD) a_s.register_new_user(USERNAME, PASSWORD)
@ -127,30 +139,16 @@ def test_register_new_user(tmp_path, mock_reset_datastore_encryptor, mock_reset_
mock_reset_datastore_encryptor.assert_called_once() mock_reset_datastore_encryptor.assert_called_once()
assert mock_reset_datastore_encryptor.call_args[0][1] != USERNAME assert mock_reset_datastore_encryptor.call_args[0][1] != USERNAME
mock_repository_encryptor.reset_key.assert_called_once()
mock_repository_encryptor.unlock.assert_called_once()
assert mock_repository_encryptor.unlock.call_args[0][0] != USERNAME
mock_reset_database.assert_called_once() mock_reset_database.assert_called_once()
@pytest.mark.slow @pytest.mark.slow
def test_authenticate__success(tmp_path, mock_unlock_datastore_encryptor): def test_authenticate__success(
mock_user_datastore = MockUserDatastore( tmp_path, mock_unlock_datastore_encryptor, mock_repository_encryptor
lambda: True,
None,
lambda _: UserCredentials(USERNAME, PASSWORD_HASH),
)
a_s = AuthenticationService(tmp_path, mock_user_datastore)
# If authentication fails, this function will raise an exception and the test will fail.
a_s.authenticate(USERNAME, PASSWORD)
mock_unlock_datastore_encryptor.assert_called_once()
@pytest.mark.slow
@pytest.mark.parametrize(
("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")]
)
def test_authenticate__failed_wrong_credentials(
tmp_path, mock_unlock_datastore_encryptor, username, password
): ):
mock_user_datastore = MockUserDatastore( mock_user_datastore = MockUserDatastore(
lambda: True, lambda: True,
@ -158,22 +156,47 @@ def test_authenticate__failed_wrong_credentials(
lambda _: UserCredentials(USERNAME, PASSWORD_HASH), lambda _: UserCredentials(USERNAME, PASSWORD_HASH),
) )
a_s = AuthenticationService(tmp_path, mock_user_datastore) a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
# If authentication fails, this function will raise an exception and the test will fail.
a_s.authenticate(USERNAME, PASSWORD)
mock_unlock_datastore_encryptor.assert_called_once()
mock_repository_encryptor.unlock.assert_called_once()
@pytest.mark.slow
@pytest.mark.parametrize(
("username", "password"), [("wrong_username", PASSWORD), (USERNAME, "wrong_password")]
)
def test_authenticate__failed_wrong_credentials(
tmp_path, mock_unlock_datastore_encryptor, username, password, mock_repository_encryptor
):
mock_user_datastore = MockUserDatastore(
lambda: True,
None,
lambda _: UserCredentials(USERNAME, PASSWORD_HASH),
)
a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
with pytest.raises(IncorrectCredentialsError): with pytest.raises(IncorrectCredentialsError):
a_s.authenticate(username, password) a_s.authenticate(username, password)
mock_unlock_datastore_encryptor.assert_not_called() mock_unlock_datastore_encryptor.assert_not_called()
mock_repository_encryptor.unlock.assert_not_called()
def test_authenticate__failed_no_registered_user(tmp_path, mock_unlock_datastore_encryptor): def test_authenticate__failed_no_registered_user(
tmp_path, mock_unlock_datastore_encryptor, mock_repository_encryptor
):
mock_user_datastore = MockUserDatastore( mock_user_datastore = MockUserDatastore(
lambda: True, None, MagicMock(side_effect=UnknownUserError) lambda: True, None, MagicMock(side_effect=UnknownUserError)
) )
a_s = AuthenticationService(tmp_path, mock_user_datastore) a_s = AuthenticationService(tmp_path, mock_user_datastore, mock_repository_encryptor)
with pytest.raises(IncorrectCredentialsError): with pytest.raises(IncorrectCredentialsError):
a_s.authenticate(USERNAME, PASSWORD) a_s.authenticate(USERNAME, PASSWORD)
mock_unlock_datastore_encryptor.assert_not_called() mock_unlock_datastore_encryptor.assert_not_called()
mock_repository_encryptor.unlock.assert_not_called()