7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
372 lines
12 KiB
Python
372 lines
12 KiB
Python
"""
|
|
Tests for SQLCipher 4.6+ Community Edition compatibility.
|
|
|
|
These tests verify that the cipher_default_* pragmas are used correctly
|
|
and that the PRAGMA ordering (before/after key) is correct.
|
|
"""
|
|
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from src.local_deep_research.database.encrypted_db import DatabaseManager
|
|
from src.local_deep_research.database.sqlcipher_compat import (
|
|
get_sqlcipher_module,
|
|
)
|
|
from src.local_deep_research.database.sqlcipher_utils import (
|
|
apply_cipher_defaults_before_key,
|
|
apply_sqlcipher_pragmas,
|
|
get_sqlcipher_settings,
|
|
set_sqlcipher_key,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_data_dir():
|
|
"""Create a temporary data directory for testing."""
|
|
temp_dir = tempfile.mkdtemp()
|
|
yield Path(temp_dir)
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_db_path(temp_data_dir):
|
|
"""Create a temporary database path."""
|
|
return temp_data_dir / "test_cipher.db"
|
|
|
|
|
|
@pytest.fixture
|
|
def sqlcipher_module():
|
|
"""Get the SQLCipher module."""
|
|
return get_sqlcipher_module()
|
|
|
|
|
|
@pytest.fixture
|
|
def db_manager(temp_data_dir, monkeypatch):
|
|
"""Create a DatabaseManager with test configuration."""
|
|
monkeypatch.setenv("LDR_DATA_DIR", str(temp_data_dir))
|
|
manager = DatabaseManager()
|
|
manager.data_dir = temp_data_dir / "encrypted_databases"
|
|
manager.data_dir.mkdir(parents=True, exist_ok=True)
|
|
return manager
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_user(temp_data_dir, monkeypatch):
|
|
"""Create a test user in auth database."""
|
|
monkeypatch.setenv("LDR_DATA_DIR", str(temp_data_dir))
|
|
|
|
from src.local_deep_research.database.auth_db import (
|
|
get_auth_db_session,
|
|
init_auth_database,
|
|
)
|
|
from src.local_deep_research.database.models.auth import User
|
|
|
|
init_auth_database()
|
|
auth_db = get_auth_db_session()
|
|
user = User(username="testuser")
|
|
auth_db.add(user)
|
|
auth_db.commit()
|
|
auth_db.close()
|
|
return user
|
|
|
|
|
|
class TestSQLCipher46Compatibility:
|
|
"""Tests for SQLCipher 4.6+ compatibility with cipher_default_* pragmas."""
|
|
|
|
def test_cipher_default_page_size_applied(
|
|
self, sqlcipher_module, temp_db_path
|
|
):
|
|
"""Verify cipher_default_page_size pragma is applied correctly."""
|
|
settings = get_sqlcipher_settings()
|
|
expected_page_size = settings["page_size"]
|
|
|
|
conn = sqlcipher_module.connect(str(temp_db_path))
|
|
cursor = conn.cursor()
|
|
|
|
# Apply cipher settings BEFORE key (SQLCipher 4.6+ requirement)
|
|
apply_cipher_defaults_before_key(cursor)
|
|
set_sqlcipher_key(cursor, "testpassword")
|
|
cursor.close()
|
|
|
|
# Verify the page size is applied
|
|
result = conn.execute("PRAGMA cipher_page_size").fetchone()
|
|
assert result is not None
|
|
assert str(result[0]) == str(expected_page_size), (
|
|
f"Expected cipher_page_size={expected_page_size}, got {result[0]}"
|
|
)
|
|
|
|
conn.close()
|
|
|
|
def test_cipher_default_hmac_algorithm_applied(
|
|
self, sqlcipher_module, temp_db_path
|
|
):
|
|
"""Verify cipher_default_hmac_algorithm pragma is applied correctly."""
|
|
settings = get_sqlcipher_settings()
|
|
expected_hmac = settings["hmac_algorithm"]
|
|
|
|
conn = sqlcipher_module.connect(str(temp_db_path))
|
|
cursor = conn.cursor()
|
|
|
|
apply_cipher_defaults_before_key(cursor)
|
|
set_sqlcipher_key(cursor, "testpassword")
|
|
cursor.close()
|
|
|
|
result = conn.execute("PRAGMA cipher_hmac_algorithm").fetchone()
|
|
assert result is not None
|
|
assert result[0] == expected_hmac, (
|
|
f"Expected cipher_hmac_algorithm={expected_hmac}, got {result[0]}"
|
|
)
|
|
|
|
conn.close()
|
|
|
|
def test_cipher_default_kdf_algorithm_applied(
|
|
self, sqlcipher_module, temp_db_path
|
|
):
|
|
"""Verify cipher_default_kdf_algorithm pragma is applied correctly."""
|
|
settings = get_sqlcipher_settings()
|
|
expected_kdf = settings["kdf_algorithm"]
|
|
|
|
conn = sqlcipher_module.connect(str(temp_db_path))
|
|
cursor = conn.cursor()
|
|
|
|
apply_cipher_defaults_before_key(cursor)
|
|
set_sqlcipher_key(cursor, "testpassword")
|
|
cursor.close()
|
|
|
|
result = conn.execute("PRAGMA cipher_kdf_algorithm").fetchone()
|
|
assert result is not None
|
|
assert result[0] == expected_kdf, (
|
|
f"Expected cipher_kdf_algorithm={expected_kdf}, got {result[0]}"
|
|
)
|
|
|
|
conn.close()
|
|
|
|
def test_pragma_order_cipher_settings_before_key(
|
|
self, sqlcipher_module, temp_db_path
|
|
):
|
|
"""Verify that cipher_default_* pragmas work when applied BEFORE key."""
|
|
settings = get_sqlcipher_settings()
|
|
|
|
conn = sqlcipher_module.connect(str(temp_db_path))
|
|
cursor = conn.cursor()
|
|
|
|
# Correct order: cipher settings BEFORE key
|
|
apply_cipher_defaults_before_key(cursor)
|
|
set_sqlcipher_key(cursor, "testpassword")
|
|
|
|
# Should be able to create tables and query
|
|
conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")
|
|
conn.execute("INSERT INTO test VALUES (1, 'hello')")
|
|
result = conn.execute("SELECT * FROM test").fetchone()
|
|
|
|
assert result == (1, "hello"), (
|
|
"Database should be accessible with correct order"
|
|
)
|
|
|
|
# Verify settings were applied
|
|
page_size = conn.execute("PRAGMA cipher_page_size").fetchone()[0]
|
|
assert str(page_size) == str(settings["page_size"])
|
|
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
def test_pragma_order_kdf_iter_after_key(
|
|
self, sqlcipher_module, temp_db_path
|
|
):
|
|
"""Verify that kdf_iter pragma works when applied AFTER key."""
|
|
settings = get_sqlcipher_settings()
|
|
|
|
conn = sqlcipher_module.connect(str(temp_db_path))
|
|
cursor = conn.cursor()
|
|
|
|
# Correct order
|
|
apply_cipher_defaults_before_key(cursor)
|
|
set_sqlcipher_key(cursor, "testpassword")
|
|
apply_sqlcipher_pragmas(cursor, creation_mode=True)
|
|
cursor.close()
|
|
|
|
# Verify kdf_iter was applied
|
|
result = conn.execute("PRAGMA kdf_iter").fetchone()
|
|
assert result is not None
|
|
assert str(result[0]) == str(settings["kdf_iterations"]), (
|
|
f"Expected kdf_iter={settings['kdf_iterations']}, got {result[0]}"
|
|
)
|
|
|
|
conn.close()
|
|
|
|
def test_cipher_default_pragmas_set_correct_defaults(
|
|
self, sqlcipher_module, temp_db_path
|
|
):
|
|
"""
|
|
Verify that cipher_default_* pragmas properly configure the database.
|
|
|
|
This test confirms that using cipher_default_page_size (not cipher_page_size)
|
|
correctly sets the page size for new databases in SQLCipher 4.6+.
|
|
"""
|
|
conn = sqlcipher_module.connect(str(temp_db_path))
|
|
cursor = conn.cursor()
|
|
|
|
# Use cipher_default_page_size (the correct way for SQLCipher 4.6+)
|
|
cursor.execute("PRAGMA cipher_default_page_size = 16384")
|
|
cursor.execute("PRAGMA key = 'testpassword'")
|
|
cursor.close()
|
|
|
|
# The page size should be 16384 as we set it
|
|
result = conn.execute("PRAGMA cipher_page_size").fetchone()
|
|
assert result[0] == "16384", (
|
|
f"cipher_default_page_size should set page size to 16384, got {result[0]}"
|
|
)
|
|
|
|
# Create a table to ensure database is usable
|
|
conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)")
|
|
conn.execute("INSERT INTO test VALUES (1)")
|
|
result = conn.execute("SELECT * FROM test").fetchone()
|
|
assert result == (1,), "Database should be functional"
|
|
|
|
conn.close()
|
|
|
|
def test_database_created_with_correct_settings(
|
|
self, db_manager, auth_user
|
|
):
|
|
"""Verify database is created with all correct SQLCipher settings."""
|
|
from sqlalchemy import text
|
|
|
|
settings = get_sqlcipher_settings()
|
|
engine = db_manager.create_user_database("testuser", "testpassword123")
|
|
|
|
with engine.connect() as conn:
|
|
# Check all cipher settings
|
|
page_size = conn.execute(text("PRAGMA cipher_page_size")).scalar()
|
|
hmac_algo = conn.execute(
|
|
text("PRAGMA cipher_hmac_algorithm")
|
|
).scalar()
|
|
kdf_algo = conn.execute(
|
|
text("PRAGMA cipher_kdf_algorithm")
|
|
).scalar()
|
|
kdf_iter = conn.execute(text("PRAGMA kdf_iter")).scalar()
|
|
|
|
assert str(page_size) == str(settings["page_size"]), (
|
|
f"cipher_page_size mismatch: expected {settings['page_size']}, got {page_size}"
|
|
)
|
|
assert hmac_algo == settings["hmac_algorithm"], (
|
|
f"cipher_hmac_algorithm mismatch: expected {settings['hmac_algorithm']}, got {hmac_algo}"
|
|
)
|
|
assert kdf_algo == settings["kdf_algorithm"], (
|
|
f"cipher_kdf_algorithm mismatch: expected {settings['kdf_algorithm']}, got {kdf_algo}"
|
|
)
|
|
assert str(kdf_iter) == str(settings["kdf_iterations"]), (
|
|
f"kdf_iter mismatch: expected {settings['kdf_iterations']}, got {kdf_iter}"
|
|
)
|
|
|
|
def test_database_reopened_with_matching_settings(
|
|
self, db_manager, auth_user
|
|
):
|
|
"""Verify database can be reopened and settings remain consistent."""
|
|
from sqlalchemy import text
|
|
|
|
settings = get_sqlcipher_settings()
|
|
|
|
# Create database
|
|
engine = db_manager.create_user_database("testuser", "testpassword123")
|
|
|
|
# Create a test table and write some data
|
|
with engine.connect() as conn:
|
|
conn.execute(
|
|
text(
|
|
"CREATE TABLE IF NOT EXISTS test_data (id INTEGER PRIMARY KEY, value TEXT)"
|
|
)
|
|
)
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO test_data (id, value) VALUES (1, 'test_value')"
|
|
)
|
|
)
|
|
conn.commit()
|
|
|
|
# Close the database
|
|
db_manager.close_user_database("testuser")
|
|
assert not db_manager.is_user_connected("testuser")
|
|
|
|
# Reopen the database
|
|
engine2 = db_manager.open_user_database("testuser", "testpassword123")
|
|
assert engine2 is not None
|
|
|
|
with engine2.connect() as conn:
|
|
# Verify settings are still correct
|
|
page_size = conn.execute(text("PRAGMA cipher_page_size")).scalar()
|
|
hmac_algo = conn.execute(
|
|
text("PRAGMA cipher_hmac_algorithm")
|
|
).scalar()
|
|
kdf_algo = conn.execute(
|
|
text("PRAGMA cipher_kdf_algorithm")
|
|
).scalar()
|
|
|
|
assert str(page_size) == str(settings["page_size"])
|
|
assert hmac_algo == settings["hmac_algorithm"]
|
|
assert kdf_algo == settings["kdf_algorithm"]
|
|
|
|
# Verify data is still there
|
|
result = conn.execute(
|
|
text("SELECT value FROM test_data WHERE id = 1")
|
|
).scalar()
|
|
assert result == "test_value", "Data should persist after reopen"
|
|
|
|
|
|
class TestSQLCipherVersionDetection:
|
|
"""Tests for SQLCipher version detection and compatibility."""
|
|
|
|
def test_sqlcipher_version_is_4x(self, sqlcipher_module, temp_db_path):
|
|
"""Verify we're testing against SQLCipher 4.x."""
|
|
conn = sqlcipher_module.connect(str(temp_db_path))
|
|
cursor = conn.cursor()
|
|
|
|
# Set key first (required to query version in some setups)
|
|
cursor.execute("PRAGMA key = 'test'")
|
|
|
|
result = conn.execute("PRAGMA cipher_version").fetchone()
|
|
assert result is not None
|
|
|
|
version = result[0]
|
|
# Should be 4.x.x (e.g., "4.6.1 community")
|
|
assert version.startswith("4."), (
|
|
f"Expected SQLCipher 4.x, got {version}"
|
|
)
|
|
|
|
cursor.close()
|
|
conn.close()
|
|
|
|
def test_cipher_settings_function_returns_valid_settings(self):
|
|
"""Verify get_sqlcipher_settings returns all required keys."""
|
|
settings = get_sqlcipher_settings()
|
|
|
|
required_keys = [
|
|
"kdf_iterations",
|
|
"page_size",
|
|
"hmac_algorithm",
|
|
"kdf_algorithm",
|
|
]
|
|
for key in required_keys:
|
|
assert key in settings, f"Missing required setting: {key}"
|
|
|
|
# Verify types
|
|
assert isinstance(settings["kdf_iterations"], int)
|
|
assert isinstance(settings["page_size"], int)
|
|
assert isinstance(settings["hmac_algorithm"], str)
|
|
assert isinstance(settings["kdf_algorithm"], str)
|
|
|
|
# Verify reasonable values
|
|
assert settings["kdf_iterations"] >= 100000, (
|
|
"KDF iterations should be >= 100000"
|
|
)
|
|
assert settings["page_size"] in [
|
|
1024,
|
|
4096,
|
|
8192,
|
|
16384,
|
|
32768,
|
|
65536,
|
|
], "Page size should be a valid SQLite page size"
|