changeset 5922:25900eaeb864

passlib integration - enhanced password hash security Docs for passlib: http://packages.python.org/passlib/ Updated docs/CHANGES about the moin integration. Updated docs/REQUIREMENTS about passlib requirements. Added/Adapted related unit tests. Added logging for password hash processing errors.
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Sat, 19 Jan 2013 00:32:21 +0100
parents fe7003b1cc4d
children 50a2730d2f95
files MoinMoin/_tests/test_user.py MoinMoin/_tests/wikiconfig.py MoinMoin/action/newaccount.py MoinMoin/config/__init__.py MoinMoin/config/multiconfig.py MoinMoin/user.py MoinMoin/userprefs/changepass.py docs/CHANGES docs/REQUIREMENTS
diffstat 9 files changed, 303 insertions(+), 78 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/_tests/test_user.py	Fri Jan 18 01:46:13 2013 +0100
+++ b/MoinMoin/_tests/test_user.py	Sat Jan 19 00:32:21 2013 +0100
@@ -13,8 +13,6 @@
 
 from MoinMoin import user, caching
 
-DEFAULT_ALG = user.DEFAULT_ALG
-
 
 class TestEncodePassword(object):
     """user: encode passwords tests"""
@@ -22,18 +20,27 @@
     def testAscii(self):
         """user: encode ascii password"""
         # u'MoinMoin' and 'MoinMoin' should be encoded to same result
-        expected = "{SSHA}xkDIIx1I7A4gC98Vt/+UelIkTDYxMjM0NQ=="
-
-        result = user.encodePassword("MoinMoin", salt='12345')
-        assert result == expected
-        result = user.encodePassword(u"MoinMoin", salt='12345')
-        assert result == expected
+        cfg = self.request.cfg
+        tests = [
+            ('{PASSLIB}', '12345', "{PASSLIB}$6$rounds=1001$12345$jrPUCzPJt1yiixDbzIgSBoKED0/DlNDTHZN3lVarCtN6IM/.LoAw5pgUQH112CErU6wS8HXTZNpqb7wVjHLs/0"),
+            ('{SSHA}', '12345', "{SSHA}xkDIIx1I7A4gC98Vt/+UelIkTDYxMjM0NQ=="),
+        ]
+        for scheme, salt, expected in tests:
+            result = user.encodePassword(cfg, "MoinMoin", salt=salt, scheme=scheme)
+            assert result == expected
+            result = user.encodePassword(cfg, u"MoinMoin", salt=salt, scheme=scheme)
+            assert result == expected
 
     def testUnicode(self):
         """ user: encode unicode password """
-        result = user.encodePassword(u'סיסמה סודית בהחלט', salt='12345') # Hebrew
-        expected = "{SSHA}YiwfeVWdVW9luqyVn8t2JivlzmUxMjM0NQ=="
-        assert result == expected
+        cfg = self.request.cfg
+        tests = [
+            ('{PASSLIB}', '12345', "{PASSLIB}$6$rounds=1001$12345$5srFB66ZCu2JgGwPgdfb1lHRmqkjnKC/RxdsFlWn2WzoQh3btIjH6Ai1LJV9iYLDa9kLP/VQYa4DHLkRnaBw8."),
+            ('{SSHA}', '12345', "{SSHA}YiwfeVWdVW9luqyVn8t2JivlzmUxMjM0NQ=="),
+            ]
+        for scheme, salt, expected in tests:
+            result = user.encodePassword(cfg, u'סיסמה סודית בהחלט', salt=salt, scheme=scheme) # Hebrew
+            assert result == expected
 
 
 class TestLoginWithPassword(object):
@@ -49,6 +56,8 @@
         self.request.user = user.User(self.request)
 
         self.user = None
+        self.passlib_support = self.request.cfg.passlib_support
+        self.password_scheme = self.request.cfg.password_scheme
 
     def teardown_method(self, method):
         """ Run after each test
@@ -114,7 +123,7 @@
         assert theuser.valid
         # Check if the stored password was auto-upgraded on login and saved
         theuser = user.User(self.request, name=name, password=password)
-        assert theuser.enc_password.startswith(DEFAULT_ALG)
+        assert theuser.enc_password.startswith(self.password_scheme)
 
     def test_auth_with_md5_stored_password(self):
         """
