04/24 Fixed keyring issues
This commit is contained in:
@@ -225,7 +225,12 @@ def load_config() -> dict:
|
||||
if data.get("host") and data.get("database") and data.get("user"):
|
||||
return data
|
||||
|
||||
# One-time migration: pull from config.ini if it still exists
|
||||
# One-time migration: pull from config.ini if it still exists.
|
||||
# IMPORTANT: decrypt_value() uses Windows DPAPI which is machine+user-scoped.
|
||||
# A config.ini created on a different machine cannot be decrypted here —
|
||||
# we catch RuntimeError (and any other error) per-field and skip gracefully,
|
||||
# letting the user re-enter credentials via the Settings dialog instead of
|
||||
# crashing the application.
|
||||
try:
|
||||
import configparser as _cp
|
||||
_cfg_file = "config.ini"
|
||||
@@ -235,13 +240,27 @@ def load_config() -> dict:
|
||||
cfg.read(_cfg_file, encoding="utf-8")
|
||||
if cfg.has_section("database"):
|
||||
section = cfg["database"]
|
||||
|
||||
def _safe_decrypt(raw: str) -> str:
|
||||
"""Decrypt a DPAPI value; return empty string on any failure."""
|
||||
try:
|
||||
return decrypt_value(raw)
|
||||
except Exception as dec_err:
|
||||
logger.warning(
|
||||
f"config.ini migration: could not decrypt a value "
|
||||
f"(DPAPI is machine-scoped — this is expected on a new "
|
||||
f"machine). Skipping migration. Detail: {dec_err}"
|
||||
)
|
||||
return ""
|
||||
|
||||
migrated = {
|
||||
"host": section.get("host", ""),
|
||||
"port": section.getint("port", 3306),
|
||||
"database": section.get("database", ""),
|
||||
"user": decrypt_value(section.get("user", "")),
|
||||
"password": decrypt_value(section.get("password", "")),
|
||||
"user": _safe_decrypt(section.get("user", "")),
|
||||
"password": _safe_decrypt(section.get("password", "")),
|
||||
}
|
||||
# Only migrate if we successfully decrypted a usable credential set
|
||||
if migrated.get("host") and migrated.get("database") and migrated.get("user"):
|
||||
_keyring_set(migrated)
|
||||
logger.info(
|
||||
@@ -249,6 +268,14 @@ def load_config() -> dict:
|
||||
"config.ini [database] section is no longer needed."
|
||||
)
|
||||
return migrated
|
||||
elif migrated.get("host"):
|
||||
# File found but decryption failed (different machine) —
|
||||
# log clearly and fall through to prompt the user
|
||||
logger.warning(
|
||||
"config.ini found but credentials could not be decrypted "
|
||||
"(created on a different machine). "
|
||||
"Please re-enter connection details in the Settings dialog."
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"config.ini migration attempt failed: {e}")
|
||||
|
||||
@@ -316,7 +343,14 @@ def reload_db_config():
|
||||
# ─── Database Configuration ───────────────────────────────────────────────────
|
||||
# Populated from the OS keychain at import time; falls back to placeholder
|
||||
# strings so the module is importable even before first-run setup has completed.
|
||||
_ini = load_config()
|
||||
# The try/except ensures a corrupt or inaccessible keychain entry never crashes
|
||||
# the import — the user will simply be shown the Settings dialog on first run.
|
||||
try:
|
||||
_ini = load_config()
|
||||
except Exception as _load_err:
|
||||
logger.warning(f"Could not load DB config at startup: {_load_err}")
|
||||
_ini = {}
|
||||
|
||||
DB_CONFIG = {
|
||||
"host": _ini.get("host", "your-mysql-host"),
|
||||
"port": _ini.get("port", 3306),
|
||||
@@ -819,144 +853,6 @@ logging.basicConfig(
|
||||
)
|
||||
logger = logging.getLogger("config")
|
||||
|
||||
# ─── Config File Load / Save ──────────────────────────────────────────────────
|
||||
import configparser as _cp
|
||||
import os as _os
|
||||
|
||||
CONFIG_FILE = "config.ini"
|
||||
APP_TITLE = "Website Checker"
|
||||
APP_VERSION = "1.0.0"
|
||||
|
||||
|
||||
# Sensitive fields that are DPAPI-encrypted in config.ini
|
||||
_DB_SENSITIVE = {"user", "password"}
|
||||
|
||||
|
||||
def load_config() -> dict:
|
||||
"""
|
||||
Load DB settings from config.ini.
|
||||
Returns a dict with keys: host, port, database, user, password.
|
||||
Sensitive fields (user, password) are decrypted transparently via DPAPI.
|
||||
Returns empty dict if the file does not exist or is incomplete.
|
||||
"""
|
||||
from utils.config_crypto import decrypt_value
|
||||
cfg = _cp.ConfigParser()
|
||||
if not _os.path.exists(CONFIG_FILE):
|
||||
return {}
|
||||
cfg.read(CONFIG_FILE, encoding="utf-8")
|
||||
if "database" not in cfg:
|
||||
return {}
|
||||
section = cfg["database"]
|
||||
try:
|
||||
return {
|
||||
"host": section.get("host", ""),
|
||||
"port": section.getint("port", 3306),
|
||||
"database": section.get("database", ""),
|
||||
"user": decrypt_value(section.get("user", "")),
|
||||
"password": decrypt_value(section.get("password", "")),
|
||||
}
|
||||
except RuntimeError as exc:
|
||||
logger.error(f"Failed to decrypt DB credentials: {exc}")
|
||||
raise
|
||||
|
||||
|
||||
def save_config(host: str, port: int, database: str, user: str, password: str):
|
||||
"""Persist DB connection settings to config.ini (sensitive fields DPAPI-encrypted)."""
|
||||
from utils.config_crypto import encrypt_value
|
||||
# Preserve any existing non-database sections (email, crypto, groq)
|
||||
cfg = _cp.ConfigParser()
|
||||
if _os.path.exists(CONFIG_FILE):
|
||||
cfg.read(CONFIG_FILE, encoding="utf-8")
|
||||
cfg["database"] = {
|
||||
"host": host,
|
||||
"port": str(port),
|
||||
"database": database,
|
||||
"user": encrypt_value(user),
|
||||
"password": encrypt_value(password),
|
||||
}
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as fh:
|
||||
cfg.write(fh)
|
||||
logger.info(f"Configuration saved to {CONFIG_FILE} (credentials encrypted).")
|
||||
|
||||
|
||||
def migrate_plaintext_config():
|
||||
"""
|
||||
One-time migration: if config.ini contains plain-text DB credentials
|
||||
(no 'dpapi:' prefix) encrypt them in-place using Windows DPAPI.
|
||||
Safe to call on every startup — is a no-op when already encrypted.
|
||||
"""
|
||||
from utils.config_crypto import encrypt_value, is_encrypted
|
||||
if not _os.path.exists(CONFIG_FILE):
|
||||
return
|
||||
cfg = _cp.ConfigParser()
|
||||
cfg.read(CONFIG_FILE, encoding="utf-8")
|
||||
changed = False
|
||||
|
||||
# DB section
|
||||
for key in ("user", "password"):
|
||||
if cfg.has_option("database", key):
|
||||
raw = cfg.get("database", key)
|
||||
if raw and not is_encrypted(raw):
|
||||
cfg.set("database", key, encrypt_value(raw))
|
||||
changed = True
|
||||
|
||||
# Email section
|
||||
if cfg.has_option("email", "smtp_password"):
|
||||
raw = cfg.get("email", "smtp_password")
|
||||
if raw and not is_encrypted(raw):
|
||||
cfg.set("email", "smtp_password", encrypt_value(raw))
|
||||
changed = True
|
||||
|
||||
# Groq section
|
||||
if cfg.has_option("groq", "api_key"):
|
||||
raw = cfg.get("groq", "api_key")
|
||||
if raw and not is_encrypted(raw):
|
||||
cfg.set("groq", "api_key", encrypt_value(raw))
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as fh:
|
||||
cfg.write(fh)
|
||||
logger.info("config.ini: plain-text credentials encrypted with Windows DPAPI.")
|
||||
|
||||
|
||||
def config_exists() -> bool:
|
||||
"""Return True if config.ini contains all required connection fields."""
|
||||
ini = load_config()
|
||||
return bool(ini.get("host") and ini.get("database") and ini.get("user"))
|
||||
|
||||
|
||||
def reload_db_config():
|
||||
"""
|
||||
Re-read config.ini and update DB_CONFIG in place.
|
||||
Also resets the connection pool so the next get_connection() uses new creds.
|
||||
"""
|
||||
global _pool, DB_CONFIG
|
||||
ini = load_config()
|
||||
DB_CONFIG.update({
|
||||
"host": ini.get("host", DB_CONFIG["host"]),
|
||||
"port": ini.get("port", DB_CONFIG["port"]),
|
||||
"database": ini.get("database", DB_CONFIG["database"]),
|
||||
"user": ini.get("user", DB_CONFIG["user"]),
|
||||
"password": ini.get("password", DB_CONFIG["password"]),
|
||||
})
|
||||
_pool = None # force pool recreation on next connection
|
||||
logger.info("DB_CONFIG reloaded from config.ini.")
|
||||
|
||||
|
||||
# ─── Database Configuration ───────────────────────────────────────────────────
|
||||
# Populated from config.ini at runtime; falls back to placeholder strings so
|
||||
# the module is importable even before first-run setup has completed.
|
||||
_ini = load_config()
|
||||
DB_CONFIG = {
|
||||
"host": _ini.get("host", "your-mysql-host"),
|
||||
"port": _ini.get("port", 3306),
|
||||
"database": _ini.get("database", "website_checker"),
|
||||
"user": _ini.get("user", "your-db-user"),
|
||||
"password": _ini.get("password", "your-db-password"),
|
||||
"connection_timeout": 10,
|
||||
}
|
||||
|
||||
# ─── Connection Pool ──────────────────────────────────────────────────────────
|
||||
_pool = None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user