changeset 3808:8c5fbc62dd1d

user storage: hash stored passwords, upgrade on use, remove charset magic
author Johannes Berg <johannes AT sipsolutions DOT net>
date Mon, 30 Jun 2008 21:41:04 +0200
parents 2ae602072baf
children 888574a117e6
files MoinMoin/user.py
diffstat 1 files changed, 40 insertions(+), 87 deletions(-) [+]
line wrap: on
line diff
--- a/MoinMoin/user.py	Mon Jun 30 21:02:41 2008 +0200
+++ b/MoinMoin/user.py	Mon Jun 30 21:41:04 2008 +0200
@@ -22,7 +22,7 @@
 # add names here to hide them in the cgitb traceback
 unsafe_names = ("id", "key", "val", "user_data", "enc_password", "recoverpass_key")
 
-import os, time, sha, codecs, hmac
+import os, time, sha, codecs, hmac, base64
 
 from MoinMoin import config, caching, wikiutil, i18n, events
 from MoinMoin.util import timefuncs, filesys, random_string
@@ -140,14 +140,9 @@
     return username or (request.cfg.show_hosts and request.remote_addr) or _("<unknown>")
 
 
-def encodePassword(pwd, charset='utf-8'):
+def encodePassword(pwd):
     """ Encode a cleartext password
 
-    Compatible to Apache htpasswd SHA encoding.
-
-    When using different encoding than 'utf-8', the encoding might fail
-    and raise UnicodeError.
-
     @param pwd: the cleartext password, (unicode)
     @param charset: charset used to encode password, used only for
         compatibility with old passwords generated on moin-1.2.
@@ -155,15 +150,13 @@
     @return: the password in apache htpasswd compatible SHA-encoding,
         or None
     """
-    import base64
+    pwd = pwd.encode('utf-8')
 
-    # Might raise UnicodeError, but we can't do anything about it here,
-    # so let the caller handle it.
-    pwd = pwd.encode(charset)
+    salt = random_string(20)
+    hash = sha.new(pwd)
+    hash.update(salt)
 
-    pwd = sha.new(pwd).digest()
-    pwd = '{SHA}' + base64.encodestring(pwd).rstrip()
-    return pwd
+    return '{SSHA}' + base64.b64encode(hash.digest() + salt).rstrip()
 
 
 def normalizeName(name):
@@ -316,11 +309,6 @@
         self.recoverpass_key = ""
 
         self.enc_password = ""
-        if password:
-            try:
-                self.enc_password = encodePassword(password)
-            except UnicodeError:
-                pass # Should never happen
 
         #self.edit_cols = 80
         self.tz_offset = int(float(self._cfg.tz_offset) * 3600)
@@ -343,17 +331,18 @@
         self._trail = []
 
         # we got an already authenticated username:
-        check_pass = 0
+        check_password = None
         if not self.id and self.auth_username:
             self.id = getUserId(request, self.auth_username)
             if not password is None:
-                check_pass = 1
+                check_password = password
         if self.id:
-            self.load_from_id(check_pass)
+            self.load_from_id(check_password)
         elif self.name:
             self.id = getUserId(self._request, self.name)
             if self.id:
-                self.load_from_id(1)
+                # no password given should fail
+                self.load_from_id(password or u'')
             else:
                 self.id = self.make_id()
         else:
@@ -407,7 +396,7 @@
         """
         return os.path.exists(self.__filename())
 
-    def load_from_id(self, check_pass=0):
+    def load_from_id(self, password=None):
         """ Load user account data from disk.
 
         Can only load user data if the id number is already known.
@@ -415,8 +404,8 @@
         This loads all member variables, except "id" and "valid" and
         those starting with an underscore.
 
-        @param check_pass: If 1, then self.enc_password must match the
-                           password in the user account file.
+        @param password: If not None, then the given password must match the
+                         password in the user account file.
         """
         if not self.exists():
             return
@@ -450,11 +439,8 @@
         # values, we set 'changed' flag, and later save the user data.
         changed = 0
 
-        if check_pass:
-            # If we have no password set, we don't accept login with username
-            if not user_data['enc_password']:
-                return
-            # Check for a valid password, possibly changing encoding
+        if password is not None:
+            # Check for a valid password, possibly changing storage
             valid, changed = self._validatePassword(user_data)
             if not valid:
                 return
@@ -502,43 +488,20 @@
             self.save()
 
     def _validatePassword(self, data):
-        """ Try to validate user password
+        """
+        Check user password.
 
         This is a private method and should not be used by clients.
 
-        In pre 1.3, the wiki used some 8 bit charset. The user password
-        was entered in this 8 bit password and passed to
-        encodePassword. So old passwords can use any of the charset
-        used.
-
-        In 1.3, we use unicode internally, so we encode the password in
-        encodePassword using utf-8.
-
-        When we compare passwords we must compare with same encoding, or
-        the passwords will not match. We don't know what encoding the
-        password on the user file uses. We may ask the wiki admin to put
-        this into the config, but he may be wrong.
+        @param data: dict with user data (from storage)
+        @rtype: 2 tuple (bool, bool)
+        @return: password is valid, enc_password changed
+        """
+        epwd = data['enc_password']
 
-        The way chosen is to try to encode and compare passwords using
-        all the encoding that were available on 1.2, until we get a
-        match, which means that the user is valid.
-
-        If we get a match, we replace the user password hash with the
-        utf-8 encoded version, and next time it will match on first try
-        as before. The user password did not change, this change is
-        completely transparent for the user. Only the sha digest will
-        change.
-
-        @param data: dict with user data
-        @rtype: 2 tuple (bool, bool)
-        @return: password is valid, password did change
-        """
-        # First try with default encoded password. Match only non empty
-        # passwords. (require non empty enc_password)
-        if self.enc_password and self.enc_password == data['enc_password']:
-            return True, False
-
-        # Try to match using one of pre 1.3 8 bit charsets
+        # If we have no password set, we don't accept login with username
+        if not epwd:
+            return False, False
 
         # Get the clear text password from the form (require non empty
         # password)
@@ -546,30 +509,20 @@
         if not password:
             return False, False
 
-        # First get all available pre13 charsets on this system
-        pre13 = ['iso-8859-1', 'iso-8859-2', 'euc-jp', 'gb2312', 'big5', ]
-        available = []
-        for charset in pre13:
-            try:
-                encoder = codecs.getencoder(charset)
-                available.append(charset)
-            except LookupError:
-                pass # missing on this system
+        if epwd[:5] == '{SHA}':
+            enc = '{SHA}' + base64.encodestring(sha.new(password).digest()).rstrip()
+            if epwd == enc:
+                data['enc_password'] = encodePassword(password)
+                return True, True
+            return False, False
 
-        # Now try to match the password
-        for charset in available:
-            # Try to encode, failure is expected
-            try:
-                enc_password = encodePassword(password, charset=charset)
-            except UnicodeError:
-                continue
-
-            # And match (require non empty enc_password)
-            if enc_password and enc_password == data['enc_password']:
-                # User password match - replace the user password in the
-                # file with self.password
-                data['enc_password'] = self.enc_password
-                return True, True
+        if epwd[:6] == '{SSHA}':
+            print epwd[6:]
+            data = base64.b64decode(epwd[6:])
+            salt = data[20:]
+            hash = sha.new(password)
+            hash.update(salt)
+            return hash.digest() == data[:20], False
 
         # No encoded password match, this must be wrong password
         return False, False