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
+5
View File
@@ -78,6 +78,11 @@ def generate_device_keypair() -> tuple[bytes, bytes]:
return bytes(private), bytes(private.public_key)
def device_public_key(device_private_key: bytes) -> bytes:
"""Re-derive the public half from a stored private key."""
return bytes(PrivateKey(device_private_key).public_key)
def wrap_master_key_for_device(master_key: bytes, device_public_key: bytes) -> bytes:
"""Seal MK to another device's public key (run on an *existing* device when
provisioning a new one). Only the target device's private key can open it."""