@@ -132,7 +141,7 @@
         assert theuser.valid
         # Check if the stored password was auto-upgraded on login and saved
         theuser = user.User(self.request, name=name, password=password)
-        assert theuser.enc_password.startswith(DEFAULT_ALG)
+        assert theuser.enc_password.startswith(self.password_scheme)
 
     def test_auth_with_des_stored_password(self):
         """
@@ -153,7 +162,7 @@
             assert theuser.valid
             # Check if the stored password was auto-upgraded on login and saved
             theuser = user.User(self.request, name=name, password=password)
-            assert theuser.enc_password.startswith(DEFAULT_ALG)
+            assert theuser.enc_password.startswith(self.password_scheme)
         except ImportError:
             py.test.skip("Platform does not provide crypt module!")
 
@@ -173,7 +182,7 @@
         assert theuser.valid
         # Check if the stored password was auto-upgraded on login and saved
         theuser = user.User(self.request, name=name, password=password)
-        assert theuser.enc_password.startswith(DEFAULT_ALG)
+        assert theuser.enc_password.startswith(self.password_scheme)
 
     def test_auth_with_ssha_stored_password(self):
         """
@@ -191,7 +200,26 @@
         assert theuser.valid
         # Check if the stored password was auto-upgraded on login and saved
         theuser = user.User(self.request, name=name, password=password)
-        assert theuser.enc_password.startswith(DEFAULT_ALG)
+        assert theuser.enc_password.startswith(self.password_scheme)
+
+    def test_auth_with_passlib_stored_password(self):
+        """
+        Create user with {PASSLIB} password and check that user can login.
+        """
+        if not self.passlib_support:
+            py.test.skip("test requires passlib, but passlib_support is False")
+        # Create test user
+        name = u'Test User'
+        password = '12345'
+        pw_hash = '{PASSLIB}$6$rounds=1001$/AVWSh/RUWpcppfl$8DCRGLaBD3KoV4Ag67sUv6b2QdrUFXk1yWCxqWnBLJ.iHSe4Piv6nqzSQgELeLPIvwTC9APaWv1XCTOHjkLOj/'
+        self.createUser(name, pw_hash, True)
+
+        # Try to "login"
+        theuser = user.User(self.request, name=name, password=password)
+        assert theuser.valid
+        # Check if the stored password was auto-upgraded on login and saved
+        theuser = user.User(self.request, name=name, password=password)
+        assert theuser.enc_password.startswith(self.password_scheme)
 
     def testSubscriptionSubscribedPage(self):
         """ user: tests isSubscribedTo  """
@@ -266,7 +294,7 @@
         self.user.name = name
         self.user.email = email
         if not pwencoded:
-            password = user.encodePassword(password)
+            password = user.encodePassword(self.request.cfg, password)
         self.user.enc_password = password
 
         # Validate that we are not modifying existing user data file!
--- a/MoinMoin/_tests/wikiconfig.py	Fri Jan 18 01:46:13 2013 +0100
+++ b/MoinMoin/_tests/wikiconfig.py	Sat Jan 19 00:32:21 2013 +0100
@@ -31,3 +31,17 @@
     # used to check if it is really a wiki we may modify
     is_test_wiki = True
 
+    # for runnging tests without passlib support:
+    #passlib_support = False
+    #password_scheme = '{SSHA}'
+
+    # for running tests with passlib support:
+    passlib_crypt_context = dict(
+        schemes=["sha512_crypt", ],
+        # for the tests, we don't want to have varying rounds
+        sha512_crypt__vary_rounds=0,
+        # for the tests, we want to have a rather low rounds count,
+        # so the tests run quickly (do NOT use low counts in production!)
+        sha512_crypt__default_rounds=1001,
+    )
+
--- a/MoinMoin/action/newaccount.py	Fri Jan 18 01:46:13 2013 +0100
+++ b/MoinMoin/action/newaccount.py	Sat Jan 19 00:32:21 2013 +0100
@@ -64,7 +64,7 @@
     # Encode password
     if password and not password.startswith('{SHA}'):
         try:
