feat: extend password options

This commit is contained in:
thePR0M3TH3AN
2025-07-30 20:22:53 -04:00
parent 3f169747d1
commit b57e19b657
4 changed files with 255 additions and 15 deletions

View File

@@ -209,10 +209,49 @@ def entry_add(
length: int = typer.Option(12, "--length"),
username: Optional[str] = typer.Option(None, "--username"),
url: Optional[str] = typer.Option(None, "--url"),
no_special: bool = typer.Option(
False, "--no-special", help="Exclude special characters", is_flag=True
),
allowed_special_chars: Optional[str] = typer.Option(
None, "--allowed-special-chars", help="Explicit set of special characters"
),
special_mode: Optional[str] = typer.Option(
None,
"--special-mode",
help="Special character mode",
),
exclude_ambiguous: bool = typer.Option(
False,
"--exclude-ambiguous",
help="Exclude ambiguous characters",
is_flag=True,
),
min_uppercase: Optional[int] = typer.Option(None, "--min-uppercase"),
min_lowercase: Optional[int] = typer.Option(None, "--min-lowercase"),
min_digits: Optional[int] = typer.Option(None, "--min-digits"),
min_special: Optional[int] = typer.Option(None, "--min-special"),
) -> None:
"""Add a new password entry and output its index."""
service = _get_entry_service(ctx)
index = service.add_entry(label, length, username, url)
kwargs = {}
if no_special:
kwargs["include_special_chars"] = False
if allowed_special_chars is not None:
kwargs["allowed_special_chars"] = allowed_special_chars
if special_mode is not None:
kwargs["special_mode"] = special_mode
if exclude_ambiguous:
kwargs["exclude_ambiguous"] = True
if min_uppercase is not None:
kwargs["min_uppercase"] = min_uppercase
if min_lowercase is not None:
kwargs["min_lowercase"] = min_lowercase
if min_digits is not None:
kwargs["min_digits"] = min_digits
if min_special is not None:
kwargs["min_special"] = min_special
index = service.add_entry(label, length, username, url, **kwargs)
typer.echo(str(index))
@@ -708,10 +747,52 @@ def fingerprint_switch(ctx: typer.Context, fingerprint: str) -> None:
@util_app.command("generate-password")
def generate_password(ctx: typer.Context, length: int = 24) -> None:
def generate_password(
ctx: typer.Context,
length: int = 24,
no_special: bool = typer.Option(
False, "--no-special", help="Exclude special characters", is_flag=True
),
allowed_special_chars: Optional[str] = typer.Option(
None, "--allowed-special-chars", help="Explicit set of special characters"
),
special_mode: Optional[str] = typer.Option(
None,
"--special-mode",
help="Special character mode",
),
exclude_ambiguous: bool = typer.Option(
False,
"--exclude-ambiguous",
help="Exclude ambiguous characters",
is_flag=True,
),
min_uppercase: Optional[int] = typer.Option(None, "--min-uppercase"),
min_lowercase: Optional[int] = typer.Option(None, "--min-lowercase"),
min_digits: Optional[int] = typer.Option(None, "--min-digits"),
min_special: Optional[int] = typer.Option(None, "--min-special"),
) -> None:
"""Generate a strong password."""
service = _get_util_service(ctx)
password = service.generate_password(length)
kwargs = {}
if no_special:
kwargs["include_special_chars"] = False
if allowed_special_chars is not None:
kwargs["allowed_special_chars"] = allowed_special_chars
if special_mode is not None:
kwargs["special_mode"] = special_mode
if exclude_ambiguous:
kwargs["exclude_ambiguous"] = True
if min_uppercase is not None:
kwargs["min_uppercase"] = min_uppercase
if min_lowercase is not None:
kwargs["min_lowercase"] = min_lowercase
if min_digits is not None:
kwargs["min_digits"] = min_digits
if min_special is not None:
kwargs["min_special"] = min_special
password = service.generate_password(length, **kwargs)
typer.echo(password)

View File

@@ -9,7 +9,8 @@ allow easy validation and documentation.
from pathlib import Path
from threading import Lock
from typing import List, Optional, Dict
from typing import List, Optional, Dict, Any
import dataclasses
import json
from pydantic import BaseModel
@@ -283,9 +284,42 @@ class EntryService:
length: int,
username: str | None = None,
url: str | None = None,
*,
include_special_chars: bool | None = None,
allowed_special_chars: str | None = None,
special_mode: str | None = None,
exclude_ambiguous: bool | None = None,
min_uppercase: int | None = None,
min_lowercase: int | None = None,
min_digits: int | None = None,
min_special: int | None = None,
) -> int:
with self._lock:
idx = self._manager.entry_manager.add_entry(label, length, username, url)
kwargs: dict[str, Any] = {}
if include_special_chars is not None:
kwargs["include_special_chars"] = include_special_chars
if allowed_special_chars is not None:
kwargs["allowed_special_chars"] = allowed_special_chars
if special_mode is not None:
kwargs["special_mode"] = special_mode
if exclude_ambiguous is not None:
kwargs["exclude_ambiguous"] = exclude_ambiguous
if min_uppercase is not None:
kwargs["min_uppercase"] = min_uppercase
if min_lowercase is not None:
kwargs["min_lowercase"] = min_lowercase
if min_digits is not None:
kwargs["min_digits"] = min_digits
if min_special is not None:
kwargs["min_special"] = min_special
idx = self._manager.entry_manager.add_entry(
label,
length,
username,
url,
**kwargs,
)
self._manager.start_background_vault_sync()
return idx
@@ -557,9 +591,50 @@ class UtilityService:
self._manager = manager
self._lock = Lock()
def generate_password(self, length: int) -> str:
def generate_password(
self,
length: int,
*,
include_special_chars: bool | None = None,
allowed_special_chars: str | None = None,
special_mode: str | None = None,
exclude_ambiguous: bool | None = None,
min_uppercase: int | None = None,
min_lowercase: int | None = None,
min_digits: int | None = None,
min_special: int | None = None,
) -> str:
with self._lock:
return self._manager.password_generator.generate_password(length)
pg = self._manager.password_generator
base_policy = getattr(pg, "policy", None)
overrides: dict[str, Any] = {}
if include_special_chars is not None:
overrides["include_special_chars"] = include_special_chars
if allowed_special_chars is not None:
overrides["allowed_special_chars"] = allowed_special_chars
if special_mode is not None:
overrides["special_mode"] = special_mode
if exclude_ambiguous is not None:
overrides["exclude_ambiguous"] = exclude_ambiguous
if min_uppercase is not None:
overrides["min_uppercase"] = int(min_uppercase)
if min_lowercase is not None:
overrides["min_lowercase"] = int(min_lowercase)
if min_digits is not None:
overrides["min_digits"] = int(min_digits)
if min_special is not None:
overrides["min_special"] = int(min_special)
if base_policy is not None and overrides:
pg.policy = dataclasses.replace(
base_policy,
**{k: overrides[k] for k in overrides if hasattr(base_policy, k)},
)
try:
return pg.generate_password(length)
finally:
pg.policy = base_policy
return pg.generate_password(length)
def verify_checksum(self) -> None:
with self._lock: