|
27 | 27 | python -m assistant.cli schedule enable <task_id> |
28 | 28 | python -m assistant.cli schedule disable <task_id> |
29 | 29 | python -m assistant.cli schedule history <task_id> |
| 30 | + python -m assistant.cli settings encryption-status |
30 | 31 | python -m assistant.cli settings encrypt |
| 32 | + python -m assistant.cli settings clear-invalid --confirm |
| 33 | + python -m assistant.cli settings reencrypt |
31 | 34 | python -m assistant.cli settings status |
32 | 35 | """ |
33 | 36 | import argparse |
@@ -816,32 +819,56 @@ async def schedule_validate_command(args): |
816 | 819 |
|
817 | 820 | # Settings commands |
818 | 821 |
|
819 | | -async def settings_encrypt_command(args): |
820 | | - """Migrate existing plaintext API keys to encrypted format.""" |
| 822 | +async def settings_encryption_status_command(args): |
| 823 | + """Show detailed encryption status for all sensitive keys.""" |
821 | 824 | service = SettingsService(config.DATABASE_PATH) |
| 825 | + status = await service.get_encryption_status() |
822 | 826 |
|
823 | | - if args.status_only: |
824 | | - # Just show encryption status |
825 | | - status = await service.get_encryption_status() |
| 827 | + if args.json: |
| 828 | + print(json.dumps(status, indent=2)) |
| 829 | + return |
826 | 830 |
|
827 | | - if args.json: |
828 | | - print(json.dumps(status, indent=2)) |
829 | | - return |
| 831 | + print("Encryption Status") |
| 832 | + print("=" * 60) |
| 833 | + print(f"Encryption available: {'Yes' if status['encryption_available'] else 'No'}") |
| 834 | + print(f"All keys encrypted: {'Yes' if status['all_encrypted'] else 'No'}") |
| 835 | + print(f"All keys decryptable: {'Yes' if status.get('all_decryptable', True) else 'No'}") |
| 836 | + print() |
830 | 837 |
|
831 | | - print("Encryption Status") |
832 | | - print("=" * 50) |
833 | | - print(f"Encryption available: {'Yes' if status['encryption_available'] else 'No'}") |
834 | | - print(f"All keys encrypted: {'Yes' if status['all_encrypted'] else 'No'}") |
| 838 | + if status.get('errors'): |
| 839 | + print("Decryption Errors (logged):") |
| 840 | + for key, error_type in status['errors'].items(): |
| 841 | + print(f" - {key}: {error_type}") |
835 | 842 | print() |
836 | | - print("API Key Status:") |
837 | | - for key, info in status['keys'].items(): |
838 | | - status_icon = "OK" if info['is_encrypted'] else ("WARN" if info['has_value'] else "N/A") |
839 | | - has_value_str = "set" if info['has_value'] else "not set" |
| 843 | + |
| 844 | + print("API Key Status:") |
| 845 | + for key, info in status['keys'].items(): |
| 846 | + has_value_str = "set" if info['has_value'] else "not set" |
| 847 | + if not info['has_value']: |
| 848 | + print(f" [N/A] {key}: {has_value_str}") |
| 849 | + elif info['can_decrypt']: |
840 | 850 | encrypted_str = "encrypted" if info['is_encrypted'] else "plaintext" |
841 | | - if info['has_value']: |
842 | | - print(f" [{status_icon}] {key}: {has_value_str}, {encrypted_str}") |
843 | | - else: |
844 | | - print(f" [N/A] {key}: {has_value_str}") |
| 851 | + status_icon = "OK" if info['is_encrypted'] else "WARN" |
| 852 | + print(f" [{status_icon}] {key}: {has_value_str}, {encrypted_str}, decryptable") |
| 853 | + else: |
| 854 | + print(f" [ERR] {key}: {has_value_str}, encrypted, CANNOT DECRYPT") |
| 855 | + |
| 856 | + print() |
| 857 | + print("Actions:") |
| 858 | + if not status.get('all_encrypted'): |
| 859 | + print(" - Run 'python -m cli settings encrypt' to encrypt plaintext keys") |
| 860 | + if not status.get('all_decryptable', True): |
| 861 | + print(" - Run 'python -m cli settings clear-invalid' to remove undecryptable keys") |
| 862 | + print(" - Or restore encryption key salt from backup and run 'python -m cli settings reencrypt'") |
| 863 | + |
| 864 | + |
| 865 | +async def settings_encrypt_command(args): |
| 866 | + """Migrate existing plaintext API keys to encrypted format.""" |
| 867 | + service = SettingsService(config.DATABASE_PATH) |
| 868 | + |
| 869 | + if args.status_only: |
| 870 | + # Just show encryption status (legacy - redirect to encryption-status) |
| 871 | + await settings_encryption_status_command(args) |
845 | 872 | return |
846 | 873 |
|
847 | 874 | # Perform migration |
@@ -869,6 +896,77 @@ async def settings_encrypt_command(args): |
869 | 896 | sys.exit(1) |
870 | 897 |
|
871 | 898 |
|
| 899 | +async def settings_clear_invalid_command(args): |
| 900 | + """Clear all encrypted keys that cannot be decrypted.""" |
| 901 | + service = SettingsService(config.DATABASE_PATH) |
| 902 | + |
| 903 | + # Confirm unless --confirm flag is set |
| 904 | + if not args.confirm: |
| 905 | + print("This will DELETE all encrypted keys that cannot be decrypted.") |
| 906 | + print("You will need to re-enter these API keys in the Settings UI.") |
| 907 | + print() |
| 908 | + print("Run with --confirm to proceed.") |
| 909 | + sys.exit(0) |
| 910 | + |
| 911 | + result = await service.clear_invalid_encrypted_keys() |
| 912 | + |
| 913 | + if args.json: |
| 914 | + print(json.dumps(result, indent=2)) |
| 915 | + return |
| 916 | + |
| 917 | + if result['success']: |
| 918 | + print("Clear Invalid Keys Complete") |
| 919 | + print("=" * 50) |
| 920 | + if result['cleared']: |
| 921 | + print(f"Cleared {len(result['cleared'])} key(s):") |
| 922 | + for key in result['cleared']: |
| 923 | + print(f" - {key}") |
| 924 | + else: |
| 925 | + print("No invalid keys found.") |
| 926 | + if result['skipped']: |
| 927 | + print(f"\nSkipped {len(result['skipped'])} key(s) (not encrypted or decryptable):") |
| 928 | + for key in result['skipped']: |
| 929 | + print(f" - {key}") |
| 930 | + else: |
| 931 | + print(f"Error: {result['error']}", file=sys.stderr) |
| 932 | + sys.exit(1) |
| 933 | + |
| 934 | + |
| 935 | +async def settings_reencrypt_command(args): |
| 936 | + """Re-encrypt all sensitive keys with the current encryption key.""" |
| 937 | + service = SettingsService(config.DATABASE_PATH) |
| 938 | + |
| 939 | + print("Re-encrypting all sensitive keys with current encryption key...") |
| 940 | + result = await service.reencrypt_with_current_key() |
| 941 | + |
| 942 | + if args.json: |
| 943 | + print(json.dumps(result, indent=2)) |
| 944 | + return |
| 945 | + |
| 946 | + if result['success']: |
| 947 | + print() |
| 948 | + print("Re-encryption Complete") |
| 949 | + print("=" * 50) |
| 950 | + if result['reencrypted']: |
| 951 | + print(f"Re-encrypted {len(result['reencrypted'])} key(s):") |
| 952 | + for key in result['reencrypted']: |
| 953 | + print(f" - {key}") |
| 954 | + else: |
| 955 | + print("No keys needed re-encryption.") |
| 956 | + if result['skipped']: |
| 957 | + print(f"\nSkipped {len(result['skipped'])} key(s) (no value set):") |
| 958 | + for key in result['skipped']: |
| 959 | + print(f" - {key}") |
| 960 | + else: |
| 961 | + print() |
| 962 | + print(f"Re-encryption failed: {result['error']}", file=sys.stderr) |
| 963 | + if result['failed']: |
| 964 | + print("\nFailed keys:") |
| 965 | + for failure in result['failed']: |
| 966 | + print(f" - {failure['key']}: {failure['error']}") |
| 967 | + sys.exit(1) |
| 968 | + |
| 969 | + |
872 | 970 | async def settings_status_command(args): |
873 | 971 | """Show current settings (with masked API keys).""" |
874 | 972 | service = SettingsService(config.DATABASE_PATH) |
@@ -1294,19 +1392,48 @@ def main(): |
1294 | 1392 | settings_parser = subparsers.add_parser("settings", help="Manage settings and encryption") |
1295 | 1393 | settings_subparsers = settings_parser.add_subparsers(dest="settings_command", help="Settings commands") |
1296 | 1394 |
|
| 1395 | + # settings encryption-status |
| 1396 | + settings_enc_status_parser = settings_subparsers.add_parser("encryption-status", help="Show detailed encryption status") |
| 1397 | + settings_enc_status_parser.add_argument( |
| 1398 | + "--json", "-j", |
| 1399 | + action="store_true", |
| 1400 | + help="Output in JSON format" |
| 1401 | + ) |
| 1402 | + |
1297 | 1403 | # settings encrypt |
1298 | 1404 | settings_encrypt_parser = settings_subparsers.add_parser("encrypt", help="Migrate API keys to encrypted format") |
1299 | 1405 | settings_encrypt_parser.add_argument( |
1300 | 1406 | "--status-only", "-s", |
1301 | 1407 | action="store_true", |
1302 | | - help="Only show encryption status without migrating" |
| 1408 | + help="Only show encryption status without migrating (redirects to encryption-status)" |
1303 | 1409 | ) |
1304 | 1410 | settings_encrypt_parser.add_argument( |
1305 | 1411 | "--json", "-j", |
1306 | 1412 | action="store_true", |
1307 | 1413 | help="Output in JSON format" |
1308 | 1414 | ) |
1309 | 1415 |
|
| 1416 | + # settings clear-invalid |
| 1417 | + settings_clear_parser = settings_subparsers.add_parser("clear-invalid", help="Clear undecryptable encrypted keys") |
| 1418 | + settings_clear_parser.add_argument( |
| 1419 | + "--confirm", "-y", |
| 1420 | + action="store_true", |
| 1421 | + help="Confirm deletion" |
| 1422 | + ) |
| 1423 | + settings_clear_parser.add_argument( |
| 1424 | + "--json", "-j", |
| 1425 | + action="store_true", |
| 1426 | + help="Output in JSON format" |
| 1427 | + ) |
| 1428 | + |
| 1429 | + # settings reencrypt |
| 1430 | + settings_reencrypt_parser = settings_subparsers.add_parser("reencrypt", help="Re-encrypt keys with current key") |
| 1431 | + settings_reencrypt_parser.add_argument( |
| 1432 | + "--json", "-j", |
| 1433 | + action="store_true", |
| 1434 | + help="Output in JSON format" |
| 1435 | + ) |
| 1436 | + |
1310 | 1437 | # settings status |
1311 | 1438 | settings_status_parser = settings_subparsers.add_parser("status", help="Show settings status") |
1312 | 1439 | settings_status_parser.add_argument( |
@@ -1389,8 +1516,14 @@ def main(): |
1389 | 1516 | schedule_parser.print_help() |
1390 | 1517 | sys.exit(1) |
1391 | 1518 | elif args.command == "settings": |
1392 | | - if args.settings_command == "encrypt": |
| 1519 | + if args.settings_command == "encryption-status": |
| 1520 | + asyncio.run(settings_encryption_status_command(args)) |
| 1521 | + elif args.settings_command == "encrypt": |
1393 | 1522 | asyncio.run(settings_encrypt_command(args)) |
| 1523 | + elif args.settings_command == "clear-invalid": |
| 1524 | + asyncio.run(settings_clear_invalid_command(args)) |
| 1525 | + elif args.settings_command == "reencrypt": |
| 1526 | + asyncio.run(settings_reencrypt_command(args)) |
1394 | 1527 | elif args.settings_command == "status": |
1395 | 1528 | asyncio.run(settings_status_command(args)) |
1396 | 1529 | else: |
|
0 commit comments