security: encrypt idempotency ledger at rest (Closes #1122)
CI / lint (pull_request) Successful in 2m49s
CI / build-release (pull_request) Successful in 5s
CI / browser-journey (pull_request) Successful in 2m57s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-19 09:29:16 +00:00
1 parent b278367e3b
commit 3822ec4536
3 files changed
+184 -9

No files matched your search

+51 -4
View File
@@ -6,6 +6,7 @@ from pathlib import Path
from typing import Any, Callable
from src.private_state import connect_private_sqlite
from src.state_encryption import PrivateStateCipher, private_state_encryption_key
@dataclass(frozen=True)
@@ -29,12 +30,17 @@ class IdempotencyLedger:
max_entries: int,
lock_timeout_seconds: float = 0.1,
clock: Callable[[], float] = time.time,
encryption_key: bytes | None = None,
) -> None:
self.path = Path(path)
self.ttl_seconds = ttl_seconds
self.max_entries = max_entries
self.lock_timeout_seconds = lock_timeout_seconds
self.clock = clock
self._cipher = PrivateStateCipher(
encryption_key if encryption_key is not None else private_state_encryption_key(),
store="idempotency-ledger",
)
self._initialize()
def _connect(self) -> sqlite3.Connection:
@@ -67,6 +73,17 @@ class IdempotencyLedger:
def _fingerprint(value: tuple[Any, ...]) -> str:
return json.dumps(value, separators=(",", ":"), sort_keys=True)
def _seal(self, value: str, *, key: str, field: str) -> str:
return self._cipher.seal(value, binding=f"{key}:{field}")
def _open(self, payload: str, *, key: str, field: str) -> tuple[str, bool]:
value, legacy = self._cipher.open(payload, binding=f"{key}:{field}")
if legacy:
return json.dumps(value, separators=(",", ":"), sort_keys=True), True
if not isinstance(value, str):
raise RuntimeError("idempotency ledger payload is invalid")
return value, legacy
def reserve(self, key: str, fingerprint: tuple[Any, ...]) -> Reservation:
encoded = self._fingerprint(fingerprint)
now = self.clock()
@@ -84,10 +101,40 @@ class IdempotencyLedger:
(key,),
).fetchone()
if row is not None:
if row[0] != encoded:
stored_fingerprint, legacy_fingerprint = self._open(
row[0], key=key, field="fingerprint"
)
if legacy_fingerprint:
connection.execute(
"UPDATE idempotency_operations SET fingerprint = ? "
"WHERE key = ? AND fingerprint = ?",
(
self._seal(
stored_fingerprint, key=key, field="fingerprint"
),
key,
row[0],
),
)
if stored_fingerprint != encoded:
return Reservation("conflict")
if row[1] == "completed":
return Reservation("completed", json.loads(row[2]))
stored_response, legacy_response = self._open(
row[2], key=key, field="response"
)
if legacy_response:
connection.execute(
"UPDATE idempotency_operations SET response_json = ? "
"WHERE key = ? AND response_json = ?",
(
self._seal(
stored_response, key=key, field="response"
),
key,
row[2],
),
)
return Reservation("completed", json.loads(stored_response))
if row[3] <= now - self.ttl_seconds:
return Reservation("uncertain")
return Reservation("pending")
@@ -110,7 +157,7 @@ class IdempotencyLedger:
connection.execute(
"INSERT INTO idempotency_operations "
"(key, fingerprint, status, created_at) VALUES (?, ?, 'pending', ?)",
(key, encoded, now),
(key, self._seal(encoded, key=key, field="fingerprint"), now),
)
except sqlite3.OperationalError as exc:
if "locked" in str(exc).lower() or "busy" in str(exc).lower():
@@ -129,7 +176,7 @@ class IdempotencyLedger:
connection.execute(
"UPDATE idempotency_operations SET status = 'completed', "
"response_json = ?, completed_at = ? WHERE key = ?",
(encoded, self.clock(), key),
(self._seal(encoded, key=key, field="response"), self.clock(), key),
)
except sqlite3.OperationalError as exc:
if "locked" in str(exc).lower() or "busy" in str(exc).lower():