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

380 lines
12 KiB
Python

#!/usr/bin/env python3
"""
Test that encryption constants never change.
These tests protect against breaking changes to encryption parameters.
If ANY of these tests fail, it means ALL existing encrypted databases
will become unreadable. REVERT THE CHANGE IMMEDIATELY.
This test file exists because a "documentation only" commit changed
the PBKDF2 salt and broke all existing user databases.
Note: New databases (v2+) use per-database random salts stored in .salt files.
Legacy databases continue to use LEGACY_PBKDF2_SALT for backwards compatibility.
"""
import hashlib
from hashlib import pbkdf2_hmac
from pathlib import Path
import pytest
class TestEncryptionConstants:
"""
Verify that encryption constants haven't changed.
CRITICAL: These values are used to derive encryption keys.
Changing ANY of them will make existing databases unreadable.
"""
def test_legacy_salt_value_is_stable(self):
"""
Ensure the legacy PBKDF2 salt value hasn't changed.
WARNING: Changing this salt will break ALL existing legacy databases!
If this test fails, you MUST revert the salt change.
Note: New databases use per-database salts, but legacy databases
still depend on this constant value.
"""
from local_deep_research.database.sqlcipher_utils import (
LEGACY_PBKDF2_SALT,
PBKDF2_PLACEHOLDER_SALT,
)
expected_salt = b"no salt"
assert LEGACY_PBKDF2_SALT == expected_salt, (
f"CRITICAL: Legacy PBKDF2 salt has changed!\n"
f"Expected: {expected_salt!r}\n"
f"Actual: {LEGACY_PBKDF2_SALT!r}\n\n"
"This will break ALL existing legacy encrypted databases!\n"
"Users will be unable to log in.\n"
"REVERT THIS CHANGE IMMEDIATELY."
)
# Also verify the alias is still pointing to the same value
assert PBKDF2_PLACEHOLDER_SALT == LEGACY_PBKDF2_SALT, (
"PBKDF2_PLACEHOLDER_SALT should be an alias for LEGACY_PBKDF2_SALT"
)
def test_kdf_iterations_stable(self):
"""
Ensure KDF iterations haven't changed.
Changing this will make existing databases unreadable.
"""
from local_deep_research.database.sqlcipher_utils import (
DEFAULT_KDF_ITERATIONS,
)
expected_iterations = 256000
assert DEFAULT_KDF_ITERATIONS == expected_iterations, (
f"CRITICAL: KDF iterations changed!\n"
f"Expected: {expected_iterations}\n"
f"Actual: {DEFAULT_KDF_ITERATIONS}\n\n"
"This will break ALL existing encrypted databases!\n"
"REVERT THIS CHANGE IMMEDIATELY."
)
def test_hmac_algorithm_stable(self):
"""
Ensure HMAC algorithm hasn't changed.
Changing this will make existing databases unreadable.
"""
from local_deep_research.database.sqlcipher_utils import (
DEFAULT_HMAC_ALGORITHM,
)
expected_algorithm = "HMAC_SHA512"
assert DEFAULT_HMAC_ALGORITHM == expected_algorithm, (
f"CRITICAL: HMAC algorithm changed!\n"
f"Expected: {expected_algorithm}\n"
f"Actual: {DEFAULT_HMAC_ALGORITHM}\n\n"
"This will break ALL existing encrypted databases!\n"
"REVERT THIS CHANGE IMMEDIATELY."
)
def test_page_size_stable(self):
"""
Ensure cipher page size hasn't changed.
Changing this will make existing databases unreadable.
"""
from local_deep_research.database.sqlcipher_utils import (
DEFAULT_PAGE_SIZE,
)
expected_page_size = 16384 # 16KB
assert DEFAULT_PAGE_SIZE == expected_page_size, (
f"CRITICAL: Page size changed!\n"
f"Expected: {expected_page_size}\n"
f"Actual: {DEFAULT_PAGE_SIZE}\n\n"
"This will break ALL existing encrypted databases!\n"
"REVERT THIS CHANGE IMMEDIATELY."
)
def test_kdf_algorithm_stable(self):
"""
Ensure KDF algorithm hasn't changed.
Changing this will make existing databases unreadable.
"""
from local_deep_research.database.sqlcipher_utils import (
DEFAULT_KDF_ALGORITHM,
)
expected_algorithm = "PBKDF2_HMAC_SHA512"
assert DEFAULT_KDF_ALGORITHM == expected_algorithm, (
f"CRITICAL: KDF algorithm changed!\n"
f"Expected: {expected_algorithm}\n"
f"Actual: {DEFAULT_KDF_ALGORITHM}\n\n"
"This will break ALL existing encrypted databases!\n"
"REVERT THIS CHANGE IMMEDIATELY."
)
def test_key_derivation_produces_expected_output(self):
"""
Verify the key derivation function produces the expected output.
This test uses a known password and verifies the derived key matches
the expected hash. This catches ANY change to the key derivation:
- Salt changes
- Iteration count changes
- Algorithm changes
- Any other parameter changes
If this test fails, existing databases WILL NOT be openable.
"""
from local_deep_research.database.sqlcipher_utils import (
PBKDF2_PLACEHOLDER_SALT,
DEFAULT_KDF_ITERATIONS,
)
# Use a known test password
test_password = "test_password_for_key_derivation_check"
# Derive the key the same way the production code does
derived_key = pbkdf2_hmac(
"sha512",
test_password.encode(),
PBKDF2_PLACEHOLDER_SALT,
DEFAULT_KDF_ITERATIONS,
)
# This is the expected hash of the derived key
# Generated with: hashlib.sha256(derived_key).hexdigest()
# If this changes, ALL existing databases will break!
# DevSkim: ignore DS173237 - This is a verification hash, not a secret
expected_key_hash = (
"cfac783084917231b28210859f7722be29b54120161f43709363c07cfc6c63ed"
)
actual_key_hash = hashlib.sha256(derived_key).hexdigest()
assert actual_key_hash == expected_key_hash, (
f"CRITICAL: Key derivation output has changed!\n"
f"Expected hash: {expected_key_hash}\n"
f"Actual hash: {actual_key_hash}\n\n"
"This means the encryption key for the same password is now different.\n"
"ALL existing user databases will be unreadable!\n"
"REVERT WHATEVER CHANGE CAUSED THIS IMMEDIATELY."
)
class TestPerDatabaseSalt:
"""
Test the per-database salt functionality (v2 databases).
New databases use random per-database salts stored in .salt files.
This provides better security than the shared legacy salt.
"""
def test_salt_file_path_generation(self):
"""
Verify salt file paths are generated correctly.
"""
from local_deep_research.database.sqlcipher_utils import (
get_salt_file_path,
)
db_path = Path("/data/user.db")
salt_path = get_salt_file_path(db_path)
assert salt_path == Path("/data/user.db.salt")
def test_create_database_salt_generates_correct_size(self, tmp_path):
"""
Verify that created salts are the correct size.
"""
from local_deep_research.database.sqlcipher_utils import (
create_database_salt,
SALT_SIZE,
)
db_path = tmp_path / "test.db"
salt = create_database_salt(db_path)
assert len(salt) == SALT_SIZE
assert db_path.with_suffix(".db.salt").exists()
def test_create_database_salt_is_random(self, tmp_path):
"""
Verify that each call to create_database_salt generates a unique salt.
"""
from local_deep_research.database.sqlcipher_utils import (
create_database_salt,
)
db_path1 = tmp_path / "test1.db"
db_path2 = tmp_path / "test2.db"
salt1 = create_database_salt(db_path1)
salt2 = create_database_salt(db_path2)
assert salt1 != salt2, "Each database should have a unique salt"
def test_get_salt_for_database_with_salt_file(self, tmp_path):
"""
Verify that get_salt_for_database returns the salt from the file.
"""
from local_deep_research.database.sqlcipher_utils import (
create_database_salt,
get_salt_for_database,
)
db_path = tmp_path / "test.db"
created_salt = create_database_salt(db_path)
retrieved_salt = get_salt_for_database(db_path)
assert retrieved_salt == created_salt
def test_get_salt_for_database_without_salt_file(self, tmp_path):
"""
Verify that get_salt_for_database returns legacy salt when no .salt file exists.
"""
from local_deep_research.database.sqlcipher_utils import (
get_salt_for_database,
LEGACY_PBKDF2_SALT,
)
db_path = tmp_path / "legacy.db"
# Don't create a salt file - simulating a legacy database
salt = get_salt_for_database(db_path)
assert salt == LEGACY_PBKDF2_SALT, (
"Should return legacy salt for databases without .salt file"
)
def test_has_per_database_salt(self, tmp_path):
"""
Verify has_per_database_salt correctly identifies v2 databases.
"""
from local_deep_research.database.sqlcipher_utils import (
create_database_salt,
has_per_database_salt,
)
new_db = tmp_path / "new.db"
legacy_db = tmp_path / "legacy.db"
# Create salt for new database
create_database_salt(new_db)
assert has_per_database_salt(new_db) is True
assert has_per_database_salt(legacy_db) is False
def test_get_salt_for_database_raises_on_corrupted_salt(self, tmp_path):
"""
Verify that get_salt_for_database raises ValueError for corrupted salt files.
If a salt file exists but has wrong size, it indicates corruption.
Falling back to legacy salt would fail anyway (wrong key), so we
raise an exception to make the failure explicit.
"""
from local_deep_research.database.sqlcipher_utils import (
get_salt_for_database,
get_salt_file_path,
)
db_path = tmp_path / "corrupted.db"
salt_file = get_salt_file_path(db_path)
# Write a corrupted salt file (wrong size)
salt_file.write_bytes(b"too short")
with pytest.raises(ValueError) as exc_info:
get_salt_for_database(db_path)
assert "unexpected size" in str(exc_info.value)
assert "corrupted" in str(exc_info.value).lower()
def test_create_database_salt_refuses_to_overwrite(self, tmp_path):
"""
Verify that create_database_salt raises FileExistsError if a salt
file already exists, preventing accidental data loss.
"""
from local_deep_research.database.sqlcipher_utils import (
create_database_salt,
)
db_path = tmp_path / "test.db"
create_database_salt(db_path)
with pytest.raises(FileExistsError):
create_database_salt(db_path)
def test_same_password_different_salts_produce_different_keys(self):
"""
Verify the core security guarantee: the same password with different
salts must produce different encryption keys.
"""
from local_deep_research.database.sqlcipher_utils import (
_get_key_from_password,
)
password = "same_password"
salt1 = b"a" * 32
salt2 = b"b" * 32
key1 = _get_key_from_password(password, salt1, 1000)
key2 = _get_key_from_password(password, salt2, 1000)
assert key1 != key2, (
"Same password with different salts MUST produce different keys"
)
def test_salt_file_has_restrictive_permissions(self, tmp_path):
"""
Verify that salt files are created with owner-only permissions (0o600).
"""
import os
import stat
from local_deep_research.database.sqlcipher_utils import (
create_database_salt,
get_salt_file_path,
)
db_path = tmp_path / "test.db"
create_database_salt(db_path)
salt_file = get_salt_file_path(db_path)
file_stat = os.stat(salt_file)
mode = stat.S_IMODE(file_stat.st_mode)
assert mode == 0o600, (
f"Salt file should have 0o600 permissions, got {oct(mode)}"
)
if __name__ == "__main__":
pytest.main([__file__, "-v"])