Support password hashes imported from other wiki systems

When converting user accounts from a different wiki system, the password hash might be different from {SSHA}, which MoinMoin uses.

When we converted http://www.gnewsense.org/ from PmWiki to MoinMoin, some of the user accounts used {DES} hashes, some used {APR1}. We wanted the >3000 users to be able to log in to the new site without any action on their part.

MoinMoin already accepts {SHA} hashes and upgrades them to {SSHA}. This patch adds support for:

   1 diff -urN moin-1.9.3/MoinMoin/user.py moin-1.9.3-patch/MoinMoin/user.py
   2 --- moin-1.9.3/MoinMoin/user.py	2010-06-26 23:46:44.000000000 +0200
   3 +++ moin-1.9.3-patch/MoinMoin/user.py	2010-10-24 00:04:12.000000000 +0200
   4 @@ -15,7 +15,8 @@
   5        * storage code
   6  
   7      @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
   8 -                2003-2007 MoinMoin:ThomasWaldmann
   9 +                2003-2007 MoinMoin:ThomasWaldmann,
  10 +                2010 Michael Foetsch <foetsch@yahoo.com>
  11      @license: GNU GPL, see COPYING for details.
  12  """
  13  
  14 @@ -24,9 +25,14 @@
  15  from MoinMoin.support.python_compatibility import hash_new, hmac_new
  16  
  17  from MoinMoin import config, caching, wikiutil, i18n, events
  18 -from MoinMoin.util import timefuncs, random_string
  19 +from MoinMoin.util import timefuncs, random_string, md5crypt
  20  from MoinMoin.wikiutil import url_quote_plus
  21  
  22 +try:
  23 +    import crypt
  24 +except ImportError:
  25 +    crypt = None
  26 +
  27  
  28  def getUserList(request):
  29      """ Get a list of all (numerical) user IDs.
  30 @@ -506,13 +512,36 @@
  31          if not password:
  32              return False, False
  33  
  34 -        if epwd[:5] == '{SHA}':
  35 -            enc = '{SHA}' + base64.encodestring(hash_new('sha1', password.encode('utf-8')).digest()).rstrip()
  36 -            if epwd == enc:
  37 -                data['enc_password'] = encodePassword(password) # upgrade to SSHA
  38 -                return True, True
  39 -            return False, False
  40 -
  41 +        # Check and upgrade passwords from earlier MoinMoin versions and
  42 +        # passwords imported from other wiki systems.
  43 +        for method in ['{SHA}', '{APR1}', '{MD5}', '{DES}']:
  44 +            if epwd.startswith(method):
  45 +                d = epwd[len(method):]
  46 +                if method == '{SHA}':
  47 +                    enc = base64.encodestring(
  48 +                        hash_new('sha1', password.encode('utf-8')).digest()).rstrip()
  49 +                elif method == '{APR1}':
  50 +                    # d is of the form "$apr1$<salt>$<hash>"
  51 +                    salt = d.split('$')[2]
  52 +                    enc = md5crypt.apache_md5_crypt(password.encode('utf-8'),
  53 +                                                    salt.encode('ascii'))
  54 +                elif method == '{MD5}':
  55 +                    # d is of the form "$1$<salt>$<hash>"
  56 +                    salt = d.split('$')[2]
  57 +                    enc = md5crypt.unix_md5_crypt(password.encode('utf-8'),
  58 +                                                  salt.encode('ascii'))
  59 +                elif method == '{DES}':
  60 +                    if crypt is None:
  61 +                        return False, False
  62 +                    # d is 2 characters salt + 11 characters hash
  63 +                    salt = d[:2]
  64 +                    enc = crypt.crypt(password.encode('utf-8'), salt.encode('ascii'))
  65 +
  66 +                if epwd == method + enc:
  67 +                    data['enc_password'] = encodePassword(password) # upgrade to SSHA
  68 +                    return True, True
  69 +                return False, False
  70 +                
  71          if epwd[:6] == '{SSHA}':
  72              data = base64.decodestring(epwd[6:])
  73              salt = data[20:]
  74 diff -urN moin-1.9.3/MoinMoin/util/md5crypt.py moin-1.9.3-patch/MoinMoin/util/md5crypt.py
  75 --- moin-1.9.3/MoinMoin/util/md5crypt.py	1970-01-01 01:00:00.000000000 +0100
  76 +++ moin-1.9.3-patch/MoinMoin/util/md5crypt.py	2010-10-23 20:42:31.000000000 +0200
  77 @@ -0,0 +1,166 @@
  78 +#########################################################
  79 +# md5crypt.py
  80 +#
  81 +# 0423.2000 by michal wallace http://www.sabren.com/
  82 +# based on perl's Crypt::PasswdMD5 by Luis Munoz (lem@cantv.net)
  83 +# based on /usr/src/libcrypt/crypt.c from FreeBSD 2.2.5-RELEASE
  84 +#
  85 +# MANY THANKS TO
  86 +#
  87 +#  Carey Evans - http://home.clear.net.nz/pages/c.evans/
  88 +#  Dennis Marti - http://users.starpower.net/marti1/
  89 +#
  90 +#  For the patches that got this thing working!
  91 +#
  92 +#########################################################
  93 +"""md5crypt.py - Provides interoperable MD5-based crypt() function
  94 +
  95 +SYNOPSIS
  96 +
  97 +        import md5crypt.py
  98 +
  99 +        cryptedpassword = md5crypt.md5crypt(password, salt);
 100 +
 101 +DESCRIPTION
 102 +
 103 +unix_md5_crypt() provides a crypt()-compatible interface to the
 104 +rather new MD5-based crypt() function found in modern operating systems.
 105 +It's based on the implementation found on FreeBSD 2.2.[56]-RELEASE and
 106 +contains the following license in it:
 107 +
 108 + "THE BEER-WARE LICENSE" (Revision 42):
 109 + <phk@login.dknet.dk> wrote this file.  As long as you retain this notice you
 110 + can do whatever you want with this stuff. If we meet some day, and you think
 111 + this stuff is worth it, you can buy me a beer in return.   Poul-Henning Kamp
 112 +
 113 +apache_md5_crypt() provides a function compatible with Apache's
 114 +.htpasswd files. This was contributed by Bryan Hart <bryan@eai.com>.
 115 +
 116 +"""
 117 +
 118 +MAGIC = '$1$'                   # Magic string
 119 +ITOA64 = "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
 120 +
 121 +import hashlib
 122 +
 123 +def to64 (v, n):
 124 +    ret = ''
 125 +    while (n - 1 >= 0):
 126 +        n = n - 1
 127 +        ret = ret + ITOA64[v & 0x3f]
 128 +        v = v >> 6
 129 +    return ret
 130 +
 131 +
 132 +def apache_md5_crypt (pw, salt):
 133 +    # change the Magic string to match the one used by Apache
 134 +    return unix_md5_crypt(pw, salt, '$apr1$')
 135 +
 136 +
 137 +def unix_md5_crypt(pw, salt, magic=None):
 138 +
 139 +    if magic==None:
 140 +        magic = MAGIC
 141 +
 142 +    # Take care of the magic string if present
 143 +    if salt[:len(magic)] == magic:
 144 +        salt = salt[len(magic):]
 145 +
 146 +
 147 +    # salt can have up to 8 characters:
 148 +    import string
 149 +    salt = string.split(salt, '$', 1)[0]
 150 +    salt = salt[:8]
 151 +
 152 +    ctx = pw + magic + salt
 153 +
 154 +    md5 = hashlib.md5()
 155 +    md5.update(pw + salt + pw)
 156 +    final = md5.digest()
 157 +
 158 +    for pl in range(len(pw),0,-16):
 159 +        if pl > 16:
 160 +            ctx = ctx + final[:16]
 161 +        else:
 162 +            ctx = ctx + final[:pl]
 163 +
 164 +
 165 +    # Now the 'weird' xform (??)
 166 +
 167 +    i = len(pw)
 168 +    while i:
 169 +        if i & 1:
 170 +            ctx = ctx + chr(0)  #if ($i & 1) { $ctx->add(pack("C", 0)); }
 171 +        else:
 172 +            ctx = ctx + pw[0]
 173 +        i = i >> 1
 174 +
 175 +    md5 = hashlib.md5()
 176 +    md5.update(ctx)
 177 +    final = md5.digest()
 178 +
 179 +    # The following is supposed to make
 180 +    # things run slower.
 181 +
 182 +    # my question: WTF???
 183 +
 184 +    for i in range(1000):
 185 +        ctx1 = ''
 186 +        if i & 1:
 187 +            ctx1 = ctx1 + pw
 188 +        else:
 189 +            ctx1 = ctx1 + final[:16]
 190 +
 191 +        if i % 3:
 192 +            ctx1 = ctx1 + salt
 193 +
 194 +        if i % 7:
 195 +            ctx1 = ctx1 + pw
 196 +
 197 +        if i & 1:
 198 +            ctx1 = ctx1 + final[:16]
 199 +        else:
 200 +            ctx1 = ctx1 + pw
 201 +
 202 +
 203 +        md5 = hashlib.md5()
 204 +        md5.update(ctx1)
 205 +        final = md5.digest()
 206 +
 207 +
 208 +    # Final xform
 209 +
 210 +    passwd = ''
 211 +
 212 +    passwd = passwd + to64((int(ord(final[0])) << 16)
 213 +                           |(int(ord(final[6])) << 8)
 214 +                           |(int(ord(final[12]))),4)
 215 +
 216 +    passwd = passwd + to64((int(ord(final[1])) << 16)
 217 +                           |(int(ord(final[7])) << 8)
 218 +                           |(int(ord(final[13]))), 4)
 219 +
 220 +    passwd = passwd + to64((int(ord(final[2])) << 16)
 221 +                           |(int(ord(final[8])) << 8)
 222 +                           |(int(ord(final[14]))), 4)
 223 +
 224 +    passwd = passwd + to64((int(ord(final[3])) << 16)
 225 +                           |(int(ord(final[9])) << 8)
 226 +                           |(int(ord(final[15]))), 4)
 227 +
 228 +    passwd = passwd + to64((int(ord(final[4])) << 16)
 229 +                           |(int(ord(final[10])) << 8)
 230 +                           |(int(ord(final[5]))), 4)
 231 +
 232 +    passwd = passwd + to64((int(ord(final[11]))), 2)
 233 +
 234 +
 235 +    return magic + salt + '$' + passwd
 236 +
 237 +
 238 +## assign a wrapper function:
 239 +md5crypt = unix_md5_crypt
 240 +
 241 +if __name__ == "__main__":
 242 +    print unix_md5_crypt("cat", "hat")
 243 +
 244 

imported_pwd_hashes.patch

Implemented by: http://hg.moinmo.in/moin/1.9/rev/69668ad0cae7


CategoryFeatureImplemented

MoinMoin: FeatureRequests/ImportedPasswordHashes (last edited 2010-10-23 22:30:34 by ThomasWaldmann)