diff --git a/pyproject.toml b/pyproject.toml index d29eaa0c..562bf343 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,8 @@ dependencies = [ "structlog>=25.4.0", "docling-serve==1.5.0", "docling-core==2.48.1", - "easyocr>=1.7.1; sys_platform != 'darwin'" + "easyocr>=1.7.1; sys_platform != 'darwin'", + "zxcvbn>=4.5.0" ] [dependency-groups] diff --git a/src/tui/screens/config.py b/src/tui/screens/config.py index 1cb03477..6852b480 100644 --- a/src/tui/screens/config.py +++ b/src/tui/screens/config.py @@ -1,6 +1,7 @@ """Configuration screen for OpenRAG TUI.""" import re +from zxcvbn import zxcvbn from textual.app import ComposeResult from textual.containers import Container, Vertical, Horizontal, ScrollableContainer from textual.screen import Screen @@ -98,35 +99,36 @@ class DocumentsPathValidator(Validator): class PasswordValidator(Validator): - """Validator for OpenSearch admin password.""" + """Validator for OpenSearch admin password using zxcvbn strength estimation.""" + + # Minimum acceptable score (0-4 scale: 0=weak, 4=very strong) + MIN_SCORE = 3 def validate(self, value: str) -> ValidationResult: # Allow empty value (will be auto-generated) if not value: return self.success() - # Minimum length: 8 characters - if len(value) < 8: - return self.failure("Password must be at least 8 characters long") + # Use zxcvbn to evaluate password strength + result = zxcvbn(value) + score = result["score"] - # Check for required character types - has_uppercase = bool(re.search(r"[A-Z]", value)) - has_lowercase = bool(re.search(r"[a-z]", value)) - has_digit = bool(re.search(r"[0-9]", value)) - has_special = bool(re.search(r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>/?]", value)) + if score < self.MIN_SCORE: + # Get feedback from zxcvbn + feedback = result.get("feedback", {}) + warning = feedback.get("warning", "") + suggestions = feedback.get("suggestions", []) - missing = [] - if not has_uppercase: - missing.append("uppercase letter") - if not has_lowercase: - missing.append("lowercase letter") - if not has_digit: - missing.append("digit") - if not has_special: - missing.append("special character") + # Build error message + strength_labels = ["very weak", "weak", "fair", "strong", "very strong"] + current_strength = strength_labels[score] - if missing: - return self.failure(f"Password must contain: {', '.join(missing)}") + if warning: + return self.failure(f"Password is {current_strength}: {warning}") + elif suggestions: + return self.failure(f"Password is {current_strength}. {suggestions[0]}") + else: + return self.failure(f"Password is {current_strength}. Use a longer, more unique password.") return self.success() @@ -200,7 +202,7 @@ class ConfigScreen(Screen): # OpenSearch Admin Password yield Label("OpenSearch Admin Password *") yield Static( - "Min 8 chars with uppercase, lowercase, digit, and special character", + "Validate your password here: https://lowe.github.io/tryzxcvbn/", classes="helper-text", ) current_value = getattr(self.env_manager.config, "opensearch_password", "") diff --git a/uv.lock b/uv.lock index 868950f3..b1aa2c8e 100644 --- a/uv.lock +++ b/uv.lock @@ -2383,6 +2383,7 @@ dependencies = [ { name = "torch", version = "2.7.1+cu128", source = { registry = "https://download.pytorch.org/whl/cu128" }, marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "torch", version = "2.8.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'linux'" }, { name = "uvicorn" }, + { name = "zxcvbn" }, ] [package.dev-dependencies] @@ -2422,6 +2423,7 @@ requires-dist = [ { name = "torch", marker = "platform_machine != 'x86_64' or sys_platform != 'linux'", specifier = ">=2.7.1" }, { name = "torch", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'", specifier = ">=2.7.1", index = "https://download.pytorch.org/whl/cu128" }, { name = "uvicorn", specifier = ">=0.35.0" }, + { name = "zxcvbn", specifier = ">=4.5.0" }, ] [package.metadata.requires-dev] @@ -4173,3 +4175,12 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] + +[[package]] +name = "zxcvbn" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/40/9366940b1484fd4e9423c8decbbf34a73bf52badb36281e082fe02b57aca/zxcvbn-4.5.0.tar.gz", hash = "sha256:70392c0fff39459d7f55d0211151401e79e76fcc6e2c22b61add62900359c7c1", size = 411249, upload-time = "2025-02-19T19:03:02.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/16/7410f8e714a109d43d17f4e27c8eabb351557653a9b570db1bd7dfdfd822/zxcvbn-4.5.0-py2.py3-none-any.whl", hash = "sha256:2b6eed621612ce6d65e6e4c7455b966acee87d0280e257956b1f06ccc66bd5ff", size = 409397, upload-time = "2025-02-19T19:03:00.521Z" }, +]