fix(ldap): replace SHA512 user passwords with SSHA

This commit is contained in:
Julian Lobbes 2022-12-05 15:58:33 +01:00
parent 5d936900be
commit d8670fc558
5 changed files with 59 additions and 18 deletions

View File

@ -50,7 +50,7 @@ If you point Lumi2 at an existing LDAP server, make sure its DIT matches the str
- `displayName` - preferred name (or nickname) - `displayName` - preferred name (or nickname)
- `mail` - email address - `mail` - email address
- `jpegPhoto` - profile picture in JPEG format - `jpegPhoto` - profile picture in JPEG format
- `userPassword` - SHA512 password hash - `userPassword` - SSHA password hash
The `uid` (username) can contain latin characters, digits, underscores, hypens and periods, and must have a letter as the first character. The `uid` (username) can contain latin characters, digits, underscores, hypens and periods, and must have a letter as the first character.

View File

@ -623,9 +623,9 @@ def get_user(connection: Connection, uid: str) -> User:
last_name = attributes['sn'][0] last_name = attributes['sn'][0]
display_name = attributes['displayName'][0] display_name = attributes['displayName'][0]
# Retrieve base64-encoded password hash prefixed with '{SHA512}' # Retrieve base64-encoded password hash prefixed with '{SSHA}'
password_hash = attributes['userPassword'][0] password_hash = attributes['userPassword'][0]
expected_hash_type = '{SHA512}' expected_hash_type = '{SSHA}'
if not password_hash.startswith(expected_hash_type): if not password_hash.startswith(expected_hash_type):
raise InvalidAttributeException( raise InvalidAttributeException(
f"Unexpected password hash in entry '{user_dn}': expected " \ f"Unexpected password hash in entry '{user_dn}': expected " \
@ -676,7 +676,7 @@ def create_user(connection: Connection, user: User) -> None:
attributes = { attributes = {
"uid": user.username, "uid": user.username,
"userPassword": "{SHA512}" + user.password_hash, "userPassword": "{SSHA}" + user.password_hash,
"cn": user.first_name, "cn": user.first_name,
"sn": user.last_name, "sn": user.last_name,
"displayName": user.display_name, "displayName": user.display_name,
@ -723,7 +723,7 @@ def update_user(connection: Connection, user: User) -> None:
user.picture.save(new_picture_bytes, format="jpeg") user.picture.save(new_picture_bytes, format="jpeg")
new_attributes = { new_attributes = {
"userPassword": [(MODIFY_REPLACE, ["{SHA512}" + user.password_hash])], "userPassword": [(MODIFY_REPLACE, ["{SSHA}" + user.password_hash])],
"mail": [(MODIFY_REPLACE, [user.email])], "mail": [(MODIFY_REPLACE, [user.email])],
"cn": [(MODIFY_REPLACE, [user.first_name])], "cn": [(MODIFY_REPLACE, [user.first_name])],
"sn": [(MODIFY_REPLACE, [user.last_name])], "sn": [(MODIFY_REPLACE, [user.last_name])],

View File

@ -2,9 +2,10 @@
from string import ascii_lowercase, ascii_uppercase, digits, whitespace from string import ascii_lowercase, ascii_uppercase, digits, whitespace
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
import hashlib from hashlib import sha1
from binascii import Error as Base64DecodeError from binascii import Error as Base64DecodeError
from pathlib import Path from pathlib import Path
from os import urandom
from PIL import Image from PIL import Image
from PIL.JpegImagePlugin import JpegImageFile from PIL.JpegImagePlugin import JpegImageFile
@ -21,7 +22,7 @@ class User:
username : str username : str
The user's username. The user's username.
password_hash : str password_hash : str
Base64-encoded SHA512 hash of the user's password. Base64-encoded SSHA hash of the user's password.
email : str email : str
The user's email address. The user's email address.
first_name : str first_name : str
@ -268,7 +269,9 @@ class User:
@staticmethod @staticmethod
def generate_password_hash(password: str) -> str: def generate_password_hash(password: str) -> str:
"""Generates a base64-encoded SHA512 hash of the input string. """Generates a base64-encoded SSHA hash of the input string.
The 4-byte salt is appended to the digest prior to base64-encoding.
Parameters Parameters
---------- ----------
@ -278,7 +281,7 @@ class User:
Returns Returns
------- -------
str str
A base64-encoded SHA512 hash digest of the input string. A base64-encoded SSHA hash digest of the input string.
Raises Raises
------ ------
@ -293,9 +296,11 @@ class User:
if not len(password): if not len(password):
raise ValueError("Input string cannot be empty.") raise ValueError("Input string cannot be empty.")
hash_bytes = hashlib.sha512() salt = urandom(4)
hash_bytes = sha1()
hash_bytes.update(bytes(password, "UTF-8")) hash_bytes.update(bytes(password, "UTF-8"))
return b64encode(hash_bytes.digest()).decode("ASCII") hash_bytes.update(salt)
return b64encode(hash_bytes.digest() + salt).decode("ASCII")
@staticmethod @staticmethod
@ -325,8 +330,8 @@ class User:
username : str username : str
The username, valid as described by User.assert_is_valid_username(). The username, valid as described by User.assert_is_valid_username().
password_hash : str password_hash : str
The user's base64-encoded SHA512-hashed password (without the The user's base64-encoded SSHA-hashed password (without the
'{SHA512}'-prefix expected by LDAP). '{SSHA}'-prefix expected by LDAP).
Must be valid as described by User.assert_is_valid_password_hash(). Must be valid as described by User.assert_is_valid_password_hash().
email : str email : str
The User's email address. The User's email address.
@ -438,6 +443,42 @@ class User:
return groups return groups
def check_password(self, password_plaintext: str) -> bool:
"""Checks the input plaintext password against this User's password hash.
Parameters
----------
password_plaintext : str
The plaintext password to check against this User's salted password
hash.
Returns
-------
True : bool
If the input password_plaintext is this User's password.
False : bool
Otherwise.
Raises
------
TypeError
If password_plaintext is not of type string.
"""
if not isinstance(password_plaintext, str):
raise TypeError(f"Expected a string but got: '{type(password_plaintext)}'.")
password_hash_bytes = b64decode(self.password_hash)
digest_bytes = password_hash_bytes[:20]
salt = password_hash_bytes[20:]
validation_hash = sha1()
validation_hash.update(bytes(password_plaintext, "UTF-8"))
validation_hash.update(salt)
return validation_hash.digest() == digest_bytes
def __eq__(self, other): def __eq__(self, other):
return self.username == other.username return self.username == other.username

View File

@ -98,7 +98,7 @@ def connection():
- displayName (nickname) - displayName (nickname)
- mail (email) - mail (email)
- jpegPhoto (profile picture) - jpegPhoto (profile picture)
- password (sha512 hash of password 'test') - password (SSHA hash of password 'test')
Both groups are of type 'groupOfUniqueNames'. Alice is a member of both Both groups are of type 'groupOfUniqueNames'. Alice is a member of both
groups. Bob is a member of 'employees'. groups. Bob is a member of 'employees'.

View File

@ -95,7 +95,7 @@
"alice" "alice"
], ],
"userPassword": [ "userPassword": [
"{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==" "{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K"
] ]
}, },
"dn": "uid=alice,ou=users,dc=example,dc=com", "dn": "uid=alice,ou=users,dc=example,dc=com",
@ -126,7 +126,7 @@
"alice" "alice"
], ],
"userPassword": [ "userPassword": [
"{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==" "{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K"
] ]
} }
}, },
@ -156,7 +156,7 @@
"bobbuilder" "bobbuilder"
], ],
"userPassword": [ "userPassword": [
"{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==" "{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K"
] ]
}, },
"dn": "uid=bobbuilder,ou=users,dc=example,dc=com", "dn": "uid=bobbuilder,ou=users,dc=example,dc=com",
@ -187,7 +187,7 @@
"bobbuilder" "bobbuilder"
], ],
"userPassword": [ "userPassword": [
"{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w==" "{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K"
] ]
} }
}, },