Commits

Roger Haase committed 2920ee8 Merge

merge

Comments (0)

Files changed (35)

MoinMoin/_tests/__init__.py

         meta[NAME] = name
     if CONTENTTYPE not in meta:
         meta[CONTENTTYPE] = u'application/octet-stream'
-    rev = item.store_revision(meta, StringIO(data))
+    rev = item.store_revision(meta, StringIO(data), return_rev=True)
     return rev
 
 def create_random_string_list(length=14, count=10):

MoinMoin/_tests/test_user.py

 # -*- coding: utf-8 -*-
 # Copyright: 2003-2004 by Juergen Hermann <jh@web.de>
 # Copyright: 2009 by ReimarBauer
+# Copyright: 2013 by ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
         assert u.exists()
 
 
-class TestLoginWithPassword(object):
-    """user: login tests"""
-
+class TestUser(object):
     def setup_method(self, method):
         # Save original user
         self.saved_user = flaskg.user
         # Restore original user
         flaskg.user = self.saved_user
 
+    # Passwords / Login -----------------------------------------------
+
     def testAsciiPassword(self):
         """ user: login with ascii password """
         # Create test user
         theUser = user.User(name=name, password=password)
         assert theUser.valid
 
-    def test_auth_with_ssha_stored_password(self):
+    def testInvalidatePassword(self):
+        """ user: test invalidation of password """
+        # Create test user
+        name = u'__Non Existent User Name__'
+        password = name
+        self.createUser(name, password)
+
+        # Try to "login"
+        theUser = user.User(name=name, password=password)
+        assert theUser.valid
+
+        # invalidate the stored password (hash)
+        theUser.set_password("") # emptry str or None means "invalidate"
+        theUser.save()
+
+        # Try to "login" with previous password
+        theUser = user.User(name=name, password=password)
+        assert not theUser.valid
+
+        # Try to "login" with empty password
+        theUser = user.User(name=name, password="")
+        assert not theUser.valid
+
+    def testPasswordHash(self):
         """
-        Create user with {SSHA} password and check that user can login.
+        Create user, set a specific pw hash and check that user can login
+        with the correct password and can not log in with a wrong password.
         """
         # Create test user
         name = u'Test User'
-        # pass = 12345
-        # salt = salt
-        password = '{SSHA}x4YEGdfI4i0qROaY3NTHCmwSJY5zYWx0'
-        self.createUser(name, password, True)
+        # sha512_crypt passlib hash for '12345':
+        pw_hash = '$6$rounds=1001$y9ObPHKb8cvRCs5G$39IW1i5w6LqXPRi4xqAu3OKv1UOpVKNkwk7zPnidsKZWqi1CrQBpl2wuq36J/s6yTxjCnmaGzv/2.dAmM8fDY/'
+        self.createUser(name, pw_hash, True)
 
-        # Try to "login"
+        # Try to "login" with correct password
         theuser = user.User(name=name, password='12345')
         assert theuser.valid
 
-    def test_auth_with_apr1_stored_password(self):
-        """
-        Create user with {APR1} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbm blaze 12345"
-        password = '{APR1}$apr1$NG3VoiU5$PSpHT6tV0ZMKkSZ71E3qg.' # 12345
-        self.createUser(name, password, True)
+        # Try to "login" with a wrong password
+        theuser = user.User(name=name, password='wrong')
+        assert not theuser.valid
 
-        # Try to "login"
-        theuser = user.User(name=name, password='12345')
-        assert theuser.valid
-
-    def test_auth_with_md5_stored_password(self):
-        """
-        Create user with {MD5} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        password = '{MD5}$1$salt$etVYf53ma13QCiRbQOuRk/' # 12345
-        self.createUser(name, password, True)
-
-        # Try to "login"
-        theuser = user.User(name=name, password='12345')
-        assert theuser.valid
-
-    def test_auth_with_des_stored_password(self):
-        """
-        Create user with {DES} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbd blaze 12345"
-        password = '{DES}gArsfn7O5Yqfo' # 12345
-        self.createUser(name, password, True)
-
-        try:
-            import crypt
-            # Try to "login"
-            theuser = user.User(name=name, password='12345')
-            assert theuser.valid
-        except ImportError:
-            pytest.skip("Platform does not provide crypt module!")
-
-    def test_auth_with_ssha256_stored_password(self):
-        """
-        Create user with {SSHA256} password and check that user can login.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with online sha256 tool
-        # pass: 12345
-        # salt: salt
-        # base64 encoded
-        password = '{SSHA256}r4ONZUfEyn9MUkcyDQkQ5MBNpdIerM24MasxFpuQBaFzYWx0'
-
-        self.createUser(name, password, True)
-
-        # Try to "login"
-        theuser = user.User(name=name, password='12345')
-        assert theuser.valid
-
-    def test_regression_user_password_started_with_sha(self):
-        # This is regression test for bug in function 'user.create_user'.
-        #
-        # This function does not encode passwords which start with '{SHA}'
-        # It treats them as already encoded SHA hashes.
-        #
-        # If user during registration specifies password starting with '{SHA}'
-        # this password will not get encoded and user object will get saved with empty enc_password
-        # field.
-        #
-        # Such situation leads to "KeyError: 'enc_password'" during
-        # user authentication.
-
-        # Any Password begins with the {SHA} symbols led to
-        # "KeyError: 'enc_password'" error during user authentication.
-        user_name = u'moin'
-        user_password = u'{SHA}LKM56'
-        user.create_user(user_name, user_password, u'moin@moinmo.in', u'')
-
-        # Try to "login"
-        theuser = user.User(name=user_name, password=user_password)
-        assert theuser.valid
+    # Subscriptions ---------------------------------------------------
 
     def testSubscriptionSubscribedPage(self):
         """ user: tests is_subscribed_to  """
         theUser.subscribe(pagename)
         assert not theUser.is_subscribed_to([testPagename]) # list(!) of pages to check
 
-    def test_upgrade_password_from_ssha_to_ssha256(self):
-        """
-        Create user with {SSHA} password and check that logging in
-        upgrades to {SSHA256}.
-        """
-        name = u'/no such user/'
-        # pass = 'MoinMoin', salt = '12345'
-        password = '{SSHA}xkDIIx1I7A4gC98Vt/+UelIkTDYxMjM0NQ=='
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='MoinMoin')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_sha_to_ssha256(self):
-        """
-        Create user with {SHA} password and check that logging in
-        upgrades to {SSHA256}.
-        """
-        name = u'/no such user/'
-        password = '{SHA}jLIjfQZ5yojbZGTqxg2pY0VROWQ=' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_apr1_to_ssha256(self):
-        """
-        Create user with {APR1} password and check that logging in
-        upgrades to {SSHA256}.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbm blaze 12345"
-        password = '{APR1}$apr1$NG3VoiU5$PSpHT6tV0ZMKkSZ71E3qg.' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_md5_to_ssha256(self):
-        """
-        Create user with {MD5} password and check that logging in
-        upgrades to {SSHA}.
-        """
-        # Create test user
-        name = u'Test User'
-        password = '{MD5}$1$salt$etVYf53ma13QCiRbQOuRk/' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
-    def test_upgrade_password_from_des_to_ssha256(self):
-        """
-        Create user with {DES} password and check that logging in
-        upgrades to {SSHA}.
-        """
-        # Create test user
-        name = u'Test User'
-        # generated with "htpasswd -nbd blaze 12345"
-        password = '{DES}gArsfn7O5Yqfo' # 12345
-        self.createUser(name, password, True)
-
-        theuser = user.User(name=name, password='12345')
-        assert theuser.enc_password[:9] == '{SSHA256}'
-
     # Bookmarks -------------------------------------------------------
 
     def test_bookmark(self):

MoinMoin/_tests/wikiconfig.py

     interwikiname = u'MoinTest'
     interwiki_map = dict(Self='http://localhost:8080/', MoinMoin='http://moinmo.in/')
     interwiki_map[interwikiname] = 'http://localhost:8080/'
+
+    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,
+    )

MoinMoin/apps/frontend/views.py

 # Copyright: 2012 MoinMoin:CheerXiao
-# Copyright: 2003-2010 MoinMoin:ThomasWaldmann
+# Copyright: 2003-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2011 MoinMoin:AkashSinha
 # Copyright: 2011 MoinMoin:ReimarBauer
 # Copyright: 2008 MoinMoin:FlorianKrupicka
     """Validator for a valid password recovery form
     """
     passwords_mismatch_msg = L_('The passwords do not match.')
-    password_encoding_problem_msg = L_('New password is unacceptable, encoding trouble.')
+    password_problem_msg = L_('New password is unacceptable, could not get processed.')
 
     def validate(self, element, state):
         if element['password1'].value != element['password2'].value:
             return self.note_error(element, state, 'passwords_mismatch_msg')
 
+        password = element['password1'].value
         try:
-            crypto.crypt_password(element['password1'].value)
-        except UnicodeError:
-            return self.note_error(element, state, 'password_encoding_problem_msg')
+            app.cfg.cache.pwd_context.encrypt(password)
+        except (ValueError, TypeError) as err:
+            return self.note_error(element, state, 'password_problem_msg')
 
         return True
 
     """
     passwords_mismatch_msg = L_('The passwords do not match.')
     current_password_wrong_msg = L_('The current password was wrong.')