-            theuser.enc_password = user.encodePassword(password)
+            theuser.enc_password = user.encodePassword(request.cfg, password)
         except UnicodeError, err:
             # Should never happen
             return "Can't encode password: %s" % wikiutil.escape(str(err))
--- a/MoinMoin/config/__init__.py	Fri Jan 18 01:46:13 2013 +0100
+++ b/MoinMoin/config/__init__.py	Sat Jan 19 00:32:21 2013 +0100
@@ -22,6 +22,16 @@
 # When creating files, we use e.g. 0666 & config.umask for the mode:
 umask = 0770
 
+# list of acceptable password hashing schemes for cfg.password_scheme,
+# here we only give reasonably good schemes, which is passlib (if we
+# have passlib) and ssha (if we only have builtin stuff):
+password_schemes_configurable = ['{PASSLIB}', '{SSHA}', ]
+
+# ordered list of supported password hashing schemes, best (passlib) should be
+# first, best builtin one should be second. this is what we support if we
+# encounter it in user profiles:
+password_schemes_supported = password_schemes_configurable + ['{SHA}', '{APR1}', '{MD5}', '{DES}', ]
+
 # Default value for the static stuff URL prefix (css, img, js).
 # Caution:
 # * do NOT use this directly, it is only the DEFAULT value to be used by
--- a/MoinMoin/config/multiconfig.py	Fri Jan 18 01:46:13 2013 +0100
+++ b/MoinMoin/config/multiconfig.py	Sat Jan 19 00:32:21 2013 +0100
@@ -427,6 +427,23 @@
                 raise error.ConfigurationError("You must set a (at least %d chars long) secret string for secrets['%s']!" % (
                     secret_min_length, secret_key_name))
 
