diff --git a/data/anaconda.conf b/data/anaconda.conf index e3781100649f..d6f3ebe0c43a 100644 --- a/data/anaconda.conf +++ b/data/anaconda.conf @@ -291,13 +291,14 @@ can_change_users = False # # quality The minimum quality score (see libpwquality). # length The minimum length of the password. +# class The minimum class of the password characters. # empty Allow an empty password. # strict Require the minimum quality. # password_policies = - root (quality 1, length 6) - user (quality 1, length 6, empty) - luks (quality 1, length 6) + root (quality 1, length 6, class 1) + user (quality 1, length 6, class 1, empty) + luks (quality 1, length 6, class 1) [License] # A path to EULA (if any) diff --git a/pyanaconda/core/configuration/ui.py b/pyanaconda/core/configuration/ui.py index 50df96f1c1a0..a952528b32d7 100644 --- a/pyanaconda/core/configuration/ui.py +++ b/pyanaconda/core/configuration/ui.py @@ -74,6 +74,7 @@ def password_policies(self): name The name of the policy. quality The minimum quality score (see libpwquality). length The minimum length of the password. + class The minimum class of the password characters. empty Allow an empty password. strict Require the minimum quality. @@ -98,7 +99,7 @@ def _convert_policy_line(cls, line): if not value and name in ("strict", "empty"): # Handle a boolean attribute. attrs[name] = True - elif value and name in ("length", "quality"): + elif value and name in ("length", "class", "quality"): # Handle an integer attribute. attrs[name] = int(value) else: @@ -119,5 +120,8 @@ def _validate_policy_attributes(attrs): if "length" not in attrs: raise ValueError("The minimal length is not specified.") + if "class" not in attrs: + raise ValueError("The minimal class is not specified.") + if "quality" not in attrs: raise ValueError("The minimal quality is not specified.") diff --git a/pyanaconda/core/constants.py b/pyanaconda/core/constants.py index 40e9adf171ba..9fb59b394c21 100644 --- a/pyanaconda/core/constants.py +++ b/pyanaconda/core/constants.py @@ -194,6 +194,10 @@ class SecretType(Enum): SecretType.PASSWORD : N_("The password is too short"), SecretType.PASSPHRASE : N_("The passphrase is too short") } +SECRET_TOO_FEW_CLASS = { + SecretType.PASSWORD : N_("The password need more character types, upper/lower letters, digits or special character"), + SecretType.PASSPHRASE : N_("The password need more character types, upper/lower letters, digits or special character") +} SECRET_WEAK = { SecretType.PASSWORD : N_("The password you have provided is weak."), SecretType.PASSPHRASE : N_("The passphrase you have provided is weak.") @@ -216,6 +220,7 @@ class SecretType(Enum): class SecretStatus(Enum): EMPTY = N_("Empty") TOO_SHORT = N_("Too short") + TOO_FEW_CLASS = N_("Class less") WEAK = N_("Weak") FAIR = N_("Fair") GOOD = N_("Good") diff --git a/pyanaconda/core/util.py b/pyanaconda/core/util.py index 8005a52eb3c4..64ca646cc541 100644 --- a/pyanaconda/core/util.py +++ b/pyanaconda/core/util.py @@ -985,6 +985,14 @@ def restorecon(paths, root, skip_nonexistent=False): return True +def get_password_character_class(password): + lower = bool(re.search('[a-z]', password)) + upper = bool(re.search('[A-Z]', password)) + digit = bool(re.search('[0-9]', password)) + special = bool(re.search(r'[?![\]{}*/\\<>":+\-@$%^&()=_#~,\';.`]', password)) + return lower + upper + digit + special + + def get_image_packages_info(max_string_chars=0): """List of strings containing versions of installer image packages. diff --git a/pyanaconda/input_checking.py b/pyanaconda/input_checking.py index c24a370a555b..6c3194255e16 100644 --- a/pyanaconda/input_checking.py +++ b/pyanaconda/input_checking.py @@ -25,6 +25,7 @@ from pyanaconda.core.kernel import kernel_arguments from pyanaconda.core import constants, regexes from pyanaconda.core import users +from pyanaconda.core.util import get_password_character_class from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.modules.common.constants.objects import USER_INTERFACE from pyanaconda.modules.common.constants.services import RUNTIME @@ -244,6 +245,8 @@ def __init__(self): self.password_quality_changed = Signal() self._length_ok = False self.length_ok_changed = Signal() + self._class_ok = False + self.class_ok_changed = Signal() @property def password_score(self): @@ -311,6 +314,20 @@ def length_ok(self, value): self._length_ok = value self.length_ok_changed.emit(value) + @property + def class_ok(self): + """Reports if the password has enough class of characters type. + + :returns: if the password has enough class of characters type + :rtype: bool + """ + return self._class_ok + + @class_ok.setter + def class_ok(self, value): + self._class_ok = value + self.class_ok_changed.emit(value) + class InputCheck(object): """Input checking base class.""" @@ -379,6 +396,7 @@ def run(self, check_request): """ length_ok = False + class_ok = False error_message = "" pw_quality = 0 try: @@ -394,8 +412,10 @@ def run(self, check_request): if check_request.policy.allow_empty and not check_request.password: # if we are OK with empty passwords, then empty passwords are also fine length wise length_ok = True + class_ok = True else: length_ok = len(check_request.password) >= check_request.policy.min_length + class_ok = get_password_character_class(check_request.password) >= check_request.policy.min_class if not check_request.password: if check_request.policy.allow_empty: @@ -411,6 +431,10 @@ def run(self, check_request): # This is because the error messages returned by libpwquality # for short passwords don't make much sense. error_message = _(constants.SECRET_TOO_SHORT[check_request.secret_type]) + elif not class_ok: + pw_score = 0 + status_text = _(constants.SecretStatus.TOO_FEW_CLASS.value) + error_message = _(constants.SECRET_TOO_FEW_CLASS[check_request.secret_type]) elif error_message: pw_score = 1 status_text = _(constants.SecretStatus.WEAK.value) @@ -436,6 +460,7 @@ def run(self, check_request): self.result.password_quality = pw_quality # pylint: disable=attribute-defined-outside-init self.result.error_message = error_message # pylint: disable=attribute-defined-outside-init self.result.length_ok = length_ok # pylint: disable=attribute-defined-outside-init + self.result.class_ok = class_ok # pylint: disable=attribute-defined-outside-init class PasswordFIPSCheck(InputCheck): diff --git a/pyanaconda/modules/common/structures/policy.py b/pyanaconda/modules/common/structures/policy.py index 1fb64121e653..2f6507d5910c 100644 --- a/pyanaconda/modules/common/structures/policy.py +++ b/pyanaconda/modules/common/structures/policy.py @@ -33,6 +33,7 @@ class PasswordPolicy(DBusData): def __init__(self): self._min_quality = UInt16(0) self._min_length = UInt16(0) + self._min_class = UInt16(0) self._allow_empty = True self._is_strict = False @@ -60,6 +61,18 @@ def min_length(self) -> UInt16: def min_length(self, value): self._min_length = UInt16(value) + @property + def min_class(self) -> UInt16: + """The minimum class of the password characters. + + :return: a number of type of password characters + """ + return self._min_class + + @min_class.setter + def min_class(self, value): + self._min_class = UInt16(value) + @property def allow_empty(self) -> Bool: """Should an empty password be allowed? @@ -99,6 +112,7 @@ def from_defaults(cls, policy_name): policy.min_quality = attrs.get("quality") policy.min_length = attrs.get("length") + policy.min_class = attrs.get("class") policy.allow_empty = attrs.get("empty", False) policy.is_strict = attrs.get("strict", False) break