-    password_encoding_problem_msg = L_('New password is unacceptable, encoding trouble.')
+    password_problem_msg = L_('New password is unacceptable, could not get processed.')
 
     def validate(self, element, state):
         if not (element['password_current'].valid and element['password1'].valid and element['password2'].valid):
         if element['password1'].value != element['password2'].value:
             return self.note_error(element, state, 'passwords_mismatch_msg')
 
+        password = element['password1'].value
         try:
-            crypto.crypt_password(element['password1'].value)
-        except UnicodeError:
-            return self.note_error(element, state, 'password_encoding_problem_msg')
+            app.cfg.cache.pwd_context.encrypt(password)
+        except (ValueError, TypeError) as err:
+            return self.note_error(element, state, 'password_problem_msg')
         return True
 
 

MoinMoin/config/default.py

 # -*- coding: utf-8 -*-
 # Copyright: 2000-2004 Juergen Hermann <jh@web.de>
-# Copyright: 2005-2011 MoinMoin:ThomasWaldmann
+# Copyright: 2005-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2008      MoinMoin:JohannesBerg
 # Copyright: 2010      MoinMoin:DiogenesAugusto
 # Copyright: 2011      MoinMoin:AkashSinha
                 raise error.ConfigurationError("You must set a (at least {0} chars long) secret string for secrets['{1}']!".format(
                     secret_min_length, secret_key_name))
 
+        from passlib.context import CryptContext
+        try:
+            self.cache.pwd_context = CryptContext(**self.passlib_crypt_context)
+        except ValueError as err:
+            raise error.ConfigurationError("passlib_crypt_context configuration is invalid [{0}].".format(err))
+
     def _config_check(self):
         """ Check namespace and warn about unknown names
 
 
     ('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`)'),
+
+    ('passlib_crypt_context', dict(
+        # schemes we want to support (or deprecated schemes for which we still have
+        # hashes in our storage).
+        # note about bcrypt: it needs additional code (that is not pure python and
+        # thus either needs compiling or installing platform-specific binaries)
+        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).
+        #deprecated=["auto"],
+        # 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',
         scroll_page_after_edit=True,
         show_comments=False,
         want_trivial=False,
+        enc_password=u'',  # empty value == invalid hash
         disabled=False,
         bookmarks={},
         quicklinks=[],

MoinMoin/constants/contenttypes.py

 
 CONTENTTYPE_USER = u'application/x.moin.userprofile'
 CONTENTTYPE_DEFAULT = u'application/octet-stream'
+CONTENTTYPE_NONEXISTENT = u'application/x-nonexistent'
 
 
 GROUP_MARKUP_TEXT = 'markup text items'

MoinMoin/converter/docbook_out.py

 logging = log.getLogger(__name__)
 
 from MoinMoin.util.tree import html, moin_page, xlink, docbook, xml
+from MoinMoin.constants.contenttypes import CONTENTTYPE_NONEXISTENT
 
 
 class Converter(object):
         """
         href = element.get(xlink.href, None)
         attrib = {}
-        mimetype = Type(_type=element.get(moin_page.type_, 'application/x-nonexistent'))
+        mimetype = Type(_type=element.get(moin_page.type_, CONTENTTYPE_NONEXISTENT))
         if href:
             attrib[docbook.fileref] = href
             if Type('image/').issupertype(mimetype):

MoinMoin/converter/html_out.py

 from MoinMoin import wikiutil
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.tree import html, moin_page, xlink, xml, Name
+from MoinMoin.constants.contenttypes import CONTENTTYPE_NONEXISTENT
 
 from MoinMoin import log
 logging = log.getLogger(__name__)
         # TODO: maybe IE8 would display transcluded external pages if we could do <object... type="text/html" ...>
         href = elem.get(xlink.href, None)
         attrib = {}
-        mimetype = Type(_type=elem.get(moin_page.type_, 'application/x-nonexistent'))
+        mimetype = Type(_type=elem.get(moin_page.type_, CONTENTTYPE_NONEXISTENT))
         # Get the object type
         obj_type = self.eval_object_type(mimetype, href)
 

MoinMoin/converter/nonexistent_in.py

 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.util.iri import Iri
 from MoinMoin.util.tree import moin_page, xlink
+from MoinMoin.constants.contenttypes import CONTENTTYPE_NONEXISTENT
 
 
 class Converter(object):
 
 from . import default_registry
 from MoinMoin.util.mime import Type, type_moin_document
-default_registry.register(Converter._factory, Type('application/x-nonexistent'), type_moin_document)
+default_registry.register(Converter._factory, Type(CONTENTTYPE_NONEXISTENT), type_moin_document)

MoinMoin/items/__init__.py

     CONTENTTYPE, SIZE, ACTION, ADDRESS, HOSTNAME, USERID, COMMENT,
     HASH_ALGORITHM, ITEMID, REVID, DATAID, CURRENT, PARENTID
     )
-from MoinMoin.constants.contenttypes import charset
+from MoinMoin.constants.contenttypes import charset, CONTENTTYPE_NONEXISTENT
+from MoinMoin.constants.itemtypes import (
+    ITEMTYPE_NONEXISTENT, ITEMTYPE_USERPROFILE, ITEMTYPE_DEFAULT,
+    )
 
 from .content import content_registry, Content, NonExistentContent, Draw
 
     def __init__(self, item, itemtype=None, contenttype=None):
         self.item = item
         self.meta = {
-            ITEMTYPE: itemtype or u'nonexistent',
-            CONTENTTYPE: contenttype or u'application/x-nonexistent'
+            ITEMTYPE: itemtype or ITEMTYPE_NONEXISTENT,
+            CONTENTTYPE: contenttype or CONTENTTYPE_NONEXISTENT
         }
         self.data = StringIO('')
         self.revid = None
                                              action=unicode(action),
                                              contenttype_current=contenttype_current,
                                              contenttype_guessed=contenttype_guessed,
+                                             return_rev=True,
                                              )
         item_modified.send(app._get_current_object(), item_name=name)
         return newrev.revid, newrev.meta[SIZE]
     """
     A "conventional" wiki item.
     """
