souleyez 2.43.26__py3-none-any.whl → 2.43.34__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +23434 -10286
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.26.dist-info/RECORD +0 -379
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.26.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
souleyez/storage/crypto.py
CHANGED
|
@@ -45,8 +45,8 @@ class CryptoManager:
|
|
|
45
45
|
self._salt: Optional[bytes] = None
|
|
46
46
|
self._last_access: Optional[datetime] = None
|
|
47
47
|
self._last_unlock_time: Optional[datetime] = None
|
|
48
|
-
self._timeout_minutes = config.get(
|
|
49
|
-
|
|
48
|
+
self._timeout_minutes = config.get("security.session_timeout_minutes", 30)
|
|
49
|
+
|
|
50
50
|
# Vault lockout tracking
|
|
51
51
|
self._failed_attempts = 0
|
|
52
52
|
self._lockout_until: Optional[datetime] = None
|
|
@@ -61,13 +61,13 @@ class CryptoManager:
|
|
|
61
61
|
"""Load existing crypto config or create new one."""
|
|
62
62
|
if self.crypto_config_path.exists():
|
|
63
63
|
try:
|
|
64
|
-
with open(self.crypto_config_path,
|
|
64
|
+
with open(self.crypto_config_path, "r") as f:
|
|
65
65
|
cfg = json.load(f)
|
|
66
|
-
self._salt = base64.b64decode(cfg.get(
|
|
67
|
-
self._encryption_enabled = cfg.get(
|
|
66
|
+
self._salt = base64.b64decode(cfg.get("salt", ""))
|
|
67
|
+
self._encryption_enabled = cfg.get("encryption_enabled", False)
|
|
68
68
|
|
|
69
69
|
# Load persisted lockout state
|
|
70
|
-
lockout_str = cfg.get(
|
|
70
|
+
lockout_str = cfg.get("lockout_until")
|
|
71
71
|
if lockout_str:
|
|
72
72
|
self._lockout_until = datetime.fromisoformat(lockout_str)
|
|
73
73
|
# Check if lockout has expired
|
|
@@ -77,20 +77,21 @@ class CryptoManager:
|
|
|
77
77
|
self._failed_attempts = 0
|
|
78
78
|
else:
|
|
79
79
|
# Active lockout - load the failed attempts count
|
|
80
|
-
self._failed_attempts = cfg.get(
|
|
80
|
+
self._failed_attempts = cfg.get("failed_attempts", 0)
|
|
81
81
|
else:
|
|
82
82
|
# No lockout - reset failed attempts (fresh start)
|
|
83
83
|
self._failed_attempts = 0
|
|
84
84
|
|
|
85
|
-
logger.info(
|
|
86
|
-
"
|
|
87
|
-
|
|
85
|
+
logger.info(
|
|
86
|
+
"Crypto config loaded",
|
|
87
|
+
extra={"encryption_enabled": self._encryption_enabled},
|
|
88
|
+
)
|
|
88
89
|
except (json.JSONDecodeError, KeyError) as e:
|
|
89
90
|
# Corrupted config, regenerate
|
|
90
|
-
logger.warning(
|
|
91
|
-
"
|
|
92
|
-
"traceback": traceback.format_exc()
|
|
93
|
-
|
|
91
|
+
logger.warning(
|
|
92
|
+
"Crypto config corrupted, regenerating",
|
|
93
|
+
extra={"error": str(e), "traceback": traceback.format_exc()},
|
|
94
|
+
)
|
|
94
95
|
self._initialize_config()
|
|
95
96
|
else:
|
|
96
97
|
logger.info("Initializing new crypto config")
|
|
@@ -101,10 +102,10 @@ class CryptoManager:
|
|
|
101
102
|
self._salt = os.urandom(32) # 256-bit salt
|
|
102
103
|
self._encryption_enabled = False
|
|
103
104
|
self._save_config()
|
|
104
|
-
logger.info(
|
|
105
|
-
"
|
|
106
|
-
"encryption_enabled": False
|
|
107
|
-
|
|
105
|
+
logger.info(
|
|
106
|
+
"Crypto config initialized",
|
|
107
|
+
extra={"salt_generated": True, "encryption_enabled": False},
|
|
108
|
+
)
|
|
108
109
|
|
|
109
110
|
def _save_config(self):
|
|
110
111
|
"""Save crypto configuration to disk."""
|
|
@@ -114,29 +115,33 @@ class CryptoManager:
|
|
|
114
115
|
existing_verification = None
|
|
115
116
|
if self.crypto_config_path.exists():
|
|
116
117
|
try:
|
|
117
|
-
with open(self.crypto_config_path,
|
|
118
|
+
with open(self.crypto_config_path, "r") as f:
|
|
118
119
|
existing_config = json.load(f)
|
|
119
|
-
existing_verification = existing_config.get(
|
|
120
|
+
existing_verification = existing_config.get("password_verification")
|
|
120
121
|
except (json.JSONDecodeError, IOError):
|
|
121
122
|
pass
|
|
122
123
|
|
|
123
124
|
config = {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
"salt": base64.b64encode(self._salt).decode("utf-8"),
|
|
126
|
+
"encryption_enabled": self._encryption_enabled,
|
|
127
|
+
"failed_attempts": self._failed_attempts,
|
|
128
|
+
"lockout_until": (
|
|
129
|
+
self._lockout_until.isoformat() if self._lockout_until else None
|
|
130
|
+
),
|
|
128
131
|
}
|
|
129
132
|
|
|
130
133
|
# Add password verification token
|
|
131
134
|
if self._fernet:
|
|
132
135
|
# Create new verification token with current fernet
|
|
133
136
|
verification_data = b"SOULEYEZ_PASSWORD_VERIFICATION_TOKEN"
|
|
134
|
-
config[
|
|
137
|
+
config["password_verification"] = self._fernet.encrypt(
|
|
138
|
+
verification_data
|
|
139
|
+
).decode("utf-8")
|
|
135
140
|
elif existing_verification:
|
|
136
141
|
# Preserve existing verification token
|
|
137
|
-
config[
|
|
142
|
+
config["password_verification"] = existing_verification
|
|
138
143
|
|
|
139
|
-
with open(self.crypto_config_path,
|
|
144
|
+
with open(self.crypto_config_path, "w") as f:
|
|
140
145
|
json.dump(config, f, indent=2)
|
|
141
146
|
# Secure permissions (readable only by owner)
|
|
142
147
|
os.chmod(self.crypto_config_path, 0o600)
|
|
@@ -148,18 +153,23 @@ class CryptoManager:
|
|
|
148
153
|
def is_unlocked(self) -> bool:
|
|
149
154
|
"""Check if crypto manager is unlocked (key loaded)."""
|
|
150
155
|
return self._fernet is not None
|
|
151
|
-
|
|
156
|
+
|
|
152
157
|
def _check_timeout(self):
|
|
153
158
|
"""Check if session has timed out and lock if necessary."""
|
|
154
159
|
if self._last_access and self._fernet:
|
|
155
160
|
elapsed = (datetime.now() - self._last_access).total_seconds() / 60
|
|
156
161
|
if elapsed > self._timeout_minutes:
|
|
157
|
-
logger.warning(
|
|
158
|
-
"
|
|
159
|
-
|
|
160
|
-
|
|
162
|
+
logger.warning(
|
|
163
|
+
"Session expired due to inactivity",
|
|
164
|
+
extra={
|
|
165
|
+
"elapsed_minutes": round(elapsed, 2),
|
|
166
|
+
"timeout_minutes": self._timeout_minutes,
|
|
167
|
+
},
|
|
168
|
+
)
|
|
161
169
|
self.lock()
|
|
162
|
-
raise RuntimeError(
|
|
170
|
+
raise RuntimeError(
|
|
171
|
+
f"Session expired after {self._timeout_minutes} minutes of inactivity. Please unlock again."
|
|
172
|
+
)
|
|
163
173
|
self._last_access = datetime.now()
|
|
164
174
|
|
|
165
175
|
def derive_key_from_password(self, password: str) -> bytes:
|
|
@@ -179,10 +189,10 @@ class CryptoManager:
|
|
|
179
189
|
algorithm=hashes.SHA256(),
|
|
180
190
|
length=32,
|
|
181
191
|
salt=self._salt,
|
|
182
|
-
iterations=config.get(
|
|
183
|
-
backend=default_backend()
|
|
192
|
+
iterations=config.get("crypto.iterations", 600000),
|
|
193
|
+
backend=default_backend(),
|
|
184
194
|
)
|
|
185
|
-
key = base64.urlsafe_b64encode(kdf.derive(password.encode(
|
|
195
|
+
key = base64.urlsafe_b64encode(kdf.derive(password.encode("utf-8")))
|
|
186
196
|
return key
|
|
187
197
|
|
|
188
198
|
def is_locked_out(self) -> bool:
|
|
@@ -209,19 +219,27 @@ class CryptoManager:
|
|
|
209
219
|
remaining = self._max_attempts - self._failed_attempts
|
|
210
220
|
|
|
211
221
|
if self._failed_attempts >= self._max_attempts:
|
|
212
|
-
self._lockout_until = datetime.now() + timedelta(
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
222
|
+
self._lockout_until = datetime.now() + timedelta(
|
|
223
|
+
minutes=self._lockout_minutes
|
|
224
|
+
)
|
|
225
|
+
logger.warning(
|
|
226
|
+
"Vault locked out",
|
|
227
|
+
extra={
|
|
228
|
+
"failed_attempts": self._failed_attempts,
|
|
229
|
+
"lockout_minutes": self._lockout_minutes,
|
|
230
|
+
},
|
|
231
|
+
)
|
|
217
232
|
# Persist lockout state to survive app restarts
|
|
218
233
|
self._save_config()
|
|
219
234
|
return (0, True)
|
|
220
235
|
|
|
221
|
-
logger.info(
|
|
222
|
-
"
|
|
223
|
-
|
|
224
|
-
|
|
236
|
+
logger.info(
|
|
237
|
+
"Failed vault unlock attempt",
|
|
238
|
+
extra={
|
|
239
|
+
"failed_attempts": self._failed_attempts,
|
|
240
|
+
"remaining_attempts": remaining,
|
|
241
|
+
},
|
|
242
|
+
)
|
|
225
243
|
# Persist failed attempt count
|
|
226
244
|
self._save_config()
|
|
227
245
|
return (remaining, False)
|
|
@@ -255,16 +273,16 @@ class CryptoManager:
|
|
|
255
273
|
# Check if locked out
|
|
256
274
|
if self.is_locked_out():
|
|
257
275
|
remaining = self.get_lockout_remaining()
|
|
258
|
-
logger.warning(
|
|
259
|
-
"remaining_seconds": remaining
|
|
260
|
-
|
|
276
|
+
logger.warning(
|
|
277
|
+
"Unlock attempt during lockout", extra={"remaining_seconds": remaining}
|
|
278
|
+
)
|
|
261
279
|
return False
|
|
262
|
-
|
|
280
|
+
|
|
263
281
|
# Reject empty passwords immediately
|
|
264
282
|
if not password or not password.strip():
|
|
265
283
|
logger.warning("Unlock failed - empty password not allowed")
|
|
266
284
|
return False
|
|
267
|
-
|
|
285
|
+
|
|
268
286
|
try:
|
|
269
287
|
key = self.derive_key_from_password(password)
|
|
270
288
|
self._fernet = Fernet(key)
|
|
@@ -272,21 +290,27 @@ class CryptoManager:
|
|
|
272
290
|
# Validate password by trying to decrypt the verification token
|
|
273
291
|
# Load current config to get verification token
|
|
274
292
|
if self.crypto_config_path.exists():
|
|
275
|
-
with open(self.crypto_config_path,
|
|
293
|
+
with open(self.crypto_config_path, "r") as f:
|
|
276
294
|
cfg = json.load(f)
|
|
277
295
|
|
|
278
|
-
verification_token = cfg.get(
|
|
296
|
+
verification_token = cfg.get("password_verification")
|
|
279
297
|
if verification_token:
|
|
280
298
|
# Try to decrypt the verification token
|
|
281
299
|
try:
|
|
282
|
-
decrypted = self._fernet.decrypt(
|
|
300
|
+
decrypted = self._fernet.decrypt(
|
|
301
|
+
verification_token.encode("utf-8")
|
|
302
|
+
)
|
|
283
303
|
if decrypted != b"SOULEYEZ_PASSWORD_VERIFICATION_TOKEN":
|
|
284
304
|
self._fernet = None
|
|
285
|
-
logger.warning(
|
|
305
|
+
logger.warning(
|
|
306
|
+
"Unlock failed - password verification failed"
|
|
307
|
+
)
|
|
286
308
|
return False
|
|
287
309
|
except Exception as decrypt_error:
|
|
288
310
|
self._fernet = None
|
|
289
|
-
logger.warning(
|
|
311
|
+
logger.warning(
|
|
312
|
+
f"Unlock failed - incorrect password: {decrypt_error}"
|
|
313
|
+
)
|
|
290
314
|
return False
|
|
291
315
|
else:
|
|
292
316
|
# No verification token yet (old version or first unlock after upgrade)
|
|
@@ -298,9 +322,10 @@ class CryptoManager:
|
|
|
298
322
|
self.reset_failed_attempts()
|
|
299
323
|
self._last_access = datetime.now()
|
|
300
324
|
self._last_unlock_time = datetime.now()
|
|
301
|
-
logger.info(
|
|
302
|
-
"
|
|
303
|
-
|
|
325
|
+
logger.info(
|
|
326
|
+
"Crypto manager unlocked",
|
|
327
|
+
extra={"encryption_enabled": self._encryption_enabled},
|
|
328
|
+
)
|
|
304
329
|
|
|
305
330
|
# Write msfrpc session file if configured (enables background worker RPC access)
|
|
306
331
|
self._write_msfrpc_session_if_configured()
|
|
@@ -308,10 +333,10 @@ class CryptoManager:
|
|
|
308
333
|
return True
|
|
309
334
|
except Exception as e:
|
|
310
335
|
self._fernet = None
|
|
311
|
-
logger.error(
|
|
312
|
-
"
|
|
313
|
-
"error_message": "<redacted>"
|
|
314
|
-
|
|
336
|
+
logger.error(
|
|
337
|
+
"Unlock failed",
|
|
338
|
+
extra={"error_type": type(e).__name__, "error_message": "<redacted>"},
|
|
339
|
+
)
|
|
315
340
|
return False
|
|
316
341
|
|
|
317
342
|
def lock(self):
|
|
@@ -334,11 +359,11 @@ class CryptoManager:
|
|
|
334
359
|
from souleyez import config as app_config
|
|
335
360
|
|
|
336
361
|
# Only write if msfrpc is enabled
|
|
337
|
-
if not app_config.get(
|
|
362
|
+
if not app_config.get("msfrpc.enabled", False):
|
|
338
363
|
return
|
|
339
364
|
|
|
340
365
|
# Get encrypted password
|
|
341
|
-
encrypted = app_config.get(
|
|
366
|
+
encrypted = app_config.get("msfrpc.password")
|
|
342
367
|
if not encrypted:
|
|
343
368
|
return
|
|
344
369
|
|
|
@@ -349,6 +374,7 @@ class CryptoManager:
|
|
|
349
374
|
|
|
350
375
|
# Write to session file
|
|
351
376
|
from souleyez.core.msf_rpc_manager import write_msfrpc_session
|
|
377
|
+
|
|
352
378
|
write_msfrpc_session(decrypted)
|
|
353
379
|
logger.debug("MSF RPC session file written for background worker")
|
|
354
380
|
|
|
@@ -359,6 +385,7 @@ class CryptoManager:
|
|
|
359
385
|
"""Clear the msfrpc session file on lock."""
|
|
360
386
|
try:
|
|
361
387
|
from souleyez.core.msf_rpc_manager import clear_msfrpc_session
|
|
388
|
+
|
|
362
389
|
clear_msfrpc_session()
|
|
363
390
|
except Exception:
|
|
364
391
|
pass # Best effort cleanup
|
|
@@ -377,71 +404,99 @@ class CryptoManager:
|
|
|
377
404
|
logger.info("Encryption already enabled")
|
|
378
405
|
return True
|
|
379
406
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
407
|
+
try:
|
|
408
|
+
# When enabling encryption (fresh or after disable), we need to:
|
|
409
|
+
# 1. Generate new salt (or use existing if valid)
|
|
410
|
+
# 2. Derive key from password
|
|
411
|
+
# 3. Create new fernet
|
|
412
|
+
# 4. Save config with new verification token
|
|
413
|
+
|
|
414
|
+
# Generate fresh salt if needed
|
|
415
|
+
if not self._salt:
|
|
416
|
+
self._salt = os.urandom(32)
|
|
417
|
+
|
|
418
|
+
# Derive key and create fernet
|
|
419
|
+
key = self.derive_key_from_password(password)
|
|
420
|
+
self._fernet = Fernet(key)
|
|
421
|
+
|
|
422
|
+
# Enable encryption and save (this creates new verification token)
|
|
423
|
+
self._encryption_enabled = True
|
|
424
|
+
self._save_config()
|
|
425
|
+
|
|
426
|
+
# Reset lockout state on successful enable
|
|
427
|
+
self.reset_failed_attempts()
|
|
428
|
+
self._last_access = datetime.now()
|
|
429
|
+
self._last_unlock_time = datetime.now()
|
|
430
|
+
|
|
431
|
+
# Migrate existing plaintext credentials
|
|
432
|
+
migrated = self._migrate_plaintext_credentials()
|
|
433
|
+
logger.info(
|
|
434
|
+
"Encryption enabled successfully",
|
|
435
|
+
extra={"credentials_migrated": migrated},
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
return True
|
|
439
|
+
except Exception as e:
|
|
440
|
+
logger.error(
|
|
441
|
+
"Failed to enable encryption",
|
|
442
|
+
extra={"error_type": type(e).__name__, "error": str(e)},
|
|
443
|
+
)
|
|
444
|
+
self._fernet = None
|
|
383
445
|
return False
|
|
384
446
|
|
|
385
|
-
self._encryption_enabled = True
|
|
386
|
-
self._save_config()
|
|
387
|
-
|
|
388
|
-
# Migrate existing plaintext credentials
|
|
389
|
-
migrated = self._migrate_plaintext_credentials()
|
|
390
|
-
logger.info("Encryption enabled successfully", extra={"credentials_migrated": migrated})
|
|
391
|
-
|
|
392
|
-
return True
|
|
393
|
-
|
|
394
447
|
def _migrate_plaintext_credentials(self) -> int:
|
|
395
448
|
"""
|
|
396
449
|
Migrate existing plaintext credentials to encrypted format.
|
|
397
|
-
|
|
450
|
+
|
|
398
451
|
Returns:
|
|
399
452
|
Number of credentials migrated
|
|
400
453
|
"""
|
|
401
454
|
from souleyez.storage.database import Database
|
|
402
|
-
|
|
455
|
+
|
|
403
456
|
if not self._fernet:
|
|
404
457
|
logger.error("Cannot migrate credentials - crypto manager not unlocked")
|
|
405
458
|
return 0
|
|
406
|
-
|
|
459
|
+
|
|
407
460
|
db = Database()
|
|
408
461
|
credentials = db.execute("SELECT id, username, password FROM credentials")
|
|
409
|
-
|
|
462
|
+
|
|
410
463
|
migrated = 0
|
|
411
464
|
for cred in credentials:
|
|
412
|
-
cred_id = cred[
|
|
413
|
-
username = cred.get(
|
|
414
|
-
password = cred.get(
|
|
415
|
-
|
|
465
|
+
cred_id = cred["id"]
|
|
466
|
+
username = cred.get("username")
|
|
467
|
+
password = cred.get("password")
|
|
468
|
+
|
|
416
469
|
updated = False
|
|
417
470
|
update_data = {}
|
|
418
|
-
|
|
471
|
+
|
|
419
472
|
# Check if username is plaintext (not encrypted)
|
|
420
473
|
if username and not self._is_encrypted(username):
|
|
421
474
|
encrypted_username = self.encrypt(username)
|
|
422
475
|
if encrypted_username:
|
|
423
|
-
update_data[
|
|
476
|
+
update_data["username"] = encrypted_username
|
|
424
477
|
updated = True
|
|
425
|
-
|
|
478
|
+
|
|
426
479
|
# Check if password is plaintext (not encrypted)
|
|
427
480
|
if password and not self._is_encrypted(password):
|
|
428
481
|
encrypted_password = self.encrypt(password)
|
|
429
482
|
if encrypted_password:
|
|
430
|
-
update_data[
|
|
483
|
+
update_data["password"] = encrypted_password
|
|
431
484
|
updated = True
|
|
432
|
-
|
|
485
|
+
|
|
433
486
|
if updated:
|
|
434
487
|
db.execute(
|
|
435
488
|
f"UPDATE credentials SET {', '.join([f'{k}=?' for k in update_data.keys()])} WHERE id = ?",
|
|
436
|
-
tuple(list(update_data.values()) + [cred_id])
|
|
489
|
+
tuple(list(update_data.values()) + [cred_id]),
|
|
437
490
|
)
|
|
438
491
|
migrated += 1
|
|
439
|
-
|
|
492
|
+
|
|
440
493
|
if migrated > 0:
|
|
441
|
-
logger.info(
|
|
442
|
-
|
|
494
|
+
logger.info(
|
|
495
|
+
f"Migrated {migrated} plaintext credentials to encrypted format"
|
|
496
|
+
)
|
|
497
|
+
|
|
443
498
|
return migrated
|
|
444
|
-
|
|
499
|
+
|
|
445
500
|
def _is_encrypted(self, value: str) -> bool:
|
|
446
501
|
"""
|
|
447
502
|
Check if a value appears to be encrypted (simple heuristic).
|
|
@@ -451,7 +506,7 @@ class CryptoManager:
|
|
|
451
506
|
return False
|
|
452
507
|
try:
|
|
453
508
|
# Fernet tokens always start with version byte (0x80 = gA in base64)
|
|
454
|
-
return value.startswith(
|
|
509
|
+
return value.startswith("gA") and len(value) > 50
|
|
455
510
|
except:
|
|
456
511
|
return False
|
|
457
512
|
|
|
@@ -496,17 +551,17 @@ class CryptoManager:
|
|
|
496
551
|
return None
|
|
497
552
|
|
|
498
553
|
try:
|
|
499
|
-
encrypted_bytes = self._fernet.encrypt(plaintext.encode(
|
|
500
|
-
logger.debug(
|
|
501
|
-
"
|
|
502
|
-
"size_bytes": len(plaintext)
|
|
503
|
-
|
|
504
|
-
return encrypted_bytes.decode(
|
|
554
|
+
encrypted_bytes = self._fernet.encrypt(plaintext.encode("utf-8"))
|
|
555
|
+
logger.debug(
|
|
556
|
+
"Data encrypted",
|
|
557
|
+
extra={"data_type": "credential_field", "size_bytes": len(plaintext)},
|
|
558
|
+
)
|
|
559
|
+
return encrypted_bytes.decode("utf-8")
|
|
505
560
|
except Exception as e:
|
|
506
|
-
logger.error(
|
|
507
|
-
"
|
|
508
|
-
"error_message": "<redacted>"
|
|
509
|
-
|
|
561
|
+
logger.error(
|
|
562
|
+
"Encryption failed",
|
|
563
|
+
extra={"error_type": type(e).__name__, "error_message": "<redacted>"},
|
|
564
|
+
)
|
|
510
565
|
raise RuntimeError("Encryption failed")
|
|
511
566
|
|
|
512
567
|
def decrypt(self, ciphertext: str) -> Optional[str]:
|
|
@@ -533,52 +588,164 @@ class CryptoManager:
|
|
|
533
588
|
return None
|
|
534
589
|
|
|
535
590
|
try:
|
|
536
|
-
decrypted_bytes = self._fernet.decrypt(ciphertext.encode(
|
|
537
|
-
logger.debug(
|
|
538
|
-
"
|
|
539
|
-
"success": True
|
|
540
|
-
|
|
541
|
-
return decrypted_bytes.decode(
|
|
591
|
+
decrypted_bytes = self._fernet.decrypt(ciphertext.encode("utf-8"))
|
|
592
|
+
logger.debug(
|
|
593
|
+
"Data decrypted",
|
|
594
|
+
extra={"data_type": "credential_field", "success": True},
|
|
595
|
+
)
|
|
596
|
+
return decrypted_bytes.decode("utf-8")
|
|
542
597
|
except InvalidToken:
|
|
543
598
|
# Data might be plaintext (migration scenario)
|
|
544
599
|
logger.warning("Decryption failed - data may be plaintext")
|
|
545
600
|
return ciphertext
|
|
546
601
|
except Exception as e:
|
|
547
|
-
logger.error(
|
|
548
|
-
"
|
|
549
|
-
"error_message": "<redacted>"
|
|
550
|
-
|
|
602
|
+
logger.error(
|
|
603
|
+
"Decryption failed",
|
|
604
|
+
extra={"error_type": type(e).__name__, "error_message": "<redacted>"},
|
|
605
|
+
)
|
|
551
606
|
raise RuntimeError("Decryption failed")
|
|
552
607
|
|
|
553
|
-
def change_password(self, old_password: str, new_password: str) ->
|
|
608
|
+
def change_password(self, old_password: str, new_password: str) -> tuple:
|
|
554
609
|
"""
|
|
555
|
-
Change master password.
|
|
610
|
+
Change master password with atomic credential re-encryption.
|
|
556
611
|
|
|
557
|
-
|
|
558
|
-
|
|
612
|
+
Safely re-encrypts all credentials with the new password. If any step
|
|
613
|
+
fails, the old password remains valid and no data is lost.
|
|
559
614
|
|
|
560
615
|
Args:
|
|
561
616
|
old_password: Current password
|
|
562
617
|
new_password: New password
|
|
563
618
|
|
|
564
619
|
Returns:
|
|
565
|
-
|
|
620
|
+
Tuple of (success: bool, error_message: str or None, credentials_migrated: int)
|
|
566
621
|
"""
|
|
567
|
-
|
|
622
|
+
from souleyez.storage.database import Database
|
|
623
|
+
|
|
624
|
+
# Step 1: Verify old password
|
|
568
625
|
if not self.unlock(old_password):
|
|
569
626
|
logger.warning("Password change failed - old password incorrect")
|
|
570
|
-
return False
|
|
627
|
+
return (False, "Incorrect current password", 0)
|
|
571
628
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
key = self.derive_key_from_password(new_password)
|
|
575
|
-
self._fernet = Fernet(key)
|
|
629
|
+
old_fernet = self._fernet
|
|
630
|
+
old_salt = self._salt
|
|
576
631
|
|
|
577
|
-
#
|
|
632
|
+
# Step 2: Read all encrypted credentials BEFORE changing anything
|
|
633
|
+
db = Database()
|
|
634
|
+
credentials = db.execute("SELECT id, username, password FROM credentials")
|
|
635
|
+
|
|
636
|
+
# Step 3: Decrypt all credentials with old key
|
|
637
|
+
decrypted_creds = []
|
|
638
|
+
for cred in credentials:
|
|
639
|
+
cred_id = cred["id"]
|
|
640
|
+
username = cred.get("username")
|
|
641
|
+
password = cred.get("password")
|
|
642
|
+
|
|
643
|
+
try:
|
|
644
|
+
decrypted_username = None
|
|
645
|
+
decrypted_password = None
|
|
646
|
+
|
|
647
|
+
if username and self._is_encrypted(username):
|
|
648
|
+
decrypted_username = old_fernet.decrypt(
|
|
649
|
+
username.encode("utf-8")
|
|
650
|
+
).decode("utf-8")
|
|
651
|
+
else:
|
|
652
|
+
decrypted_username = username
|
|
653
|
+
|
|
654
|
+
if password and self._is_encrypted(password):
|
|
655
|
+
decrypted_password = old_fernet.decrypt(
|
|
656
|
+
password.encode("utf-8")
|
|
657
|
+
).decode("utf-8")
|
|
658
|
+
else:
|
|
659
|
+
decrypted_password = password
|
|
660
|
+
|
|
661
|
+
decrypted_creds.append(
|
|
662
|
+
{
|
|
663
|
+
"id": cred_id,
|
|
664
|
+
"username": decrypted_username,
|
|
665
|
+
"password": decrypted_password,
|
|
666
|
+
}
|
|
667
|
+
)
|
|
668
|
+
except Exception as e:
|
|
669
|
+
# Rollback - don't change anything
|
|
670
|
+
logger.error(
|
|
671
|
+
"Password change failed - could not decrypt credential",
|
|
672
|
+
extra={"cred_id": cred_id, "error": str(e)},
|
|
673
|
+
)
|
|
674
|
+
return (False, f"Failed to decrypt credential ID {cred_id}", 0)
|
|
675
|
+
|
|
676
|
+
# Step 4: Generate new salt and key
|
|
677
|
+
new_salt = os.urandom(32)
|
|
678
|
+
self._salt = new_salt
|
|
679
|
+
new_key = self.derive_key_from_password(new_password)
|
|
680
|
+
new_fernet = Fernet(new_key)
|
|
681
|
+
|
|
682
|
+
# Step 5: Re-encrypt all credentials with new key
|
|
683
|
+
re_encrypted = []
|
|
684
|
+
for cred in decrypted_creds:
|
|
685
|
+
try:
|
|
686
|
+
encrypted_username = None
|
|
687
|
+
encrypted_password = None
|
|
688
|
+
|
|
689
|
+
if cred["username"]:
|
|
690
|
+
encrypted_username = new_fernet.encrypt(
|
|
691
|
+
cred["username"].encode("utf-8")
|
|
692
|
+
).decode("utf-8")
|
|
693
|
+
|
|
694
|
+
if cred["password"]:
|
|
695
|
+
encrypted_password = new_fernet.encrypt(
|
|
696
|
+
cred["password"].encode("utf-8")
|
|
697
|
+
).decode("utf-8")
|
|
698
|
+
|
|
699
|
+
re_encrypted.append(
|
|
700
|
+
{
|
|
701
|
+
"id": cred["id"],
|
|
702
|
+
"username": encrypted_username,
|
|
703
|
+
"password": encrypted_password,
|
|
704
|
+
}
|
|
705
|
+
)
|
|
706
|
+
except Exception as e:
|
|
707
|
+
# Rollback - restore old salt and don't update anything
|
|
708
|
+
self._salt = old_salt
|
|
709
|
+
self._fernet = old_fernet
|
|
710
|
+
logger.error(
|
|
711
|
+
"Password change failed - could not re-encrypt credential",
|
|
712
|
+
extra={"cred_id": cred["id"], "error": str(e)},
|
|
713
|
+
)
|
|
714
|
+
return (False, f"Failed to re-encrypt credential ID {cred['id']}", 0)
|
|
715
|
+
|
|
716
|
+
# Step 6: Update database with re-encrypted credentials
|
|
717
|
+
migrated = 0
|
|
718
|
+
try:
|
|
719
|
+
for cred in re_encrypted:
|
|
720
|
+
db.execute(
|
|
721
|
+
"UPDATE credentials SET username = ?, password = ? WHERE id = ?",
|
|
722
|
+
(cred["username"], cred["password"], cred["id"]),
|
|
723
|
+
)
|
|
724
|
+
migrated += 1
|
|
725
|
+
except Exception as e:
|
|
726
|
+
# Database update failed - this is problematic but old salt is still in config
|
|
727
|
+
# The safest thing is to not update the config file
|
|
728
|
+
self._salt = old_salt
|
|
729
|
+
self._fernet = old_fernet
|
|
730
|
+
logger.error(
|
|
731
|
+
"Password change failed - database update error",
|
|
732
|
+
extra={"error": str(e), "credentials_updated": migrated},
|
|
733
|
+
)
|
|
734
|
+
return (
|
|
735
|
+
False,
|
|
736
|
+
f"Database error after updating {migrated} credentials",
|
|
737
|
+
migrated,
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
# Step 7: Only NOW update the fernet and save config
|
|
741
|
+
self._fernet = new_fernet
|
|
578
742
|
self._save_config()
|
|
579
743
|
|
|
580
|
-
logger.info(
|
|
581
|
-
|
|
744
|
+
logger.info(
|
|
745
|
+
"Master password changed successfully",
|
|
746
|
+
extra={"credentials_migrated": migrated},
|
|
747
|
+
)
|
|
748
|
+
return (True, None, migrated)
|
|
582
749
|
|
|
583
750
|
|
|
584
751
|
# Singleton accessor
|