#!/usr/bin/env python3 """ Command-line interface for managing the encrypted secrets store. Usage: python -m secrets.cli init # Initialize new secrets store python -m secrets.cli set KEY VALUE # Set a secret python -m secrets.cli get KEY # Get a secret python -m secrets.cli delete KEY # Delete a secret python -m secrets.cli list # List all secret keys python -m secrets.cli change-password # Change master password python -m secrets.cli export FILE # Export encrypted backup python -m secrets.cli import FILE # Import encrypted backup python -m secrets.cli migrate-from-env # Migrate secrets from .env file """ import sys import argparse import getpass from pathlib import Path from .store import SecretsStore, SecretsStoreError, InvalidMasterPassword def get_password(prompt: str = "Master password: ", confirm: bool = False) -> str: """ Securely get password from user. Args: prompt: Password prompt confirm: If True, ask for confirmation Returns: Password string """ password = getpass.getpass(prompt) if confirm: confirm_password = getpass.getpass("Confirm password: ") if password != confirm_password: print("Error: Passwords do not match", file=sys.stderr) sys.exit(1) return password def cmd_init(args): """Initialize a new secrets store.""" store = SecretsStore() if store.is_initialized: print("Error: Secrets store is already initialized", file=sys.stderr) print(f"Location: {store.secrets_file}", file=sys.stderr) sys.exit(1) password = get_password("Create master password: ", confirm=True) if len(password) < 8: print("Error: Password must be at least 8 characters", file=sys.stderr) sys.exit(1) store.initialize(password) print(f"Secrets store initialized at {store.secrets_file}") def cmd_set(args): """Set a secret value.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) password = get_password() try: store.unlock(password) except InvalidMasterPassword: print("Error: Invalid master password", file=sys.stderr) sys.exit(1) store.set(args.key, args.value) print(f"✓ Secret '{args.key}' saved") def cmd_get(args): """Get a secret value.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) password = get_password() try: store.unlock(password) except InvalidMasterPassword: print("Error: Invalid master password", file=sys.stderr) sys.exit(1) value = store.get(args.key) if value is None: print(f"Error: Secret '{args.key}' not found", file=sys.stderr) sys.exit(1) # Print to stdout (can be captured) print(value) def cmd_delete(args): """Delete a secret.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) password = get_password() try: store.unlock(password) except InvalidMasterPassword: print("Error: Invalid master password", file=sys.stderr) sys.exit(1) if store.delete(args.key): print(f"✓ Secret '{args.key}' deleted") else: print(f"Error: Secret '{args.key}' not found", file=sys.stderr) sys.exit(1) def cmd_list(args): """List all secret keys.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) password = get_password() try: store.unlock(password) except InvalidMasterPassword: print("Error: Invalid master password", file=sys.stderr) sys.exit(1) keys = store.list_keys() if not keys: print("No secrets stored") else: print(f"Stored secrets ({len(keys)}):") for key in sorted(keys): # Show key and value length for verification value = store.get(key) value_str = str(value) value_preview = value_str[:50] + "..." if len(value_str) > 50 else value_str print(f" {key}: {value_preview}") def cmd_change_password(args): """Change the master password.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) current_password = get_password("Current master password: ") new_password = get_password("New master password: ", confirm=True) if len(new_password) < 8: print("Error: Password must be at least 8 characters", file=sys.stderr) sys.exit(1) try: store.change_master_password(current_password, new_password) except InvalidMasterPassword: print("Error: Invalid current password", file=sys.stderr) sys.exit(1) def cmd_export(args): """Export encrypted secrets to a backup file.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) output_path = Path(args.file) if output_path.exists() and not args.force: print(f"Error: File {output_path} already exists. Use --force to overwrite.", file=sys.stderr) sys.exit(1) try: store.export_encrypted(output_path) except SecretsStoreError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) def cmd_import(args): """Import encrypted secrets from a backup file.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) input_path = Path(args.file) if not input_path.exists(): print(f"Error: File {input_path} does not exist", file=sys.stderr) sys.exit(1) password = get_password() try: store.import_encrypted(input_path, password) except InvalidMasterPassword: print("Error: Invalid master password or incompatible backup", file=sys.stderr) sys.exit(1) except SecretsStoreError as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) def cmd_migrate_from_env(args): """Migrate secrets from .env file to encrypted store.""" store = SecretsStore() if not store.is_initialized: print("Error: Secrets store is not initialized. Run 'init' first.", file=sys.stderr) sys.exit(1) # Look for .env file backend_root = Path(__file__).parent.parent.parent env_file = backend_root / ".env" if not env_file.exists(): print(f"Error: .env file not found at {env_file}", file=sys.stderr) sys.exit(1) password = get_password() try: store.unlock(password) except InvalidMasterPassword: print("Error: Invalid master password", file=sys.stderr) sys.exit(1) # Parse .env file (simple parser - doesn't handle all edge cases) migrated = 0 skipped = 0 with open(env_file) as f: for line_num, line in enumerate(f, 1): line = line.strip() # Skip empty lines and comments if not line or line.startswith('#'): continue # Parse KEY=VALUE format if '=' not in line: print(f"Warning: Skipping invalid line {line_num}: {line}", file=sys.stderr) skipped += 1 continue key, value = line.split('=', 1) key = key.strip() value = value.strip() # Remove quotes if present if value.startswith('"') and value.endswith('"'): value = value[1:-1] elif value.startswith("'") and value.endswith("'"): value = value[1:-1] # Check if key already exists existing = store.get(key) if existing is not None: print(f"Warning: Secret '{key}' already exists, skipping", file=sys.stderr) skipped += 1 continue store.set(key, value) print(f"✓ Migrated: {key}") migrated += 1 print(f"\nMigration complete: {migrated} secrets migrated, {skipped} skipped") if not args.keep_env: # Ask for confirmation before deleting .env confirm = input(f"\nDelete {env_file}? [y/N]: ").strip().lower() if confirm == 'y': env_file.unlink() print(f"✓ Deleted {env_file}") else: print(f"Kept {env_file} (consider deleting it manually)") def main(): """Main CLI entry point.""" parser = argparse.ArgumentParser( description="Manage encrypted secrets store", formatter_class=argparse.RawDescriptionHelpFormatter, ) subparsers = parser.add_subparsers(dest='command', help='Command to run') subparsers.required = True # init parser_init = subparsers.add_parser('init', help='Initialize new secrets store') parser_init.set_defaults(func=cmd_init) # set parser_set = subparsers.add_parser('set', help='Set a secret value') parser_set.add_argument('key', help='Secret key name') parser_set.add_argument('value', help='Secret value') parser_set.set_defaults(func=cmd_set) # get parser_get = subparsers.add_parser('get', help='Get a secret value') parser_get.add_argument('key', help='Secret key name') parser_get.set_defaults(func=cmd_get) # delete parser_delete = subparsers.add_parser('delete', help='Delete a secret') parser_delete.add_argument('key', help='Secret key name') parser_delete.set_defaults(func=cmd_delete) # list parser_list = subparsers.add_parser('list', help='List all secret keys') parser_list.set_defaults(func=cmd_list) # change-password parser_change = subparsers.add_parser('change-password', help='Change master password') parser_change.set_defaults(func=cmd_change_password) # export parser_export = subparsers.add_parser('export', help='Export encrypted backup') parser_export.add_argument('file', help='Output file path') parser_export.add_argument('--force', action='store_true', help='Overwrite existing file') parser_export.set_defaults(func=cmd_export) # import parser_import = subparsers.add_parser('import', help='Import encrypted backup') parser_import.add_argument('file', help='Input file path') parser_import.set_defaults(func=cmd_import) # migrate-from-env parser_migrate = subparsers.add_parser('migrate-from-env', help='Migrate from .env file') parser_migrate.add_argument('--keep-env', action='store_true', help='Keep .env file after migration') parser_migrate.set_defaults(func=cmd_migrate_from_env) args = parser.parse_args() try: args.func(args) except KeyboardInterrupt: print("\nAborted", file=sys.stderr) sys.exit(130) except Exception as e: print(f"Error: {e}", file=sys.stderr) sys.exit(1) if __name__ == '__main__': main()