Repository: privacyidea/privacyidea

Issue:
Wrong password hash for sqlresolver
When creating a SQLresolver with ssha512 the passwords are still hashed with ssha256.

see:
Is it the correct hash?
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/resolvers/SQLIdResolver.py#L509

Bummer, we still hash ssha256?
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/resolvers/SQLIdResolver.py#L532

Wrong password hash for sqlresolver
When creating a SQLresolver with ssha512 the passwords are still hashed with ssha256.

see:
Is it the correct hash?
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/resolvers/SQLIdResolver.py#L509

Bummer, we still hash ssha256?
https://github.com/privacyidea/privacyidea/blob/master/privacyidea/lib/resolvers/SQLIdResolver.py#L532



Discussion:
It seems like adding new users is broken if *any* Password Hash Type is selected in the UI because we actually double-hash the password then:

First, we use the specified Password Hash Type to hash the password here:
https://github.com/privacyidea/privacyidea/blob/f9c2fbb1138d73e05da0c48f75b99d1e734659e0/privacyidea/lib/resolvers/SQLIdResolver.py#L509
and then, we hash the resulting hash *again* with SSHA256:
https://github.com/privacyidea/privacyidea/blob/f9c2fbb1138d73e05da0c48f75b99d1e734659e0/privacyidea/lib/resolvers/SQLIdResolver.py#L536

In case the Password Hash Type field in the UI is left blank, it works because the password is only hashed with SSHA256.
It seems like adding new users is broken if *any* Password Hash Type is selected in the UI because we actually double-hash the password then:

First, we use the specified Password Hash Type to hash the password here:
https://github.com/privacyidea/privacyidea/blob/f9c2fbb1138d73e05da0c48f75b99d1e734659e0/privacyidea/lib/resolvers/SQLIdResolver.py#L509
and then, we hash the resulting hash *again* with SSHA256:
https://github.com/privacyidea/privacyidea/blob/f9c2fbb1138d73e05da0c48f75b99d1e734659e0/privacyidea/lib/resolvers/SQLIdResolver.py#L536

In case the Password Hash Type field in the UI is left blank, it works because the password is only hashed with SSHA256.

Resolving patch:
diff --git a/privacyidea/lib/resolvers/SQLIdResolver.py b/privacyidea/lib/resolvers/SQLIdResolver.py
--- a/privacyidea/lib/resolvers/SQLIdResolver.py
+++ b/privacyidea/lib/resolvers/SQLIdResolver.py
@@ -348,7 +348,7 @@ def loadConfig(self, config):
         self.password = config.get('Password', "")
         self.table = config.get('Table', "")
         self._editable = config.get("Editable", False)
-        self.password_hash_type = config.get("Password_Hash_Type")
+        self.password_hash_type = config.get("Password_Hash_Type", "SSHA256")
         usermap = config.get('Map', {})
         self.map = yaml.safe_load(usermap)
         self.reverse_map = dict([[v, k] for k, v in self.map.items()])
@@ -505,37 +505,31 @@ def add_user(self, attributes=None):
         determine the way how to create the UID.
         """
         attributes = attributes or {}
-        if "password" in attributes and self.password_hash_type:
-            attributes["password"] = hash_password(attributes["password"],
-                                                   self.password_hash_type)
-
-        kwargs = self._attributes_to_db_columns(attributes)
+        kwargs = self.prepare_attributes_for_db(attributes)
         log.debug("Insert new user with attributes {0!s}".format(kwargs))
         r = self.TABLE.insert(**kwargs)
         self.db.commit()
         # Return the UID of the new object
         return getattr(r, self.map.get("userid"))
 
-    def _attributes_to_db_columns(self, attributes):
+    def prepare_attributes_for_db(self, attributes):
         """
-        takes the attributes and maps them to the DB columns
-        :param attributes:
-        :return: dict with column name as keys and values
+        Given a dictionary of attributes, return a dictionary
+        mapping columns to values.
+        If the attributes contain a password, hash the password according to the
+        configured password hash type.
+
+        :param attributes: attributes dictionary
+        :return: dictionary with column name as keys
         """
+        attributes = attributes.copy()
+        if "password" in attributes:
+            attributes["password"] = hash_password(attributes["password"],
+                                                   self.password_hash_type)
         columns = {}
-        for fieldname in attributes.keys():
-            if self.map.get(fieldname):
-                if fieldname == "password":
-                    password = attributes.get(fieldname)
-                    # Create a {SSHA256} password
-                    salt = geturandom(16)
-                    hr = hashlib.sha256(password)
-                    hr.update(salt)
-                    hash_bin = hr.digest()
-                    hash_b64 = b64encode(hash_bin + salt)
-                    columns[self.map.get(fieldname)] = "{SSHA256}" + hash_b64
-                else:
-                    columns[self.map.get(fieldname)] = attributes.get(fieldname)
+        for fieldname in attributes:
+            if fieldname in self.map:
+                columns[self.map[fieldname]] = attributes[fieldname]
         return columns
 
     def delete_user(self, uid):
@@ -582,7 +576,7 @@ def update_user(self, uid, attributes=None):
         :return: True in case of success
         """
         attributes = attributes or {}