-    itemtype = u'default'
+    itemtype = ITEMTYPE_DEFAULT
     display_name = L_('Default')
     description = L_('Wiki item')
     order = -10
     Currently userprofile is implemented as a contenttype. This is a stub of an
     itemtype implementation of userprofile.
     """
-    itemtype = u'userprofile'
+    itemtype = ITEMTYPE_USERPROFILE
     display_name = L_('User profile')
     description = L_('User profile item (not implemented yet!)')
 
     A dummy Item for nonexistent items (when modifying, a nonexistent item with
     undetermined itemtype)
     """
-    itemtype = u'nonexistent'
+    itemtype = ITEMTYPE_NONEXISTENT
     shown = False
 
     def _convert(self, doc):

MoinMoin/items/content.py

 from MoinMoin.forms import File
 from MoinMoin.constants.contenttypes import (
     GROUP_MARKUP_TEXT, GROUP_OTHER_TEXT, GROUP_IMAGE, GROUP_AUDIO, GROUP_VIDEO,
-    GROUP_DRAWING, GROUP_OTHER,
+    GROUP_DRAWING, GROUP_OTHER, CONTENTTYPE_NONEXISTENT,
     )
 from MoinMoin.constants.keys import (
     NAME, NAME_EXACT, WIKINAME, CONTENTTYPE, SIZE, TAGS, HASH_ALGORITHM
 @register
 class NonExistentContent(Content):
     """Dummy Content to use with NonExistent."""
-    contenttype = 'application/x-nonexistent'
+    contenttype = CONTENTTYPE_NONEXISTENT
     group = None
 
     def do_get(self, force_attachment=False, mimetype=None):

MoinMoin/script/__init__.py

     manager.add_command("save", serialization.Serialize())
     manager.add_command("load", serialization.Deserialize())
     from MoinMoin.script.account.create import Create_User
-    manager.add_command("account_create", Create_User())
+    manager.add_command("account-create", Create_User())
     from MoinMoin.script.account.disable import Disable_User
-    manager.add_command("account_disable", Disable_User())
+    manager.add_command("account-disable", Disable_User())
     from MoinMoin.script.account.resetpw import Set_Password
-    manager.add_command("account_password", Set_Password())
+    manager.add_command("account-password", Set_Password())
     from MoinMoin.script.maint.reduce_revisions import Reduce_Revisions
-    manager.add_command("maint_reduce_revisions", Reduce_Revisions())
+    manager.add_command("maint-reduce-revisions", Reduce_Revisions())
     from MoinMoin.script.maint.set_meta import Set_Meta
-    manager.add_command("maint_set_meta", Set_Meta())
+    manager.add_command("maint-set-meta", Set_Meta())
     from MoinMoin.script.maint import modify_item
     manager.add_command("item-get", modify_item.GetItem())
     manager.add_command("item-put", modify_item.PutItem())
     from MoinMoin.script.maint.modified_systemitems import Modified_SystemItems
-    manager.add_command("maint_modified_systemitems", Modified_SystemItems())
+    manager.add_command("maint-modified-systemitems", Modified_SystemItems())
     from MoinMoin.script.migration.moin19.import19 import ImportMoin19
     manager.add_command("import19", ImportMoin19())
 

MoinMoin/script/account/resetpw.py

-# Copyright: 2006 MoinMoin:ThomasWaldmann
+# Copyright: 2006-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2008 MoinMoin:JohannesBerg
 # Copyright: 2011 MoinMoin:ReimarBauer
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 from flask import g as flaskg
 from flask.ext.script import Command, Option
 
+from MoinMoin.constants.keys import ITEMID, NAME, NAME_EXACT, EMAIL
 from MoinMoin import user
 from MoinMoin.app import before_wiki
-from MoinMoin.util import crypto
+
+
+class Fault(Exception):
+    """something went wrong"""
+
+class NoSuchUser(Fault):
+    """raised if no such user exists"""
+
+class MailFailed(Fault):
+    """raised if e-mail sending failed"""
+
+
+def set_password(uid, password, notify=False):
+    u = user.User(uid)
+    if u and u.exists():
+        u.set_password(password)
+        u.save()
+        if notify and not u.disabled and u.email:
+            mailok, msg = u.mail_password_recovery()
+            if not mailok:
+                raise MailFailed(msg)
+    else:
+        raise NoSuchUser('User does not exist (name: %r id: %r)!' % (u.name, u.id))
 
 
 class Set_Password(Command):
                help='Set password for the user with user name NAME.'),
         Option('--uid', '-u', required=False, dest='uid', type=unicode,
                help='Set password for the user with user id UID.'),
-        Option('--password', '-p', required=True, dest='password', type=unicode,
+        Option('--password', '-p', required=False, dest='password', type=unicode,
                help='New password for this account.'),
+        Option('--all-users', '-a', required=False, dest='all_users', action='store_true', default=False,
+            help='Reset password for ALL users.'),
+        Option('--notify', '-N', required=False, dest='notify', action='store_true', default=False,
+            help='Notify user(s), send them an E-Mail with a password reset link.'),
+        Option('--verbose', '-v', required=False, dest='verbose', action='store_true', default=False,
+            help='Verbose operation'),
     )
 
-    def run(self, name, uid, password):
-        flags_given = name or uid
+    def run(self, name, uid, password, all_users, notify, verbose):
+        flags_given = name or uid or all_users
         if not flags_given:
             print 'incorrect number of arguments'
             import sys
 
         before_wiki()
         if uid:
-            u = user.User(uid)
+            query = {ITEMID: uid}
         elif name:
-            u = user.User(auth_username=name)
+            query = {NAME_EXACT: name}
+        elif all_users:
+            query = {}
 
-        if not u.exists():
-            print 'This user "{0!r}" does not exists!'.format(u.name)
-            return
-
-        u.enc_password = crypto.crypt_password(password)
-        u.save()
-        print 'Password set.'
+        # sorting the list so we have some specific, reproducable order
+        uids_metas = sorted([(rev.meta[ITEMID], rev.meta) for rev in user.search_users(**query)])
+        total = len(uids_metas)
+        for nr, (uid, meta) in enumerate(uids_metas, start=1):
+            name = meta[NAME]
+            email = meta[EMAIL]
+            try:
+                set_password(uid, password, notify=notify)
+            except Fault, err:
+                status = "FAILURE: [%s]" % str(err)
+            else:
+                status = "SUCCESS"
+            if verbose:
+                print "uid %s, name %s, email %s (%05d / %05d) %s" % (uid, name, email, nr, total, status)

MoinMoin/script/migration/moin19/import19.py

                 'editor_default', # not used any more
                 'editor_ui', # not used any more
                 'external_target', # ancient, not used any more
-                'passwd', # ancient, not used any more (use enc_passwd)
+                'passwd', # ancient, not used any more (use enc_password)
                 'show_emoticons', # ancient, not used any more
                 'show_fancy_diff', # kind of diff display now depends on mimetype
                 'show_fancy_links', # not used any more (now link rendering depends on theme)
             if key in metadata and metadata[key] in [u'', tuple(), {}, [], ]:
                 del metadata[key]
 
+        # moin2 only supports passlib generated hashes, drop everything else
+        # (users need to do pw recovery in case they are affected)
+        pw = metadata.get('enc_password')
+        if pw is not None:
+            if pw.startswith('{PASSLIB}'):
+                # take it, but strip the prefix as moin2 does not use that any more
+                metadata['enc_password'] = pw[len('{PASSLIB}'):]
+            else:
+                # drop old, unsupported (and also more or less unsafe) hashing scheme
+                del metadata['enc_password']
+
         # TODO quicklinks and subscribed_items - check for non-interwiki elements and convert them to interwiki
 
         return metadata

MoinMoin/storage/middleware/_tests/test_indexing.py

         item_name = u'foo'
         data = 'bar'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name), StringIO(data))
+        rev = item.store_revision(dict(name=item_name), StringIO(data),
+                                  return_rev=True)
         revid = rev.revid
         # check if we have the revision now:
         item = self.imw[item_name]
         data = 'bar'
         newdata = 'baz'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name, comment=u'spam'), StringIO(data))
