Compare commits

..

3 Commits

5 changed files with 18 additions and 59 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` - SSHA password hash - `userPassword` - SHA512 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 '{SSHA}' # Retrieve base64-encoded password hash prefixed with '{SHA512}'
password_hash = attributes['userPassword'][0] password_hash = attributes['userPassword'][0]
expected_hash_type = '{SSHA}' expected_hash_type = '{SHA512}'
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": "{SSHA}" + user.password_hash, "userPassword": "{SHA512}" + 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, ["{SSHA}" + user.password_hash])], "userPassword": [(MODIFY_REPLACE, ["{SHA512}" + 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,10 +2,9 @@
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
from hashlib import sha1 import hashlib
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
@ -22,7 +21,7 @@ class User:
username : str username : str
The user's username. The user's username.
password_hash : str password_hash : str
Base64-encoded SSHA hash of the user's password. Base64-encoded SHA512 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
@ -269,9 +268,7 @@ class User:
@staticmethod @staticmethod
def generate_password_hash(password: str) -> str: def generate_password_hash(password: str) -> str:
"""Generates a base64-encoded SSHA hash of the input string. """Generates a base64-encoded SHA512 hash of the input string.
The 4-byte salt is appended to the digest prior to base64-encoding.
Parameters Parameters
---------- ----------
@ -281,7 +278,7 @@ class User:
Returns Returns
------- -------
str str
A base64-encoded SSHA hash digest of the input string. A base64-encoded SHA512 hash digest of the input string.
Raises Raises
------ ------
@ -296,11 +293,9 @@ class User:
if not len(password): if not len(password):
raise ValueError("Input string cannot be empty.") raise ValueError("Input string cannot be empty.")
salt = urandom(4) hash_bytes = hashlib.sha512()
hash_bytes = sha1()
hash_bytes.update(bytes(password, "UTF-8")) hash_bytes.update(bytes(password, "UTF-8"))
hash_bytes.update(salt) return b64encode(hash_bytes.digest()).decode("ASCII")
return b64encode(hash_bytes.digest() + salt).decode("ASCII")
@staticmethod @staticmethod
@ -330,8 +325,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 SSHA-hashed password (without the The user's base64-encoded SHA512-hashed password (without the
'{SSHA}'-prefix expected by LDAP). '{SHA512}'-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.
@ -443,42 +438,6 @@ 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 (SSHA hash of password 'test') - password (sha512 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": [
"{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K" "{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="
] ]
}, },
"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": [
"{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K" "{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="
] ]
} }
}, },
@ -156,7 +156,7 @@
"bobbuilder" "bobbuilder"
], ],
"userPassword": [ "userPassword": [
"{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K" "{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="
] ]
}, },
"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": [
"{SSHA}LitOJVWDVqqglNuHS0o1ypQjyd4TUP4K" "{SHA512}7iaw3Ur350mqGo7jwQrpkj9hiYB3Lkc/iBml1JQODbJ6wYX4oOHV+E+IvIh/1nsUNzLDBMxfqa2Ob1f1ACio/w=="
] ]
} }
}, },