Skip to content

Commit 5744952

Browse files
committed
Island: Use SHA256 to hash OTPs
The encryption algorithm is not deterministic, making searching impossible. Salted SHA256 is considered to be secure enough for one-time passwords with a 2-minute TTL. Issue #3078 PR #3204
1 parent 768a656 commit 5744952

File tree

2 files changed

+27
-15
lines changed

2 files changed

+27
-15
lines changed

monkey/monkey_island/cc/services/authentication_service/mongo_otp_repository.py

+24-9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import hashlib
2+
import secrets
13
from functools import lru_cache
24
from typing import Any, Mapping
35

@@ -12,7 +14,6 @@
1214
StorageError,
1315
UnknownRecordError,
1416
)
15-
from monkey_island.cc.server_utils.encryption import ILockableEncryptor
1617

1718
from .i_otp_repository import IOTPRepository
1819

@@ -21,21 +22,36 @@ class MongoOTPRepository(IOTPRepository):
2122
def __init__(
2223
self,
2324
mongo_client: MongoClient,
24-
encryptor: ILockableEncryptor,
2525
):
26-
self._encryptor = encryptor
26+
# SECURITY: A new salt is generated for each instance of this repository. This effectively
27+
# makes all preexisting OTPS invalid on Island startup.
28+
self._salt = secrets.token_bytes(16)
29+
2730
self._otp_collection = mongo_client.monkey_island.otp
2831
self._otp_collection.create_index("otp", unique=True)
2932

3033
def insert_otp(self, otp: OTP, expiration: float):
3134
try:
32-
encrypted_otp = self._encryptor.encrypt(otp.get_secret_value().encode())
3335
self._otp_collection.insert_one(
34-
{"otp": encrypted_otp, "expiration_time": expiration, "used": False}
36+
{"otp": self._hash_otp(otp), "expiration_time": expiration, "used": False}
3537
)
3638
except Exception as err:
3739
raise StorageError(f"Error inserting OTP: {err}")
3840

41+
def _hash_otp(self, otp: OTP) -> bytes:
42+
# SECURITY: A single round of salted SHA256 is usually not considered sufficient for
43+
# protecting passwords. However, OTPs have a very short life span (2 minutes at the time of
44+
# this writing). Additionally, they can only be used once. Finally, they are 32 bytes long.
45+
# At the present time, we consider this to be sufficient protection. I'm unaware of any
46+
# technology in existence that can brute force SHA256 for (roughly) 48-byte inputs in under
47+
# 2 minutes.
48+
#
49+
# Note that if any of these conditions change (timeouts become very long or OTPs become very
50+
# short), this should be revisited. For now, we prefer the significantly faster performance
51+
# of a single round of salted SHA256 over a more secure but slower algorithm.
52+
otp_bytes = otp.get_secret_value().encode()
53+
return hashlib.sha256(self._salt + otp_bytes).digest()
54+
3955
def set_used(self, otp: OTP):
4056
try:
4157
otp_id = self._get_otp_object_id(otp)
@@ -71,11 +87,10 @@ def _get_otp_document(self, otp: OTP) -> Mapping[str, Any]:
7187

7288
@lru_cache
7389
def _get_otp_object_id(self, otp: OTP) -> ObjectId:
74-
otp_str = otp.get_secret_value()
75-
7690
try:
77-
encrypted_otp = self._encryptor.encrypt(otp_str.encode())
78-
otp_dict = self._otp_collection.find_one({"otp": encrypted_otp}, [MONGO_OBJECT_ID_KEY])
91+
otp_dict = self._otp_collection.find_one(
92+
{"otp": self._hash_otp(otp)}, [MONGO_OBJECT_ID_KEY]
93+
)
7994
except Exception as err:
8095
raise RetrievalError(f"Error retrieving OTP: {err}")
8196

monkey/tests/unit_tests/monkey_island/cc/services/authentication_service/test_mongo_otp_repository.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
StorageError,
1212
UnknownRecordError,
1313
)
14-
from monkey_island.cc.server_utils.encryption import ILockableEncryptor
1514
from monkey_island.cc.services.authentication_service.i_otp_repository import IOTPRepository
1615
from monkey_island.cc.services.authentication_service.mongo_otp_repository import MongoOTPRepository
1716

@@ -35,10 +34,8 @@ def mongo_client() -> mongomock.MongoClient:
3534

3635

3736
@pytest.fixture
38-
def otp_repository(
39-
mongo_client: mongomock.MongoClient, repository_encryptor: ILockableEncryptor
40-
) -> IOTPRepository:
41-
return MongoOTPRepository(mongo_client, repository_encryptor)
37+
def otp_repository(mongo_client: mongomock.MongoClient) -> IOTPRepository:
38+
return MongoOTPRepository(mongo_client)
4239

4340

4441
@pytest.fixture
@@ -59,7 +56,7 @@ def error_raising_mongo_client() -> mongomock.MongoClient:
5956
def error_raising_otp_repository(
6057
error_raising_mongo_client, repository_encryptor
6158
) -> IOTPRepository:
62-
return MongoOTPRepository(error_raising_mongo_client, repository_encryptor)
59+
return MongoOTPRepository(error_raising_mongo_client)
6360

6461

6562
def test_insert_otp(otp_repository: IOTPRepository):

0 commit comments

Comments
 (0)