From bcf00de9a1258259a817515585ceb60f4750bece Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Tue, 4 Nov 2025 00:06:37 +0530 Subject: [PATCH 1/6] Updated readme code example and added config generation --- README.md | 50 ++++++++++++++++--- .../keepersdk/authentication/configuration.py | 7 ++- 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5a1262cf..7d7100f1 100644 --- a/README.md +++ b/README.md @@ -131,7 +131,44 @@ The Keeper SDK uses a configuration storage system to manage authentication sett - **InMemoryConfigurationStorage**: Temporary in-memory storage for testing - **Custom implementations**: Implement your own configuration storage -No additional configuration files are required for basic usage. Authentication settings are managed programmatically through the SDK. +#### **Requirement for client** + +If you are accessing keepersdk from a new device, you need to ensure that there is a config.json file present from which the sdk reads credentials. This ensures that the client doesn't contain any hardcoded credentials. Create the .json file in .keeper folder of current user, you might need to create a .keeper folder. A sample showing the structure of the config.json needed is shown below: + +``` +{ + "users": [ + { + "user": "username@yourcompany.com", + "password":"yourpassword", + "server": "keepersecurity.com", + "last_device": { + "device_token": "" + } + } + ], + "servers": [ + { + "server": "keepersecurity.com", + "server_key_id": 10 + } + ], + "devices": [ + { + "device_token": "", + "private_key": "", + "server_info": [ + { + "server": "keepersecurity.com", + "clone_code": "" + } + ] + } + ], + "last_login": "username@yourcompany.com", + "last_server": "keepersecurity.com" +} +``` ### SDK Usage Example @@ -146,20 +183,17 @@ from keepersdk.vault import sqlite_storage, vault_online, vault_record # Initialize configuration and authentication context config = configuration.JsonConfigurationStorage() -server = 'keepersecurity.com' # can be set to keepersecurity.com, keepersecurity.com.au, keepersecurity.jp, keepersecurity.eu, keepersecurity.ca, govcloud.keepersecurity.us -keeper_endpoint = endpoint.KeeperEndpoint(config, keeper_server=server) +keeper_endpoint = endpoint.KeeperEndpoint(config) login_auth_context = login_auth.LoginAuth(keeper_endpoint) # Authenticate user -login_auth_context.login('username@company.com') - -# Complete password verification -# Note: In case 2fa and device approval flows fail, we can use verify password -# login_auth_context.login_step.verify_password('your_secure_password') +login_auth_context.login(config.get().users()[0].username, config.get().users()[0].password) while not login_auth_context.login_step.is_final(): if isinstance(login_auth_context.login_step, login_auth.LoginStepDeviceApproval): login_auth_context.login_step.send_push(login_auth.DeviceApprovalChannel.KeeperPush) + print("Device approval request sent. Login to existing vault/console or ask admin to approve this device and then press return/enter to resume") + input() elif isinstance(login_auth_context.login_step, login_auth.LoginStepPassword): password = getpass.getpass('Enter password: ') login_auth_context.login_step.verify_password(password) diff --git a/keepersdk-package/src/keepersdk/authentication/configuration.py b/keepersdk-package/src/keepersdk/authentication/configuration.py index f360af4f..1957555b 100644 --- a/keepersdk-package/src/keepersdk/authentication/configuration.py +++ b/keepersdk-package/src/keepersdk/authentication/configuration.py @@ -704,7 +704,7 @@ class JsonFileLoader(IJsonLoader): def __init__(self, file_name: Optional[str]=None) -> None: IJsonLoader.__init__(self) if not file_name: - file_name = 'authentication.json' + file_name = 'config.json' if os.path.isfile(file_name): self.file_path = os.path.abspath(file_name) else: @@ -712,6 +712,11 @@ def __init__(self, file_name: Optional[str]=None) -> None: if not os.path.exists(keeper_dir): os.mkdir(keeper_dir) self.file_path = os.path.join(keeper_dir, file_name) + + # Create the file if it doesn't exist with a blank JSON object + if not os.path.exists(self.file_path): + with open(self.file_path, 'w') as f: + json.dump({}, f) def load_json(self): with open(self.file_path, 'rb') as f: From bd00e2dd064217545ad388ee7946ba7fc8a5e468 Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Tue, 4 Nov 2025 15:57:12 +0530 Subject: [PATCH 2/6] Keeper SDK password generate implementation --- .../keepercli/commands/password_generate.py | 354 +++++++++++++++++ .../src/keepercli/helpers/password_utils.py | 375 ++++++++++++++++++ .../src/keepercli/register_commands.py | 3 +- 3 files changed, 731 insertions(+), 1 deletion(-) create mode 100644 keepercli-package/src/keepercli/commands/password_generate.py create mode 100644 keepercli-package/src/keepercli/helpers/password_utils.py diff --git a/keepercli-package/src/keepercli/commands/password_generate.py b/keepercli-package/src/keepercli/commands/password_generate.py new file mode 100644 index 00000000..532889e0 --- /dev/null +++ b/keepercli-package/src/keepercli/commands/password_generate.py @@ -0,0 +1,354 @@ +""" +Password generation command for Keeper CLI. + +This module provides the CLI interface for generating passwords with +optional BreachWatch scanning and various output formats. +""" + +import argparse +import json +from typing import Any, Optional, List, Dict + +try: + import pyperclip + CLIPBOARD_AVAILABLE = True +except ImportError: + CLIPBOARD_AVAILABLE = False + +# Constants +MAX_PASSWORD_COUNT = 1000 +MAX_PASSWORD_LENGTH = 256 +MAX_DICE_ROLLS = 40 +MIN_PASSWORD_COUNT = 1 +MIN_PASSWORD_LENGTH = 1 +MIN_DICE_ROLLS = 1 +DEFAULT_JSON_INDENT = 2 +COMPLEXITY_RULES_COUNT = 4 + +from . import base +from .. import api +from ..helpers.password_utils import ( + PasswordGenerationService, GenerationRequest, GeneratedPassword, + BreachStatus, PasswordStrength +) +from ..params import KeeperParams + +logger = api.get_logger() + + +class PasswordGenerateCommand(base.ArgparseCommand): + """Command for generating passwords with optional BreachWatch scanning.""" + + def __init__(self): + """Initialize the password generate command.""" + self.parser = argparse.ArgumentParser( + prog='generate', + description='Generate secure passwords with optional BreachWatch scanning' + ) + PasswordGenerateCommand.add_arguments_to_parser(self.parser) + super().__init__(self.parser) + + @staticmethod + def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: + """Add password generation arguments to parser.""" + # Output options + parser.add_argument('--clipboard', '-cc', dest='clipboard', action='store_true', + help='Copy generated passwords to clipboard') + parser.add_argument('--quiet', '-q', dest='quiet', action='store_true', + help='Only print password list (minimal output)') + parser.add_argument('--password-list', '-p', dest='password_list', action='store_true', + help='Include password list in addition to formatted output') + parser.add_argument('--output', '-o', dest='output_file', action='store', + help='Write output to specified file') + parser.add_argument('--format', '-f', dest='output_format', action='store', + choices=['table', 'json'], default='table', + help='Output format (default: table)') + parser.add_argument('--json-indent', '-i', dest='json_indent', action='store', type=int, default=DEFAULT_JSON_INDENT, + help='JSON format indent level (default: 2)') + + # Generation options + parser.add_argument('--number', '-n', dest='number', type=int, default=1, + help='Number of passwords to generate (default: 1)') + parser.add_argument('--no-breachwatch', '-nb', dest='no_breachwatch', action='store_true', + help='Skip BreachWatch scanning') + + # Random password options + random_group = parser.add_argument_group('Random Password Options') + random_group.add_argument('--length', dest='length', type=int, default=20, + help='Password length (default: 20)') + random_group.add_argument('--count', '-c', dest='length', type=int, metavar='LENGTH', + help='Length of password') + random_group.add_argument('--rules', '-r', dest='rules', action='store', + help='Complexity rules as comma-separated integers: uppercase,lowercase,digits,symbols') + random_group.add_argument('--symbols', '-s', dest='symbols', type=int, + help='Minimum number of symbol characters') + random_group.add_argument('--digits', '-d', dest='digits', type=int, + help='Minimum number of digit characters') + random_group.add_argument('--uppercase', '-u', dest='uppercase', type=int, + help='Minimum number of uppercase characters') + random_group.add_argument('--lowercase', '-l', dest='lowercase', type=int, + help='Minimum number of lowercase characters') + + # Special password types + special_group = parser.add_argument_group('Special Password Types') + special_group.add_argument('--crypto', dest='crypto', action='store_true', + help='Generate crypto-style strong password') + special_group.add_argument('--recoveryphrase', dest='recoveryphrase', action='store_true', + help='Generate 24-word recovery phrase') + + # Diceware options + diceware_group = parser.add_argument_group('Diceware Options') + diceware_group.add_argument('--dice-rolls', '-dr', dest='dice_rolls', type=int, + help='Number of dice rolls for diceware generation') + diceware_group.add_argument('--delimiter', '-dl', dest='delimiter', + choices=['-', '+', ':', '.', '/', '_', '=', ' '], default=' ', + help='Word delimiter for diceware (default: space)') + diceware_group.add_argument('--word-list', dest='word_list', + help='Path to custom word list file for diceware') + + def execute(self, context: KeeperParams, **kwargs) -> Any: + """Execute the password generation command.""" + try: + request = self._create_generation_request(**kwargs) + service = self._create_password_service(context, request) + passwords = self._generate_passwords(service, request) + self._output_results(passwords, **kwargs) + + except Exception as e: + logger.error(f"Password generation failed: {e}") + raise base.CommandError(f"Password generation failed: {e}") + + def _create_password_service(self, context: KeeperParams, request: GenerationRequest) -> PasswordGenerationService: + """Create password generation service with optional BreachWatch.""" + breach_watch = None + + if not request.enable_breach_scan: + logger.debug("BreachWatch scanning disabled by user") + elif context.vault and context.vault.breach_watch_plugin(): + breach_watch_plugin = context.vault.breach_watch_plugin() + breach_watch = breach_watch_plugin.breach_watch + logger.debug("Using BreachWatch for password scanning") + else: + logger.debug("BreachWatch not available (vault not initialized or feature not enabled)") + request.enable_breach_scan = False + + return PasswordGenerationService(breach_watch) + + def _generate_passwords(self, service: PasswordGenerationService, request: GenerationRequest) -> List[GeneratedPassword]: + """Generate passwords using the service.""" + if request.enable_breach_scan and service.breach_watch: + logger.info(f"Generating {request.count} password(s) with BreachWatch scanning...") + else: + logger.info(f"Generating {request.count} password(s)...") + + return service.generate_passwords(request) + + def _create_generation_request(self, **kwargs) -> GenerationRequest: + """Create a GenerationRequest from command line arguments.""" + count = self._validate_count(kwargs.get('number', 1)) + length = self._validate_length(kwargs.get('length', 20)) + algorithm = self._determine_algorithm(kwargs) + + symbols, digits, uppercase, lowercase = self._validate_complexity_parameters(kwargs) + rules = self._validate_rules(kwargs.get('rules')) + dice_rolls = self._validate_dice_rolls(kwargs.get('dice_rolls')) + + return GenerationRequest( + length=length, + count=count, + algorithm=algorithm, + symbols=symbols, + digits=digits, + uppercase=uppercase, + lowercase=lowercase, + rules=rules, + dice_rolls=dice_rolls, + delimiter=kwargs.get('delimiter', ' '), + word_list_file=kwargs.get('word_list'), + enable_breach_scan=not kwargs.get('no_breachwatch', False) + # max_breach_attempts uses GenerationRequest default value + ) + + def _validate_count(self, count: int) -> int: + """Validate password count parameter.""" + if count < MIN_PASSWORD_COUNT: + raise base.CommandError(f'Number of passwords must be at least {MIN_PASSWORD_COUNT}') + if count > MAX_PASSWORD_COUNT: + raise base.CommandError(f'Number of passwords cannot exceed {MAX_PASSWORD_COUNT}') + return count + + def _validate_length(self, length: int) -> int: + """Validate password length parameter.""" + if length < MIN_PASSWORD_LENGTH: + raise base.CommandError(f'Password length must be at least {MIN_PASSWORD_LENGTH}') + if length > MAX_PASSWORD_LENGTH: + raise base.CommandError(f'Password length cannot exceed {MAX_PASSWORD_LENGTH}') + return length + + def _determine_algorithm(self, kwargs: Dict[str, Any]) -> str: + """Determine password generation algorithm from arguments.""" + if kwargs.get('crypto'): + return 'crypto' + elif kwargs.get('recoveryphrase'): + return 'recovery' + elif kwargs.get('dice_rolls'): + return 'diceware' + else: + return 'random' # default + + def _validate_complexity_parameters(self, kwargs: Dict[str, Any]) -> tuple: + """Validate complexity parameters (symbols, digits, uppercase, lowercase).""" + symbols = kwargs.get('symbols') + digits = kwargs.get('digits') + uppercase = kwargs.get('uppercase') + lowercase = kwargs.get('lowercase') + + # Ensure complexity parameters are non-negative + for param_name, param_value in [('symbols', symbols), ('digits', digits), + ('uppercase', uppercase), ('lowercase', lowercase)]: + if param_value is not None and param_value < 0: + raise base.CommandError(f'{param_name.capitalize()} count cannot be negative') + + return symbols, digits, uppercase, lowercase + + def _validate_rules(self, rules: Optional[str]) -> Optional[str]: + """Validate complexity rules format.""" + if not rules: + return rules + + try: + rule_parts = [x.strip() for x in rules.split(',')] + if len(rule_parts) != COMPLEXITY_RULES_COUNT: + raise ValueError(f"Rules must have exactly {COMPLEXITY_RULES_COUNT} comma-separated values") + for part in rule_parts: + if not part.isdigit(): + raise ValueError("All rule values must be non-negative integers") + except ValueError as e: + raise base.CommandError(f'Invalid rules format: {e}. Expected format: "upper,lower,digits,symbols"') + + return rules + + def _validate_dice_rolls(self, dice_rolls: Optional[int]) -> Optional[int]: + """Validate diceware dice rolls parameter.""" + if dice_rolls is None: + return dice_rolls + + if dice_rolls < MIN_DICE_ROLLS: + raise base.CommandError(f'Dice rolls must be at least {MIN_DICE_ROLLS}') + if dice_rolls > MAX_DICE_ROLLS: + raise base.CommandError(f'Dice rolls cannot exceed {MAX_DICE_ROLLS}') + + return dice_rolls + + def _output_results(self, passwords: List[GeneratedPassword], **kwargs) -> None: + """Format and output the generated passwords.""" + output_format = kwargs.get('output_format', 'table') + quiet = kwargs.get('quiet', False) + password_list = kwargs.get('password_list', False) + output_file = kwargs.get('output_file') + clipboard = kwargs.get('clipboard', False) + + # Generate output + if quiet: + output = self._format_password_list(passwords) + elif output_format == 'json': + output = self._format_json(passwords, kwargs.get('json_indent', DEFAULT_JSON_INDENT)) + else: # table format + output = self._format_table(passwords) + + # Add password list if requested + if password_list and not quiet: + output += '\n\n' + self._format_password_list(passwords) + + # Handle clipboard + if clipboard: + if CLIPBOARD_AVAILABLE: + try: + pyperclip.copy(output) + logger.info("Generated passwords copied to clipboard") + except Exception as e: + logger.warning(f"Failed to copy to clipboard: {e}") + else: + logger.warning("Clipboard functionality not available. Install 'pyperclip' package.") + + # Output to file or console + if output_file: + try: + with open(output_file, 'w', encoding='utf-8') as f: + f.write(output) + logger.info(f"Output written to: {output_file}") + except Exception as e: + logger.error(f"Failed to write to file {output_file}: {e}") + raise base.CommandError('generate', f"File write error: {e}") + else: + print(output) + + def _format_table(self, passwords: List[GeneratedPassword]) -> str: + """Format passwords as a table with Keeper-style formatting.""" + if not passwords: + return "No passwords generated." + + lines = [] + + # Determine if BreachWatch column is needed + has_breach_info = any(pwd.breach_status is not None for pwd in passwords) + + # Add BreachWatch scan header if applicable + if has_breach_info: + scan_count = len([pwd for pwd in passwords if pwd.breach_status != BreachStatus.SKIPPED]) + if scan_count > 0: + lines.append(f"Breachwatch: {scan_count} password{'s' if scan_count != 1 else ''} to scan") + + # Column headers + if has_breach_info: + header = f" {'Strength(%)':<12} {'BreachWatch':<12} {'Password'}" + else: + header = f" {'Strength(%)':<12} {'Password'}" + lines.append(header) + + # Password rows + for i, pwd in enumerate(passwords, 1): + strength_display = str(pwd.strength_score) + + if has_breach_info: + breach_display = self._get_breach_display(pwd) + line = f"{i:<5}{strength_display:<12} {breach_display:<12} {pwd.password}" + else: + line = f"{i:<5}{strength_display:<12} {pwd.password}" + + lines.append(line) + + return '\n'.join(lines) + + def _format_json(self, passwords: List[GeneratedPassword], indent: int) -> str: + """Format passwords as JSON.""" + data = [] + for pwd in passwords: + entry = { + 'password': pwd.password, + 'strength': pwd.strength_score + } + + if pwd.breach_status is not None: + entry['breach_watch'] = self._get_breach_display(pwd) + + data.append(entry) + + return json.dumps(data, indent=indent if indent > 0 else None, ensure_ascii=False) + + def _format_password_list(self, passwords: List[GeneratedPassword]) -> str: + """Format as simple password list.""" + return '\n'.join(pwd.password for pwd in passwords) + + def _get_breach_display(self, password: GeneratedPassword) -> str: + """Get display string for breach status.""" + if password.breach_status == BreachStatus.PASSED: + return "Passed" + elif password.breach_status == BreachStatus.FAILED: + return "Failed" + elif password.breach_status == BreachStatus.SKIPPED: + return "Skipped" + elif password.breach_status == BreachStatus.ERROR: + return "Error" + else: + return "Unknown" diff --git a/keepercli-package/src/keepercli/helpers/password_utils.py b/keepercli-package/src/keepercli/helpers/password_utils.py new file mode 100644 index 00000000..048f9b77 --- /dev/null +++ b/keepercli-package/src/keepercli/helpers/password_utils.py @@ -0,0 +1,375 @@ +""" +Password generation utilities for Keeper CLI. + +This module provides password generation functionality with optional BreachWatch integration. +""" + +import dataclasses +from typing import Optional, List, Dict, Any, Union, Iterator, Tuple +from enum import Enum + +import os +import secrets + +from keepersdk import generator, utils + +# Constants +BREACHWATCH_MAX = 5 +DEFAULT_PASSWORD_LENGTH = 20 +DEFAULT_DICEWARE_ROLLS = 5 +RECOVERY_PHRASE_WORDS = 24 +STRENGTH_WEAK_THRESHOLD = 40 +STRENGTH_FAIR_THRESHOLD = 60 +STRENGTH_GOOD_THRESHOLD = 80 +CRYPTO_MIN_CHAR_RATIO = 6 # Minimum 1/6th of password length for each character type +DEFAULT_DICEWARE_FILE = 'diceware.wordlist.asc.txt' +RECOVERY_WORDLIST_FILE = 'bip-39.english.txt' + + +class CustomDicewareGenerator(generator.PasswordGenerator): + """Custom Diceware generator with delimiter support.""" + + def __init__(self, number_of_rolls: int, word_list_file: Optional[str] = None, delimiter: str = ' '): + self._number_of_rolls = number_of_rolls if number_of_rolls > 0 else DEFAULT_DICEWARE_ROLLS + self._delimiter = delimiter + self._vocabulary = self._load_word_list(word_list_file) + + def _load_word_list(self, word_list_file: Optional[str]) -> List[str]: + """Load and validate diceware word list from file.""" + dice_path = self._get_word_list_path(word_list_file) + + if not os.path.isfile(dice_path): + raise FileNotFoundError(f'Word list file "{dice_path}" not found.') + + return self._parse_word_list_file(dice_path) + + def _get_word_list_path(self, word_list_file: Optional[str]) -> str: + """Get the full path to the word list file.""" + if word_list_file: + dice_path = os.path.join(os.path.dirname(generator.__file__), 'resources', word_list_file) + if not os.path.isfile(dice_path): + dice_path = os.path.expanduser(word_list_file) + return dice_path + else: + return os.path.join(os.path.dirname(generator.__file__), 'resources', DEFAULT_DICEWARE_FILE) + + def _parse_word_list_file(self, dice_path: str) -> List[str]: + """Parse word list file and validate uniqueness.""" + vocabulary = [] + line_count = 0 + unique_words = set() + + with open(dice_path, 'r') as dw: + for line in dw.readlines(): + if not line or line.startswith('--'): + continue + + line_count += 1 + words = [x.strip() for x in line.split()] + word = words[1] if len(words) >= 2 else words[0] + vocabulary.append(word) + unique_words.add(word.lower()) + + if line_count != len(unique_words): + raise Exception(f'Word list file "{dice_path}" contains non-unique words.') + + return vocabulary + + def generate(self) -> str: + if not self._vocabulary: + raise Exception('Diceware word list was not loaded') + + words = [secrets.choice(self._vocabulary) for _ in range(self._number_of_rolls)] + self.shuffle(words) + return self._delimiter.join(words) + + +class PasswordStrength(Enum): + """Password strength levels.""" + WEAK = "WEAK" + FAIR = "FAIR" + GOOD = "GOOD" + STRONG = "STRONG" + + +class BreachStatus(Enum): + """BreachWatch scan results.""" + PASSED = "PASSED" + FAILED = "FAILED" + SKIPPED = "SKIPPED" + ERROR = "ERROR" + + +@dataclasses.dataclass +class GeneratedPassword: + """Data model for a generated password with analysis results.""" + password: str + strength_score: int + strength_level: PasswordStrength + breach_status: Optional[BreachStatus] = None + breach_details: Optional[str] = None + + +@dataclasses.dataclass +class GenerationRequest: + """Configuration for password generation request.""" + # Generation parameters + length: int = 20 + count: int = 1 + algorithm: str = 'random' # 'random', 'diceware', 'crypto', 'recovery' + + # Random password parameters + symbols: Optional[int] = None + digits: Optional[int] = None + uppercase: Optional[int] = None + lowercase: Optional[int] = None + rules: Optional[str] = None + + # Diceware parameters + dice_rolls: Optional[int] = None + delimiter: str = ' ' + word_list_file: Optional[str] = None + + # BreachWatch parameters + enable_breach_scan: bool = True + max_breach_attempts: int = BREACHWATCH_MAX + + +class PasswordGenerationService: + """ + Service for generating passwords with optional BreachWatch integration. + + This service provides a unified interface for password generation, + strength analysis, and breach detection. + """ + + def __init__(self, breach_watch=None): + """ + Initialize the password generation service. + + Args: + breach_watch: Optional BreachWatch instance for breach scanning + """ + self.breach_watch = breach_watch + + def generate_passwords(self, request: GenerationRequest) -> List[GeneratedPassword]: + """ + Generate passwords according to the provided request. + + Args: + request: Configuration for password generation + + Returns: + List of generated passwords with analysis results + + Raises: + ValueError: If generation parameters are invalid + """ + password_generator = self._create_generator(request) + + if not request.enable_breach_scan or not self.breach_watch: + return self._generate_passwords_without_breach_scan(password_generator, request.count) + else: + return self._generate_passwords_with_breach_scan(password_generator, request) + + def _generate_passwords_without_breach_scan(self, generator: generator.PasswordGenerator, count: int) -> List[GeneratedPassword]: + """Generate passwords with strength analysis only (no BreachWatch).""" + new_passwords = [generator.generate() for _ in range(count)] + return [self._analyze_password(p, enable_breach_scan=False) for p in new_passwords] + + def _generate_passwords_with_breach_scan(self, password_generator: generator.PasswordGenerator, request: GenerationRequest) -> List[GeneratedPassword]: + """Generate passwords with BreachWatch scanning and retry logic.""" + passwords = [] + breachwatch_count = 0 + + while len(passwords) < request.count: + new_passwords = [password_generator.generate() for _ in range(request.count - len(passwords))] + breachwatch_count += 1 + breachwatch_maxed = breachwatch_count >= request.max_breach_attempts + + try: + scanned_passwords = self._scan_passwords_for_breaches(new_passwords, breachwatch_maxed) + passwords.extend(scanned_passwords) + except Exception: + # BreachWatch failed - fallback to strength-only analysis + fallback_passwords = [self._analyze_password(p, enable_breach_scan=False) for p in new_passwords] + passwords.extend(fallback_passwords) + break + + return passwords[:request.count] + + def _scan_passwords_for_breaches(self, passwords_to_scan: List[str], accept_breached: bool) -> List[GeneratedPassword]: + """Scan passwords using BreachWatch and return analyzed results.""" + scanned_passwords = [] + euids_to_cleanup = [] + + try: + # Perform batch scan + for breach_result in self.breach_watch.scan_passwords(passwords_to_scan): + password = breach_result[0] + status = breach_result[1] if len(breach_result) > 1 else None + + # Collect EUIDs for cleanup + if status and hasattr(status, 'euid') and status.euid: + euids_to_cleanup.append(status.euid) + + # Process scan result + analyzed_password = self._process_breach_scan_result(password, status, accept_breached) + if analyzed_password: + scanned_passwords.append(analyzed_password) + finally: + # Always attempt cleanup (security requirement) + self._cleanup_breach_scan_euids(euids_to_cleanup) + + return scanned_passwords + + def _process_breach_scan_result(self, password: str, status: Any, accept_breached: bool) -> Optional[GeneratedPassword]: + """Process a single breach scan result and return analyzed password if acceptable.""" + strength_score = utils.password_score(password) + strength_level = self._get_strength_level(strength_score) + + if status and hasattr(status, 'breachDetected'): + if status.breachDetected: + # Password failed BreachWatch - only accept if maxed out attempts + if accept_breached: + return GeneratedPassword( + password=password, + strength_score=strength_score, + strength_level=strength_level, + breach_status=BreachStatus.FAILED + ) + return None # Reject breached password, retry + else: + # Password passed BreachWatch + return GeneratedPassword( + password=password, + strength_score=strength_score, + strength_level=strength_level, + breach_status=BreachStatus.PASSED + ) + else: + # BreachWatch error - treat as passed if maxed, otherwise retry + if accept_breached: + return GeneratedPassword( + password=password, + strength_score=strength_score, + strength_level=strength_level, + breach_status=BreachStatus.ERROR + ) + return None # Retry on error unless maxed out + + def _cleanup_breach_scan_euids(self, euids: List[str]) -> None: + """Clean up BreachWatch scan EUIDs (security requirement).""" + if euids and self.breach_watch: + try: + self.breach_watch.delete_euids(euids) + except Exception: + pass # Best effort cleanup + + def _create_generator(self, request: GenerationRequest) -> generator.PasswordGenerator: + """Create appropriate password generator based on request.""" + algorithm = request.algorithm.lower() + + if algorithm == 'crypto': + # Generate crypto-style strong password (high complexity) + crypto_length = request.length or DEFAULT_PASSWORD_LENGTH + # Distribute characters for high security (minimum 1 of each type) + min_each = max(1, crypto_length // CRYPTO_MIN_CHAR_RATIO) + return generator.KeeperPasswordGenerator( + length=crypto_length, + symbols=min_each, + digits=min_each, + caps=min_each, + lower=min_each + ) + elif algorithm == 'recovery': + return CustomDicewareGenerator( + RECOVERY_PHRASE_WORDS, word_list_file=RECOVERY_WORDLIST_FILE, delimiter=' ' + ) + elif algorithm == 'diceware': + dice_rolls = request.dice_rolls or DEFAULT_DICEWARE_ROLLS + return CustomDicewareGenerator( + dice_rolls, + word_list_file=request.word_list_file, + delimiter=request.delimiter + ) + else: # 'random' or default + # Handle rules-based generation (from original code) + if request.rules and all(i is None for i in (request.symbols, request.digits, request.uppercase, request.lowercase)): + kpg = generator.KeeperPasswordGenerator.create_from_rules(request.rules, request.length) + if kpg is None: + # Log warning and fall back to default (from original code) + return generator.KeeperPasswordGenerator(length=request.length) + return kpg + else: + # If rules provided with individual params, ignore rules (from original code) + return generator.KeeperPasswordGenerator( + length=request.length, + symbols=request.symbols, + digits=request.digits, + caps=request.uppercase, + lower=request.lowercase + ) + + def _analyze_password(self, password: str, enable_breach_scan: bool = True) -> GeneratedPassword: + """Analyze a single password for strength and breaches.""" + # Calculate strength + strength_score = utils.password_score(password) + strength_level = self._get_strength_level(strength_score) + + # Initialize breach status + breach_status = None + breach_details = None + + # Perform breach scan if enabled and available + if enable_breach_scan and self.breach_watch: + try: + scan_results = list(self.breach_watch.scan_passwords([password])) + if scan_results: + _, status = scan_results[0] + + # Clean up EUID if present + if status and hasattr(status, 'euid') and status.euid: + try: + self.breach_watch.delete_euids([status.euid]) + except Exception: + # Log but don't fail - cleanup is best effort + pass + + if status and hasattr(status, 'breachDetected'): + breach_status = ( + BreachStatus.FAILED if status.breachDetected + else BreachStatus.PASSED + ) + else: + breach_status = BreachStatus.ERROR + breach_details = "Scan result incomplete" + else: + breach_status = BreachStatus.ERROR + breach_details = "No scan results returned" + except Exception as e: + breach_status = BreachStatus.ERROR + breach_details = f"Scan failed: {str(e)}" + else: + breach_status = BreachStatus.SKIPPED + + return GeneratedPassword( + password=password, + strength_score=strength_score, + strength_level=strength_level, + breach_status=breach_status, + breach_details=breach_details + ) + + + @staticmethod + def _get_strength_level(score: int) -> PasswordStrength: + """Convert numeric score to strength level.""" + if score < STRENGTH_WEAK_THRESHOLD: + return PasswordStrength.WEAK + elif score < STRENGTH_FAIR_THRESHOLD: + return PasswordStrength.FAIR + elif score < STRENGTH_GOOD_THRESHOLD: + return PasswordStrength.GOOD + else: + return PasswordStrength.STRONG diff --git a/keepercli-package/src/keepercli/register_commands.py b/keepercli-package/src/keepercli/register_commands.py index fb7a5ec9..75633ea1 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -27,7 +27,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS if not scopes or bool(scopes & base.CommandScope.Vault): from .commands import (vault_folder, vault, vault_record, record_edit, importer_commands, breachwatch, record_type, secrets_manager, share_management, password_report, trash, record_file_report, - record_handling_commands, register) + record_handling_commands, register, password_generate) commands.register_command('sync-down', vault.SyncDownCommand(), base.CommandScope.Vault, 'd') commands.register_command('cd', vault_folder.FolderCdCommand(), base.CommandScope.Vault) @@ -58,6 +58,7 @@ def register_commands(commands: base.CliCommands, scopes: Optional[base.CommandS commands.register_command('file-report', record_file_report.RecordFileReportCommand(), base.CommandScope.Vault) commands.register_command('import', importer_commands.ImportCommand(), base.CommandScope.Vault) commands.register_command('export', importer_commands.ExportCommand(), base.CommandScope.Vault) + commands.register_command('generate', password_generate.PasswordGenerateCommand(), base.CommandScope.Vault, 'gen') commands.register_command('breachwatch', breachwatch.BreachWatchCommand(), base.CommandScope.Vault, 'bw') commands.register_command('password-report', password_report.PasswordReportCommand(), base.CommandScope.Vault) commands.register_command('record-type-add', record_type.RecordTypeAddCommand(), base.CommandScope.Vault) From 1b4ae13f94171f2b07af2465f4993ca242b15f2a Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Tue, 11 Nov 2025 00:47:54 +0530 Subject: [PATCH 3/6] Generate command review comment changes --- .../keepercli/commands/password_generate.py | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/keepercli-package/src/keepercli/commands/password_generate.py b/keepercli-package/src/keepercli/commands/password_generate.py index 532889e0..3b016ed2 100644 --- a/keepercli-package/src/keepercli/commands/password_generate.py +++ b/keepercli-package/src/keepercli/commands/password_generate.py @@ -9,11 +9,7 @@ import json from typing import Any, Optional, List, Dict -try: - import pyperclip - CLIPBOARD_AVAILABLE = True -except ImportError: - CLIPBOARD_AVAILABLE = False +import pyperclip # Constants MAX_PASSWORD_COUNT = 1000 @@ -108,9 +104,13 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: def execute(self, context: KeeperParams, **kwargs) -> Any: """Execute the password generation command.""" + # Verify vault is initialized + if not context.vault: + raise base.CommandError('Vault is not initialized. Please log in to initialize the vault.') + try: request = self._create_generation_request(**kwargs) - service = self._create_password_service(context, request) + service = self._create_password_service(context.vault, request) passwords = self._generate_passwords(service, request) self._output_results(passwords, **kwargs) @@ -118,18 +118,18 @@ def execute(self, context: KeeperParams, **kwargs) -> Any: logger.error(f"Password generation failed: {e}") raise base.CommandError(f"Password generation failed: {e}") - def _create_password_service(self, context: KeeperParams, request: GenerationRequest) -> PasswordGenerationService: + def _create_password_service(self, vault, request: GenerationRequest) -> PasswordGenerationService: """Create password generation service with optional BreachWatch.""" breach_watch = None if not request.enable_breach_scan: logger.debug("BreachWatch scanning disabled by user") - elif context.vault and context.vault.breach_watch_plugin(): - breach_watch_plugin = context.vault.breach_watch_plugin() + elif vault.breach_watch_plugin(): + breach_watch_plugin = vault.breach_watch_plugin() breach_watch = breach_watch_plugin.breach_watch logger.debug("Using BreachWatch for password scanning") else: - logger.debug("BreachWatch not available (vault not initialized or feature not enabled)") + logger.warning("BreachWatch plugin not available, enable it to use") request.enable_breach_scan = False return PasswordGenerationService(breach_watch) @@ -262,14 +262,11 @@ def _output_results(self, passwords: List[GeneratedPassword], **kwargs) -> None: # Handle clipboard if clipboard: - if CLIPBOARD_AVAILABLE: - try: - pyperclip.copy(output) - logger.info("Generated passwords copied to clipboard") - except Exception as e: - logger.warning(f"Failed to copy to clipboard: {e}") - else: - logger.warning("Clipboard functionality not available. Install 'pyperclip' package.") + try: + pyperclip.copy(output) + logger.info("Generated passwords copied to clipboard") + except Exception as e: + logger.warning(f"Failed to copy to clipboard: {e}") # Output to file or console if output_file: From db9aee6f4664853e6abd9c87a1a425b1cae0295d Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Tue, 11 Nov 2025 12:25:21 +0530 Subject: [PATCH 4/6] Added password generation examples and cleaned up code for consistency. --- .../password/advanced_password_generation.py | 235 +++++++++++ .../password/basic_password_generation.py | 166 ++++++++ .../comprehensive_password_generation.py | 387 ++++++++++++++++++ .../password/crypto_password_generation.py | 178 ++++++++ .../password/diceware_password_generation.py | 193 +++++++++ .../password/recovery_phrase_generation.py | 178 ++++++++ examples/wallet_backup.txt | 4 + .../keepercli/commands/password_generate.py | 18 +- .../src/keepercli/helpers/password_utils.py | 40 +- 9 files changed, 1350 insertions(+), 49 deletions(-) create mode 100644 examples/password/advanced_password_generation.py create mode 100644 examples/password/basic_password_generation.py create mode 100644 examples/password/comprehensive_password_generation.py create mode 100644 examples/password/crypto_password_generation.py create mode 100644 examples/password/diceware_password_generation.py create mode 100644 examples/password/recovery_phrase_generation.py create mode 100644 examples/wallet_backup.txt diff --git a/examples/password/advanced_password_generation.py b/examples/password/advanced_password_generation.py new file mode 100644 index 00000000..02347436 --- /dev/null +++ b/examples/password/advanced_password_generation.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def generate_advanced_passwords( + context: KeeperParams, + number: Optional[int] = None, + length: Optional[int] = None, + symbols: Optional[int] = None, + digits: Optional[int] = None, + uppercase: Optional[int] = None, + lowercase: Optional[int] = None, + rules: Optional[str] = None, + output_format: Optional[str] = None, + output_file: Optional[str] = None, + clipboard: Optional[bool] = None, + password_list: Optional[bool] = None, +): + """ + Generate advanced passwords with complexity rules and BreachWatch scanning. + + This function uses the Keeper CLI `PasswordGenerateCommand` to generate passwords + with specific complexity requirements, BreachWatch scanning, and various output options. + """ + try: + command = PasswordGenerateCommand() + + kwargs = { + 'number': number or 3, + 'length': length or 24, + 'symbols': symbols, + 'digits': digits, + 'uppercase': uppercase, + 'lowercase': lowercase, + 'rules': rules, + 'output_format': output_format or 'table', + 'output_file': output_file, + 'clipboard': clipboard or False, + 'password_list': password_list or False, + 'no_breachwatch': False, # Enable BreachWatch scanning + } + + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error generating advanced passwords: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate advanced passwords with complexity rules using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python advanced_password_generation.py + python advanced_password_generation.py --length 32 --symbols 4 --digits 4 + python advanced_password_generation.py --rules "3,3,3,3" --output passwords.json --format json + python advanced_password_generation.py --clipboard --password-list + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + parser.add_argument( + '-n', '--number', + type=int, + default=3, + help='Number of passwords to generate (default: 3)' + ) + parser.add_argument( + '-l', '--length', + type=int, + default=24, + help='Password length (default: 24)' + ) + parser.add_argument( + '-s', '--symbols', + type=int, + help='Minimum number of symbol characters' + ) + parser.add_argument( + '-d', '--digits', + type=int, + help='Minimum number of digit characters' + ) + parser.add_argument( + '-u', '--uppercase', + type=int, + help='Minimum number of uppercase characters' + ) + parser.add_argument( + '--lowercase', + type=int, + help='Minimum number of lowercase characters' + ) + parser.add_argument( + '-r', '--rules', + help='Complexity rules as comma-separated integers: uppercase,lowercase,digits,symbols' + ) + parser.add_argument( + '-f', '--format', + choices=['table', 'json'], + default='table', + help='Output format (default: table)' + ) + parser.add_argument( + '-o', '--output', + help='Write output to specified file' + ) + parser.add_argument( + '--clipboard', + action='store_true', + help='Copy generated passwords to clipboard' + ) + parser.add_argument( + '-p', '--password-list', + action='store_true', + help='Include password list in addition to formatted output' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # Set some defaults for better demo when no args provided + if not any([args.symbols, args.digits, args.uppercase, args.lowercase, args.rules]): + # If no complexity rules specified, use some defaults for demo + if args.length == 24 and args.number == 3: # Using defaults + args.symbols = 3 + args.digits = 3 + args.uppercase = 3 + args.lowercase = 3 + + context = None + try: + context = login_to_keeper_with_config(args.config) + + # Show what we're generating + using_defaults = (args.number == 3 and args.length == 24 and not args.rules and + not any([args.output, args.clipboard, args.password_list])) + + if using_defaults: + logger.info("Running with enhanced defaults - generating 3 advanced passwords of length 24") + logger.info("Complexity: 3+ symbols, 3+ digits, 3+ uppercase, 3+ lowercase") + logger.info("Use --help to see all available options") + else: + logger.info(f'Generating {args.number} advanced password(s)...') + logger.info(f'Length: {args.length}') + if args.symbols: logger.info(f'Minimum symbols: {args.symbols}') + if args.digits: logger.info(f'Minimum digits: {args.digits}') + if args.uppercase: logger.info(f'Minimum uppercase: {args.uppercase}') + if args.lowercase: logger.info(f'Minimum lowercase: {args.lowercase}') + if args.rules: logger.info(f'Complexity rules: {args.rules}') + + logger.info('BreachWatch scanning: Enabled') + + generate_advanced_passwords( + context, + number=args.number, + length=args.length, + symbols=args.symbols, + digits=args.digits, + uppercase=args.uppercase, + lowercase=args.lowercase, + rules=args.rules, + output_format=args.format, + output_file=args.output, + clipboard=args.clipboard, + password_list=args.password_list, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/password/basic_password_generation.py b/examples/password/basic_password_generation.py new file mode 100644 index 00000000..7d8e2858 --- /dev/null +++ b/examples/password/basic_password_generation.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def generate_basic_passwords( + context: KeeperParams, + number: Optional[int] = None, + length: Optional[int] = None, + output_format: Optional[str] = None, + quiet: Optional[bool] = None, + no_breachwatch: Optional[bool] = None, +): + """ + Generate basic random passwords. + + This function uses the Keeper CLI `PasswordGenerateCommand` to generate basic random passwords + with customizable length and count. + """ + try: + command = PasswordGenerateCommand() + + kwargs = { + 'number': number or 1, + 'length': length or 20, + 'output_format': output_format or 'table', + 'quiet': quiet or False, + 'no_breachwatch': no_breachwatch or False, + } + + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error generating passwords: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate basic passwords using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python basic_password_generation.py + python basic_password_generation.py --number 5 --length 16 + python basic_password_generation.py --quiet --no-breachwatch + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + parser.add_argument( + '-n', '--number', + type=int, + default=3, + help='Number of passwords to generate (default: 3)' + ) + parser.add_argument( + '-l', '--length', + type=int, + default=20, + help='Password length (default: 20)' + ) + parser.add_argument( + '-f', '--format', + choices=['table', 'json'], + default='table', + help='Output format (default: table)' + ) + parser.add_argument( + '-q', '--quiet', + action='store_true', + help='Only print password list (minimal output)' + ) + parser.add_argument( + '--no-breachwatch', + action='store_true', + help='Skip BreachWatch scanning' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + context = None + try: + context = login_to_keeper_with_config(args.config) + + # Show what we're generating if using defaults + if not any([args.number != 3, args.length != 20, args.format != 'table', args.quiet, args.no_breachwatch]): + logger.info("Running with default settings - generating 3 passwords of length 20 with BreachWatch scanning") + logger.info("Use --help to see all available options") + else: + logger.info(f'Generating {args.number} password(s) of length {args.length}...') + + generate_basic_passwords( + context, + number=args.number, + length=args.length, + output_format=args.format, + quiet=args.quiet, + no_breachwatch=args.no_breachwatch, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/password/comprehensive_password_generation.py b/examples/password/comprehensive_password_generation.py new file mode 100644 index 00000000..a5d65b10 --- /dev/null +++ b/examples/password/comprehensive_password_generation.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def demonstrate_all_password_types(context: KeeperParams): + """ + Demonstrate all available password generation types and features. + """ + command = PasswordGenerateCommand() + + print("\n" + "="*80) + print("COMPREHENSIVE PASSWORD GENERATION DEMONSTRATION") + print("="*80) + + # 1. Basic Random Passwords + print("\n1. BASIC RANDOM PASSWORDS (Default)") + print("-" * 40) + kwargs = { + 'number': 3, + 'length': 16, + 'output_format': 'table', + 'no_breachwatch': True, # Skip for demo speed + } + command.execute(context=context, **kwargs) + + # 2. Advanced Random with Complexity Rules + print("\n2. ADVANCED RANDOM WITH COMPLEXITY RULES") + print("-" * 40) + print("Rules: 3 uppercase, 3 lowercase, 3 digits, 2 symbols") + kwargs = { + 'number': 2, + 'length': 20, + 'uppercase': 3, + 'lowercase': 3, + 'digits': 3, + 'symbols': 2, + 'output_format': 'table', + 'no_breachwatch': True, + } + command.execute(context=context, **kwargs) + + # 3. Using Rules String Format + print("\n3. USING RULES STRING FORMAT") + print("-" * 40) + print("Rules string: '4,4,4,3' (uppercase,lowercase,digits,symbols)") + kwargs = { + 'number': 2, + 'length': 24, + 'rules': '4,4,4,3', + 'output_format': 'table', + 'no_breachwatch': True, + } + command.execute(context=context, **kwargs) + + # 4. Diceware Passwords + print("\n4. DICEWARE PASSWORDS") + print("-" * 40) + print("Using 6 dice rolls with space delimiter") + kwargs = { + 'number': 3, + 'dice_rolls': 6, + 'delimiter': ' ', + 'output_format': 'table', + 'no_breachwatch': True, + } + command.execute(context=context, **kwargs) + + # 5. Diceware with Different Delimiter + print("\n5. DICEWARE WITH DASH DELIMITER") + print("-" * 40) + print("Using 5 dice rolls with dash delimiter") + kwargs = { + 'number': 2, + 'dice_rolls': 5, + 'delimiter': '-', + 'output_format': 'table', + 'no_breachwatch': True, + } + command.execute(context=context, **kwargs) + + # 6. Crypto-style Passwords + print("\n6. CRYPTO-STYLE PASSWORDS") + print("-" * 40) + print("High-entropy passwords for cryptocurrency applications") + kwargs = { + 'crypto': True, + 'number': 2, + 'output_format': 'table', + 'no_breachwatch': True, + } + command.execute(context=context, **kwargs) + + # 7. Recovery Phrases + print("\n7. RECOVERY PHRASES (24-word)") + print("-" * 40) + print("Mnemonic phrases for wallet recovery") + kwargs = { + 'recoveryphrase': True, + 'number': 1, + 'output_format': 'table', + 'no_breachwatch': True, + } + command.execute(context=context, **kwargs) + + # 8. JSON Output Format + print("\n8. JSON OUTPUT FORMAT") + print("-" * 40) + print("Same data in JSON format with indentation") + kwargs = { + 'number': 2, + 'length': 16, + 'output_format': 'json', + 'json_indent': 2, + 'no_breachwatch': True, + } + command.execute(context=context, **kwargs) + + # 9. With BreachWatch Scanning (if available) + print("\n9. WITH BREACHWATCH SCANNING") + print("-" * 40) + print("Scanning passwords against known breaches") + kwargs = { + 'number': 2, + 'length': 16, + 'output_format': 'table', + 'no_breachwatch': False, # Enable BreachWatch + } + try: + command.execute(context=context, **kwargs) + except Exception as e: + logger.warning(f"BreachWatch scanning failed: {e}") + logger.info("This may occur if BreachWatch is not enabled or configured") + + print("\n" + "="*80) + print("DEMONSTRATION COMPLETE") + print("="*80) + +def generate_custom_passwords( + context: KeeperParams, + password_type: str, + **kwargs +): + """ + Generate passwords based on specified type and parameters. + """ + try: + command = PasswordGenerateCommand() + + # Set default parameters based on type + if password_type == 'basic': + default_kwargs = { + 'number': 3, + 'length': 20, + 'output_format': 'table', + } + elif password_type == 'advanced': + default_kwargs = { + 'number': 3, + 'length': 24, + 'symbols': 3, + 'digits': 3, + 'uppercase': 3, + 'lowercase': 3, + 'output_format': 'table', + } + elif password_type == 'diceware': + default_kwargs = { + 'number': 3, + 'dice_rolls': 6, + 'delimiter': ' ', + 'output_format': 'table', + } + elif password_type == 'crypto': + default_kwargs = { + 'crypto': True, + 'number': 3, + 'output_format': 'table', + } + elif password_type == 'recovery': + default_kwargs = { + 'recoveryphrase': True, + 'number': 2, + 'output_format': 'table', + } + else: + raise ValueError(f"Unknown password type: {password_type}") + + # Merge user parameters with defaults + final_kwargs = {**default_kwargs, **kwargs} + + command.execute(context=context, **final_kwargs) + return True + + except Exception as e: + logger.error(f'Error generating {password_type} passwords: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Comprehensive password generation example using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python comprehensive_password_generation.py --demo + python comprehensive_password_generation.py --type basic --number 5 --length 16 + python comprehensive_password_generation.py --type advanced --symbols 4 --digits 4 + python comprehensive_password_generation.py --type diceware --dice-rolls 8 --delimiter "-" + python comprehensive_password_generation.py --type crypto --number 3 --format json + python comprehensive_password_generation.py --type recovery --output recovery.txt + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + parser.add_argument( + '--demo', + action='store_true', + help='Run comprehensive demonstration of all password types' + ) + parser.add_argument( + '--type', + choices=['basic', 'advanced', 'diceware', 'crypto', 'recovery'], + help='Type of password to generate' + ) + parser.add_argument( + '-n', '--number', + type=int, + help='Number of passwords to generate' + ) + parser.add_argument( + '-l', '--length', + type=int, + help='Password length (for basic/advanced types)' + ) + parser.add_argument( + '--symbols', + type=int, + help='Minimum number of symbol characters' + ) + parser.add_argument( + '--digits', + type=int, + help='Minimum number of digit characters' + ) + parser.add_argument( + '--uppercase', + type=int, + help='Minimum number of uppercase characters' + ) + parser.add_argument( + '--lowercase', + type=int, + help='Minimum number of lowercase characters' + ) + parser.add_argument( + '--dice-rolls', + type=int, + help='Number of dice rolls for diceware generation' + ) + parser.add_argument( + '--delimiter', + choices=['-', '+', ':', '.', '/', '_', '=', ' '], + help='Word delimiter for diceware' + ) + parser.add_argument( + '-f', '--format', + choices=['table', 'json'], + default='table', + help='Output format (default: table)' + ) + parser.add_argument( + '-o', '--output', + help='Write output to specified file' + ) + parser.add_argument( + '--no-breachwatch', + action='store_true', + help='Skip BreachWatch scanning' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + # If no arguments provided, default to demo mode + if not args.demo and not args.type: + args.demo = True + logger.info("No arguments provided, running comprehensive demonstration") + logger.info("Use --help to see all available options") + + context = None + try: + context = login_to_keeper_with_config(args.config) + + if args.demo: + demonstrate_all_password_types(context) + else: + # Build kwargs from command line arguments + kwargs = { + 'output_format': args.format, + 'no_breachwatch': args.no_breachwatch, + } + + if args.number: + kwargs['number'] = args.number + if args.length: + kwargs['length'] = args.length + if args.symbols: + kwargs['symbols'] = args.symbols + if args.digits: + kwargs['digits'] = args.digits + if args.uppercase: + kwargs['uppercase'] = args.uppercase + if args.lowercase: + kwargs['lowercase'] = args.lowercase + if args.dice_rolls: + kwargs['dice_rolls'] = args.dice_rolls + if args.delimiter: + kwargs['delimiter'] = args.delimiter + if args.output: + kwargs['output_file'] = args.output + + generate_custom_passwords(context, args.type, **kwargs) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/password/crypto_password_generation.py b/examples/password/crypto_password_generation.py new file mode 100644 index 00000000..71c5577b --- /dev/null +++ b/examples/password/crypto_password_generation.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def generate_crypto_passwords( + context: KeeperParams, + number: Optional[int] = None, + output_format: Optional[str] = None, + output_file: Optional[str] = None, + clipboard: Optional[bool] = None, + password_list: Optional[bool] = None, + no_breachwatch: Optional[bool] = None, +): + """ + Generate crypto-style strong passwords. + + This function uses the Keeper CLI `PasswordGenerateCommand` to generate crypto-style passwords + that are optimized for high security applications like cryptocurrency wallets. + """ + try: + command = PasswordGenerateCommand() + + kwargs = { + 'crypto': True, + 'number': number or 3, + 'output_format': output_format or 'table', + 'output_file': output_file, + 'clipboard': clipboard or False, + 'password_list': password_list or False, + 'no_breachwatch': no_breachwatch or False, + } + + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error generating crypto passwords: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate crypto-style passwords using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python crypto_password_generation.py + python crypto_password_generation.py --number 5 + python crypto_password_generation.py --clipboard --format json + python crypto_password_generation.py --output crypto_passwords.txt --password-list + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + parser.add_argument( + '-n', '--number', + type=int, + default=3, + help='Number of passwords to generate (default: 3)' + ) + parser.add_argument( + '-f', '--format', + choices=['table', 'json'], + default='table', + help='Output format (default: table)' + ) + parser.add_argument( + '-o', '--output', + help='Write output to specified file' + ) + parser.add_argument( + '--clipboard', + action='store_true', + help='Copy generated passwords to clipboard' + ) + parser.add_argument( + '-p', '--password-list', + action='store_true', + help='Include password list in addition to formatted output' + ) + parser.add_argument( + '--no-breachwatch', + action='store_true', + help='Skip BreachWatch scanning' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + context = None + try: + context = login_to_keeper_with_config(args.config) + + using_defaults = (args.number == 3 and args.format == 'table' and + not any([args.output, args.clipboard, args.password_list, args.no_breachwatch])) + + if using_defaults: + logger.info("Running with default settings - generating 3 crypto-style passwords") + logger.info("These passwords are optimized for high-security applications like cryptocurrency") + logger.info("Use --help to see all available options") + else: + logger.info(f'Generating {args.number} crypto-style password(s)...') + logger.info('These passwords are optimized for high-security applications') + + generate_crypto_passwords( + context, + number=args.number, + output_format=args.format, + output_file=args.output, + clipboard=args.clipboard, + password_list=args.password_list, + no_breachwatch=args.no_breachwatch, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/password/diceware_password_generation.py b/examples/password/diceware_password_generation.py new file mode 100644 index 00000000..e260ce51 --- /dev/null +++ b/examples/password/diceware_password_generation.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def generate_diceware_passwords( + context: KeeperParams, + number: Optional[int] = None, + dice_rolls: Optional[int] = None, + delimiter: Optional[str] = None, + word_list: Optional[str] = None, + output_format: Optional[str] = None, + quiet: Optional[bool] = None, + no_breachwatch: Optional[bool] = None, +): + """ + Generate diceware passwords. + + This function uses the Keeper CLI `PasswordGenerateCommand` to generate diceware-style passwords + using dice rolls to select words from a word list. + """ + try: + command = PasswordGenerateCommand() + + kwargs = { + 'number': number or 3, + 'dice_rolls': dice_rolls or 6, + 'delimiter': delimiter or ' ', + 'word_list': word_list, + 'output_format': output_format or 'table', + 'quiet': quiet or False, + 'no_breachwatch': no_breachwatch or False, + } + + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error generating diceware passwords: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate diceware passwords using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python diceware_password_generation.py + python diceware_password_generation.py --dice-rolls 8 --delimiter "-" + python diceware_password_generation.py --word-list custom_words.txt --quiet + python diceware_password_generation.py --number 5 --delimiter "_" --format json + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + parser.add_argument( + '-n', '--number', + type=int, + default=3, + help='Number of passwords to generate (default: 3)' + ) + parser.add_argument( + '--dice-rolls', + type=int, + default=6, + help='Number of dice rolls for diceware generation (default: 6)' + ) + parser.add_argument( + '--delimiter', + choices=['-', '+', ':', '.', '/', '_', '=', ' '], + default=' ', + help='Word delimiter for diceware (default: space)' + ) + parser.add_argument( + '--word-list', + help='Path to custom word list file for diceware' + ) + parser.add_argument( + '-f', '--format', + choices=['table', 'json'], + default='table', + help='Output format (default: table)' + ) + parser.add_argument( + '-q', '--quiet', + action='store_true', + help='Only print password list (minimal output)' + ) + parser.add_argument( + '--no-breachwatch', + action='store_true', + help='Skip BreachWatch scanning' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + context = None + try: + context = login_to_keeper_with_config(args.config) + + # Show what we're generating + using_defaults = (args.number == 3 and args.dice_rolls == 6 and args.delimiter == ' ' and + not args.word_list and args.format == 'table' and not args.quiet and not args.no_breachwatch) + + if using_defaults: + logger.info("Running with default settings - generating 3 diceware passwords") + logger.info("Using 6 dice rolls with space delimiter and default word list") + logger.info("Use --help to see all available options") + else: + logger.info(f'Generating {args.number} diceware password(s)...') + logger.info(f'Dice rolls: {args.dice_rolls}') + logger.info(f'Word delimiter: "{args.delimiter}"') + if args.word_list: + logger.info(f'Custom word list: {args.word_list}') + else: + logger.info('Using default word list') + + generate_diceware_passwords( + context, + number=args.number, + dice_rolls=args.dice_rolls, + delimiter=args.delimiter, + word_list=args.word_list, + output_format=args.format, + quiet=args.quiet, + no_breachwatch=args.no_breachwatch, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/password/recovery_phrase_generation.py b/examples/password/recovery_phrase_generation.py new file mode 100644 index 00000000..9518785b --- /dev/null +++ b/examples/password/recovery_phrase_generation.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' KeeperParams: + """ + Login to Keeper with a configuration file. + + This function logs in to Keeper using the provided configuration file. + It reads the configuration file, extracts the username, + and returns a Authenticated KeeperParams Context object. + """ + if not os.path.exists(filename): + raise FileNotFoundError(f'Config file {filename} not found') + with open(filename, 'r') as f: + config_data = json.load(f) + username = config_data.get('user', config_data.get('username')) + password = config_data.get('password', '') + if not username: + raise ValueError('Username not found in config file') + context = KeeperParams(config_filename=filename, config=config_data) + if username: + context.username = username + if password: + context.password = password + logged_in = LoginFlow.login(context, username=username, password=password or None, resume_session=bool(username)) + if not logged_in: + raise Exception('Failed to authenticate with Keeper') + return context + +def generate_recovery_phrases( + context: KeeperParams, + number: Optional[int] = None, + output_format: Optional[str] = None, + output_file: Optional[str] = None, + clipboard: Optional[bool] = None, + password_list: Optional[bool] = None, + no_breachwatch: Optional[bool] = None, +): + """ + Generate 24-word recovery phrases. + + This function uses the Keeper CLI `PasswordGenerateCommand` to generate recovery phrases + suitable for cryptocurrency wallets and other applications requiring mnemonic phrases. + """ + try: + command = PasswordGenerateCommand() + + kwargs = { + 'recoveryphrase': True, + 'number': number or 2, + 'output_format': output_format or 'table', + 'output_file': output_file, + 'clipboard': clipboard or False, + 'password_list': password_list or False, + 'no_breachwatch': no_breachwatch or True, # Recovery phrases usually skip BreachWatch + } + + command.execute(context=context, **kwargs) + return True + + except Exception as e: + logger.error(f'Error generating recovery phrases: {str(e)}') + return False + + +if __name__ == '__main__': + parser = argparse.ArgumentParser( + description='Generate 24-word recovery phrases using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python recovery_phrase_generation.py + python recovery_phrase_generation.py --number 3 + python recovery_phrase_generation.py --output recovery_phrases.txt --password-list + python recovery_phrase_generation.py --clipboard --format json + ''' + ) + + parser.add_argument( + '-c', '--config', + default='myconfig.json', + help='Configuration file (default: myconfig.json)' + ) + parser.add_argument( + '-n', '--number', + type=int, + default=2, + help='Number of recovery phrases to generate (default: 2)' + ) + parser.add_argument( + '-f', '--format', + choices=['table', 'json'], + default='table', + help='Output format (default: table)' + ) + parser.add_argument( + '-o', '--output', + help='Write output to specified file' + ) + parser.add_argument( + '--clipboard', + action='store_true', + help='Copy generated phrases to clipboard' + ) + parser.add_argument( + '-p', '--password-list', + action='store_true', + help='Include phrase list in addition to formatted output' + ) + parser.add_argument( + '--enable-breachwatch', + action='store_true', + help='Enable BreachWatch scanning (disabled by default for recovery phrases)' + ) + + args = parser.parse_args() + + if not os.path.exists(args.config): + logger.error(f'Config file {args.config} not found') + sys.exit(1) + + context = None + try: + context = login_to_keeper_with_config(args.config) + + using_defaults = (args.number == 2 and args.format == 'table' and + not any([args.output, args.clipboard, args.password_list, args.enable_breachwatch])) + + if using_defaults: + logger.info("Running with default settings - generating 2 recovery phrases") + logger.info("These are 24-word phrases suitable for cryptocurrency wallet recovery") + logger.info("Use --help to see all available options") + else: + logger.info(f'Generating {args.number} recovery phrase(s)...') + logger.info('These are 24-word phrases suitable for cryptocurrency wallets') + + generate_recovery_phrases( + context, + number=args.number, + output_format=args.format, + output_file=args.output, + clipboard=args.clipboard, + password_list=args.password_list, + no_breachwatch=not args.enable_breachwatch, + ) + + except Exception as e: + logger.error(f'Error: {str(e)}') + sys.exit(1) + finally: + if context: + context.clear_session() diff --git a/examples/wallet_backup.txt b/examples/wallet_backup.txt new file mode 100644 index 00000000..27bcc839 --- /dev/null +++ b/examples/wallet_backup.txt @@ -0,0 +1,4 @@ + Strength(%) BreachWatch Password +1 100 Skipped helmet prosper artefact paddle style cannon slow left original logic hawk loyal middle sugar enable carbon asset hood patient apple ability seminar fold family +2 100 Skipped diet join choose indoor sponsor use item suggest front hour feature tribe butter kitchen give art awkward seven negative refuse man wise act collect +3 100 Skipped canyon boss vibrant share suspect ladder mimic spring filter scene level chuckle predict hood vintage claim train draw smoke pave neutral possible jungle enable \ No newline at end of file diff --git a/keepercli-package/src/keepercli/commands/password_generate.py b/keepercli-package/src/keepercli/commands/password_generate.py index 3b016ed2..bf7a192d 100644 --- a/keepercli-package/src/keepercli/commands/password_generate.py +++ b/keepercli-package/src/keepercli/commands/password_generate.py @@ -11,7 +11,6 @@ import pyperclip -# Constants MAX_PASSWORD_COUNT = 1000 MAX_PASSWORD_LENGTH = 256 MAX_DICE_ROLLS = 40 @@ -47,7 +46,6 @@ def __init__(self): @staticmethod def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: """Add password generation arguments to parser.""" - # Output options parser.add_argument('--clipboard', '-cc', dest='clipboard', action='store_true', help='Copy generated passwords to clipboard') parser.add_argument('--quiet', '-q', dest='quiet', action='store_true', @@ -62,13 +60,11 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument('--json-indent', '-i', dest='json_indent', action='store', type=int, default=DEFAULT_JSON_INDENT, help='JSON format indent level (default: 2)') - # Generation options parser.add_argument('--number', '-n', dest='number', type=int, default=1, help='Number of passwords to generate (default: 1)') parser.add_argument('--no-breachwatch', '-nb', dest='no_breachwatch', action='store_true', help='Skip BreachWatch scanning') - # Random password options random_group = parser.add_argument_group('Random Password Options') random_group.add_argument('--length', dest='length', type=int, default=20, help='Password length (default: 20)') @@ -85,14 +81,12 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: random_group.add_argument('--lowercase', '-l', dest='lowercase', type=int, help='Minimum number of lowercase characters') - # Special password types special_group = parser.add_argument_group('Special Password Types') special_group.add_argument('--crypto', dest='crypto', action='store_true', help='Generate crypto-style strong password') special_group.add_argument('--recoveryphrase', dest='recoveryphrase', action='store_true', help='Generate 24-word recovery phrase') - # Diceware options diceware_group = parser.add_argument_group('Diceware Options') diceware_group.add_argument('--dice-rolls', '-dr', dest='dice_rolls', type=int, help='Number of dice rolls for diceware generation') @@ -104,7 +98,6 @@ def add_arguments_to_parser(parser: argparse.ArgumentParser) -> None: def execute(self, context: KeeperParams, **kwargs) -> Any: """Execute the password generation command.""" - # Verify vault is initialized if not context.vault: raise base.CommandError('Vault is not initialized. Please log in to initialize the vault.') @@ -248,19 +241,16 @@ def _output_results(self, passwords: List[GeneratedPassword], **kwargs) -> None: output_file = kwargs.get('output_file') clipboard = kwargs.get('clipboard', False) - # Generate output if quiet: output = self._format_password_list(passwords) elif output_format == 'json': output = self._format_json(passwords, kwargs.get('json_indent', DEFAULT_JSON_INDENT)) - else: # table format + else: output = self._format_table(passwords) - # Add password list if requested if password_list and not quiet: output += '\n\n' + self._format_password_list(passwords) - # Handle clipboard if clipboard: try: pyperclip.copy(output) @@ -268,7 +258,6 @@ def _output_results(self, passwords: List[GeneratedPassword], **kwargs) -> None: except Exception as e: logger.warning(f"Failed to copy to clipboard: {e}") - # Output to file or console if output_file: try: with open(output_file, 'w', encoding='utf-8') as f: @@ -287,23 +276,18 @@ def _format_table(self, passwords: List[GeneratedPassword]) -> str: lines = [] - # Determine if BreachWatch column is needed has_breach_info = any(pwd.breach_status is not None for pwd in passwords) - # Add BreachWatch scan header if applicable if has_breach_info: scan_count = len([pwd for pwd in passwords if pwd.breach_status != BreachStatus.SKIPPED]) if scan_count > 0: lines.append(f"Breachwatch: {scan_count} password{'s' if scan_count != 1 else ''} to scan") - # Column headers if has_breach_info: header = f" {'Strength(%)':<12} {'BreachWatch':<12} {'Password'}" else: header = f" {'Strength(%)':<12} {'Password'}" lines.append(header) - - # Password rows for i, pwd in enumerate(passwords, 1): strength_display = str(pwd.strength_score) diff --git a/keepercli-package/src/keepercli/helpers/password_utils.py b/keepercli-package/src/keepercli/helpers/password_utils.py index 048f9b77..f1d723ab 100644 --- a/keepercli-package/src/keepercli/helpers/password_utils.py +++ b/keepercli-package/src/keepercli/helpers/password_utils.py @@ -13,7 +13,6 @@ from keepersdk import generator, utils -# Constants BREACHWATCH_MAX = 5 DEFAULT_PASSWORD_LENGTH = 20 DEFAULT_DICEWARE_ROLLS = 5 @@ -21,7 +20,7 @@ STRENGTH_WEAK_THRESHOLD = 40 STRENGTH_FAIR_THRESHOLD = 60 STRENGTH_GOOD_THRESHOLD = 80 -CRYPTO_MIN_CHAR_RATIO = 6 # Minimum 1/6th of password length for each character type +CRYPTO_MIN_CHAR_RATIO = 6 DEFAULT_DICEWARE_FILE = 'diceware.wordlist.asc.txt' RECOVERY_WORDLIST_FILE = 'bip-39.english.txt' @@ -113,24 +112,20 @@ class GeneratedPassword: @dataclasses.dataclass class GenerationRequest: """Configuration for password generation request.""" - # Generation parameters length: int = 20 count: int = 1 - algorithm: str = 'random' # 'random', 'diceware', 'crypto', 'recovery' + algorithm: str = 'random' - # Random password parameters symbols: Optional[int] = None digits: Optional[int] = None uppercase: Optional[int] = None lowercase: Optional[int] = None rules: Optional[str] = None - # Diceware parameters dice_rolls: Optional[int] = None delimiter: str = ' ' word_list_file: Optional[str] = None - # BreachWatch parameters enable_breach_scan: bool = True max_breach_attempts: int = BREACHWATCH_MAX @@ -173,7 +168,7 @@ def generate_passwords(self, request: GenerationRequest) -> List[GeneratedPasswo return self._generate_passwords_with_breach_scan(password_generator, request) def _generate_passwords_without_breach_scan(self, generator: generator.PasswordGenerator, count: int) -> List[GeneratedPassword]: - """Generate passwords with strength analysis only (no BreachWatch).""" + """Generate passwords with strength analysis only.""" new_passwords = [generator.generate() for _ in range(count)] return [self._analyze_password(p, enable_breach_scan=False) for p in new_passwords] @@ -191,7 +186,6 @@ def _generate_passwords_with_breach_scan(self, password_generator: generator.Pas scanned_passwords = self._scan_passwords_for_breaches(new_passwords, breachwatch_maxed) passwords.extend(scanned_passwords) except Exception: - # BreachWatch failed - fallback to strength-only analysis fallback_passwords = [self._analyze_password(p, enable_breach_scan=False) for p in new_passwords] passwords.extend(fallback_passwords) break @@ -204,21 +198,17 @@ def _scan_passwords_for_breaches(self, passwords_to_scan: List[str], accept_brea euids_to_cleanup = [] try: - # Perform batch scan for breach_result in self.breach_watch.scan_passwords(passwords_to_scan): password = breach_result[0] status = breach_result[1] if len(breach_result) > 1 else None - # Collect EUIDs for cleanup if status and hasattr(status, 'euid') and status.euid: euids_to_cleanup.append(status.euid) - # Process scan result analyzed_password = self._process_breach_scan_result(password, status, accept_breached) if analyzed_password: scanned_passwords.append(analyzed_password) finally: - # Always attempt cleanup (security requirement) self._cleanup_breach_scan_euids(euids_to_cleanup) return scanned_passwords @@ -230,7 +220,6 @@ def _process_breach_scan_result(self, password: str, status: Any, accept_breache if status and hasattr(status, 'breachDetected'): if status.breachDetected: - # Password failed BreachWatch - only accept if maxed out attempts if accept_breached: return GeneratedPassword( password=password, @@ -238,9 +227,8 @@ def _process_breach_scan_result(self, password: str, status: Any, accept_breache strength_level=strength_level, breach_status=BreachStatus.FAILED ) - return None # Reject breached password, retry + return None else: - # Password passed BreachWatch return GeneratedPassword( password=password, strength_score=strength_score, @@ -248,7 +236,6 @@ def _process_breach_scan_result(self, password: str, status: Any, accept_breache breach_status=BreachStatus.PASSED ) else: - # BreachWatch error - treat as passed if maxed, otherwise retry if accept_breached: return GeneratedPassword( password=password, @@ -256,24 +243,22 @@ def _process_breach_scan_result(self, password: str, status: Any, accept_breache strength_level=strength_level, breach_status=BreachStatus.ERROR ) - return None # Retry on error unless maxed out + return None def _cleanup_breach_scan_euids(self, euids: List[str]) -> None: - """Clean up BreachWatch scan EUIDs (security requirement).""" + """Clean up BreachWatch scan EUIDs.""" if euids and self.breach_watch: try: self.breach_watch.delete_euids(euids) except Exception: - pass # Best effort cleanup + pass def _create_generator(self, request: GenerationRequest) -> generator.PasswordGenerator: """Create appropriate password generator based on request.""" algorithm = request.algorithm.lower() if algorithm == 'crypto': - # Generate crypto-style strong password (high complexity) crypto_length = request.length or DEFAULT_PASSWORD_LENGTH - # Distribute characters for high security (minimum 1 of each type) min_each = max(1, crypto_length // CRYPTO_MIN_CHAR_RATIO) return generator.KeeperPasswordGenerator( length=crypto_length, @@ -293,16 +278,13 @@ def _create_generator(self, request: GenerationRequest) -> generator.PasswordGen word_list_file=request.word_list_file, delimiter=request.delimiter ) - else: # 'random' or default - # Handle rules-based generation (from original code) + else: if request.rules and all(i is None for i in (request.symbols, request.digits, request.uppercase, request.lowercase)): kpg = generator.KeeperPasswordGenerator.create_from_rules(request.rules, request.length) if kpg is None: - # Log warning and fall back to default (from original code) return generator.KeeperPasswordGenerator(length=request.length) return kpg else: - # If rules provided with individual params, ignore rules (from original code) return generator.KeeperPasswordGenerator( length=request.length, symbols=request.symbols, @@ -313,27 +295,21 @@ def _create_generator(self, request: GenerationRequest) -> generator.PasswordGen def _analyze_password(self, password: str, enable_breach_scan: bool = True) -> GeneratedPassword: """Analyze a single password for strength and breaches.""" - # Calculate strength strength_score = utils.password_score(password) strength_level = self._get_strength_level(strength_score) - # Initialize breach status breach_status = None breach_details = None - - # Perform breach scan if enabled and available if enable_breach_scan and self.breach_watch: try: scan_results = list(self.breach_watch.scan_passwords([password])) if scan_results: _, status = scan_results[0] - # Clean up EUID if present if status and hasattr(status, 'euid') and status.euid: try: self.breach_watch.delete_euids([status.euid]) except Exception: - # Log but don't fail - cleanup is best effort pass if status and hasattr(status, 'breachDetected'): From 23d998f62cae771468bde72f330e7883784aae9e Mon Sep 17 00:00:00 2001 From: ukumar-ks Date: Tue, 11 Nov 2025 17:59:31 +0530 Subject: [PATCH 5/6] Input argument changes in example --- .../password/advanced_password_generation.py | 137 +----------------- .../password/basic_password_generation.py | 68 +-------- .../password/crypto_password_generation.py | 79 +--------- .../password/diceware_password_generation.py | 100 +------------ .../password/recovery_phrase_generation.py | 79 +--------- 5 files changed, 29 insertions(+), 434 deletions(-) diff --git a/examples/password/advanced_password_generation.py b/examples/password/advanced_password_generation.py index 02347436..e8235d1a 100644 --- a/examples/password/advanced_password_generation.py +++ b/examples/password/advanced_password_generation.py @@ -53,45 +53,16 @@ def login_to_keeper_with_config(filename: str) -> KeeperParams: raise Exception('Failed to authenticate with Keeper') return context -def generate_advanced_passwords( - context: KeeperParams, - number: Optional[int] = None, - length: Optional[int] = None, - symbols: Optional[int] = None, - digits: Optional[int] = None, - uppercase: Optional[int] = None, - lowercase: Optional[int] = None, - rules: Optional[str] = None, - output_format: Optional[str] = None, - output_file: Optional[str] = None, - clipboard: Optional[bool] = None, - password_list: Optional[bool] = None, -): +def generate_advanced_passwords(context: KeeperParams): """ Generate advanced passwords with complexity rules and BreachWatch scanning. This function uses the Keeper CLI `PasswordGenerateCommand` to generate passwords - with specific complexity requirements, BreachWatch scanning, and various output options. + with specific complexity requirements and BreachWatch scanning. """ try: command = PasswordGenerateCommand() - - kwargs = { - 'number': number or 3, - 'length': length or 24, - 'symbols': symbols, - 'digits': digits, - 'uppercase': uppercase, - 'lowercase': lowercase, - 'rules': rules, - 'output_format': output_format or 'table', - 'output_file': output_file, - 'clipboard': clipboard or False, - 'password_list': password_list or False, - 'no_breachwatch': False, # Enable BreachWatch scanning - } - - command.execute(context=context, **kwargs) + command.execute(context=context, number=3, length=24, symbols=3, digits=3, uppercase=3, lowercase=3) return True except Exception as e: @@ -106,9 +77,6 @@ def generate_advanced_passwords( epilog=''' Examples: python advanced_password_generation.py - python advanced_password_generation.py --length 32 --symbols 4 --digits 4 - python advanced_password_generation.py --rules "3,3,3,3" --output passwords.json --format json - python advanced_password_generation.py --clipboard --password-list ''' ) @@ -117,62 +85,6 @@ def generate_advanced_passwords( default='myconfig.json', help='Configuration file (default: myconfig.json)' ) - parser.add_argument( - '-n', '--number', - type=int, - default=3, - help='Number of passwords to generate (default: 3)' - ) - parser.add_argument( - '-l', '--length', - type=int, - default=24, - help='Password length (default: 24)' - ) - parser.add_argument( - '-s', '--symbols', - type=int, - help='Minimum number of symbol characters' - ) - parser.add_argument( - '-d', '--digits', - type=int, - help='Minimum number of digit characters' - ) - parser.add_argument( - '-u', '--uppercase', - type=int, - help='Minimum number of uppercase characters' - ) - parser.add_argument( - '--lowercase', - type=int, - help='Minimum number of lowercase characters' - ) - parser.add_argument( - '-r', '--rules', - help='Complexity rules as comma-separated integers: uppercase,lowercase,digits,symbols' - ) - parser.add_argument( - '-f', '--format', - choices=['table', 'json'], - default='table', - help='Output format (default: table)' - ) - parser.add_argument( - '-o', '--output', - help='Write output to specified file' - ) - parser.add_argument( - '--clipboard', - action='store_true', - help='Copy generated passwords to clipboard' - ) - parser.add_argument( - '-p', '--password-list', - action='store_true', - help='Include password list in addition to formatted output' - ) args = parser.parse_args() @@ -180,52 +92,15 @@ def generate_advanced_passwords( logger.error(f'Config file {args.config} not found') sys.exit(1) - # Set some defaults for better demo when no args provided - if not any([args.symbols, args.digits, args.uppercase, args.lowercase, args.rules]): - # If no complexity rules specified, use some defaults for demo - if args.length == 24 and args.number == 3: # Using defaults - args.symbols = 3 - args.digits = 3 - args.uppercase = 3 - args.lowercase = 3 - context = None try: context = login_to_keeper_with_config(args.config) - # Show what we're generating - using_defaults = (args.number == 3 and args.length == 24 and not args.rules and - not any([args.output, args.clipboard, args.password_list])) - - if using_defaults: - logger.info("Running with enhanced defaults - generating 3 advanced passwords of length 24") - logger.info("Complexity: 3+ symbols, 3+ digits, 3+ uppercase, 3+ lowercase") - logger.info("Use --help to see all available options") - else: - logger.info(f'Generating {args.number} advanced password(s)...') - logger.info(f'Length: {args.length}') - if args.symbols: logger.info(f'Minimum symbols: {args.symbols}') - if args.digits: logger.info(f'Minimum digits: {args.digits}') - if args.uppercase: logger.info(f'Minimum uppercase: {args.uppercase}') - if args.lowercase: logger.info(f'Minimum lowercase: {args.lowercase}') - if args.rules: logger.info(f'Complexity rules: {args.rules}') - + logger.info("Generating 3 advanced passwords of length 24...") + logger.info("Complexity: 3+ symbols, 3+ digits, 3+ uppercase, 3+ lowercase") logger.info('BreachWatch scanning: Enabled') - generate_advanced_passwords( - context, - number=args.number, - length=args.length, - symbols=args.symbols, - digits=args.digits, - uppercase=args.uppercase, - lowercase=args.lowercase, - rules=args.rules, - output_format=args.format, - output_file=args.output, - clipboard=args.clipboard, - password_list=args.password_list, - ) + generate_advanced_passwords(context) except Exception as e: logger.error(f'Error: {str(e)}') diff --git a/examples/password/basic_password_generation.py b/examples/password/basic_password_generation.py index 7d8e2858..145e0113 100644 --- a/examples/password/basic_password_generation.py +++ b/examples/password/basic_password_generation.py @@ -53,32 +53,16 @@ def login_to_keeper_with_config(filename: str) -> KeeperParams: raise Exception('Failed to authenticate with Keeper') return context -def generate_basic_passwords( - context: KeeperParams, - number: Optional[int] = None, - length: Optional[int] = None, - output_format: Optional[str] = None, - quiet: Optional[bool] = None, - no_breachwatch: Optional[bool] = None, -): +def generate_basic_passwords(context: KeeperParams): """ Generate basic random passwords. This function uses the Keeper CLI `PasswordGenerateCommand` to generate basic random passwords - with customizable length and count. + with default settings. """ try: command = PasswordGenerateCommand() - - kwargs = { - 'number': number or 1, - 'length': length or 20, - 'output_format': output_format or 'table', - 'quiet': quiet or False, - 'no_breachwatch': no_breachwatch or False, - } - - command.execute(context=context, **kwargs) + command.execute(context=context, number=1, length=20) return True except Exception as e: @@ -93,8 +77,6 @@ def generate_basic_passwords( epilog=''' Examples: python basic_password_generation.py - python basic_password_generation.py --number 5 --length 16 - python basic_password_generation.py --quiet --no-breachwatch ''' ) @@ -103,34 +85,6 @@ def generate_basic_passwords( default='myconfig.json', help='Configuration file (default: myconfig.json)' ) - parser.add_argument( - '-n', '--number', - type=int, - default=3, - help='Number of passwords to generate (default: 3)' - ) - parser.add_argument( - '-l', '--length', - type=int, - default=20, - help='Password length (default: 20)' - ) - parser.add_argument( - '-f', '--format', - choices=['table', 'json'], - default='table', - help='Output format (default: table)' - ) - parser.add_argument( - '-q', '--quiet', - action='store_true', - help='Only print password list (minimal output)' - ) - parser.add_argument( - '--no-breachwatch', - action='store_true', - help='Skip BreachWatch scanning' - ) args = parser.parse_args() @@ -142,21 +96,9 @@ def generate_basic_passwords( try: context = login_to_keeper_with_config(args.config) - # Show what we're generating if using defaults - if not any([args.number != 3, args.length != 20, args.format != 'table', args.quiet, args.no_breachwatch]): - logger.info("Running with default settings - generating 3 passwords of length 20 with BreachWatch scanning") - logger.info("Use --help to see all available options") - else: - logger.info(f'Generating {args.number} password(s) of length {args.length}...') + logger.info("Generating 1 basic password of length 20...") - generate_basic_passwords( - context, - number=args.number, - length=args.length, - output_format=args.format, - quiet=args.quiet, - no_breachwatch=args.no_breachwatch, - ) + generate_basic_passwords(context) except Exception as e: logger.error(f'Error: {str(e)}') diff --git a/examples/password/crypto_password_generation.py b/examples/password/crypto_password_generation.py index 71c5577b..14b55468 100644 --- a/examples/password/crypto_password_generation.py +++ b/examples/password/crypto_password_generation.py @@ -53,15 +53,7 @@ def login_to_keeper_with_config(filename: str) -> KeeperParams: raise Exception('Failed to authenticate with Keeper') return context -def generate_crypto_passwords( - context: KeeperParams, - number: Optional[int] = None, - output_format: Optional[str] = None, - output_file: Optional[str] = None, - clipboard: Optional[bool] = None, - password_list: Optional[bool] = None, - no_breachwatch: Optional[bool] = None, -): +def generate_crypto_passwords(context: KeeperParams): """ Generate crypto-style strong passwords. @@ -70,18 +62,7 @@ def generate_crypto_passwords( """ try: command = PasswordGenerateCommand() - - kwargs = { - 'crypto': True, - 'number': number or 3, - 'output_format': output_format or 'table', - 'output_file': output_file, - 'clipboard': clipboard or False, - 'password_list': password_list or False, - 'no_breachwatch': no_breachwatch or False, - } - - command.execute(context=context, **kwargs) + command.execute(context=context, crypto=True, number=3) return True except Exception as e: @@ -96,9 +77,6 @@ def generate_crypto_passwords( epilog=''' Examples: python crypto_password_generation.py - python crypto_password_generation.py --number 5 - python crypto_password_generation.py --clipboard --format json - python crypto_password_generation.py --output crypto_passwords.txt --password-list ''' ) @@ -107,37 +85,6 @@ def generate_crypto_passwords( default='myconfig.json', help='Configuration file (default: myconfig.json)' ) - parser.add_argument( - '-n', '--number', - type=int, - default=3, - help='Number of passwords to generate (default: 3)' - ) - parser.add_argument( - '-f', '--format', - choices=['table', 'json'], - default='table', - help='Output format (default: table)' - ) - parser.add_argument( - '-o', '--output', - help='Write output to specified file' - ) - parser.add_argument( - '--clipboard', - action='store_true', - help='Copy generated passwords to clipboard' - ) - parser.add_argument( - '-p', '--password-list', - action='store_true', - help='Include password list in addition to formatted output' - ) - parser.add_argument( - '--no-breachwatch', - action='store_true', - help='Skip BreachWatch scanning' - ) args = parser.parse_args() @@ -149,26 +96,10 @@ def generate_crypto_passwords( try: context = login_to_keeper_with_config(args.config) - using_defaults = (args.number == 3 and args.format == 'table' and - not any([args.output, args.clipboard, args.password_list, args.no_breachwatch])) - - if using_defaults: - logger.info("Running with default settings - generating 3 crypto-style passwords") - logger.info("These passwords are optimized for high-security applications like cryptocurrency") - logger.info("Use --help to see all available options") - else: - logger.info(f'Generating {args.number} crypto-style password(s)...') - logger.info('These passwords are optimized for high-security applications') + logger.info("Generating 3 crypto-style passwords...") + logger.info("These passwords are optimized for high-security applications like cryptocurrency") - generate_crypto_passwords( - context, - number=args.number, - output_format=args.format, - output_file=args.output, - clipboard=args.clipboard, - password_list=args.password_list, - no_breachwatch=args.no_breachwatch, - ) + generate_crypto_passwords(context) except Exception as e: logger.error(f'Error: {str(e)}') diff --git a/examples/password/diceware_password_generation.py b/examples/password/diceware_password_generation.py index e260ce51..100603e1 100644 --- a/examples/password/diceware_password_generation.py +++ b/examples/password/diceware_password_generation.py @@ -9,7 +9,7 @@ # Copyright 2025 Keeper Security Inc. # Contact: commander@keepersecurity.com # -# Example showing how to generate diceware passwords +# Example showing how to generate diceware passwords with hyphen delimiter # using the Keeper CLI package. # @@ -53,36 +53,16 @@ def login_to_keeper_with_config(filename: str) -> KeeperParams: raise Exception('Failed to authenticate with Keeper') return context -def generate_diceware_passwords( - context: KeeperParams, - number: Optional[int] = None, - dice_rolls: Optional[int] = None, - delimiter: Optional[str] = None, - word_list: Optional[str] = None, - output_format: Optional[str] = None, - quiet: Optional[bool] = None, - no_breachwatch: Optional[bool] = None, -): +def generate_diceware_passwords(context: KeeperParams): """ Generate diceware passwords. This function uses the Keeper CLI `PasswordGenerateCommand` to generate diceware-style passwords - using dice rolls to select words from a word list. + using dice rolls to select words from a word list with hyphen (-) delimiter. """ try: command = PasswordGenerateCommand() - - kwargs = { - 'number': number or 3, - 'dice_rolls': dice_rolls or 6, - 'delimiter': delimiter or ' ', - 'word_list': word_list, - 'output_format': output_format or 'table', - 'quiet': quiet or False, - 'no_breachwatch': no_breachwatch or False, - } - - command.execute(context=context, **kwargs) + command.execute(context=context, number=1, dice_rolls=6, delimiter='-') return True except Exception as e: @@ -92,14 +72,11 @@ def generate_diceware_passwords( if __name__ == '__main__': parser = argparse.ArgumentParser( - description='Generate diceware passwords using Keeper SDK', + description='Generate diceware passwords with hyphen delimiter using Keeper SDK', formatter_class=argparse.RawDescriptionHelpFormatter, epilog=''' Examples: python diceware_password_generation.py - python diceware_password_generation.py --dice-rolls 8 --delimiter "-" - python diceware_password_generation.py --word-list custom_words.txt --quiet - python diceware_password_generation.py --number 5 --delimiter "_" --format json ''' ) @@ -108,44 +85,6 @@ def generate_diceware_passwords( default='myconfig.json', help='Configuration file (default: myconfig.json)' ) - parser.add_argument( - '-n', '--number', - type=int, - default=3, - help='Number of passwords to generate (default: 3)' - ) - parser.add_argument( - '--dice-rolls', - type=int, - default=6, - help='Number of dice rolls for diceware generation (default: 6)' - ) - parser.add_argument( - '--delimiter', - choices=['-', '+', ':', '.', '/', '_', '=', ' '], - default=' ', - help='Word delimiter for diceware (default: space)' - ) - parser.add_argument( - '--word-list', - help='Path to custom word list file for diceware' - ) - parser.add_argument( - '-f', '--format', - choices=['table', 'json'], - default='table', - help='Output format (default: table)' - ) - parser.add_argument( - '-q', '--quiet', - action='store_true', - help='Only print password list (minimal output)' - ) - parser.add_argument( - '--no-breachwatch', - action='store_true', - help='Skip BreachWatch scanning' - ) args = parser.parse_args() @@ -157,33 +96,10 @@ def generate_diceware_passwords( try: context = login_to_keeper_with_config(args.config) - # Show what we're generating - using_defaults = (args.number == 3 and args.dice_rolls == 6 and args.delimiter == ' ' and - not args.word_list and args.format == 'table' and not args.quiet and not args.no_breachwatch) - - if using_defaults: - logger.info("Running with default settings - generating 3 diceware passwords") - logger.info("Using 6 dice rolls with space delimiter and default word list") - logger.info("Use --help to see all available options") - else: - logger.info(f'Generating {args.number} diceware password(s)...') - logger.info(f'Dice rolls: {args.dice_rolls}') - logger.info(f'Word delimiter: "{args.delimiter}"') - if args.word_list: - logger.info(f'Custom word list: {args.word_list}') - else: - logger.info('Using default word list') + logger.info("Generating 1 diceware password...") + logger.info("Using 6 dice rolls with hyphen (-) delimiter and default word list") - generate_diceware_passwords( - context, - number=args.number, - dice_rolls=args.dice_rolls, - delimiter=args.delimiter, - word_list=args.word_list, - output_format=args.format, - quiet=args.quiet, - no_breachwatch=args.no_breachwatch, - ) + generate_diceware_passwords(context) except Exception as e: logger.error(f'Error: {str(e)}') diff --git a/examples/password/recovery_phrase_generation.py b/examples/password/recovery_phrase_generation.py index 9518785b..60656d57 100644 --- a/examples/password/recovery_phrase_generation.py +++ b/examples/password/recovery_phrase_generation.py @@ -53,15 +53,7 @@ def login_to_keeper_with_config(filename: str) -> KeeperParams: raise Exception('Failed to authenticate with Keeper') return context -def generate_recovery_phrases( - context: KeeperParams, - number: Optional[int] = None, - output_format: Optional[str] = None, - output_file: Optional[str] = None, - clipboard: Optional[bool] = None, - password_list: Optional[bool] = None, - no_breachwatch: Optional[bool] = None, -): +def generate_recovery_phrases(context: KeeperParams): """ Generate 24-word recovery phrases. @@ -70,18 +62,7 @@ def generate_recovery_phrases( """ try: command = PasswordGenerateCommand() - - kwargs = { - 'recoveryphrase': True, - 'number': number or 2, - 'output_format': output_format or 'table', - 'output_file': output_file, - 'clipboard': clipboard or False, - 'password_list': password_list or False, - 'no_breachwatch': no_breachwatch or True, # Recovery phrases usually skip BreachWatch - } - - command.execute(context=context, **kwargs) + command.execute(context=context, recoveryphrase=True, number=2, no_breachwatch=True) return True except Exception as e: @@ -96,9 +77,6 @@ def generate_recovery_phrases( epilog=''' Examples: python recovery_phrase_generation.py - python recovery_phrase_generation.py --number 3 - python recovery_phrase_generation.py --output recovery_phrases.txt --password-list - python recovery_phrase_generation.py --clipboard --format json ''' ) @@ -107,37 +85,6 @@ def generate_recovery_phrases( default='myconfig.json', help='Configuration file (default: myconfig.json)' ) - parser.add_argument( - '-n', '--number', - type=int, - default=2, - help='Number of recovery phrases to generate (default: 2)' - ) - parser.add_argument( - '-f', '--format', - choices=['table', 'json'], - default='table', - help='Output format (default: table)' - ) - parser.add_argument( - '-o', '--output', - help='Write output to specified file' - ) - parser.add_argument( - '--clipboard', - action='store_true', - help='Copy generated phrases to clipboard' - ) - parser.add_argument( - '-p', '--password-list', - action='store_true', - help='Include phrase list in addition to formatted output' - ) - parser.add_argument( - '--enable-breachwatch', - action='store_true', - help='Enable BreachWatch scanning (disabled by default for recovery phrases)' - ) args = parser.parse_args() @@ -149,26 +96,10 @@ def generate_recovery_phrases( try: context = login_to_keeper_with_config(args.config) - using_defaults = (args.number == 2 and args.format == 'table' and - not any([args.output, args.clipboard, args.password_list, args.enable_breachwatch])) - - if using_defaults: - logger.info("Running with default settings - generating 2 recovery phrases") - logger.info("These are 24-word phrases suitable for cryptocurrency wallet recovery") - logger.info("Use --help to see all available options") - else: - logger.info(f'Generating {args.number} recovery phrase(s)...') - logger.info('These are 24-word phrases suitable for cryptocurrency wallets') + logger.info("Generating 2 recovery phrases...") + logger.info("These are 24-word phrases suitable for cryptocurrency wallet recovery") - generate_recovery_phrases( - context, - number=args.number, - output_format=args.format, - output_file=args.output, - clipboard=args.clipboard, - password_list=args.password_list, - no_breachwatch=not args.enable_breachwatch, - ) + generate_recovery_phrases(context) except Exception as e: logger.error(f'Error: {str(e)}') From 167964498f6f62823cf067b78f0389f6758857c6 Mon Sep 17 00:00:00 2001 From: adeshmukh-ks Date: Wed, 12 Nov 2025 15:46:21 +0530 Subject: [PATCH 6/6] Refactoring changes --- .../password/advanced_password_generation.py | 21 ++- .../password/basic_password_generation.py | 22 ++- .../comprehensive_password_generation.py | 22 ++- .../password/crypto_password_generation.py | 22 ++- .../password/diceware_password_generation.py | 22 ++- .../password/recovery_phrase_generation.py | 22 ++- .../keepercli/commands/share_management.py | 23 ++- keepersdk-package/README.md | 137 ------------------ 8 files changed, 125 insertions(+), 166 deletions(-) diff --git a/examples/password/advanced_password_generation.py b/examples/password/advanced_password_generation.py index e8235d1a..57c3930a 100644 --- a/examples/password/advanced_password_generation.py +++ b/examples/password/advanced_password_generation.py @@ -27,6 +27,22 @@ logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__name__) +def get_default_config_path() -> str: + """ + Get the default config file path following the same logic as JsonFileLoader. + + First checks if 'config.json' exists in the current directory. + If not, uses ~/.keeper/config.json. + """ + file_name = 'config.json' + if os.path.isfile(file_name): + return os.path.abspath(file_name) + else: + keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper') + if not os.path.exists(keeper_dir): + os.mkdir(keeper_dir) + return os.path.join(keeper_dir, file_name) + def login_to_keeper_with_config(filename: str) -> KeeperParams: """ Login to Keeper with a configuration file. @@ -80,10 +96,11 @@ def generate_advanced_passwords(context: KeeperParams): ''' ) + default_config = get_default_config_path() parser.add_argument( '-c', '--config', - default='myconfig.json', - help='Configuration file (default: myconfig.json)' + default=default_config, + help=f'Configuration file (default: {default_config})' ) args = parser.parse_args() diff --git a/examples/password/basic_password_generation.py b/examples/password/basic_password_generation.py index 145e0113..bb46c550 100644 --- a/examples/password/basic_password_generation.py +++ b/examples/password/basic_password_generation.py @@ -18,7 +18,6 @@ import os import sys import logging -from typing import Optional from keepercli.commands.password_generate import PasswordGenerateCommand from keepercli.params import KeeperParams @@ -27,6 +26,22 @@ logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__name__) +def get_default_config_path() -> str: + """ + Get the default config file path following the same logic as JsonFileLoader. + + First checks if 'config.json' exists in the current directory. + If not, uses ~/.keeper/config.json. + """ + file_name = 'config.json' + if os.path.isfile(file_name): + return os.path.abspath(file_name) + else: + keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper') + if not os.path.exists(keeper_dir): + os.mkdir(keeper_dir) + return os.path.join(keeper_dir, file_name) + def login_to_keeper_with_config(filename: str) -> KeeperParams: """ Login to Keeper with a configuration file. @@ -80,10 +95,11 @@ def generate_basic_passwords(context: KeeperParams): ''' ) + default_config = get_default_config_path() parser.add_argument( '-c', '--config', - default='myconfig.json', - help='Configuration file (default: myconfig.json)' + default=default_config, + help=f'Configuration file (default: {default_config})' ) args = parser.parse_args() diff --git a/examples/password/comprehensive_password_generation.py b/examples/password/comprehensive_password_generation.py index a5d65b10..89085fe0 100644 --- a/examples/password/comprehensive_password_generation.py +++ b/examples/password/comprehensive_password_generation.py @@ -18,12 +18,27 @@ import os import sys import logging -from typing import Optional from keepercli.commands.password_generate import PasswordGenerateCommand from keepercli.params import KeeperParams from keepercli.login import LoginFlow +def get_default_config_path() -> str: + """ + Get the default config file path following the same logic as JsonFileLoader. + + First checks if 'config.json' exists in the current directory. + If not, uses ~/.keeper/config.json. + """ + file_name = 'config.json' + if os.path.isfile(file_name): + return os.path.abspath(file_name) + else: + keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper') + if not os.path.exists(keeper_dir): + os.mkdir(keeper_dir) + return os.path.join(keeper_dir, file_name) + logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__name__) @@ -262,10 +277,11 @@ def generate_custom_passwords( ''' ) + default_config = get_default_config_path() parser.add_argument( '-c', '--config', - default='myconfig.json', - help='Configuration file (default: myconfig.json)' + default=default_config, + help=f'Configuration file (default: {default_config})' ) parser.add_argument( '--demo', diff --git a/examples/password/crypto_password_generation.py b/examples/password/crypto_password_generation.py index 14b55468..24353959 100644 --- a/examples/password/crypto_password_generation.py +++ b/examples/password/crypto_password_generation.py @@ -18,7 +18,6 @@ import os import sys import logging -from typing import Optional from keepercli.commands.password_generate import PasswordGenerateCommand from keepercli.params import KeeperParams @@ -27,6 +26,22 @@ logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__name__) +def get_default_config_path() -> str: + """ + Get the default config file path following the same logic as JsonFileLoader. + + First checks if 'config.json' exists in the current directory. + If not, uses ~/.keeper/config.json. + """ + file_name = 'config.json' + if os.path.isfile(file_name): + return os.path.abspath(file_name) + else: + keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper') + if not os.path.exists(keeper_dir): + os.mkdir(keeper_dir) + return os.path.join(keeper_dir, file_name) + def login_to_keeper_with_config(filename: str) -> KeeperParams: """ Login to Keeper with a configuration file. @@ -80,10 +95,11 @@ def generate_crypto_passwords(context: KeeperParams): ''' ) + default_config = get_default_config_path() parser.add_argument( '-c', '--config', - default='myconfig.json', - help='Configuration file (default: myconfig.json)' + default=default_config, + help=f'Configuration file (default: {default_config})' ) args = parser.parse_args() diff --git a/examples/password/diceware_password_generation.py b/examples/password/diceware_password_generation.py index 100603e1..9fc0b9e1 100644 --- a/examples/password/diceware_password_generation.py +++ b/examples/password/diceware_password_generation.py @@ -18,7 +18,6 @@ import os import sys import logging -from typing import Optional from keepercli.commands.password_generate import PasswordGenerateCommand from keepercli.params import KeeperParams @@ -27,6 +26,22 @@ logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__name__) +def get_default_config_path() -> str: + """ + Get the default config file path following the same logic as JsonFileLoader. + + First checks if 'config.json' exists in the current directory. + If not, uses ~/.keeper/config.json. + """ + file_name = 'config.json' + if os.path.isfile(file_name): + return os.path.abspath(file_name) + else: + keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper') + if not os.path.exists(keeper_dir): + os.mkdir(keeper_dir) + return os.path.join(keeper_dir, file_name) + def login_to_keeper_with_config(filename: str) -> KeeperParams: """ Login to Keeper with a configuration file. @@ -80,10 +95,11 @@ def generate_diceware_passwords(context: KeeperParams): ''' ) + default_config = get_default_config_path() parser.add_argument( '-c', '--config', - default='myconfig.json', - help='Configuration file (default: myconfig.json)' + default=default_config, + help=f'Configuration file (default: {default_config})' ) args = parser.parse_args() diff --git a/examples/password/recovery_phrase_generation.py b/examples/password/recovery_phrase_generation.py index 60656d57..2b648ea5 100644 --- a/examples/password/recovery_phrase_generation.py +++ b/examples/password/recovery_phrase_generation.py @@ -18,7 +18,6 @@ import os import sys import logging -from typing import Optional from keepercli.commands.password_generate import PasswordGenerateCommand from keepercli.params import KeeperParams @@ -27,6 +26,22 @@ logging.basicConfig(level=logging.INFO, format='%(message)s') logger = logging.getLogger(__name__) +def get_default_config_path() -> str: + """ + Get the default config file path following the same logic as JsonFileLoader. + + First checks if 'config.json' exists in the current directory. + If not, uses ~/.keeper/config.json. + """ + file_name = 'config.json' + if os.path.isfile(file_name): + return os.path.abspath(file_name) + else: + keeper_dir = os.path.join(os.path.expanduser('~'), '.keeper') + if not os.path.exists(keeper_dir): + os.mkdir(keeper_dir) + return os.path.join(keeper_dir, file_name) + def login_to_keeper_with_config(filename: str) -> KeeperParams: """ Login to Keeper with a configuration file. @@ -80,10 +95,11 @@ def generate_recovery_phrases(context: KeeperParams): ''' ) + default_config = get_default_config_path() parser.add_argument( '-c', '--config', - default='myconfig.json', - help='Configuration file (default: myconfig.json)' + default=default_config, + help=f'Configuration file (default: {default_config})' ) args = parser.parse_args() diff --git a/keepercli-package/src/keepercli/commands/share_management.py b/keepercli-package/src/keepercli/commands/share_management.py index 4843b8da..ab628f57 100644 --- a/keepercli-package/src/keepercli/commands/share_management.py +++ b/keepercli-package/src/keepercli/commands/share_management.py @@ -130,7 +130,7 @@ def execute(self, context: KeeperParams, **kwargs) -> None: emails = kwargs.get('email') or [] if not emails: - raise ValueError('share-record', '\'email\' parameter is missing') + raise ValueError('\'email\' parameter is missing') force = kwargs.get('force') action = kwargs.get('action', ShareAction.GRANT.value) @@ -270,7 +270,7 @@ def prep_request(context: KeeperParams, pass if record_uid is None and folder_uid is None and shared_folder_uid is None: - raise ValueError('share-record', 'Enter name or uid of existing record or shared folder') + raise ValueError('Enter name or uid of existing record or shared folder') # Collect record UIDs record_uids = set() @@ -288,7 +288,7 @@ def prep_request(context: KeeperParams, record_uids = {uid for uid in folders if uid in record_cache} elif shared_folder_uid: if not recursive: - raise ValueError('share-record', '--recursive parameter is required') + raise ValueError('--recursive parameter is required') if isinstance(shared_folder_uid, str): sf = vault.vault_data.load_shared_folder(shared_folder_uid=shared_folder_uid) if sf and sf.record_permissions: @@ -301,10 +301,10 @@ def prep_request(context: KeeperParams, record_uids.update(x.record_uid for x in sf.record_permissions) if not record_uids: - raise ValueError('share-record', 'There are no records to share selected') + raise ValueError('There are no records to share selected') if action == 'owner' and len(emails) > 1: - raise ValueError('share-record', 'You can transfer ownership to a single account only') + raise ValueError('You can transfer ownership to a single account only') all_users = {email.casefold() for email in emails} @@ -321,7 +321,7 @@ def prep_request(context: KeeperParams, all_users.intersection_update(vault.keeper_auth._key_cache.keys()) if not all_users: - raise ValueError('share-record', 'Nothing to do.') + raise ValueError('Nothing to do.') # Load records in shared folders if shared_folder_uid: @@ -675,7 +675,7 @@ def get_folder_by_uid(uid): shared_folder_uids.update(share_admin_folder_uids or []) if not shared_folder_uids: - raise ValueError('share-folder', 'Enter name of at least one existing folder') + raise ValueError('Enter name of at least one existing folder') action = kwargs.get('action') or ShareAction.GRANT.value @@ -1059,7 +1059,7 @@ def execute(self, context: KeeperParams, **kwargs): record_uids = self._resolve_record_uids(context, vault, records, kwargs.get('recursive', False)) if not record_uids: - raise base.CommandError('one-time-share', 'No records found') + raise base.CommandError('No records found') applications = self._get_applications(vault, record_uids) table_data = self._build_share_table(applications, kwargs) @@ -1234,11 +1234,10 @@ def execute(self, context: KeeperParams, **kwargs): record_names = [record_names] if not record_names: self.get_parser().print_help() - return None + raise base.CommandError('No records provided') if not period_str: - logger.warning('URL expiration period parameter \"--expire\" is required.') self.get_parser().print_help() - return None + raise base.CommandError('URL expiration period parameter \"--expire\" is required.') period = self._validate_and_parse_expiration(period_str) @@ -1251,7 +1250,7 @@ def _validate_and_parse_expiration(self, period_str): period = timeout_utils.parse_timeout(period_str) SIX_MONTHS_IN_SECONDS = 182 * 24 * 60 * 60 if period.total_seconds() > SIX_MONTHS_IN_SECONDS: - raise base.CommandError('one-time-share', 'URL expiration period cannot be greater than 6 months.') + raise base.CommandError('URL expiration period cannot be greater than 6 months.') return period def _create_share_urls(self, context: KeeperParams, vault, record_names: list, period, name: str, is_editable: bool): diff --git a/keepersdk-package/README.md b/keepersdk-package/README.md index a79e48a0..fbec39de 100644 --- a/keepersdk-package/README.md +++ b/keepersdk-package/README.md @@ -245,140 +245,3 @@ if isinstance(login_auth_context.login_step, login_auth.LoginStepConnected): - Consider using environment variables or secure vaults for credential management --- - -## Keeper CLI - -### About Keeper CLI - -Keeper CLI is a powerful command-line interface that provides direct access to Keeper Vault and Enterprise Console features. It enables users to: - -- Manage vault records, folders, and attachments from the terminal -- Perform enterprise administration tasks (user management, team operations, role assignments) -- Execute batch operations and automation scripts -- Generate audit reports and monitor security events -- Configure Secrets Manager applications -- Import and export vault data - -Keeper CLI is ideal for system administrators, DevOps engineers, and power users who prefer terminal-based workflows. - -### CLI Installation - -#### From Source - -```bash -# Clone the repository -git clone https://github.com/Keeper-Security/keeper-sdk-python -cd keeper-sdk-python/keepercli-package - -# Install dependencies -pip install -e . -``` - -### CLI Environment Setup - -**Complete Setup from Source:** - -**Step 1: Create and Activate Virtual Environment** - -```bash -# Create virtual environment -python3 -m venv venv - -# Activate virtual environment -# On macOS/Linux: -source venv/bin/activate -# On Windows: -venv\Scripts\activate -``` - -**Step 2: Install Keeper SDK (Required Dependency)** - -```bash -cd keepersdk-package -pip install -e . -``` - -**Step 3: Install Keeper CLI** - -```bash -cd ../keepercli-package -pip install -e . -``` - -### CLI Usage - -Once installed, launch Keeper CLI: - -```bash -# Run Keeper CLI -python -m keepercli -``` - -**Common CLI Commands:** - -```bash -# Login to your Keeper account -Not Logged In> login - -# List all vault records -My Vault> list - -# Search for a specific record -My Vault> search - -# Display record details -My Vault> get - -# Add a new record -My Vault> add-record - -# Sync vault with server -My Vault> sync-down - -# Enterprise user management -My Vault> enterprise-user list -My Vault> enterprise-user add -My Vault> enterprise-user edit - -# Team management -My Vault> enterprise-team list -My Vault> enterprise-team add - -# Generate audit report -My Vault> audit-report - -# Exit CLI -My Vault> quit -``` - -**Interactive Mode:** - -Keeper CLI provides an interactive shell with command history, tab completion, and contextual help: - -```bash -My Vault> help # Display all available commands -My Vault> help # Get help for a specific command -My Vault> my-command --help # Display command-specific options -``` - ---- - -## Contributing - -We welcome contributions from the community! Please feel free to submit pull requests, report issues, or suggest enhancements through our [GitHub repository](https://github.com/Keeper-Security/keeper-sdk-python). - ---- - -## License - -This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. - ---- - -## Support - -For support, documentation, and additional resources: - -- **Documentation**: [Keeper Security Developer Portal](https://docs.keeper.io/) -- **Support**: [Keeper Security Support](https://www.keepersecurity.com/support.html) -- **Community**: [Keeper Security GitHub](https://github.com/Keeper-Security) \ No newline at end of file