Add files via upload

This commit is contained in:
Harold Finch
2023-04-10 07:18:32 +02:00
committed by GitHub
parent 06ddbf431f
commit 65875d8fef
100 changed files with 84692 additions and 42 deletions

567
libs/iredpwd.py Normal file
View File

@@ -0,0 +1,567 @@
# Author: Zhang Huangbin <zhb@iredmail.org>
import crypt
import hashlib
import random
import string
import subprocess
from base64 import b64encode, b64decode
from hmac import compare_digest
from os import urandom
from typing import Union, List
import settings
from libs import iredutils
def __has_non_ascii_character(s):
"""
Detect whether a string contains non-ascii character or not.
integer ordinal of a one-character string between 32 and 126 are digits,
letters, punctuation.
Reference: http://docs.python.org/2/library/string.html#string.printable
"""
for i in s:
try:
if not (32 <= ord(i) <= 126):
return True
except TypeError:
# ord() will raise TypeError for non-ascii character
return True
return False
def verify_new_password(
newpw, confirmpw, min_passwd_length=None, max_passwd_length=None, db_settings=None
):
# Confirm password
if newpw == confirmpw:
passwd = newpw
else:
return False, "PW_MISMATCH"
# Empty password is not allowed.
if not passwd:
return False, "PW_EMPTY"
errors = []
# Non-ascii character is not allowed
if __has_non_ascii_character(passwd):
errors.append("PW_NON_ASCII")
# Get settings from db
if not db_settings:
params = [
"min_passwd_length",
"max_passwd_length",
"password_has_letter",
"password_has_uppercase",
"password_has_number",
"password_has_special_char",
]
db_settings = iredutils.get_settings_from_db(params=params)
# Get and verify password length
if not min_passwd_length or not isinstance(min_passwd_length, int):
min_passwd_length = db_settings["min_passwd_length"]
if not max_passwd_length or not isinstance(max_passwd_length, int):
max_passwd_length = db_settings["max_passwd_length"]
if len(passwd) < min_passwd_length:
errors.append("PW_SHORTER_THAN_MIN_LENGTH")
if max_passwd_length > 0:
if len(passwd) > max_passwd_length:
errors.append("PW_GREATER_THAN_MAX_LENGTH")
# Password restriction rules
if db_settings["password_has_letter"]:
if not (set(passwd) & set(string.ascii_letters)):
errors.append("PW_NO_LETTER")
if db_settings["password_has_uppercase"]:
if not (set(passwd) & set(string.ascii_uppercase)):
errors.append("PW_NO_UPPERCASE")
if db_settings["password_has_number"]:
if not (set(passwd) & set(string.digits)):
errors.append("PW_NO_DIGIT_NUMBER")
if db_settings["password_has_special_char"]:
if not (set(passwd) & set(settings.PASSWORD_SPECIAL_CHARACTERS)):
errors.append("PW_NO_SPECIAL_CHAR")
if errors:
return False, ",".join(errors)
else:
return True, passwd
def generate_random_password(length=10, db_settings=None):
try:
length = int(length)
if length <= 0:
length = 10
elif length <= 10:
# We should always suggest a strong password
length = 10
except:
length = 10
if not db_settings:
params = [
"min_passwd_length",
"max_passwd_length",
"password_has_letter",
"password_has_uppercase",
"password_has_number",
"password_has_special_char",
]
db_settings = iredutils.get_settings_from_db(account="global", params=params)
if length < db_settings["min_passwd_length"]:
length = db_settings["min_passwd_length"]
numbers = "23456789" # No 0, 1
letters = "abcdefghjkmnpqrstuvwxyz" # no i, l
uppercases = "ABCDEFGHJKLMNPQRSTUVWXYZ" # no I
opts = []
if db_settings["password_has_letter"]:
opts += random.choice(letters)
length -= 1
if db_settings["password_has_uppercase"]:
opts += random.choice(uppercases)
length -= 1
if db_settings["password_has_number"]:
opts += random.choice(numbers)
length -= 1
if (
db_settings["password_has_special_char"]
and settings.PASSWORD_SPECIAL_CHARACTERS
):
# Don't use few characters which may mess HTML render.
while True:
char = random.choice(settings.PASSWORD_SPECIAL_CHARACTERS)
if char in ["'", '"', ">", "<"]:
continue
else:
opts += char
length -= 1
break
opts += list(iredutils.generate_random_strings(length))
password = ""
for _ in range(len(opts)):
one = random.choice(opts)
password += one
opts.remove(one)
return password
def generate_bcrypt_password(p) -> str:
if isinstance(p, str):
p = p.encode()
try:
import bcrypt
except:
return generate_ssha_password(p)
return "{CRYPT}" + bcrypt.hashpw(p, bcrypt.gensalt()).decode()
def verify_bcrypt_password(challenge_password: str, plain_password: str) -> bool:
try:
import bcrypt
except:
return False
crypt_suffixes = ("{CRYPT}$2a$", "{CRYPT}$2b$",
"{crypt}$2a$", "{crypt}$2b$")
blf_crypt_suffixes = ("{BLF-CRYPT}", "{blf-crypt}")
if challenge_password.startswith(crypt_suffixes):
challenge_password = challenge_password[7:]
elif challenge_password.startswith(blf_crypt_suffixes):
challenge_password = challenge_password[11:]
return bcrypt.checkpw(plain_password.encode(), challenge_password.encode())
def generate_md5_password(p: str) -> str:
return crypt.crypt(p, salt=crypt.METHOD_MD5)
def verify_md5_password(challenge_password: Union[str, bytes],
plain_password: str) -> bool:
"""Verify salted MD5 password"""
if isinstance(challenge_password, bytes):
challenge_password = challenge_password.decode()
if challenge_password.startswith(("{MD5}", "{md5}")):
challenge_password = challenge_password[5:]
elif challenge_password.startswith(("{CRYPT}", "{crypt}")):
challenge_password = challenge_password[7:]
if not (challenge_password.startswith("$")
and len(challenge_password) == 34
and challenge_password.count("$") == 3):
return False
return compare_digest(challenge_password,
crypt.crypt(plain_password, challenge_password))
def generate_plain_md5_password(p: Union[str, bytes]) -> str:
if isinstance(p, str):
p = p.encode()
p = p.strip()
return hashlib.md5(p).hexdigest()
def verify_plain_md5_password(challenge_password, plain_password):
if challenge_password.startswith(("{PLAIN-MD5}", "{plain-md5}")):
challenge_password = challenge_password[11:]
if challenge_password == generate_plain_md5_password(plain_password):
return True
else:
return False
def generate_ssha_password(p: Union[str, bytes]) -> str:
if isinstance(p, str):
p = p.encode()
p = p.strip()
salt = urandom(8)
pw = hashlib.sha1(p)
pw.update(salt)
return "{SSHA}" + b64encode(pw.digest() + salt).decode()
def verify_ssha_password(challenge_password: Union[str, bytes],
plain_password: Union[str, bytes]) -> bool:
"""Verify SHA or SSHA (salted SHA) hash with or without prefix {SHA}, {SSHA}"""
if isinstance(challenge_password, bytes):
challenge_password = challenge_password.decode()
if isinstance(plain_password, str):
plain_password = plain_password.encode()
if challenge_password.startswith(("{SSHA}", "{ssha}")):
challenge_password = challenge_password[6:]
elif challenge_password.startswith(("{SHA}", "{sha}")):
challenge_password = challenge_password[5:]
if len(challenge_password) < 20:
# Not a valid SSHA hash
return False
try:
challenge_bytes = b64decode(challenge_password)
digest = challenge_bytes[:20]
salt = challenge_bytes[20:]
hr = hashlib.sha1(plain_password)
hr.update(salt)
return digest == hr.digest()
except:
return False
def generate_sha512_password(p: Union[str, bytes]) -> str:
"""Generate SHA512 password with prefix '{SHA512}'."""
if isinstance(p, str):
p = p.encode()
p = p.strip()
pw = hashlib.sha512(p)
return "{SHA512}" + b64encode(pw.digest()).decode()
def verify_sha512_password(challenge_password: Union[str, bytes],
plain_password: Union[str, bytes]) -> bool:
"""Verify SHA512 password with or without prefix '{SHA512}'."""
if isinstance(challenge_password, bytes):
challenge_password = challenge_password.decode()
if isinstance(plain_password, str):
plain_password = plain_password.encode()
if challenge_password.startswith(("{SHA512}", "{sha512}")):
challenge_password = challenge_password[8:]
if len(challenge_password) != 88:
return False
try:
challenge_bytes = b64decode(challenge_password)
digest = challenge_bytes[:64]
hr = hashlib.sha512(plain_password)
return digest == hr.digest() # bytes == bytes
except:
return False
def verify_sha512_crypt_password(challenge_password: Union[str, bytes],
plain_password: str) -> bool:
"""Verify SHA512 password with prefix '{SHA512-CRYPT}'."""
if not challenge_password.startswith(("{SHA512-CRYPT}", "{sha512-crypt}")):
return False
challenge_password = challenge_password[14:]
return compare_digest(challenge_password,
crypt.crypt(plain_password, challenge_password))
def generate_ssha512_password(p: Union[str, bytes]) -> str:
"""Generate salted SHA512 password with prefix '{SSHA512}'."""
if isinstance(p, str):
p = p.encode()
p = p.strip()
salt = urandom(8)
pw = hashlib.sha512(p)
pw.update(salt)
return "{SSHA512}" + b64encode(pw.digest() + salt).decode()
def verify_ssha512_password(challenge_password: Union[str, bytes],
plain_password: Union[str, bytes]) -> bool:
"""Verify SSHA512 password with or without prefix '{SSHA512}'."""
if isinstance(challenge_password, bytes):
challenge_password = challenge_password.decode()
if isinstance(plain_password, str):
plain_password = plain_password.encode()
if challenge_password.startswith(("{SSHA512}", "{ssha512}")):
challenge_password = challenge_password[9:]
# With SSHA512, hash itself is 64 bytes (512 bits/8 bits per byte),
# everything after that 64 bytes is the salt.
if len(challenge_password) < 64:
return False
try:
challenge_bytes = b64decode(challenge_password)
digest = challenge_bytes[:64]
salt = challenge_bytes[64:]
hr = hashlib.sha512(plain_password)
hr.update(salt)
return digest == hr.digest()
except:
return False
def generate_password_with_doveadmpw(scheme: str, plain_password: str) -> str:
"""Generate password hash with `doveadm pw` command.
Return SSHA instead if no 'doveadm' command found or other error raised."""
# scheme: CRAM-MD5, NTLM
scheme = scheme.upper()
p = str(plain_password).strip()
try:
pp = subprocess.Popen(
args=["doveadm", "pw", "-s", scheme, "-p", p],
stdout=subprocess.PIPE
)
pw = pp.communicate()[0].decode()
if scheme in settings.HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME:
pw = pw.lstrip("{" + scheme + "}")
# remove '\n'
pw = pw.strip()
return pw
except:
return generate_ssha_password(p)
def verify_password_with_doveadmpw(challenge_password: Union[str, bytes],
plain_password: str) -> bool:
"""Verify password hash with `doveadm pw` command."""
if isinstance(challenge_password, bytes):
challenge_password = challenge_password.decode()
try:
cmd = [
"doveadm",
"pw",
"-t",
challenge_password.strip(),
"-p",
plain_password.strip(),
]
_return_code = subprocess.call(cmd, stdin=None, stdout=None, stderr=None, shell=False)
if _return_code == 0:
return True
except:
pass
return False
def generate_cram_md5_password(p):
return generate_password_with_doveadmpw("CRAM-MD5", p)
def verify_cram_md5_password(challenge_password, plain_password):
"""Verify CRAM-MD5 hash with 'doveadm pw' command."""
if not challenge_password.lower().strip().startswith("{cram-md5}"):
return False
return verify_password_with_doveadmpw(challenge_password, plain_password)
def generate_ntlm_password(p):
return generate_password_with_doveadmpw("NTLM", p)
def verify_ntlm_password(challenge_password, plain_password):
"""Verify NTLM hash with 'doveadm pw' command."""
if "NTLM" not in settings.HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME:
if not challenge_password.startswith(("{NTLM}", "{ntlm}")):
# Prefix '{NTLM}' so that doveadm can verify it.
challenge_password = "{NTLM}" + challenge_password
else:
if not challenge_password.startswith(("{NTLM}", "{ntlm}")):
return False
return verify_password_with_doveadmpw(challenge_password, plain_password)
def generate_password_hash(p: Union[str, bytes],
pwscheme: str = None) -> Union[str, List[str]]:
"""Generate password for LDAP mail user and admin."""
if isinstance(p, bytes):
p = p.decode()
p = p.strip()
pwscheme = pwscheme or settings.DEFAULT_PASSWORD_SCHEME
# Supports returning multiple passwords.
pw_schemes = pwscheme.split("+")
pws = []
for scheme in pw_schemes:
if scheme == "BCRYPT":
pw_hash = generate_bcrypt_password(p)
elif scheme == "SSHA512":
pw_hash = generate_ssha512_password(p)
elif scheme == "SHA512":
pw_hash = generate_sha512_password(p)
elif scheme == "SSHA":
pw_hash = generate_ssha_password(p)
elif scheme == "MD5":
pw_hash = "{CRYPT}" + generate_md5_password(p)
elif scheme == "CRAM-MD5":
pw_hash = generate_cram_md5_password(p)
elif scheme == "PLAIN-MD5":
pw_hash = generate_plain_md5_password(p)
elif scheme == "NTLM":
pw_hash = generate_ntlm_password(p)
elif scheme == "PLAIN":
if "PLAIN" in settings.HASHES_WITHOUT_PREFIXED_PASSWORD_SCHEME:
pw_hash = p
else:
pw_hash = "{PLAIN}" + p
else:
pw_hash = p
pws.append(pw_hash)
if len(pws) == 1:
return pws[0]
else:
return pws
def verify_password_hash(challenge_password: Union[str, bytes],
plain_password: Union[str, bytes]) -> bool:
if isinstance(challenge_password, bytes):
challenge_password = challenge_password.decode()
if isinstance(plain_password, bytes):
plain_password = plain_password.decode()
# Check plain password and MD5 first.
if challenge_password in [
plain_password,
"{PLAIN}" + plain_password,
"{plain}" + plain_password,
]:
return True
elif verify_md5_password(challenge_password, plain_password):
return True
upwd = challenge_password.upper()
if upwd.startswith(("{SSHA}", "{SHA}")):
return verify_ssha_password(challenge_password, plain_password)
elif upwd.startswith("{SSHA512}"):
return verify_ssha512_password(challenge_password, plain_password)
elif upwd.startswith(("{CRYPT}$2A$", "{CRYPT}$2B$", "{BLF-CRYPT}$2A$", "{BLF-CRYPT}$2B$", "{BLF-CRYPT}$2Y$")):
return verify_bcrypt_password(challenge_password, plain_password)
elif upwd.startswith(("{CRYPT}$6$", "{CRYPT}$2B$")):
# CRYPT-SHA-512
return verify_password_with_doveadmpw(challenge_password, plain_password)
elif upwd.startswith("{SHA512}"):
return verify_sha512_password(challenge_password, plain_password)
elif upwd.startswith("{PLAIN-MD5}"):
return verify_plain_md5_password(challenge_password, plain_password)
elif upwd.startswith("{SHA512-CRYPT}"):
return verify_sha512_crypt_password(challenge_password, plain_password)
elif upwd.startswith("{CRAM-MD5}"):
return verify_cram_md5_password(challenge_password, plain_password)
elif upwd.startswith("{NTLM}"):
return verify_ntlm_password(challenge_password, plain_password)
return False
def is_supported_password_scheme(pw_hash):
if not (pw_hash.startswith("{") and "}" in pw_hash):
return False
# Extract scheme name from password hash: "{SSHA}xxxx" -> "SSHA"
try:
scheme = pw_hash.split("}", 1)[0].split("{", 1)[-1]
scheme = scheme.upper()
if scheme in [
"PLAIN",
"CRYPT",
"MD5",
"PLAIN-MD5",
"SHA",
"SSHA",
"SHA512",
"SSHA512",
"SHA512-CRYPT",
"BCRYPT",
"CRAM-MD5",
"NTLM",
]:
return True
except:
pass
return False