Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

334 lines
11 KiB
Python

"""
Tests for SQLCipher key derivation functions.
These tests verify that password-to-key derivation is consistent,
deterministic, and secure.
"""
import shutil
import tempfile
from pathlib import Path
import pytest
from src.local_deep_research.database.sqlcipher_utils import (
LEGACY_PBKDF2_SALT,
_get_key_from_password,
get_sqlcipher_settings,
set_sqlcipher_key,
set_sqlcipher_rekey,
)
from src.local_deep_research.database.sqlcipher_compat import (
get_sqlcipher_module,
)
@pytest.fixture
def temp_db_path():
"""Create a temporary database path."""
temp_dir = tempfile.mkdtemp()
db_path = Path(temp_dir) / "test_key.db"
yield db_path
shutil.rmtree(temp_dir)
@pytest.fixture
def sqlcipher_module():
"""Get the SQLCipher module."""
return get_sqlcipher_module()
class TestKeyDerivation:
"""Tests for password-to-key derivation."""
def test_key_derivation_deterministic(self):
"""Verify same password always produces same key."""
password = "test_password_123"
key1 = _get_key_from_password(
password,
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
key2 = _get_key_from_password(
password,
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
assert key1 == key2, "Same password should always produce same key"
def test_key_derivation_different_passwords(self):
"""Verify different passwords produce different keys."""
key1 = _get_key_from_password(
"password1",
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
key2 = _get_key_from_password(
"password2",
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
assert key1 != key2, "Different passwords should produce different keys"
def test_key_hex_encoding_only_hex_chars(self):
"""Verify derived key only contains hex characters (SQL injection safe)."""
key = _get_key_from_password(
"any_password",
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
hex_string = key.hex()
# Verify only hex characters
valid_hex_chars = set("0123456789abcdef")
assert all(c in valid_hex_chars for c in hex_string), (
"Key hex encoding should only contain [0-9a-f]"
)
def test_special_characters_in_password(self):
"""Verify passwords with special characters are handled correctly."""
special_passwords = [
"password with spaces",
"password'with'quotes",
'password"with"double"quotes',
"password\\with\\backslashes",
"password\nwith\nnewlines",
"password\twith\ttabs",
"unicode: 日本語 emoji: 🔐🔑",
"sql_injection: '; DROP TABLE users; --",
"null_byte: \x00test",
]
keys = []
for password in special_passwords:
try:
key = _get_key_from_password(
password,
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
keys.append(key)
# Verify key is valid bytes
assert isinstance(key, bytes), (
f"Key should be bytes for: {password}"
)
assert len(key) > 0, f"Key should not be empty for: {password}"
except Exception as e:
pytest.fail(f"Password '{password}' caused error: {e}")
# All keys should be unique
assert len(set(keys)) == len(keys), (
"All special passwords should produce unique keys"
)
def test_key_length_correct(self):
"""Verify derived key has correct length (64 bytes = 512 bits for SHA512)."""
key = _get_key_from_password(
"test_password",
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
# SHA512-based PBKDF2 produces 64-byte key (512 bits)
assert len(key) == 64, f"Key should be 64 bytes, got {len(key)}"
def test_empty_password_produces_key(self):
"""Verify empty password still produces a valid key (validation is elsewhere)."""
# Note: Empty password validation happens in DatabaseManager, not in key derivation
# The key derivation function itself should work with any input
key = _get_key_from_password(
"", LEGACY_PBKDF2_SALT, get_sqlcipher_settings()["kdf_iterations"]
)
assert isinstance(key, bytes), (
"Empty password should still produce bytes"
)
assert len(key) == 64, (
"Key length should be consistent (64 bytes for SHA512)"
)
class TestKeyDerivationWithDatabase:
"""Tests for key derivation in actual database operations."""
def test_derived_key_opens_database(self, sqlcipher_module, temp_db_path):
"""Verify derived key can create and open a database."""
from src.local_deep_research.database.sqlcipher_utils import (
apply_cipher_defaults_before_key,
)
password = "test_database_password"
# Create database
conn = sqlcipher_module.connect(str(temp_db_path))
cursor = conn.cursor()
# New database, so use creation_mode=True
apply_cipher_defaults_before_key(cursor)
set_sqlcipher_key(cursor, password)
cursor.close()
conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)")
conn.execute("INSERT INTO test VALUES (1)")
conn.commit() # Important: commit before closing
conn.close()
# Reopen with same password
from src.local_deep_research.database.sqlcipher_utils import (
apply_sqlcipher_pragmas,
)
conn2 = sqlcipher_module.connect(str(temp_db_path))
cursor2 = conn2.cursor()
# Existing database: key first, then cipher_* pragmas
set_sqlcipher_key(cursor2, password)
apply_sqlcipher_pragmas(cursor2, creation_mode=False)
cursor2.close()
result = conn2.execute("SELECT * FROM test").fetchone()
assert result == (1,), "Should be able to read data with same password"
conn2.close()
def test_wrong_password_cannot_open(self, sqlcipher_module, temp_db_path):
"""Verify wrong password cannot open database."""
from src.local_deep_research.database.sqlcipher_utils import (
apply_cipher_defaults_before_key,
)
# Create database with password1
conn = sqlcipher_module.connect(str(temp_db_path))
cursor = conn.cursor()
# New database, so use creation_mode=True
apply_cipher_defaults_before_key(cursor)
set_sqlcipher_key(cursor, "password1")
cursor.close()
conn.execute("CREATE TABLE test (id INTEGER PRIMARY KEY)")
conn.commit()
conn.close()
# Try to open with password2
from src.local_deep_research.database.sqlcipher_utils import (
apply_sqlcipher_pragmas,
)
conn2 = sqlcipher_module.connect(str(temp_db_path))
cursor2 = conn2.cursor()
# Existing database: key first, then cipher_* pragmas
set_sqlcipher_key(cursor2, "password2")
apply_sqlcipher_pragmas(cursor2, creation_mode=False)
cursor2.close()
# Should fail when trying to access data
with pytest.raises(Exception):
conn2.execute("SELECT * FROM test").fetchone()
conn2.close()
def test_rekey_uses_same_derivation(self, sqlcipher_module, temp_db_path):
"""Verify rekey produces key compatible with set_sqlcipher_key."""
from src.local_deep_research.database.sqlcipher_utils import (
apply_cipher_defaults_before_key,
)
# Create database
conn = sqlcipher_module.connect(str(temp_db_path))
cursor = conn.cursor()
# New database, so use creation_mode=True
apply_cipher_defaults_before_key(cursor)
set_sqlcipher_key(cursor, "original_password")
cursor.close()
conn.execute("CREATE TABLE test (id INTEGER, value TEXT)")
conn.execute("INSERT INTO test VALUES (1, 'secret_data')")
conn.commit()
# Rekey to new password
set_sqlcipher_rekey(conn, "new_password")
conn.close()
# Open with new password using set_sqlcipher_key
from src.local_deep_research.database.sqlcipher_utils import (
apply_sqlcipher_pragmas,
)
conn2 = sqlcipher_module.connect(str(temp_db_path))
cursor2 = conn2.cursor()
# Existing database: key first, then cipher_* pragmas
set_sqlcipher_key(cursor2, "new_password")
apply_sqlcipher_pragmas(cursor2, creation_mode=False)
cursor2.close()
# Should be able to read data
result = conn2.execute("SELECT value FROM test WHERE id = 1").fetchone()
assert result[0] == "secret_data", "Data should be readable after rekey"
conn2.close()
# Original password should no longer work
conn3 = sqlcipher_module.connect(str(temp_db_path))
cursor3 = conn3.cursor()
# Existing database: key first, then cipher_* pragmas
set_sqlcipher_key(cursor3, "original_password")
apply_sqlcipher_pragmas(cursor3, creation_mode=False)
cursor3.close()
with pytest.raises(Exception):
conn3.execute("SELECT * FROM test").fetchone()
conn3.close()
class TestKeyDerivationSecurity:
"""Security-focused tests for key derivation."""
def test_pbkdf2_iterations_sufficient(self):
"""Verify KDF iterations are set to a secure value."""
settings = get_sqlcipher_settings()
iterations = settings["kdf_iterations"]
# OWASP recommends at least 600,000 for PBKDF2-HMAC-SHA512
# We use 256,000 which is a reasonable balance for performance
assert iterations >= 100000, (
f"KDF iterations should be at least 100000, got {iterations}"
)
def test_key_not_stored_in_plaintext(self):
"""Verify derived key is bytes, not plaintext password."""
password = "my_secret_password"
key = _get_key_from_password(
password,
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
# Key should be bytes, not string
assert isinstance(key, bytes), "Key should be bytes"
# Key should not contain the password
assert password.encode() not in key, (
"Key should not contain plaintext password"
)
def test_different_salts_would_produce_different_keys(self):
"""
Document that our fixed salt means same password = same key.
This is intentional for database compatibility - if salt changed,
existing databases would become inaccessible.
"""
# Same password should always produce same key (fixed salt)
key1 = _get_key_from_password(
"test",
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
key2 = _get_key_from_password(
"test",
LEGACY_PBKDF2_SALT,
get_sqlcipher_settings()["kdf_iterations"],
)
assert key1 == key2, (
"Fixed salt ensures consistent key derivation for database compatibility"
)