+        rev = item.store_revision(dict(name=item_name, comment=u'spam'), StringIO(data),
+                                  return_rev=True)
         revid = rev.revid
         # clear revision:
         item.store_revision(dict(name=item_name, revid=revid, comment=u'no spam'), StringIO(newdata), overwrite=True)
         item_name = u'foo'
         item = self.imw[item_name]
         rev = item.store_revision(dict(name=item_name, mtime=1),
-                                  StringIO('bar'), trusted=True)
+                                  StringIO('bar'), trusted=True, return_rev=True)
         revid0 = rev.revid
         rev = item.store_revision(dict(name=item_name, mtime=2),
-                                  StringIO('baz'), trusted=True)
+                                  StringIO('baz'), trusted=True, return_rev=True)
         revid1 = rev.revid
         rev = item.store_revision(dict(name=item_name, mtime=3),
-                                  StringIO('...'), trusted=True)
+                                  StringIO('...'), trusted=True, return_rev=True)
         revid2 = rev.revid
         print "revids:", revid0, revid1, revid2
         # destroy a non-current revision:
         item_name = u'foo'
         item = self.imw[item_name]
         rev = item.store_revision(dict(name=item_name, mtime=1),
-                                  StringIO('bar'), trusted=True)
+                                  StringIO('bar'), trusted=True, return_rev=True)
         revids.append(rev.revid)
         rev = item.store_revision(dict(name=item_name, mtime=2),
-                                  StringIO('baz'), trusted=True)
+                                  StringIO('baz'), trusted=True, return_rev=True)
         revids.append(rev.revid)
         # destroy item:
         item.destroy_all_revisions()
         item_name = u'bar'
         item = self.imw[item_name]
         item.store_revision(dict(name=item_name), StringIO('1st'))
-        expected_rev = item.store_revision(dict(name=item_name), StringIO('2nd'))
+        expected_rev = item.store_revision(dict(name=item_name), StringIO('2nd'),
+                                           return_rev=True)
         revs = list(self.imw.documents(name=item_name))
         assert len(revs) == 1  # there is only 1 latest revision
         assert expected_rev.revid == revs[0].revid  # it is really the latest one
         item_name = u'foo'
         data = 'bar'
         item = self.imw[item_name]
-        rev = item.store_revision(dict(name=item_name), StringIO(data))
+        rev = item.store_revision(dict(name=item_name), StringIO(data), return_rev=True)
         print repr(rev.meta)
         assert rev.meta[NAME] == item_name
         assert rev.meta[SIZE] == len(data)
     def test_documents(self):
         item_name = u'foo'
         item = self.imw[item_name]
-        rev1 = item.store_revision(dict(name=item_name), StringIO('x'))
-        rev2 = item.store_revision(dict(name=item_name), StringIO('xx'))
-        rev3 = item.store_revision(dict(name=item_name), StringIO('xxx'))
+        rev1 = item.store_revision(dict(name=item_name), StringIO('x'), return_rev=True)
+        rev2 = item.store_revision(dict(name=item_name), StringIO('xx'), return_rev=True)
+        rev3 = item.store_revision(dict(name=item_name), StringIO('xxx'), return_rev=True)
         rev = self.imw.document(idx_name=ALL_REVS, size=2)
         assert rev
         assert rev.revid == rev2.revid
         item_name = u'foo'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=item_name, mtime=1),
-                                StringIO('does not count, different name'), trusted=True)
+                                StringIO('does not count, different name'),
+                                trusted=True, return_rev=True)
         expected_latest_revids.append(r.revid)
         item_name = u'bar'
         item = self.imw[item_name]
         item.store_revision(dict(name=item_name, mtime=1),
                             StringIO('1st'), trusted=True)
         r = item.store_revision(dict(name=item_name, mtime=2),
-                                StringIO('2nd'), trusted=True)
+                                StringIO('2nd'), trusted=True, return_rev=True)
         expected_latest_revids.append(r.revid)
 
         # now we remember the index contents built that way:
         item_name = u'updated'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=item_name, mtime=1),
-                                StringIO('updated 1st'), trusted=True)
+                                StringIO('updated 1st'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         # we update this item below, so we don't add it to expected_latest_revids
         item_name = u'destroyed'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=item_name, mtime=1),
-                                StringIO('destroyed 1st'), trusted=True)
+                                StringIO('destroyed 1st'),
+                                trusted=True, return_rev=True)
         destroy_revid = r.revid
         # we destroy this item below, so we don't add it to expected_all_revids
         # we destroy this item below, so we don't add it to expected_latest_revids
         item_name = u'stayssame'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=item_name, mtime=1),
-                                StringIO('stayssame 1st'), trusted=True)
+                                StringIO('stayssame 1st'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         # we update this item below, so we don't add it to expected_latest_revids
         r = item.store_revision(dict(name=item_name, mtime=2),
-                                StringIO('stayssame 2nd'), trusted=True)
+                                StringIO('stayssame 2nd'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         expected_latest_revids.append(r.revid)
 
         item_name = u'updated'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=item_name, mtime=2),
-                                StringIO('updated 2nd'), trusted=True)
+                                StringIO('updated 2nd'), trusted=True,
+                                return_rev=True)
         expected_all_revids.append(r.revid)
         expected_latest_revids.append(r.revid)
         missing_revids.append(r.revid)
         item_name = u'added'
         item = self.imw[item_name]
         r = item.store_revision(dict(name=item_name, mtime=1),
-                                StringIO('added 1st'), trusted=True)
+                                StringIO('added 1st'),
+                                trusted=True, return_rev=True)
         expected_all_revids.append(r.revid)
         expected_latest_revids.append(r.revid)
         missing_revids.append(r.revid)
         data = 'some test content'
         item = self.imw[item_name]
         data_file = StringIO(data)
-        with item.store_revision(meta, data_file) as rev:
+        with item.store_revision(meta, data_file, return_rev=True) as rev:
             assert rev.data.read() == data
             revid = rev.revid
         with pytest.raises(ValueError):
         data = 'some test content\n'
         item = self.imw[item_name]
         data_file = StringIO(data)
-        with item.store_revision(meta, data_file) as rev:
+        with item.store_revision(meta, data_file, return_rev=True) as rev:
             expected_revid = rev.revid
         doc = self.imw._document(content=u'test')
         assert doc is not None
     def test_documents(self):
         item_name = u'public'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, acl=u'joe:read'), StringIO('public content'))
+        r = item.store_revision(dict(name=item_name, acl=u'joe:read'),
+                                StringIO('public content'), return_rev=True)
         revid_public = r.revid
         revids = [rev.revid for rev in self.imw.documents()
                   if rev.meta[NAME] != u'joe'] # the user profile is a revision in the backend
     def test_getitem(self):
         item_name = u'public'
         item = self.imw[item_name]
-        r = item.store_revision(dict(name=item_name, acl=u'joe:read'), StringIO('public content'))
+        r = item.store_revision(dict(name=item_name, acl=u'joe:read'),
+                                StringIO('public content'), return_rev=True)
         revid_public = r.revid
         # now testing:
         item_name = u'public'

MoinMoin/storage/middleware/_tests/test_protecting.py

         revids = []
         for item_name, acl, content in items:
             item = self.imw[item_name]
-            r = item.store_revision(dict(name=item_name, acl=acl), StringIO(content))
+            r = item.store_revision(dict(name=item_name, acl=acl),
+                                    StringIO(content), return_rev=True)
             revids.append(r.revid)
         return revids
 

MoinMoin/storage/middleware/indexing.py

                        contenttype_current=None,
                        contenttype_guessed=None,
                        acl_parent=None,