+        if self.password_scheme not in config.password_schemes_configurable:
+            raise error.ConfigurationError("not supported: password_scheme = %r" % self.password_scheme)
+
+        if self.passlib_support:
+            try:
+                from passlib.context import CryptContext
+            except ImportError, err:
+                raise error.ConfigurationError("Wiki is configured to use passlib, but importing passlib failed [%s]!" % str(err))
+            try:
+                self.cache.pwd_context = CryptContext(**self.passlib_crypt_context)
+            except (ValueError, KeyError), err:
+                # ValueError: wrong configuration values
+                # KeyError: unsupported hash (seen with passlib 1.3)
+                raise error.ConfigurationError("passlib_crypt_context configuration is invalid [%s]." % str(err))
+        elif self.password_scheme == '{PASSLIB}':
+            raise error.ConfigurationError("passlib_support is switched off, thus you can't use password_scheme = '{PASSLIB}'.")
+
     def calc_secrets(self):
         """ make up some 'secret' using some config values """
         varnames = ['data_dir', 'data_underlay_dir', 'language_default',
@@ -784,6 +801,35 @@
     ('password_checker', DefaultExpression('_default_password_checker'),
      'checks whether a password is acceptable (default check is length >= 6, at least 4 different chars, no keyboard sequence, not username used somehow (you can switch this off by using `None`)'),
 
+    ('password_scheme', '{PASSLIB}',
+     'Either "{PASSLIB}" (default) to use passlib for creating and upgrading password hashes (see also passlib_crypt_context for passlib configuration), '
+     'or "{SSHA}" (or any other of the builtin password schemes) to not use passlib (not recommended).'),
+
+    ('passlib_support', True,
+     'If True (default), import passlib and support password hashes offered by it.'),
+
+    ('passlib_crypt_context', dict(
+        # schemes we want to support (or deprecated schemes for which we still have
+        # hashes in our storage).
+        # note: bcrypt: we did not include it as it needs additional code (that is not pure python
+        #       and thus either needs compiling or installing platform-specific binaries) and
+        #       also there was some bcrypt issue in passlib < 1.5.3.
+        #       pbkdf2_sha512: not included as it needs at least passlib 1.4.0
+        #       sha512_crypt: supported since passlib 1.3.0 (first public release)
+        schemes=["sha512_crypt", ],
+        # default scheme for creating new pw hashes (if not given, passlib uses first from schemes)
+        #default="sha512_crypt",
+        # deprecated schemes get auto-upgraded to the default scheme at login
+        # time or when setting a password (including doing a moin account pwreset).
+        # for passlib >= 1.6, giving ["auto"] means that all schemes except the default are deprecated:
+        #deprecated=["auto"],
+        # to support also older passlib versions, rather give a explicit list:
+        #deprecated=[],
+        # vary rounds parameter randomly when creating new hashes...
+        #all__vary_rounds=0.1,
+    ),
+    "passlib CryptContext arguments, see passlib docs"),
+
   )),
   # ==========================================================================
   'spam_leech_dos': ('Anti-Spam/Leech/DOS',
--- a/MoinMoin/user.py	Fri Jan 18 01:46:13 2013 +0100
+++ b/MoinMoin/user.py	Sat Jan 19 00:32:21 2013 +0100
@@ -28,6 +28,9 @@
 except ImportError:
     crypt = None
 
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
 from MoinMoin.support.python_compatibility import hash_new, hmac_new
 
 from MoinMoin import config, caching, wikiutil, i18n, events
@@ -35,8 +38,6 @@
 from MoinMoin.util import timefuncs, random_string
 from MoinMoin.wikiutil import url_quote_plus
 
-DEFAULT_ALG = '{SSHA}'  # default algorithm for pw hashing
-
 
 def getUserList(request):
     """ Get a list of all (numerical) user IDs.
@@ -149,23 +150,35 @@
     return username or (request.cfg.show_hosts and request.remote_addr) or _("<unknown>")
 
 
-def encodePassword(pwd, salt=None):
-    """ Encode a cleartext password using the DEFAULT_ALG algorithm.
+def encodePassword(cfg, pwd, salt=None, scheme=None):
+    """ Encode a cleartext password using the default algorithm.
 
+    @param cfg: the wiki config
     @param pwd: the cleartext password, (unicode)
-    @param salt: the salt for the password (string)
+    @param salt: the salt for the password (string) or None to generate a
+                 random salt.
+    @param scheme: scheme to use (by default will use cfg.password_scheme)
     @rtype: string
     @return: the password hash in apache htpasswd compatible encoding,
     """
-    pwd = pwd.encode('utf-8')
-
-    if salt is None:
-        salt = random_string(20)
-    assert isinstance(salt, str)
-    hash = hash_new('sha1', pwd)
-    hash.update(salt)
-
-    return '{SSHA}' + base64.encodestring(hash.digest() + salt).rstrip()
+    if scheme is None:
+        scheme = cfg.password_scheme
+        configured_scheme = True
+    else:
+        configured_scheme = False
+    if scheme == '{PASSLIB}':
+        return '{PASSLIB}' + cfg.cache.pwd_context.encrypt(pwd, salt=salt)
+    elif scheme == '{SSHA}':
+        pwd = pwd.encode('utf-8')
+        if salt is None:
+            salt = random_string(20)
+        assert isinstance(salt, str)
+        hash = hash_new('sha1', pwd)
+        hash.update(salt)
+        return '{SSHA}' + base64.encodestring(hash.digest() + salt).rstrip()
+    else:
+        # should never happen as we check the value of cfg.password_scheme
+        raise NotImplementedError
 
 
 class Fault(Exception):
@@ -188,7 +201,7 @@
             # set a invalid password hash
             u.enc_password = ''
         else:
-            u.enc_password = encodePassword(newpass)
+            u.enc_password = encodePassword(request.cfg, newpass)
         u.save()
         if notify and not u.disabled and u.email:
             mailok, msg = u.mailAccountData()
@@ -348,7 +361,7 @@
         self.recoverpass_key = ""
 
         if password:
-            self.enc_password = encodePassword(password)
+            self.enc_password = encodePassword(self._cfg, password)
 
         #self.edit_cols = 80
         self.tz_offset = int(float(self._cfg.tz_offset) * 3600)
@@ -386,7 +399,7 @@
         if not self.id:
             self.id = self.make_id()
             if password is not None:
-                self.enc_password = encodePassword(password)
+                self.enc_password = encodePassword(self._cfg, password)
 
         # "may" so we can say "if user.may.read(pagename):"
         if self._cfg.SecurityPolicy:
@@ -545,48 +558,76 @@
         if not password:
             return False, False
 
-        # Check password and upgrade weak hashes to strong DEFAULT_ALG:
-        for method in ['{SSHA}', '{SHA}', '{APR1}', '{MD5}', '{DES}']:
-            if epwd.startswith(method):
-                d = epwd[len(method):]
-                if method == '{SSHA}':
-                    d = base64.decodestring(d)
-                    salt = d[20:]
-                    hash = hash_new('sha1', password.encode('utf-8'))
-                    hash.update(salt)
-                    enc = base64.encodestring(hash.digest() + salt).rstrip()
-
-                elif method == '{SHA}':
-                    enc = base64.encodestring(
-                        hash_new('sha1', password.encode('utf-8')).digest()).rstrip()
+        password_correct = recompute_hash = False
+        wanted_scheme = self._cfg.password_scheme
 
-                elif method == '{APR1}':
-                    # d is of the form "$apr1$<salt>$<hash>"
-                    salt = d.split('$')[2]
-                    enc = md5crypt.apache_md5_crypt(password.encode('utf-8'),
-                                                    salt.encode('ascii'))
-                elif method == '{MD5}':
-                    # d is of the form "$1$<salt>$<hash>"
-                    salt = d.split('$')[2]
-                    enc = md5crypt.unix_md5_crypt(password.encode('utf-8'),
-                                                  salt.encode('ascii'))
-                elif method == '{DES}':
-                    if crypt is None:
-                        return False, False
-                    # d is 2 characters salt + 11 characters hash
-                    salt = d[:2]
-                    enc = crypt.crypt(password.encode('utf-8'), salt.encode('ascii'))
+        # Check password and upgrade weak hashes to strong default algorithm:
+        for scheme in config.password_schemes_supported:
+            if epwd.startswith(scheme):
+                is_passlib = False
+                d = epwd[len(scheme):]
 
-                if safe_str_equal(epwd, method + enc):
-                    # hashes match, password is correct
-                    do_upgrade = method != DEFAULT_ALG
-                    if do_upgrade:
-                        # upgrade pw hash to default algorithm
-                        data['enc_password'] = encodePassword(password)
-                    return True, do_upgrade
+                if scheme == '{PASSLIB}':
+                    # a password hash to be checked by passlib library code
+                    if not self._cfg.passlib_support:
+                        logging.error('in user profile %r, password hash with {PASSLIB} scheme encountered, but passlib_support is False' % (self.id, ))
+                    else:
+                        pwd_context = self._cfg.cache.pwd_context
+                        try:
+                            password_correct = pwd_context.verify(password, d)
+                        except ValueError, err:
+                            # can happen for unknown scheme
+                            logging.error('in user profile %r, verifying the passlib pw hash crashed [%s]' % (self.id, str(err)))
+                        if password_correct:
+                            # check if we need to recompute the hash. this is needed if either the
+                            # passlib hash scheme / hash params changed or if we shall change to a
+                            # builtin hash scheme (not recommended):
+                            if not hasattr(pwd_context, 'needs_update'):
+                                # older passlib versions (like 1.3.0) didn't have that method
+                                pwd_context.needs_update = pwd_context.hash_needs_update
+                            recompute_hash = pwd_context.needs_update(d) or wanted_scheme != '{PASSLIB}'
 
-                # wrong password
-                return False, False
+                else:
+                    # a password hash to be checked by legacy, builtin code
+                    if scheme == '{SSHA}':
+                        d = base64.decodestring(d)
+                        salt = d[20:]
+                        hash = hash_new('sha1', password.encode('utf-8'))
+                        hash.update(salt)
+                        enc = base64.encodestring(hash.digest() + salt).rstrip()
+
+                    elif scheme == '{SHA}':
+                        enc = base64.encodestring(
+                            hash_new('sha1', password.encode('utf-8')).digest()).rstrip()
+
+                    elif scheme == '{APR1}':
+                        # d is of the form "$apr1$<salt>$<hash>"
+                        salt = d.split('$')[2]
+                        enc = md5crypt.apache_md5_crypt(password.encode('utf-8'),
+                                                        salt.encode('ascii'))
+                    elif scheme == '{MD5}':
+                        # d is of the form "$1$<salt>$<hash>"
+                        salt = d.split('$')[2]
+                        enc = md5crypt.unix_md5_crypt(password.encode('utf-8'),
+                                                      salt.encode('ascii'))
+                    elif scheme == '{DES}':
+                        if crypt is None:
+                            return False, False
+                        # d is 2 characters salt + 11 characters hash
+                        salt = d[:2]
+                        enc = crypt.crypt(password.encode('utf-8'), salt.encode('ascii'))
+
+                    else:
+                        logging.error('in user profile %r, password hash with unknown scheme encountered: %r' % (self.id, scheme))
+                        raise NotImplementedError
+
+                    if safe_str_equal(epwd, scheme + enc):
+                        password_correct = True
+                        recompute_hash = scheme != wanted_scheme
+
+                if recompute_hash:
+                    data['enc_password'] = encodePassword(self._cfg, password)
+                return password_correct, recompute_hash
 
         # unsupported algorithm
         return False, False
@@ -1066,7 +1107,7 @@
         if not safe_str_equal(h, parts[1]):
             return False
         self.recoverpass_key = ""
-        self.enc_password = encodePassword(newpass)
+        self.enc_password = encodePassword(self._cfg, newpass)
         self.save()
         return True
 
--- a/MoinMoin/userprefs/changepass.py	Fri Jan 18 01:46:13 2013 +0100
+++ b/MoinMoin/userprefs/changepass.py	Sat Jan 19 00:32:21 2013 +0100
@@ -62,7 +62,7 @@
                 return 'error', _("Password not acceptable: %s") % pw_error
 
         try:
-            self.request.user.enc_password = user.encodePassword(password)
+            self.request.user.enc_password = user.encodePassword(request.cfg, password)
             self.request.user.save()
             return 'info', _("Your password has been changed.")
         except UnicodeError, err:
--- a/docs/CHANGES	Fri Jan 18 01:46:13 2013 +0100
+++ b/docs/CHANGES	Sat Jan 19 00:32:21 2013 +0100
@@ -16,14 +16,86 @@
     editor_force = True
     editor_default = 'text'  # internal default, just for completeness
 
-Version 1.9.6:
-
+Version 1.9.current:
   SECURITY HINT: make sure you have allow_xslt = False (or just do not use
   allow_xslt at all in your wiki configs, False is the internal default).
   Allowing XSLT/4suite is very dangerous, see HelpOnConfiguration wiki page.
 
   HINT: Python >= 2.5 is maybe required! See docs/REQUIREMENTS for details.
 
+  New features:
+   * passlib support - enhanced password hash security
+
+    Docs for passlib: http://packages.python.org/passlib/
+
+    If cfg.passlib_support is True (default), we try to import passlib and set it
+    up using the configuration given in cfg.passlib_crypt_context (default is to
+    use sha512_crypt with default configuration from passlib).
+
+    The passlib docs recommend 3 hashing schemes that have good security, but
+    some of them have additional requirements:
+    sha512_crypt needs passlib >= 1.3.0, no other requirements.
+    pbkdf2_sha512 needs passlib >= 1.4.0, no other requirements.
+    bcrypt has additional binary/compiled package requirements, please refer to
+    the passlib docs.
+
+    cfg.password_scheme should be '{PASSLIB}' (default) to tell that passlib is
+    wanted for new password hash creation and also for upgrading existing
+    password hashes.
+
+    For the moin code as distributed in our download release archive, passlib
+    support should just work, as we have passlib 1.6.1 bundled with MoinMoin
+    as MoinMoin/support/passlib. If you use some other moin package, please
+    first check if you have moin AND passlib installed (and also find out the
+    passlib version you have installed).
+
+    If you do NOT want to (not recommended!) or can't use (still using python
+    2.4?) passlib, you can disable it your wiki config:
+
+        passlib_support = False  # do not import passlib
+        password_scheme = '{SSHA}'  # use best builtin hash (like moin < 1.9.7)
+
+    Please note that after you have used moin with passlib support and have user
+    profiles with passlib hashes, you can't just switch off passlib support,
+    because if you did, moin would not be able to log in users with passlib
+    password hashes. Password recovery would still work, though.
+
+    password_scheme always gives the password scheme that is wanted for new or
+    recomputed password hashes. The code is able to upgrade and downgrade hashes
+    at login time and also when setting / resetting passwords for one or all
+    users (via the wiki web interface or via moin account resetpw script
+    command).
+
+    So, if you want that everybody uses strong, passlib-created hashes,
+    resetting the passwords for all users is strongly recommended:
+    First have passlib support switched on (it is on by default), use
+    password_scheme = '{PASSLIB}' (also default), then reset all passwords.
+
+    Same procedure can be used to go back to weaker builtin hashes (not
+    recommended): First switch off passlib support, use password_scheme =
+    '{SSHA}', then reset all passwords.
+
+    Wiki farm admins sharing the same user_dir between multiple wikis must use
+    consistent password hashing / passlib configuration settings for all wikis
+    sharing the same user_dir. Using the builtin defaults or doing the
+    configuration in farmconfig.py is recommended.
+
+    Admins are advised to read the passlib docs (especially when experiencing
+    too slow logins or when running old passlib versions which may not have
+    appropriate defaults for nowadays):
+    http://packages.python.org/passlib/new_app_quickstart.html#choosing-a-hash
+    http://packages.python.org/passlib/password_hash_api.html#choosing-the-right-rounds-value
+
+  * Password mass reset support:
+    Resetting the passwords of all wiki users can be done using:
+    moin ... --verbose account resetpw --all-users --notify
+
+    This is useful to make sure everybody sets a new password and moin computes
+    the password hash using the current configuration.
+
+
+Version 1.9.6:
+
   Fixes:
   * fix remote code execution vulnerability in twikidraw/anywikidraw action
   * fix path traversal vulnerability in AttachFile action
