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)
- `mail` - email address
- `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.

View File

@ -623,9 +623,9 @@ def get_user(connection: Connection, uid: str) -> User:
last_name = attributes['sn'][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]
expected_hash_type = '{SHA512}'
expected_hash_type = '{SSHA}'
if not password_hash.startswith(expected_hash_type):
raise InvalidAttributeException(
f"Unexpected password hash in entry '{user_dn}': expected " \
@ -676,7 +676,7 @@ def create_user(connection: Connection, user: User) -> None:
attributes = {
"uid": user.username,
"userPassword": "{SHA512}" + user.password_hash,
"userPassword": "{SSHA}" + user.password_hash,
"cn": user.first_name,
"sn": user.last_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")
new_attributes = {
"userPassword": [(MODIFY_REPLACE, ["{SHA512}" + user.password_hash])],
"userPassword": [(MODIFY_REPLACE, ["{SSHA}" + user.password_hash])],
"mail": [(MODIFY_REPLACE, [user.email])],
"cn": [(MODIFY_REPLACE, [user.first_name])],
"sn": [(MODIFY_REPLACE, [user.last_name])],

View File

@ -2,9 +2,10 @@
from string import ascii_lowercase, ascii_uppercase, digits, whitespace
from base64 import b64encode, b64decode
import hashlib
from hashlib import sha1
from binascii import Error as Base64DecodeError
from pathlib import Path
from os import urandom
from PIL import Image
from PIL.JpegImagePlugin import JpegImageFile
@ -21,7 +22,7 @@ class User:
username : str
The user's username.
password_hash : str
Base64-encoded SHA512 hash of the user's password.
Base64-encoded SSHA hash of the user's password.
email : str
The user's email address.
first_name : str
@ -268,7 +269,9 @@ class User:
@staticmethod
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
----------
@ -278,7 +281,7 @@ class User:
Returns
-------
str
A base64-encoded SHA512 hash digest of the input string.
A base64-encoded SSHA hash digest of the input string.
Raises
------
@ -293,9 +296,11 @@ class User:
if not len(password):
raise ValueError("Input string cannot be empty.")
hash_bytes = hashlib.sha512()
salt = urandom(4)
hash_bytes = sha1()
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
@ -325,8 +330,8 @@ class User:
username : str
The username, valid as described by User.assert_is_valid_username().
password_hash : str
The user's base64-encoded SHA512-hashed password (without the
'{SHA512}'-prefix expected by LDAP).
The user's base64-encoded SSHA-hashed password (without the
'{SSHA}'-prefix expected by LDAP).
Must be valid as described by User.assert_is_valid_password_hash().
email : str
The User's email address.
@ -438,6 +443,42 @@ class User:
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):
return self.username == other.username

View File

@ -98,7 +98,7 @@ def connection():
- displayName (nickname)
- mail (email)
- 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
groups. Bob is a member of 'employees'.

View File

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