+                       return_rev=False,
                        ):
         """
         Store a revision into the backend, write metadata and data to it.
         :type meta: dict
         :type data: open file (file must be closed by caller)
         :param overwrite: if True, allow overwriting of existing revs.
-        :returns: a Revision instance of the just created revision
+        :param return_rev: if True, return a Revision instance of the just created revision
+        :returns: a Revision instance or None
         """
         if remote_addr is None:
             try:
         self.indexer.index_revision(meta, content)
         if not overwrite:
             self._current = self.indexer._document(revid=revid)
-        return Revision(self, revid)
+        if return_rev:
+            return Revision(self, revid)
 
     def store_all_revisions(self, meta, data):
         """

MoinMoin/storage/middleware/protecting.py

     def get_revision(self, revid):
         return self[revid]
 
-    def store_revision(self, meta, data, overwrite=False, **kw):
+    def store_revision(self, meta, data, overwrite=False, return_rev=False, **kw):
         self.require(WRITE)
         if not self:
             self.require(CREATE)
         if overwrite:
             self.require(DESTROY)
-        rev = self.item.store_revision(meta, data, overwrite=overwrite, **kw)
-        return ProtectedRevision(self.protector, rev, p_item=self)
+        rev = self.item.store_revision(meta, data, overwrite=overwrite, return_rev=return_rev, **kw)
+        if return_rev:
+            return ProtectedRevision(self.protector, rev, p_item=self)
 
     def store_all_revisions(self, meta, data):
         self.require(DESTROY)

MoinMoin/storage/stores/__init__.py

 from abc import abstractmethod
 from collections import Mapping, MutableMapping
 
+from MoinMoin.util.StringIOClosing import StringIO
 
 class StoreBase(Mapping):
     """
         """
 
 
+class BytesMutableStoreMixin(object):
+    """
+    mix this into a FileMutableStore to get a BytesMutableStore, like shown here:
+
+    class BytesStore(BytesMutableStoreMixin, FileStore, BytesMutableStoreBase):
+        # that's all, nothing more needed
+    """
+    def __getitem__(self, key):
+        with super(BytesMutableStoreMixin, self).__getitem__(key) as stream:
+            return stream.read()
+
+    def __setitem__(self, key, value):
+        with StringIO(value) as stream:
+            super(BytesMutableStoreMixin, self).__setitem__(key, stream)
+
+
 class FileMutableStoreBase(MutableStoreBase):
     @abstractmethod
     def __setitem__(self, key, stream):
               closing that file later. caller must not rely on some specific
               file pointer position after we return.
         """
+
+class FileMutableStoreMixin(object):
+    """
+    mix this into a BytesMutableStore to get a FileMutableStore, like shown here:
+
+    class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase)
+        # that's all, nothing more needed
+    """
+    def __getitem__(self, key):
+        value = super(FileMutableStoreMixin, self).__getitem__(key)
+        return StringIO(value)
+
+    def __setitem__(self, key, stream):
+        value = stream.read()
+        super(FileMutableStoreMixin, self).__setitem__(key, value)

MoinMoin/storage/stores/fs.py

 import errno
 import shutil
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 
-class _Store(MutableStoreBase):
+class FileStore(FileMutableStoreBase):
     """
     A simple filesystem-based store.
 
     def __delitem__(self, key):
         os.remove(self._mkpath(key))
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
-    def __getitem__(self, key):
-        try:
-            with open(self._mkpath(key), 'rb') as f:
-                return f.read() # better use get_file() and read smaller blocks for big files
-        except IOError as e:
-            if e.errno == errno.ENOENT:
-                raise KeyError(key)
-            raise
-
-    def __setitem__(self, key, value):
-        with open(self._mkpath(key), "wb") as f:
-            f.write(value)
-
-
-class FileStore(_Store, FileMutableStoreBase):
     def __getitem__(self, key):
         try:
             return open(self._mkpath(key), 'rb')
         with open(self._mkpath(key), "wb") as f:
             blocksize = 64 * 1024
             shutil.copyfileobj(stream, f, blocksize)
+
+
+class BytesStore(BytesMutableStoreMixin, FileStore, BytesMutableStoreBase):
+    """filesystem BytesStore"""

MoinMoin/storage/stores/kc.py

 from __future__ import absolute_import, division
 
 import os, errno
-from StringIO import StringIO
 
 from kyotocabinet import *
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     Kyoto cabinet based store.
     """
     def __delitem__(self, key):
         self._db.remove(key)
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         value = self._db.get(key)
         if value is None:
             raise KeyError("set error: " + str(self._db.error()))
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        value = self._db.get(key)
-        if value is None:
-            raise KeyError("get error: " + str(self._db.error()))
-        return StringIO(value)
-
-    def __setitem__(self, key, stream):
-        if not self._db.set(key, stream.read()):
-            raise KeyError("set error: " + str(self._db.error()))
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """Kyoto Cabinet FileStore"""

MoinMoin/storage/stores/kt.py

 import urllib
 from httplib import HTTPConnection
 
-from StringIO import StringIO
-
 from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
 
 

MoinMoin/storage/stores/memory.py

 
 from __future__ import absolute_import, division
 
-from StringIO import StringIO
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
 
-
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     A simple dict-based in-memory store. No persistence!
     """
     def __delitem__(self, key):
         del self._st[key]
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         return self._st[key]
 
         self._st[key] = value
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        return StringIO(self._st[key])
-
-    def __setitem__(self, key, stream):
-        self._st[key] = stream.read()
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """memory FileStore"""

MoinMoin/storage/stores/sqla.py

 
 from __future__ import absolute_import, division
 
-from StringIO import StringIO
-
 from sqlalchemy import create_engine, select, MetaData, Table, Column, String, Binary
 from sqlalchemy.pool import StaticPool
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 KEY_LEN = 128
 VALUE_LEN = 1024 * 1024 # 1MB binary data
 
 
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     A simple dict-based in-memory store. No persistence!
     """
     def __delitem__(self, key):
         self.table.delete().where(self.table.c.key == key).execute()
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         value = select([self.table.c.value], self.table.c.key == key).execute().fetchone()
         if value is not None:
         self.table.insert().execute(key=key, value=value)
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        value = select([self.table.c.value], self.table.c.key == key).execute().fetchone()
-        if value is not None:
-            return StringIO(value[0])
-        else:
-            raise KeyError(key)
-
-    def __setitem__(self, key, stream):
-        self.table.insert().execute(key=key, value=stream.read())
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """sqlalchemy FileStore"""

MoinMoin/storage/stores/sqlite.py

 
 from __future__ import absolute_import, division
 
-from StringIO import StringIO
 import zlib
 from sqlite3 import *
 
-from . import MutableStoreBase, BytesMutableStoreBase, FileMutableStoreBase
+from . import (BytesMutableStoreBase, FileMutableStoreBase,
+               BytesMutableStoreMixin, FileMutableStoreMixin)
 
 
-class _Store(MutableStoreBase):
+class BytesStore(BytesMutableStoreBase):
     """
     A simple sqlite3 based store.
     """
             value = zlib.decompress(value)
         return value
 
-
-class BytesStore(_Store, BytesMutableStoreBase):
     def __getitem__(self, key):
         rows = list(self.conn.execute("select value from {0} where key=?".format(self.table_name), (key, )))
         if not rows:
             self.conn.execute('insert into {0} values (?, ?)'.format(self.table_name), (key, buffer(value)))
 
 
-class FileStore(_Store, FileMutableStoreBase):
-    def __getitem__(self, key):
-        rows = list(self.conn.execute("select value from {0} where key=?".format(self.table_name), (key, )))
-        if not rows:
-            raise KeyError(key)
-        value = str(rows[0]['value'])
-        return StringIO(self._decompress(value))
-
-    def __setitem__(self, key, stream):
-        value = stream.read()
-        value = self._compress(value)
-        with self.conn:
-            self.conn.execute('insert into {0} values (?, ?)'.format(self.table_name), (key, buffer(value)))
+class FileStore(FileMutableStoreMixin, BytesStore, FileMutableStoreBase):
+    """sqlite FileStore"""
 # Copyright: 2000-2004 Juergen Hermann <jh@web.de>