--- a/docs/REQUIREMENTS	Fri Jan 18 01:46:13 2013 +0100
+++ b/docs/REQUIREMENTS	Sat Jan 19 00:32:21 2013 +0100
@@ -19,6 +19,9 @@
 Python requirements might be different for (linux or other) distribution
 packages, depending on the werkzeug version they use. Usually you do not have
 to care then, because the package maintainer already did it for you.
+You also can't use the stronger password hashes provided by passlib as it
+requires at least python 2.5 and you need to disable it in your wiki config,
+so moin does not try to use it.
 
 Python 3.x won't work for MoinMoin for now.
 
@@ -98,6 +101,17 @@
 
 A) Stuff below MoinMoin/support/:
 
+passlib (password hashing library)
+==================================
+shipped: 1.6.1
+minimum: 1.3(?)
+
+Note: moin could work without passlib also (NOT RECOMMENDED), but would not
+support stronger password hashes then, also would not be able to process such
+hashes from existing user profiles (that were made with passlib) and also
+would be less secure due to the weaker builtin hash methods.
+
+
 flup (cgi/fastcgi/scgi/ajp to WSGI adapter)
 ===========================================
 shipped: 1.0.2+, from repo: hg clone -r 8d52f88effa3 http://hg.saddi.com/flup-server