Repository: Mailu/Mailu

Issue:
support PBKDF2 and use it as default
PBKDF2-SHA512 is as secure as SHA512-CRYPT but works in constant-time based on the rounds, not the length of the password - eliminating the leakage of password-length based on timing

this should also reduce the performance impact as reported by #546
(in my local tests with the script by @t-t-yano PBKDF2-SHA512 is 2-3x faster than SHA512-CRYPT)


Discussion:
Oh well :grimacing:

If I understand it correct, before the merge of #612 the password was never actually passed to dovecot, right?

Now that it is passed the problem is: passlib uses a different prefix than dovecot to indicate that the hash is a PBKDF2

Hash-Prefixes in
Passlib: `$pbkdf2-sha512$`
Dovecot: `$1$`

(btw, dovecot are you drunk? the prefix `$1$` is used for the least secure supported hash (MD5-CRYPT)  _AND_ for one of the most secure? …also: that is not documented, one has to grep it from the source code by errormessage :disappointed:)
I created an alternative PR #647 that just works so I will close this one now.
The password is not passed to Dovecot. What is weird is this change worked pretty well for me. Let's switch to the alternative anyway.

Edit: hm. Indeed, the password is passed to Dovecot since the abstract db thing. I will probably open a discussion about this. Anyway, let's switch to #647 indeed.

Resolving patch:
diff --git a/core/admin/mailu/__init__.py b/core/admin/mailu/__init__.py
--- a/core/admin/mailu/__init__.py
+++ b/core/admin/mailu/__init__.py
@@ -12,7 +12,7 @@
 import socket
 import uuid
 
-from werkzeug.contrib import fixers
+from werkzeug.contrib import fixers, profiler
 
 # Create application
 app = flask.Flask(__name__)
@@ -62,7 +62,10 @@
     'HOST_IMAP': 'imap',
     'HOST_POP3': 'imap',
     'HOST_SMTP': 'smtp',
+    'HOST_WEBMAIL': 'webmail',
+    'HOST_FRONT': 'front',
     'HOST_AUTHSMTP': os.environ.get('HOST_SMTP', 'smtp'),
+    'POD_ADDRESS_RANGE': None
 }
 
 # Load configuration from the environment if available
@@ -80,6 +83,10 @@
     import flask_debugtoolbar
     toolbar = flask_debugtoolbar.DebugToolbarExtension(app)
 
+# Profiler
+if app.config.get("DEBUG"):
+    app.wsgi_app = profiler.ProfilerMiddleware(app.wsgi_app, restrictions=[30])
+
 # Manager commnad
 manager = flask_script.Manager(app)
 manager.add_command('db', flask_migrate.MigrateCommand)
@@ -129,4 +136,5 @@ def __call__(self, environ, start_response):
             environ['SCRIPT_NAME'] = prefix
         return self.app(environ, start_response)
 
+
 app.wsgi_app = PrefixMiddleware(fixers.ProxyFix(app.wsgi_app))
diff --git a/core/admin/mailu/internal/views/dovecot.py b/core/admin/mailu/internal/views/dovecot.py
--- a/core/admin/mailu/internal/views/dovecot.py
+++ b/core/admin/mailu/internal/views/dovecot.py
@@ -1,14 +1,24 @@
-from mailu import db, models
+from mailu import db, models, app
 from mailu.internal import internal
 
 import flask
+import socket
 
 
 @internal.route("/dovecot/passdb/<user_email>")
 def dovecot_passdb_dict(user_email):
     user = models.User.query.get(user_email) or flask.abort(404)
+    allow_nets = []
+    allow_nets.append(
+        app.config.get("POD_ADDRESS_RANGE") or
+        socket.gethostbyname(app.config["HOST_FRONT"])
+    )
+    allow_nets.append(socket.gethostbyname(app.config["HOST_WEBMAIL"]))
+    print(allow_nets)
     return flask.jsonify({
-        "password": user.password,
+        "password": None,
+        "nopassword": "Y",
+        "allow_nets": ",".join(allow_nets)
     })
 
 
diff --git a/core/admin/mailu/models.py b/core/admin/mailu/models.py
--- a/core/admin/mailu/models.py
+++ b/core/admin/mailu/models.py
@@ -276,7 +276,8 @@ def destination(self):
         else:
             return self.email
 
-    scheme_dict = {'BLF-CRYPT': "bcrypt",
+    scheme_dict = {'PBKDF2': "pbkdf2_sha512",
+                   'BLF-CRYPT': "bcrypt",
                    'SHA512-CRYPT': "sha512_crypt",
                    'SHA256-CRYPT': "sha256_crypt",
                    'MD5-CRYPT': "md5_crypt",
@@ -287,8 +288,14 @@ def destination(self):
     )
 
     def check_password(self, password):
+        context = User.pw_context
         reference = re.match('({[^}]+})?(.*)', self.password).group(2)
-        return User.pw_context.verify(password, reference)
+        result = context.verify(password, reference)
+        if result and context.identify(reference) != context.default_scheme():
+            self.set_password(password)
+            db.session.add(self)
+            db.session.commit()
+        return result
 
     def set_password(self, password, hash_scheme=app.config['PASSWORD_SCHEME'], raw=False):
         """Set password for user with specified encryption scheme


Test patch: