feat(hub): zero-knowledge SSH key-vault sync (ADR-052 D-4) + accept ADR-052#299
Merged
Conversation
…dge vault Director accepted B2. ADR-052 → Accepted; D-6 (hub PTY-relay + A2A-relay auth) marked DEFERRED per direction. forbidden-pattern #15 amended: narrows "no SSH secrets on the hub" → "no secrets the hub can read or use; client-side-encrypted zero-knowledge vault ciphertext permitted" (the hub never holds the vault key or plaintext, so it can never authenticate as the user). This is the carve-out that authorizes the vault sync store landing in the next commit. Co-Authored-By: Claude Opus 4.8 <[email protected]>
The hub as a blind cross-device sync store for the client-side-encrypted SSH key
vault. It holds only opaque ciphertext it can never decrypt, keyed to the calling
principal (server-derived from the token scope, never client-supplied — a caller
can only ever reach their own vault). Migration 0061 adds key_vaults (one sealed
blob per principal, versioned) + key_vault_devices (per-device public key + the
vault key wrapped to it, both opaque).
Endpoints under /v1/teams/{team}/vault (bearer + teamGate):
GET /vault pull the sealed blob (404 if none)
PUT /vault push with optimistic concurrency (base_version;
409 on stale/duplicate — pull and retry)
GET /vault/devices list enrolled devices + wrapped keys
PUT /vault/devices/{device} enroll a device (pubkey) / wrap the key to it
DELETE /vault/devices/{device} revoke a device envelope
Payloads capped (2 MiB); audited (vault.push / vault.device.enroll|revoke). Tests
cover push/pull round-trip, optimistic-concurrency conflicts, the new-device
pubkey guard, and device enroll/wrap/list/revoke. go build/vet clean; package
tests green incl. -race on the vault suite.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
… escrow Two director refinements: - D-3 revised: the vault seals the WHOLE personal setup — connection bookmarks (host/port/username/auth-method/jump/tmux) AND keys/passphrases/passwords — as one client-encrypted bundle, so a new device is directly reusable. Personal connection metadata now lives INSIDE the zero-knowledge vault (hub-blind, not even hostnames visible), superseding the earlier "sync via hub-visible ssh_hint" framing; ssh_hint keeps its separate team-host-binding purpose. - D-4: recovery escrow decided. The vault key is also wrapped under a director-held recovery code (self-escrowed offline), stored on the hub as an opaque recovery envelope — so losing every enrolled device is survivable. Hub never holds the recovery key; zero-knowledge holds. Re-key re-wraps it; operator/Shamir escrow are same-slot extensions. Co-Authored-By: Claude Opus 4.8 <[email protected]>
Adds the recovery escrow to the vault sync store (ADR-052 D-4). Migration 0061 gains
recovery_envelope / recovery_hint / recovery_updated_at on key_vaults (the vault key
wrapped under the director's escrowed recovery key — opaque to the hub, never the
recovery key itself). New endpoints:
GET /v1/teams/{team}/vault/recovery fetch the recovery envelope (recovering device)
PUT /v1/teams/{team}/vault/recovery set/replace it (vault must exist -> 404 if not)
DELETE /v1/teams/{team}/vault/recovery clear it
Audited (vault.recovery.set/clear). Test covers set-before-vault 404, missing-envelope
400, set/get round-trip, survival across a vault version bump, and clear. The whole
connection+key bundle (D-3) needs no hub change — the vault blob is opaque, so a bigger
sealed bundle just flows through the existing store. go build/vet clean; vault suite
green incl. -race.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Owner
Author
|
Extended per director direction (2 commits added):
Re-verified: |
This was referenced Jul 5, 2026
physercoe
pushed a commit
that referenced
this pull request
Jul 5, 2026
Dart client for the zero-knowledge vault endpoints (hub store shipped in #299). Thin transport over the blind blob store — everything is opaque client-encrypted base64 (sealed by services/vault/vault_crypto.dart), the hub never sees plaintext. VaultApi (wired into the HubClient facade as `client.vault`): - pullVault / pushVault — pull the sealed bundle; push with optimistic concurrency (base_version; HubApiError 409 on stale -> pull, re-seal, retry). pullVault returns null on 404 so a fresh client knows to create. - getRecovery / setRecovery / deleteRecovery — the recovery envelope. - listDevices / putDevice / deleteDevice — per-device enrollment + wrapped keys. Mirrors the existing sub-client pattern (AttentionApi etc.); like its siblings it's a thin pass-through validated by flutter analyze + device testing (no live-server unit harness exists for sub-clients). Flutter SDK isn't available locally, so CI is the gate. Co-Authored-By: Claude Opus 4.8 <[email protected]>
physercoe
pushed a commit
that referenced
this pull request
Jul 5, 2026
Surfaces the zero-knowledge SSH key-vault sync (ADR-052 D-4) in the mobile app end-to-end: hub blind blob store (#299), Dart crypto core (#300), VaultApi sync client, and the Settings › Data › Key vault sync flow. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the first build of ADR-052 (director-chosen B2): the hub as a blind cross-device sync store for the client-side-encrypted SSH key vault. Also flips ADR-052 to Accepted and applies the forbidden-pattern #15 amendment that authorizes it (the amendment lands with the code it permits, per the forbidden-patterns rule).
What this is
The hub holds only opaque ciphertext it can never decrypt, keyed to the calling principal (server-derived from the token scope — never client-supplied, so a caller can only ever reach their own vault). This is defense-in-depth; the real protection is that only enrolled devices hold the key to decrypt. The hub never holds the vault key or any plaintext.
Changes
0061—key_vaults(one sealed blob per principal, versioned) +key_vault_devices(per-device public key + the vault key wrapped to it, both opaque).handlers_vault.go— endpoints under/v1/teams/{team}/vault(bearer +teamGate):GET /vault— pull the sealed blob (404 if none)PUT /vault— push with optimistic concurrency (base_version; 409 on stale/duplicate → pull and retry)GET /vault/devices— list enrolled devices + wrapped keysPUT /vault/devices/{device}— enroll a device (pubkey) / wrap the vault key to itDELETE /vault/devices/{device}— revoke a device envelopevault.push/vault.device.enroll|revoke).Verification
go build ./...+go vetclean; fullinternal/serverpackage tests green (236s);-raceon the vault suite green (24.7s). New tests cover push/pull round-trip, optimistic-concurrency conflicts, the new-device pubkey guard, and device enroll/wrap/list/revoke.Notes / deferred
principalFromScope, never empty). Fine for the zero-knowledge model; a hard per-human key is a later concern.🤖 Generated with Claude Code