-# Copyright: 2003-2012 MoinMoin:ThomasWaldmann
+# Copyright: 2003-2013 MoinMoin:ThomasWaldmann
 # Copyright: 2007 MoinMoin:JohannesBerg
 # Copyright: 2007 MoinMoin:HeinrichWendel
 # Copyright: 2008 MoinMoin:ChristopherDenter
 
 from whoosh.query import Term, And, Or
 
+from MoinMoin import log
+logging = log.getLogger(__name__)
+
 from MoinMoin import wikiutil
 from MoinMoin.config import CONTENTTYPE_USER
 from MoinMoin.constants.keys import *
 from MoinMoin.i18n import _, L_, N_
 from MoinMoin.mail import sendmail
 from MoinMoin.util.interwiki import getInterwikiHome, getInterwikiName, is_local_wiki
-from MoinMoin.util.crypto import crypt_password, upgrade_password, valid_password, \
-                                 generate_token, valid_token, make_uuid
+from MoinMoin.util.crypto import generate_token, valid_token, make_uuid
 from MoinMoin.storage.error import NoSuchItemError, ItemAlreadyExistsError, NoSuchRevisionError
 
 
         if pw_error:
             return _("Password not acceptable: %(msg)s", msg=pw_error)
 
-    try:
-        theuser.set_password(password, is_encrypted)
-    except UnicodeError as err:
-        # Should never happen
-        return "Can't encode password: %(msg)s" % dict(msg=str(err))
+    theuser.set_password(password, is_encrypted)
 
     # try to get the email, for new users it is required
     if validate and not email:
         if not pw_hash or not password:
             return False, False
 
-        # check the password against the password hash
-        if not valid_password(password, pw_hash):
-            return False, False
+        pwd_context = self._cfg.cache.pwd_context
+        password_correct = False
+        recomputed_hash = None
+        try:
+            password_correct, recomputed_hash = pwd_context.verify_and_update(password, pw_hash)
+        except (ValueError, TypeError) as err:
+            logging.error('in user profile %r, verifying the passlib pw hash raised an Exception [%s]' % (self.id, str(err)))
+        else:
+            if recomputed_hash is not None:
+                data[ENC_PASSWORD] = recomputed_hash
+        return password_correct, bool(recomputed_hash)
 
-        new_pw_hash = upgrade_password(password, pw_hash)
-        if not new_pw_hash:
-            return True, False
+    def set_password(self, password, is_encrypted=False, salt=None):
+        """
+        Set or update the password (hash) stored for this user.
 
-        data[ENC_PASSWORD] = new_pw_hash
-        return True, True
-
-    def set_password(self, password, is_encrypted=False):
-        if not is_encrypted:
-            password = crypt_password(password)
+        :param password: the new password (or pw hash)
+                         giving an empty string or None as password will invalidate the stored
+                         password hash (meaning that it will not match against any given password)
+        :param is_encrypted: if False (default), the password is given as plaintext and will be
+                             "encrypted" (hashed) before getting stored.
+                             if True, the already "encrypted" password hash is given in param
+                             password and will be stored "as is" - this is mainly useful for tests.
+        :param salt: if None (default), passlib will generate and use a random salt.
+                     Otherwise, the given salt will be used - this is mainly useful for tests.
+        """
+        if not password:
+            # invalidate the pw hash
+            password = ''
+        elif not is_encrypted:
+            password = self._cfg.cache.pwd_context.encrypt(password, salt=salt)
         self.profile[ENC_PASSWORD] = password
         # Invalidate all other browser sessions except this one.
         session['user.session_token'] = self.generate_session_token(False)

MoinMoin/util/StringIOClosing.py

+from StringIO import StringIO as StringIOBase
+
+
+class StringIO(StringIOBase):
+    """
+    same as StringIO from stdlib, but enhanced with a context manager, so it
+    can be used within a "with" statement and gets automatically closed when
+    the with-block is left. The standard "file" object behaves that way, so
+    a StringIO "file emulation" should behave the same.
+    """
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_tb):
+        self.close()

MoinMoin/util/_tests/test_crypto.py

 # -*- coding: utf-8 -*-
-# Copyright: 2011 by MoinMoin:ThomasWaldmann
+# Copyright: 2011-2013 by MoinMoin:ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
         assert result == expected, ('Expected length "%(expected)s" but got "%(result)s"') % locals()
 
 
-class TestEncodePassword(object):
-    """crypto: encode passwords tests"""
-
-    def testAscii(self):
-        """user: encode ascii password"""
-        # u'MoinMoin' and 'MoinMoin' should be encoded to same result
-        expected = "{SSHA256}n0JB8FCTQCpQeg0bmdgvTGwPKvxm8fVNjSRD+JGNs50xMjM0NQ=="
-
-        result = crypto.crypt_password("MoinMoin", salt='12345')
-        assert result == expected
-        result = crypto.crypt_password(u"MoinMoin", salt='12345')
-        assert result == expected
-
-    def testUnicode(self):
-        """ user: encode unicode password """
-        result = crypto.crypt_password(u'סיסמה סודית בהחלט', salt='12345') # Hebrew
-        expected = "{SSHA256}pdYvYv+4Vph259sv/HAm7zpZTv4sBKX9ITOX/m00HMsxMjM0NQ=="
-        assert result == expected
-
-    def testupgradepassword(self):
-        """ return new password hash with better hash """
-        result = crypto.upgrade_password(u'MoinMoin', "junk_hash")
-        assert result.startswith('{SSHA256}')
-
-    def testvalidpassword(self):
-        """ validate user password """
-        hash_val = crypto.crypt_password(u"MoinMoin", salt='12345')
-        result = crypto.valid_password(u'MoinMoin', hash_val)
-        assert result
-        with pytest.raises(ValueError):
-            invalid_result = crypto.valid_password("MoinMoin", '{junk_value}')
-
-    def testvalidpassword2(self):
-        """ validate user password """
-        hash_val = crypto.crypt_password(u"MoinMoin")
-        result = crypto.valid_password('MoinMoin', hash_val)
-        assert result
-        result = crypto.valid_password('WrongPassword', hash_val)
-        assert not result
-        with pytest.raises(ValueError):
-            invalid_result = crypto.valid_password("MoinMoin", '{junk_value}')
-
-
 class TestToken(object):
     """ tests for the generated tokens """
 
         result = crypto.valid_token(test_key, test_token)
         assert not result
 
+
+class TestCacheKey(object):
+    """ tests for cache key generation """
+
     def test_cache_key(self):
         """ The key must be different for different <kw> """
         test_kw1 = {'MoinMoin': 'value1'}
         result2 = crypto.cache_key(**test_kw2)
         assert result1 != result2, ("Expected different keys for different <kw> but got the same")
 
+
 coverage_modules = ['MoinMoin.util.crypto']

MoinMoin/util/_tests/test_md5crypt.py

-# -*- coding: utf-8 -*-
-# Copyright: 2011 Prashant Kumar <contactprashantat AT gmail DOT com>
-# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
-
-"""
-MoinMoin - MoinMoin.util.md5crypt Tests
-"""
-
-
-import pytest
-from MoinMoin.util import md5crypt
-
-def test_unix_md5_crypt():
-    # when magic != None
-    result = md5crypt.unix_md5_crypt('test_pass', 'Moin_test', '$test_magic$')
-    expected = '$test_magic$Moin_tes$JRfmeHgnmCVhVYW.bTtiY1'
-    assert result == expected
-
-    # when magic == None
-    result = md5crypt.unix_md5_crypt('test_pass', 'Moin_test', None)
-    expected = '$1$Moin_tes$hArc67BzmDWtyWWKO5uxQ1'
-    assert result == expected
-
-def test_apache_md5_crypt():
-    # Here magic == '$apr1$'
-    result = md5crypt.apache_md5_crypt('test_pass', 'Moin_test')
-    expected = '$apr1$Moin_tes$4/5zV8nADrNv3BJcY1rZX1'
-    assert result == expected

