security: encrypt synchronized Today state (Closes #1112)
CI / lint (pull_request) Successful in 2m36s
CI / build-release (pull_request) Successful in 6s
CI / browser-journey (pull_request) Successful in 2m43s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-19 04:34:55 +00:00
1 parent c219f130c8
commit db3ca83f29
3 files changed
+380 -91

No files matched your search

+106
View File
@@ -4,9 +4,115 @@ import pytest
import httpx
from src import main
from src.state_encryption import PrivateStateEncryptionError
from src.today_store import TodayPlanFull, TodaySessionConflict, TodayStore
def test_task_bearing_today_journey_is_encrypted_at_rest_and_survives_restart(tmp_path):
path = tmp_path / "today.sqlite3"
key = b"t" * 32
store = TodayStore(path, encryption_key=key, clock=lambda: 1234.5)
issue = "issue:private-roadmap/launch:4242:"
device = "private-phone-canary"
recap_session = "private-recap-canary"
plan = store.apply_batch("Timmy", [
{"operation_id": "add", "action": "add", "item_id": issue},
{"operation_id": "configure", "action": "configure", "item_id": "plan",
"capacity_minutes": 180, "estimates": {issue: 75}},
])
session = store.update_session(
"timmy", base_revision=0, device_id=device, identity=issue,
elapsed_ms=90_000, running=True,
)
recap = store.save_recap("timmy", recap_session, [
{"identity": issue, "estimate_minutes": 75, "actual_minutes": 91}
])
assert store.begin_time_log("timmy", recap_session, issue, 91) == "claimed"
store.finish_time_log("timmy", recap_session, issue, succeeded=True)
retained = b"".join(
candidate.read_bytes()
for candidate in (path, path.with_name(path.name + "-wal"))
if candidate.exists()
)
for canary in (issue, device, recap_session):
assert canary.encode() not in retained
assert retained.count(b"v1:") >= 4
reopened = TodayStore(path, encryption_key=key)
assert reopened.get("timmy") == {
key: value for key, value in plan.items()
if key not in {"accepted_operation_ids", "duplicate_operation_ids", "rejected_operations"}
}
assert reopened.get_session("timmy") == session
assert reopened.list_recaps("timmy") == [recap]
assert reopened.begin_time_log("timmy", recap_session, issue, 91) == "logged"
def test_legacy_today_rows_migrate_on_use_without_changing_behavior(tmp_path):
path = tmp_path / "today.sqlite3"
key = b"m" * 32
store = TodayStore(path, encryption_key=key)
issue = "issue:legacy-private/project:7:"
with sqlite3.connect(path) as connection:
connection.execute(
"INSERT INTO today_plans VALUES (?, ?, ?, ?, ?, ?, ?)",
("timmy", 4, f'["{issue}"]', 120, f'{{"{issue}":45}}', "2026-08-19", "UTC"),
)
connection.execute(
"INSERT INTO today_sessions VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
("timmy", 3, "legacy-phone", issue, 5000, 1, None, 99.0),
)
connection.execute(
"INSERT INTO today_recaps VALUES (?, ?, ?, ?)",
("timmy", "legacy-session", 88.0,
f'[{ {"identity": issue, "estimate_minutes": 45, "actual_minutes": 50} }]'.replace("'", '"')),
)
connection.execute(
"INSERT INTO today_time_logs VALUES (?, ?, ?, ?, ?)",
("timmy", "legacy-session", issue, 50, "pending"),
)
assert store.get("timmy")["revision"] == 4
assert store.get_session("timmy")["revision"] == 3
assert store.list_recaps("timmy")[0]["items"][0]["identity"] == issue
assert store.begin_time_log("timmy", "legacy-session", issue, 50) == "pending"
retained = path.read_bytes()
for canary in (issue, "legacy-phone", "legacy-session"):
assert canary.encode() not in retained
@pytest.mark.anyio
async def test_tampered_or_account_substituted_today_state_fails_closed_with_retry(monkeypatch, tmp_path):
path = tmp_path / "today.sqlite3"
store = TodayStore(path, encryption_key=b"a" * 32)
store.apply("timmy", "seed", "add", "issue:private/repo:9:")
with sqlite3.connect(path) as connection:
payload = connection.execute(
"SELECT ids FROM today_plans WHERE login = 'timmy'"
).fetchone()[0]
connection.execute(
"INSERT INTO today_plans(login, revision, ids, estimates) VALUES ('alexander', 1, ?, '{}')",
(payload,),
)
with pytest.raises(PrivateStateEncryptionError):
store.get("alexander")
async def user():
return {"login": "alexander"}
monkeypatch.setattr(main, "current_user", user)
monkeypatch.setattr(main, "_today_store", lambda: store)
with pytest.raises(main.HTTPException) as raised:
await main.get_today_plan()
assert raised.value.status_code == 503
assert raised.value.detail == "Today synchronization is unavailable"
assert raised.value.headers == {"Retry-After": "1"}
def test_active_session_is_durable_and_account_scoped(tmp_path):
path = tmp_path / "today.sqlite3"
store = TodayStore(path, clock=lambda: 1234.5)