From ae9206e968b10de6c456a76f914f8d86dff02f6f Mon Sep 17 00:00:00 2001 From: Florent Daigniere Date: Wed, 10 Feb 2021 13:51:07 +0100 Subject: [PATCH] Implement a simple credential cache --- core/admin/mailu/models.py | 23 +++++++++++++++++++++++ towncrier/newsfragments/1194.feature | 1 + 2 files changed, 24 insertions(+) create mode 100644 towncrier/newsfragments/1194.feature diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py index a63c33a5..c7787e74 100644 --- a/core/admin/mailu/models.py +++ b/core/admin/mailu/models.py @@ -305,6 +305,7 @@ class User(Base, Email): """ __tablename__ = "user" _ctx = None + _credential_cache = {} domain = db.relationship(Domain, backref=db.backref('users', cascade='all, delete-orphan')) @@ -382,6 +383,17 @@ class User(Base, Email): return User._ctx def check_password(self, password): + cache_result = self._credential_cache.get(self.get_id()) + current_salt = self.password.split('$')[3] if len(self.password.split('$')) == 5 else None + if cache_result and current_salt: + cache_salt, cache_hash = cache_result + if cache_salt == current_salt: + return hash.pbkdf2_sha256.verify(password, cache_hash) + else: + # the cache is local per gunicorn; the password has changed + # so the local cache can be invalidated + del self._credential_cache[self.get_id()] + reference = self.password # strip {scheme} if that's something mailu has added # passlib will identify *crypt based hashes just fine @@ -396,6 +408,17 @@ class User(Base, Email): self.password = new_hash db.session.add(self) db.session.commit() + + if result: + """The credential cache uses a low number of rounds to be fast. +While it's not meant to be persisted to cold-storage, no additional measures +are taken to ensure it isn't (mlock(), encrypted swap, ...) on the basis that +we have little control over GC and string interning anyways. + + An attacker that can dump the process' memory is likely to find credentials +in clear-text regardless of the presence of the cache. + """ + self._credential_cache[self.get_id()] = (self.password.split('$')[3], hash.pbkdf2_sha256.using(rounds=1).hash(password)) return result def set_password(self, password, hash_scheme=None, raw=False): diff --git a/towncrier/newsfragments/1194.feature b/towncrier/newsfragments/1194.feature new file mode 100644 index 00000000..0cd2a9e9 --- /dev/null +++ b/towncrier/newsfragments/1194.feature @@ -0,0 +1 @@ +Add a credential cache to speedup authentication requests.