Compare commits
3 Commits
5a4ea98e5d
...
0a2d5eca3f
Author | SHA1 | Date | |
---|---|---|---|
0a2d5eca3f | |||
19aba86bd0 | |||
3ef08de465 |
@ -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.
|
||||||
|
|
||||||
|
@ -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])],
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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'.
|
||||||
|
@ -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=="
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user