diff --git a/examples/password/advanced_password_generation.py b/examples/password/advanced_password_generation.py new file mode 100644 index 00000000..57c3930a --- /dev/null +++ b/examples/password/advanced_password_generation.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 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. + + 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): + """ + Generate advanced passwords with complexity rules and BreachWatch scanning. + + This function uses the Keeper CLI `PasswordGenerateCommand` to generate passwords + with specific complexity requirements and BreachWatch scanning. + """ + try: + command = PasswordGenerateCommand() + command.execute(context=context, number=3, length=24, symbols=3, digits=3, uppercase=3, lowercase=3) + 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 + ''' + ) + + default_config = get_default_config_path() + parser.add_argument( + '-c', '--config', + default=default_config, + help=f'Configuration file (default: {default_config})' + ) + + 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) + + 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) + + 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..bb46c550 --- /dev/null +++ b/examples/password/basic_password_generation.py @@ -0,0 +1,124 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 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. + + 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): + """ + Generate basic random passwords. + + This function uses the Keeper CLI `PasswordGenerateCommand` to generate basic random passwords + with default settings. + """ + try: + command = PasswordGenerateCommand() + command.execute(context=context, number=1, length=20) + 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 + ''' + ) + + default_config = get_default_config_path() + parser.add_argument( + '-c', '--config', + default=default_config, + help=f'Configuration file (default: {default_config})' + ) + + 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) + + logger.info("Generating 1 basic password of length 20...") + + generate_basic_passwords(context) + + 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..89085fe0 --- /dev/null +++ b/examples/password/comprehensive_password_generation.py @@ -0,0 +1,403 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 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__) + +def login_to_keeper_with_config(filename: str) -> 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 + ''' + ) + + default_config = get_default_config_path() + parser.add_argument( + '-c', '--config', + default=default_config, + help=f'Configuration file (default: {default_config})' + ) + 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..24353959 --- /dev/null +++ b/examples/password/crypto_password_generation.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 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. + + 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): + """ + 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() + command.execute(context=context, crypto=True, number=3) + 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 + ''' + ) + + default_config = get_default_config_path() + parser.add_argument( + '-c', '--config', + default=default_config, + help=f'Configuration file (default: {default_config})' + ) + + 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) + + logger.info("Generating 3 crypto-style passwords...") + logger.info("These passwords are optimized for high-security applications like cryptocurrency") + + generate_crypto_passwords(context) + + 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..9fc0b9e1 --- /dev/null +++ b/examples/password/diceware_password_generation.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 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. + + 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): + """ + 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 with hyphen (-) delimiter. + """ + try: + command = PasswordGenerateCommand() + command.execute(context=context, number=1, dice_rolls=6, delimiter='-') + 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 with hyphen delimiter using Keeper SDK', + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=''' +Examples: + python diceware_password_generation.py + ''' + ) + + default_config = get_default_config_path() + parser.add_argument( + '-c', '--config', + default=default_config, + help=f'Configuration file (default: {default_config})' + ) + + 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) + + logger.info("Generating 1 diceware password...") + logger.info("Using 6 dice rolls with hyphen (-) delimiter and default word list") + + generate_diceware_passwords(context) + + 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..2b648ea5 --- /dev/null +++ b/examples/password/recovery_phrase_generation.py @@ -0,0 +1,125 @@ +#!/usr/bin/env python3 +# _ __ +# | |/ /___ ___ _ __ ___ _ _ ® +# | ' 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. + + 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): + """ + 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() + command.execute(context=context, recoveryphrase=True, number=2, no_breachwatch=True) + 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 + ''' + ) + + default_config = get_default_config_path() + parser.add_argument( + '-c', '--config', + default=default_config, + help=f'Configuration file (default: {default_config})' + ) + + 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) + + logger.info("Generating 2 recovery phrases...") + logger.info("These are 24-word phrases suitable for cryptocurrency wallet recovery") + + generate_recovery_phrases(context) + + 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 new file mode 100644 index 00000000..bf7a192d --- /dev/null +++ b/keepercli-package/src/keepercli/commands/password_generate.py @@ -0,0 +1,335 @@ +""" +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 + +import pyperclip + +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.""" + 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)') + + 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_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_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_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.""" + 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.vault, 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, 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 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.warning("BreachWatch plugin not available, enable it to use") + 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) + + 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: + output = self._format_table(passwords) + + if password_list and not quiet: + output += '\n\n' + self._format_password_list(passwords) + + if clipboard: + try: + pyperclip.copy(output) + logger.info("Generated passwords copied to clipboard") + except Exception as e: + logger.warning(f"Failed to copy to clipboard: {e}") + + 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 = [] + + has_breach_info = any(pwd.breach_status is not None for pwd in passwords) + + 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") + + if has_breach_info: + header = f" {'Strength(%)':<12} {'BreachWatch':<12} {'Password'}" + else: + header = f" {'Strength(%)':<12} {'Password'}" + lines.append(header) + 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/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/keepercli-package/src/keepercli/helpers/password_utils.py b/keepercli-package/src/keepercli/helpers/password_utils.py new file mode 100644 index 00000000..f1d723ab --- /dev/null +++ b/keepercli-package/src/keepercli/helpers/password_utils.py @@ -0,0 +1,351 @@ +""" +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 + +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 +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.""" + length: int = 20 + count: int = 1 + algorithm: str = 'random' + + symbols: Optional[int] = None + digits: Optional[int] = None + uppercase: Optional[int] = None + lowercase: Optional[int] = None + rules: Optional[str] = None + + dice_rolls: Optional[int] = None + delimiter: str = ' ' + word_list_file: Optional[str] = None + + 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.""" + 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: + 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: + 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 + + if status and hasattr(status, 'euid') and status.euid: + euids_to_cleanup.append(status.euid) + + analyzed_password = self._process_breach_scan_result(password, status, accept_breached) + if analyzed_password: + scanned_passwords.append(analyzed_password) + finally: + 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: + if accept_breached: + return GeneratedPassword( + password=password, + strength_score=strength_score, + strength_level=strength_level, + breach_status=BreachStatus.FAILED + ) + return None + else: + return GeneratedPassword( + password=password, + strength_score=strength_score, + strength_level=strength_level, + breach_status=BreachStatus.PASSED + ) + else: + if accept_breached: + return GeneratedPassword( + password=password, + strength_score=strength_score, + strength_level=strength_level, + breach_status=BreachStatus.ERROR + ) + return None + + def _cleanup_breach_scan_euids(self, euids: List[str]) -> None: + """Clean up BreachWatch scan EUIDs.""" + if euids and self.breach_watch: + try: + self.breach_watch.delete_euids(euids) + except Exception: + pass + + def _create_generator(self, request: GenerationRequest) -> generator.PasswordGenerator: + """Create appropriate password generator based on request.""" + algorithm = request.algorithm.lower() + + if algorithm == 'crypto': + crypto_length = request.length or DEFAULT_PASSWORD_LENGTH + 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: + 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: + return generator.KeeperPasswordGenerator(length=request.length) + return kpg + else: + 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.""" + strength_score = utils.password_score(password) + strength_level = self._get_strength_level(strength_score) + + breach_status = None + breach_details = None + 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] + + if status and hasattr(status, 'euid') and status.euid: + try: + self.breach_watch.delete_euids([status.euid]) + except Exception: + 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 a98fc542..0c8ad236 100644 --- a/keepercli-package/src/keepercli/register_commands.py +++ b/keepercli-package/src/keepercli/register_commands.py @@ -28,8 +28,8 @@ 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) commands.register_command('ls', vault_folder.FolderListCommand(), base.CommandScope.Vault) @@ -59,6 +59,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) 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