Coverage for src / local_deep_research / database / encryption_check.py: 98%
32 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-02-25 01:07 +0000
1"""
2Check for SQLCipher availability and provide fallback options.
3"""
5import warnings
6from typing import Optional, Tuple
8from loguru import logger
11def check_sqlcipher_available() -> Tuple[bool, Optional[str]]:
12 """
13 Check if SQLCipher is available for use.
15 Returns:
16 Tuple of (is_available, error_message)
17 """
18 try:
19 # Try to import sqlcipher3 (covers both sqlcipher3 and sqlcipher3-binary)
20 import importlib.util
22 if importlib.util.find_spec("sqlcipher3") is not None:
23 return True, None
24 except ImportError:
25 pass
27 # Check if sqlcipher command is available
28 import subprocess
30 try:
31 result = subprocess.run(
32 ["sqlcipher", "--version"],
33 capture_output=True,
34 text=True,
35 timeout=5,
36 )
37 if result.returncode == 0: 37 ↛ 45line 37 didn't jump to line 45 because the condition on line 37 was always true
38 return (
39 False,
40 "SQLCipher is installed but Python bindings are missing. Reinstall the project with: pdm install",
41 )
42 except (subprocess.TimeoutExpired, FileNotFoundError):
43 pass
45 return (
46 False,
47 "SQLCipher is not installed. See docs/SQLCIPHER_INSTALL.md for installation instructions.",
48 )
51def warn_if_no_encryption():
52 """Warn user if encryption is not available."""
53 available, message = check_sqlcipher_available()
55 if not available:
56 warning_msg = (
57 "\n" + "=" * 60 + "\n"
58 "⚠️ SECURITY WARNING: Database Encryption Not Available\n"
59 + "="
60 * 60
61 + "\n"
62 f"{message}\n\n"
63 "Your data will be stored in UNENCRYPTED SQLite databases.\n"
64 "This means:\n"
65 " - API keys are stored in plain text\n"
66 " - Research data is not encrypted at rest\n"
67 " - Anyone with file system access can read your data\n\n"
68 "For production use, we strongly recommend installing SQLCipher.\n"
69 + "="
70 * 60
71 )
73 warnings.warn(warning_msg, category=UserWarning, stacklevel=2)
74 logger.warning(
75 "Running without database encryption - SQLCipher not available"
76 )
78 return available
81def get_connection_string(db_path: str, password: Optional[str] = None) -> str:
82 """
83 Get the appropriate connection string based on SQLCipher availability.
85 Args:
86 db_path: Path to the database file
87 password: Password for encryption (ignored if SQLCipher not available)
89 Returns:
90 SQLAlchemy connection string
91 """
92 available, _ = check_sqlcipher_available()
94 if available and password:
95 # Use encrypted connection
96 return f"sqlite+pysqlcipher://:{password}@/{db_path}"
97 else:
98 # Fall back to regular SQLite
99 if password:
100 logger.warning(
101 "Password provided but SQLCipher not available - using unencrypted database"
102 )
103 return f"sqlite:///{db_path}"