Add cloud key store, API client, and sync identity flows

cloud_keys: device private key + master key live in the OS keychain
(keyring), namespaced per cloud account — never in the local DB. Headless
installs without a keychain get a hard error, not a plaintext fallback.

cloud_api: async httpx client for the bearer-key surface (devices,
account-key escrow, object push/commit, sync pull). Presigned blob
transfers use a separate unauthenticated client so the bearer key never
reaches the storage host.

cloud_account: the design-doc §9 flows — first-device setup mints MK +
recovery phrase (returned for one-time display), later devices register
and either adopt a wrapped MK provisioned by an existing device or
restore from the phrase. sync_device_id lands on cloud_settings with an
idempotent migration.

Tests run the real flows against an in-process fake cloud and assert the
master key and phrase never appear in any request body. A full round-trip
integration test (encrypt -> push -> pull -> decrypt -> tombstone) runs
against a live dev server when VOICEBOX_CLOUD_TEST_API/KEY are set.
This commit is contained in:
Jamie Pine
2026-07-01 15:02:11 -07:00
parent 9a8425f401
commit 9b0e024d3b
9 changed files with 783 additions and 1 deletions
+11
View File
@@ -43,6 +43,7 @@ def run_migrations(engine) -> None:
_migrate_generation_versions(engine, inspector, tables)
_migrate_capture_settings(engine, inspector, tables)
_migrate_mcp_bindings(engine, inspector, tables)
_migrate_cloud_settings(engine, inspector, tables)
_normalize_storage_paths(engine, tables)
@@ -283,6 +284,16 @@ def _migrate_mcp_bindings(engine, inspector, tables: set[str]) -> None:
)
def _migrate_cloud_settings(engine, inspector, tables: set[str]) -> None:
"""Add ``sync_device_id`` — the server-assigned id from registering this
install as an encryption-capable sync device."""
if "cloud_settings" not in tables:
return
columns = _get_columns(inspector, "cloud_settings")
if "sync_device_id" not in columns:
_add_column(engine, "cloud_settings", "sync_device_id VARCHAR", "sync_device_id")
def _supports_drop_column(engine) -> bool:
"""Whether ``ALTER TABLE … DROP COLUMN`` is supported by the dialect +
runtime. Non-SQLite dialects (Postgres, MySQL) have supported it for
+4
View File
@@ -253,6 +253,10 @@ class CloudSettings(Base):
device_name = Column(String, nullable=True)
account_user_id = Column(String, nullable=True)
connected_at = Column(DateTime, nullable=True)
# Server-assigned sync device id (device table on the cloud side). Set when
# this install registers as an encryption-capable device; the matching
# X25519 private key lives in the OS keychain (services/cloud_keys.py).
sync_device_id = Column(String, nullable=True)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)