security: encrypt synchronized Later state (Closes #1114)
This commit is contained in:
1 parent
9763bb410b
commit
f4d8d64b9d
3 files changed
+206
-27
No files matched your search
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
import sqlite3
|
||||
|
||||
import httpx
|
||||
@@ -5,6 +6,10 @@ import pytest
|
||||
|
||||
from src import main
|
||||
from src.later_store import LaterStore
|
||||
from src.state_encryption import PrivateStateEncryptionError
|
||||
|
||||
|
||||
PRIVATE_KEY = b"l" * 32
|
||||
|
||||
|
||||
@pytest.mark.anyio
|
||||
@@ -65,6 +70,105 @@ def test_deferrals_are_durable_revisioned_idempotent_and_account_scoped(tmp_path
|
||||
) == {"revision": 2, "records": {}}
|
||||
|
||||
|
||||
def test_later_plans_encrypt_private_content_and_authenticate_the_account(tmp_path):
|
||||
path = tmp_path / "later.sqlite3"
|
||||
item_id = "issue:private/repository:363:"
|
||||
wake_at = "2026-08-10T09:17:00.000Z"
|
||||
store = LaterStore(path, encryption_key=PRIVATE_KEY)
|
||||
|
||||
expected = store.apply(
|
||||
"timmy", "op-private", "defer", item_id,
|
||||
wake_at=wake_at, handoff="today",
|
||||
)
|
||||
|
||||
with sqlite3.connect(path) as connection:
|
||||
records = connection.execute(
|
||||
"SELECT records FROM later_plans WHERE login = 'timmy'"
|
||||
).fetchone()[0]
|
||||
stored_item_id = connection.execute(
|
||||
"SELECT item_id FROM later_item_revisions WHERE login = 'timmy'"
|
||||
).fetchone()[0]
|
||||
assert records.startswith("v1:")
|
||||
assert stored_item_id.startswith("v1:")
|
||||
assert item_id not in records + stored_item_id
|
||||
assert wake_at not in records
|
||||
assert "today" not in records
|
||||
assert LaterStore(path, encryption_key=PRIVATE_KEY).get("timmy") == expected
|
||||
with sqlite3.connect(path) as connection:
|
||||
connection.execute(
|
||||
"INSERT INTO later_plans(login, revision, records) VALUES (?, ?, ?)",
|
||||
("alexander", 1, records),
|
||||
)
|
||||
with pytest.raises(PrivateStateEncryptionError, match="private state could not be decrypted"):
|
||||
store.get("alexander")
|
||||
with pytest.raises(PrivateStateEncryptionError, match="private state could not be decrypted"):
|
||||
LaterStore(path, encryption_key=b"x" * 32).get("timmy")
|
||||
|
||||
|
||||
def test_later_read_atomically_migrates_legacy_private_content_without_new_revision(tmp_path):
|
||||
path = tmp_path / "later.sqlite3"
|
||||
item_id = "issue:legacy/private:17:"
|
||||
records = {item_id: {"wake_at": "2026-08-20T08:00:00.000Z", "handoff": "today"}}
|
||||
store = LaterStore(path, encryption_key=PRIVATE_KEY)
|
||||
with sqlite3.connect(path) as connection:
|
||||
connection.execute(
|
||||
"INSERT INTO later_plans(login, revision, records) VALUES (?, ?, ?)",
|
||||
("timmy", 7, json.dumps(records)),
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO later_item_revisions(login, item_id, revision) VALUES (?, ?, ?)",
|
||||
("timmy", item_id, 7),
|
||||
)
|
||||
|
||||
assert store.get("timmy") == {"revision": 7, "records": records}
|
||||
with sqlite3.connect(path) as connection:
|
||||
migrated_plan = connection.execute(
|
||||
"SELECT revision, records FROM later_plans WHERE login = 'timmy'"
|
||||
).fetchone()
|
||||
migrated_revision = connection.execute(
|
||||
"SELECT revision, item_id FROM later_item_revisions WHERE login = 'timmy'"
|
||||
).fetchone()
|
||||
assert migrated_plan[0] == migrated_revision[0] == 7
|
||||
assert migrated_plan[1].startswith("v1:")
|
||||
assert migrated_revision[1].startswith("v1:")
|
||||
assert item_id not in migrated_plan[1] + migrated_revision[1]
|
||||
|
||||
|
||||
def test_duplicate_write_migrates_legacy_later_content_without_advancing_revision(tmp_path):
|
||||
path = tmp_path / "later.sqlite3"
|
||||
item_id = "issue:legacy/private:18:"
|
||||
records = {item_id: "2026-08-21T08:00:00.000Z"}
|
||||
store = LaterStore(path, encryption_key=PRIVATE_KEY)
|
||||
with sqlite3.connect(path) as connection:
|
||||
connection.execute(
|
||||
"INSERT INTO later_plans(login, revision, records) VALUES (?, ?, ?)",
|
||||
("timmy", 4, json.dumps(records)),
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO later_item_revisions(login, item_id, revision) VALUES (?, ?, ?)",
|
||||
("timmy", item_id, 4),
|
||||
)
|
||||
connection.execute(
|
||||
"INSERT INTO later_operations(login, operation_id, created_at) VALUES (?, ?, ?)",
|
||||
("timmy", "already-confirmed", 1.0),
|
||||
)
|
||||
|
||||
result = store.apply(
|
||||
"timmy", "already-confirmed", "restore", item_id, base_revision=4,
|
||||
)
|
||||
|
||||
assert result == {"revision": 4, "records": records}
|
||||
with sqlite3.connect(path) as connection:
|
||||
stored = connection.execute(
|
||||
"SELECT records FROM later_plans WHERE login = 'timmy'"
|
||||
).fetchone()[0]
|
||||
stored_item_id = connection.execute(
|
||||
"SELECT item_id FROM later_item_revisions WHERE login = 'timmy'"
|
||||
).fetchone()[0]
|
||||
assert stored.startswith("v1:")
|
||||
assert stored_item_id.startswith("v1:")
|
||||
|
||||
|
||||
def test_next_day_today_handoff_is_preserved_without_changing_legacy_deferrals(tmp_path):
|
||||
store = LaterStore(tmp_path / "later.sqlite3")
|
||||
|
||||
|
||||
Reference in new issue
Block a user