-        params = self._attributes_to_db_columns(attributes)
+        params = self.prepare_attributes_for_db(attributes)
         kwargs = {self.map.get("userid"): uid}
         r = self.TABLE.filter_by(**kwargs).update(params)
         self.db.commit()


Test patch:
diff --git a/tests/test_lib_resolver.py b/tests/test_lib_resolver.py
--- a/tests/test_lib_resolver.py
+++ b/tests/test_lib_resolver.py
@@ -138,7 +138,6 @@ class SQLResolverTestCase(MyTestCase):
                   'Database': "testuser.sqlite",
                   'Table': 'users',
                   'Encoding': 'utf8',
-                  'Passwort_Hash_Type': "SSHA",
                   'Map': '{ "username": "username", \
                     "userid" : "id", \
                     "email" : "email", \
@@ -265,6 +264,7 @@ def test_03_testconnection(self):
 
     def test_05_add_user_update_delete(self):
         y = SQLResolver()
+        # This has no password hash type set at all
         y.loadConfig(self.parameters)
         uid = y.add_user({"username":"achmed",
                           "email": "achmed@world.net",
@@ -273,12 +273,18 @@ def test_05_add_user_update_delete(self):
         self.assertTrue(uid > 8)
         self.assertTrue(y.checkPass(uid, "passw0rd"))
         self.assertFalse(y.checkPass(uid, "password"))
+        # check that we actually store SSHA256
+        stored_password = y.TABLE.filter_by(username="achmed").first().password
+        self.assertTrue(stored_password.startswith("{SSHA256}"), stored_password)
 
         uid = y.getUserId("achmed")
         self.assertTrue(uid > 8)
 
         r = y.update_user(uid, {"username": "achmed2",
                                 "password": "test"})
+        # check that we actually store SSHA256
+        stored_password = y.TABLE.filter_by(username="achmed2").first().password
+        self.assertTrue(stored_password.startswith("{SSHA256}"), stored_password)
         uname = y.getUsername(uid)
         self.assertEqual(uname, "achmed2")
         r = y.checkPass(uid, "test")
@@ -328,6 +334,65 @@ def test_06_append_where_filter(self):
         userlist = y.getUserList()
         self.assertTrue(len(userlist) == 0, userlist)
 
+    def test_07_add_user_update_delete_ssha512(self):
+        y = SQLResolver()
+        parameters = self.parameters.copy()
+        # sha256 at first
+        parameters["Password_Hash_Type"] = "SSHA256"
+        y.loadConfig(parameters)
+        uid = y.add_user({"username":"achmed",
+                          "email": "achmed@world.net",
+                          "password": "passw0rd",
+                          "mobile": "12345"})
+        self.assertTrue(uid > 8)
+        self.assertTrue(y.checkPass(uid, "passw0rd"))
+        self.assertFalse(y.checkPass(uid, "password"))
+        # check that we actually store SSHA256 at first
+        stored_password = y.TABLE.filter_by(username="achmed").first().password
+        self.assertTrue(stored_password.startswith("{SSHA256}"), stored_password)
+
+        r = y.update_user(uid, {"username": "achmed2",
+                                "password": "test"})
+        stored_password = y.TABLE.filter_by(username="achmed2").first().password
+        self.assertTrue(stored_password.startswith("{SSHA256}"), stored_password)
+        uname = y.getUsername(uid)
+        self.assertEqual(uname, "achmed2")
+        r = y.checkPass(uid, "test")
+        self.assertTrue(r)
+
+        # change to ssha512
+        y = SQLResolver()
+        parameters["Password_Hash_Type"] = "SSHA512"
+        y.loadConfig(parameters)
+
+        r = y.update_user(uid, {"username": "achmed2",
+                                "password": "test2"})
+        stored_password = y.TABLE.filter_by(username="achmed2").first().password
+        self.assertTrue(stored_password.startswith("{SSHA512}"), stored_password)
+        self.assertTrue(y.checkPass(uid, "test2"))
+        self.assertFalse(y.checkPass(uid, "test"))
+
+        # Now we delete the user
+        y.delete_user(uid)
+        # Now there should be no achmed anymore
+        uid = y.getUserId("achmed2")
+        self.assertFalse(uid)
+        uid = y.getUserId("achmed")
+        self.assertFalse(uid)
+
+        # Add a new user
+        uid = y.add_user({"username":"hans",
+                          "email": "hans@world.net",
+                          "password": u"foo",
+                          "mobile": "12345"})
+        self.assertTrue(y.checkPass(uid, u"foo"))
+        self.assertFalse(y.checkPass(uid, u"bar"))
+        # check that we actually store SSHA512 now
+        stored_password = y.TABLE.filter_by(username="hans").first().password
+        self.assertTrue(stored_password.startswith("{SSHA512}"), stored_password)
+
+        y.delete_user(uid)
+
     def test_99_testconnection_fail(self):
         y = SQLResolver()
         self.parameters['Database'] = "does_not_exist"