MoinMoin/util/crypto.py

-# Copyright: 2000-2004 Juergen Hermann <jh@web.de>
-# Copyright: 2003-2011 MoinMoin:ThomasWaldmann
-# Copyright: 2007 MoinMoin:JohannesBerg
-# Copyright: 2007 MoinMoin:HeinrichWendel
-# Copyright: 2008 MoinMoin:ChristopherDenter
-# Copyright: 2010 MoinMoin:DiogenesAugusto
+# Copyright: 2012-2013 MoinMoin:ThomasWaldmann
 # License: GNU GPL v2 (or any later version), see LICENSE.txt for details.
 
 """
 
 Features:
 
-- generate strong, salted cryptographic password hashes for safe pw storage
-- verify cleartext password against any supported crypto (see METHODS)
-- support old (weak) password crypto so one can import existing password
-  databases
-- supports password hash upgrades to stronger methods if the cleartext
-  password is available (usually at login time)
 - generate password recovery tokens
 - verify password recovery tokens
 - generate random strings of given length (for salting)
 
 from __future__ import absolute_import, division
 
-import base64
 import hashlib
 import hmac
-import random
 import time
 
-# Note: have the (strong) method that crypt_password() uses at index 0:
-METHODS = ['{SSHA256}', '{SSHA}', '{SHA}', ]
-
-try:
-    from . import md5crypt
-    METHODS.extend(['{APR1}', '{MD5}', ])
-except ImportError:
-    pass
-
-try:
-    import crypt
-    METHODS.extend(['{DES}', ])
-except ImportError:
-    pass
-
 from uuid import uuid4
 
 make_uuid = lambda: unicode(uuid4().hex)
 UUID_LEN = len(make_uuid())
 
-# random stuff
+from passlib.utils import rng, getrandstr, getrandbytes, consteq, generate_password
+
 
 def random_string(length, allowed_chars=None):
     """
     Generate a random string with given length consisting of the given characters.
 
+    Note: this is now just a little wrapper around passlib's randomness code.
+
     :param length: length of the string
     :param allowed_chars: string with allowed characters or None
                           to indicate all 256 byte values should be used
     :returns: random string
     """
     if allowed_chars is None:
-        s = ''.join([chr(random.randint(0, 255)) for dummy in xrange(length)])
+        s = getrandbytes(rng, length)
     else:
-        s = ''.join([random.choice(allowed_chars) for dummy in xrange(length)])
+        s = getrandstr(rng, allowed_chars, length)
     return s
 
 
-# password stuff
-
-def crypt_password(password, salt=None):
-    """
-    Crypt/Hash a cleartext password
-
-    :param password: cleartext password [unicode]
-    :param salt: salt for the password [str] or None to generate a random salt
-    :rtype: str
-    :returns: the SSHA256 password hash
-    """
-    password = password.encode('utf-8')
-    if salt is None:
-        salt = random_string(32)
-    assert isinstance(salt, str)
-    h = hashlib.new('sha256', password)
-    h.update(salt)
-    return '{SSHA256}' + base64.encodestring(h.digest() + salt).rstrip()
-
-
-def upgrade_password(password, pw_hash):
-    """
-    Upgrade a password to a better hash, if needed
-
-    :param password: cleartext password [unicode]
-    :param pw_hash: password hash (with hash type prefix)
-    :rtype: str
-    :returns: new password hash (or None, if unchanged)
-    """
-    if not pw_hash.startswith('{SSHA256}'):
-        # pw_hash using some old hash method, upgrade to better method
-        return crypt_password(password)
-
-
-def valid_password(password, pw_hash):
-    """
-    Validate a user password.
-
-    :param password: cleartext password to verify [unicode]
-    :param pw_hash: password hash (with hash type prefix)
-    :rtype: bool
-    :returns: password is valid
-    """
-    # encode password
-    pw_utf8 = password.encode('utf-8')
-
-    for method in METHODS:
-        if pw_hash.startswith(method):
-            d = pw_hash[len(method):]
-            if method == '{SSHA256}':
-                ph = base64.decodestring(d)
-                # ph is of the form "<hash><salt>"
-                salt = ph[32:]
-                h = hashlib.new('sha256', pw_utf8)
-                h.update(salt)
-                enc = base64.encodestring(h.digest() + salt).rstrip()
-            elif method == '{SSHA}':
-                ph = base64.decodestring(d)
-                # ph is of the form "<hash><salt>"
-                salt = ph[20:]
-                h = hashlib.new('sha1', pw_utf8)
-                h.update(salt)
-                enc = base64.encodestring(h.digest() + salt).rstrip()
-            elif method == '{SHA}':
-                h = hashlib.new('sha1', pw_utf8)
-                enc = base64.encodestring(h.digest()).rstrip()
-            elif method == '{APR1}':
-                # d is of the form "$apr1$<salt>$<hash>"
-                salt = d.split('$')[2]
-                enc = md5crypt.apache_md5_crypt(pw_utf8, salt.encode('ascii'))
-            elif method == '{MD5}':
-                # d is of the form "$1$<salt>$<hash>"
-                salt = d.split('$')[2]
-                enc = md5crypt.unix_md5_crypt(pw_utf8, salt.encode('ascii'))
-            elif method == '{DES}':
-                # d is 2 characters salt + 11 characters hash
-                salt = d[:2]
-                enc = crypt.crypt(pw_utf8, salt.encode('ascii'))
-            else:
-                raise ValueError("missing password hash method {0} handler".format(method))
-            return pw_hash == method + enc
-    else:
-        idx = pw_hash.index('}') + 1
-        raise ValueError("unsupported password hash method {0!r}".format(pw_hash[:idx]))
-
-
 # password recovery token
 
 def generate_token(key=None, stamp=None):
     :param key: give it to recompute some specific token for verification
     :param stamp: give it to recompute some specific token for verification
     :rtype: 2-tuple
-    :returns: key, token
+    :returns: key, token (both unicode)
     """
     if key is None:
-        key = random_string(64, "abcdefghijklmnopqrstuvwxyz0123456789")
+        key = generate_password(size=32)
     if stamp is None:
         stamp = int(time.time())
