Files
ai/backend.old/src/secrets_manager/cli.py
2026-03-11 18:47:11 -04:00

375 lines
11 KiB
Python

#!/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()