-    h = hmac.new(str(key), str(stamp), digestmod=hashlib.sha1).hexdigest()
-    token = str(stamp) + '-' + h
-    return key, token
+    h = hmac.new(str(key), str(stamp), digestmod=hashlib.sha256).hexdigest()
+    token = u"{0}-{1}".format(stamp, h)
+    return unicode(key), token
 
 
-def valid_token(key, token, timeout=12*60*60):
+def valid_token(key, token, timeout=2*60*60):
     """
     check if token is valid with respect to the secret key,
     the token must not be older than timeout seconds.
     if timeout and stamp + timeout < time.time():
         return False
     expected_token = generate_token(key, stamp)[1]
-    return token == expected_token
+    return consteq(token, expected_token)
 
 
 # miscellaneous

MoinMoin/util/md5crypt.py

-#########################################################
-# md5crypt.py
-#
-# 0423.2000 by michal wallace http://www.sabren.com/
-# based on perl's Crypt::PasswdMD5 by Luis Munoz (lem@cantv.net)
-# based on /usr/src/libcrypt/crypt.c from FreeBSD 2.2.5-RELEASE
-#
-# MANY THANKS TO
-#
-#  Carey Evans - http://home.clear.net.nz/pages/c.evans/
-#  Dennis Marti - http://users.starpower.net/marti1/
-#
-#  For the patches that got this thing working!
-#
-#########################################################
-"""md5crypt.py - Provides interoperable MD5-based crypt() function
-
-SYNOPSIS
-
-        import md5crypt.py
-
-        cryptedpassword = md5crypt.md5crypt(password, salt);
-
-DESCRIPTION
-
-unix_md5_crypt() provides a crypt()-compatible interface to the
-rather new MD5-based crypt() function found in modern operating systems.
-It's based on the implementation found on FreeBSD 2.2.[56]-RELEASE and
-contains the following license in it:
-
- "THE BEER-WARE LICENSE" (Revision 42):
- <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
- can do whatever you want with this stuff. If we meet some day, and you think
- this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
-
-apache_md5_crypt() provides a function compatible with Apache's
-.htpasswd files. This was contributed by Bryan Hart <bryan@eai.com>.
-
-"""
-
-MAGIC = '$1$'                   # Magic string
-ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
-
-try:
-    import hashlib
-    hash_md5 = hashlib.md5
-except ImportError:
-    # maybe we have python < 2.5 (no hashlib)
-    import md5
-    hash_md5 = md5.new
-
-
-def to64(v, n):
-    ret = ''
-    while (n - 1 >= 0):
-        n = n - 1
-        ret = ret + ITOA64[v & 0x3f]
-        v = v >> 6
-    return ret
-
-
-def apache_md5_crypt(pw, salt):
-    # change the Magic string to match the one used by Apache
-    return unix_md5_crypt(pw, salt, '$apr1$')
-
-
-def unix_md5_crypt(pw, salt, magic=None):
-
-    if magic is None:
-        magic = MAGIC
-
-    # Take care of the magic string if present
-    if salt[:len(magic)] == magic:
-        salt = salt[len(magic):]
-
-
-    # salt can have up to 8 characters:
-    import string
-    salt = string.split(salt, '$', 1)[0]
-    salt = salt[:8]
-
-    ctx = pw + magic + salt
-
-    md5 = hash_md5()
-    md5.update(pw + salt + pw)
-    final = md5.digest()
-
-    for pl in range(len(pw), 0, -16):
-        if pl > 16:
-            ctx = ctx + final[:16]
-        else:
-            ctx = ctx + final[:pl]
-
-
-    # Now the 'weird' xform (??)
-
-    i = len(pw)
-    while i:
-        if i & 1:
-            ctx = ctx + chr(0)  #if ($i & 1) { $ctx->add(pack("C", 0)); }
-        else:
-            ctx = ctx + pw[0]
-        i = i >> 1
-
-    md5 = hash_md5()
-    md5.update(ctx)
-    final = md5.digest()
-
-    # The following is supposed to make
-    # things run slower.
-
-    # my question: WTF???
-
-    for i in range(1000):
-        ctx1 = ''
-        if i & 1:
-            ctx1 = ctx1 + pw
-        else:
-            ctx1 = ctx1 + final[:16]
-
-        if i % 3:
-            ctx1 = ctx1 + salt
-
-        if i % 7:
-            ctx1 = ctx1 + pw
-
-        if i & 1:
-            ctx1 = ctx1 + final[:16]
-        else:
-            ctx1 = ctx1 + pw
-
-
-        md5 = hash_md5()
-        md5.update(ctx1)
-        final = md5.digest()
-
-
-    # Final xform
-
-    passwd = ''
-
-    passwd = passwd + to64((int(ord(final[0])) << 16)
-                           |(int(ord(final[6])) << 8)
-                           |(int(ord(final[12]))), 4)
-
-    passwd = passwd + to64((int(ord(final[1])) << 16)
-                           |(int(ord(final[7])) << 8)
-                           |(int(ord(final[13]))), 4)
-
-    passwd = passwd + to64((int(ord(final[2])) << 16)
-                           |(int(ord(final[8])) << 8)
-                           |(int(ord(final[14]))), 4)
-
-    passwd = passwd + to64((int(ord(final[3])) << 16)
-                           |(int(ord(final[9])) << 8)
-                           |(int(ord(final[15]))), 4)
-
-    passwd = passwd + to64((int(ord(final[4])) << 16)
-                           |(int(ord(final[10])) << 8)
-                           |(int(ord(final[5]))), 4)
-
-    passwd = passwd + to64((int(ord(final[11]))), 2)
-
-
-    return magic + salt + '$' + passwd
-
-
-## assign a wrapper function:
-md5crypt = unix_md5_crypt
-
-if __name__ == "__main__":
-    print unix_md5_crypt("cat", "hat")

docs/admin/configure.rst

 
 Password storage
 ----------------
-Moin never stores passwords in clear text, but always as cryptographic hash
-with a random salt. Currently ssha256 is the default.
+Moin never stores wiki user passwords in clear text, but uses strong
+cryptographic hashes provided by the "passlib" library, see there for details:
+
+    http://packages.python.org/passlib/.
+
+The passlib docs recommend 3 hashing schemes that have good security:
+sha512_crypt, pbkdf2_sha512 and bcrypt (bcrypt has additional binary/compiled
+package requirements, please refer to the passlib docs in case you want to use
+it).
+
+By default, we use sha512_crypt hashes with default parameters as provided
+by passlib (this is same algorithm as moin >= 1.9.7 used by default).
+
+In case you experience slow logins or feel that you might need to tweak the
+hash generation for other reasons, please read the passlib docs. moin allows
+you to configure passlib's CryptContext params within the wiki config, the
+default is this:
+
+::
+    passlib_crypt_context = dict(
+        schemes=["sha512_crypt", ],
+    )
 
 
 Authorization

docs/admin/upgrade.rst

 
 From moin < 1.9
 ===============
-If you run an older moin version than 1.9, please first upgrade to moin 1.9.x
-before upgrading to moin2. 
-You may want to run 1.9.x for a while to be sure everything is working as expected.
+If you run an older moin version than 1.9, please first upgrade to a recent
+moin 1.9.x version (preferably >= 1.9.7) before upgrading to moin2.
+You may want to run that for a while to be sure everything is working as expected.
 
 Note: Both moin 1.9.x and moin2 are WSGI applications.
 Upgrading to 1.9 first also makes sense concerning the WSGI / server side.
 
 From moin 1.9.x
 ===============
+
+If you want to keep your user's password hashes and migrate them to moin2,
+make sure you use moin >= 1.9.7 WITH enabled passlib support and that all
+password hashes stored in user profiles are {PASSLIB} hashes. Other hashes
+will get removed in the migration process and users will need to do password
+recovery via email (or with admin help, if that does not work).
+
+
 Backup
 ------
 Have a backup of everything, so you can go back in case it doesn't do what
     sitename = u'...' # same as in 1.9
     item_root = u'...' # see page_front_page in 1.9
 
+    # if you had a custom passlib_crypt_context in 1.9, put it here
+
     # configure backend and ACLs to use in future
     # TODO
 
 pep8ignore =
  *.py E121 E122 E123 E124 E125 E126 E127 E128  # continuation line indentation
  *.py E225  # missing whitespace around operator
+ *.py E226  # missing optional whitespace around operator
  *.py E261  # less than 2 blanks before inline comment
  *.py E301 E302  # separate toplevel definitions with 2 empty lines, method defs inside class by 1 empty line
  *.py E401  # imports on separate lines
         'whoosh>=2.4.0', # needed for indexed search
         'sphinx>=1.1', # needed to build the docs
         'pdfminer', # pdf -> text/plain conversion
+        'passlib>=1.6.0', # strong password hashing (1.6 needed for consteq)
         'XStatic>=0.0.2', # support for static file pypi packages
         'XStatic-CKEditor>=3.6.1.2',
         'XStatic-jQuery>=1.8.2',