docs(security): propose helper credential pairing - #102
Conversation
Define a fail-closed mutual pairing and credential lifecycle for a future diagnostics capability, including canonical signature domains, homeserver and folder bindings, multi-installation authorization, rotation, revocation, recovery, replay defense, and downgrade behavior. Keep the design explicitly unimplemented and subject to human security and product approval.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughAdds a proposed architecture decision for fail-closed diagnostics-helper credentials and mutual pairing, covering cryptographic messages, QR bootstrap, isolated storage, authorization lifecycle, compatibility, privacy restrictions, testing, and approval gates. ChangesDiagnostics helper pairing
Estimated code review effort: 2 (Simple) | ~10 minutes Poem
🚥 Pre-merge checks | ✅ 7✅ Passed checks (7 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Specify byte-exact bootstrap derivations, bounded pairing envelopes, lifecycle signer domains, and rotation/revocation digest chains for independent review.
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md`:
- Line 51: Update the bootstrap HMAC specification in the diagnostics helper
credentials decision to explicitly use bootstrap_secret as the HMAC-SHA-256 key,
with the stated domain and deterministic map excluding HMAC and signature fields
as the message. Add a golden vector validating this exact derivation, including
the resulting HMAC.
- Around line 167-169: Replace the cross-device atomic-commit claims in the
authorization and key-rotation flows with an explicit authenticated
pending/finalize/abort protocol. Define expiring pending records, require both
peers to reach the same terminal state before enabling capabilities, and specify
crash/network recovery for one-sided commits; do not treat local revocation
alone as recovery.
- Around line 146-148: Clarify the binding lifecycle in the diagnostics pairing
design: make homeserver and folder bindings stable for their helper/folder
scope, or explicitly per authorization with a deterministic reuse rule, and
ensure active records never regenerate an already-used binding. Update the
pairing-invitation flow around the binding generation described at line 163 to
reuse the existing binding safely, and add a multi-installation test covering
two app installations paired to the same folder.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 212deb3d-8500-409d-9c91-d059fd5ec9bd
📒 Files selected for processing (1)
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
📜 Review details
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{md,sh,go,swift}
📄 CodeRabbit inference engine (README.md)
Keep Relay-side request observation and wake-ups received on the iPhone as separate diagnostics evidence; one must not be treated as proof of the other.
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
docs/decisions/**/*
📄 CodeRabbit inference engine (docs/decisions/014-vault-subfolders-override-stray-root-config.md)
docs/decisions/**/*: Classify a connected root as a container, never as a vault-as-root, when it contains at least one direct subdirectory containing.obsidian/, regardless of whether the root itself contains.obsidian/. UseVaultManager.rootIsItselfVaultas the single classification source for all sites, includinggrantAccess, share acceptance, andresolveSharePath.
Do not inspect the contents of the root-level.obsidian/directory to distinguish a real vault from a stray configuration; use the presence of vault subfolders as the stronger signal.
Maintain regression coverage for root classification, including a root with both a stray root-level.obsidian/and one or more vault subfolders, using theVaultRootClassificationTestssuite.
docs/decisions/**/*: Every Relay provision request must require a locally verified, active Relay entitlement and its signed StoreKit transaction; never send a placeholder when this evidence is unavailable.
Preserve existing local and remote registration evidence across verification failures, network failures, and partial multi-homeserver failures.
Persist verified migration success independently for each homeserver.
Treat pre-migration success flags as indicating that migration is required, not that migration has been verified.
Do not clear registrations before migration; transient migration failures must not disable an otherwise working paid setup.
Do not use one global migration flag; one homeserver's failure must not hide or roll back another homeserver's success.
docs/decisions/**/*: Keep background start, local data progress, upload, download, and full-roundtrip proof as independent fields; never derive a global success flag.
Set only fresh local data progress after a successful fileItemFinishednewer than both the check cursor and nanosecond start time within one stable engine generation.
Keep manual results in memory and isolate them per folder and its sole connected peer; preserve partial, unsupported...
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
docs/**/*.md
📄 CodeRabbit inference engine (docs/troubleshooting.md)
docs/**/*.md: After applying each troubleshooting fix, retry from the app to confirm whether the issue is resolved.
For a missing Device ID or “Sync Engine Not Running,” force-close and reopen VaultSync, keep it foregrounded for 20–30 seconds, confirm the Device ID, rescan the vault, and reboot if necessary.
vaultsync-notifymust read the Syncthing API key fromconfig.xml; permission or authentication failures should be diagnosed as wrong-file or read-permission problems before changing credentials.
Runvaultsync-notifyas the user owning Syncthing’s mode-0600config.xml, or configure the appropriate containerPUID/PGID.
SetSYNCTHING_CONFIGwhen the Syncthing configuration is not auto-detected, and remove or correctSYNCTHING_API_KEYoverrides that replace automatic detection.
For relay failures, verify internet access,RELAY_URL, server egress rules, relay health with curl, and the app’s relay diagnostics; confirm delivery through an updated “Last Trigger Received” timestamp.
A relay HTTP 429 from--doctorcounts as success because it proves the trigger endpoint is reachable; an inactive subscription is a warning fixed through app subscription or provisioning.
Peer-state warnings from--doctormust not fail the command: offline peers are normal, and--healthcheckmust skip peer-state diagnostics.
For missing pending shares, verify the desktop folder shares the exact iOS Device ID, ensure Syncthing is online and unpaused, refresh pending shares, and re-share if needed.
For APNs issues, retry APNs registration, verify a token appears, retry provisioning, and confirm delivery with “Last Trigger Received”; notification permission is not required for silent wake-ups.
When connecting Obsidian storage, chooseOn My iPhone → Obsidianor the vault root containing.obsidian, then confirm detected vaults or pending shares.
VaultSync must not move, recreate, or delete vault folders automatically; recovery from moved or deleted folders requires ma...
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
docs/decisions/**/*.{md,markdown}
📄 CodeRabbit inference engine (docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md)
Update PRIVACY.md before introducing any runtime credential or pairing transport.
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
**
⚙️ CodeRabbit configuration file
**:![]()
VaultSync
Self-hosted Obsidian vault sync for iPhone and iPad.
Your notes sync peer-to-peer over Syncthing, straight into Obsidian's iOS sandbox — no note cloud, no account, no tracking.![]()
![]()
![]()
🔭 Why VaultSync
- Peer-to-peer & private — syncs directly between your own devices over Syncthing. No note cloud, no account, no tracking.
- Lands in Obsidian — files sync into Obsidian's iOS sandbox, where the app already looks for them.
- Pair by QR, resolve conflicts — connect your server in seconds; settle Markdown conflicts with side-by-side diffs.
- Server changes can wake your iPhone — optional Cloud Relay asks iOS to wake the app after the helper observes a server change. T...
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
docs/**
⚙️ CodeRabbit configuration file
docs/**: # ArchitectureVaultSync embeds Syncthing's Go reference implementation as an iOS library via gomobile — no reimplementation of the protocol in Swift, and guaranteed wire compatibility.
┌─────────────────────────────────┐ │ SwiftUI Frontend │ iOS-native UI, Swift 6 ├─────────────────────────────────┤ │ Swift ↔ Go Bridge │ thin API via gomobile │ │ → exported as .xcframework ├─────────────────────────────────┤ │ syncthing/lib (Go) │ protocol, discovery, sync └─────────────────────────────────┘ ↕ filesystem ┌─────────────────────────────────┐ │ Obsidian Vault (direct) │ Obsidian's iOS sandbox └─────────────────────────────────┘🔄 Sync strategy
- Foreground — Syncthing runs unrestricted: immediate, continuous sync.
- Background —
BGAppRefreshTask(requested ~15 min out; iOS decides the actual timing) +BGProcessingTask(overnight catch-up: multi-minute budget while charging with network) +BGContinuedProcessingTask(iOS 26+, longer runtime for user-initiated tasks). A ~30s grace window after backgrounding lets in-flight work finish.- Push (Cloud Relay) — optional. Near-realtime
server → iPhonewake-ups via APNs silent push. See relay-spec.md.VaultSync is intentionally asymmetric:
Direction Path Server → iPhone vaultsync-notifyspots outgoing changes → Cloud Relay silent push → VaultSync wakes and pulls.iPhone → Server iOS doesn't guarantee timely background execution for local edits. The reliable path is to open VaultSync and let embedded Syncthing run in the foreground — a Shortcuts automation can do that automatically whenever you leave Obsidian. Cloud Relay is a
server → iPhoneacceleration path, not a guarantee of symmetric real-time background sync.Relay and sync proof hierarchy
VaultSync models proof as independent fi...
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
docs/decisions/**
⚙️ CodeRabbit configuration file
docs/decisions/**: # 001 — Two Syncthing folders never overlap on disk (three enforcement layers)Context: A server sharing more than one vault could hand the second share the same local folder as the first (1.6.0–1.7.0) or a subfolder inside an existing vault (1.7.1). Overlapping folders sync each other's content as their own — deleting the stray copy on any peer would have deleted the inner vault everywhere (
#45).Decision: The no-overlap invariant (equal, nested, or containing paths) is enforced in three independent layers, each with its own tests: the Go hard floor (
AcceptPendingFolder/AddFolderreject withfolderPathOverlapError), the Swift mapping (VaultManager.resolveSharePathreturnsnilrather than an overlapping path), and the launch shield (PathCollisionGuardpauses already-overlapping folders exactly once).Why: The single-layer version failed twice — both
#45bugs lived in the Swift mapping. The Go floor backstops future mapping bugs, the Swift layer turns a hard engine error into user guidance, and only the shield catches damage that predates the fix.Rejected alternative: Enforcing only in the Swift mapping — the proven failure mode; a bug there would silently re-open the hole with no backstop.
Links:
#45, PR#47(same-folder merge, 1.7.1), PR#51(nesting, 1.7.2).
docs/decisions/**: # 002 — Recovery from data damage is never automaticContext: When VaultSync detects existing damage (two vaults merged into one folder, or one nested inside another,
#45), an automatic repair would have to move, rename, delete, or re-accept user data whose true state only the user knows — after a merge, the app cannot tell which files belong to which vault.Decision: Recovery is always: pause the affected folders (exactly once — a folder the user deliberately resumes is never re-paused), explain the problem in a critical issue with concrete recovery steps, and let the user act.
Why: Sync propagates every local act...
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
**/*
⚙️ CodeRabbit configuration file
**/*: VaultSync syncs private Obsidian notes through Syncthing. Treat data loss,
privacy leaks, security regressions, and broken sync behavior as high priority.
Do not nitpick formatting unless it affects maintainability, correctness, or public API clarity.
Flag any accidental logging, telemetry, crash reporting, or network transfer of note contents,
vault paths, filenames with private context, API keys, APNs tokens, relay keys, or security-scoped bookmark data.
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
**/*.md
⚙️ CodeRabbit configuration file
**/*.md: Review public documentation for technical accuracy, privacy/security claims, App Store-facing wording,
setup correctness, and consistency with the free app plus optional Cloud Relay subscription model.
Files:
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
🧠 Learnings (1)
📓 Common learnings
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Treat this design as proposed only: it is not implemented, independently reviewed, approved, or authorized for runtime use; require human security and product approval before implementation.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Keep the diagnostics capability separate from existing Syncthing pairing, Trigger v1, Cloud Relay, APNs, StoreKit, folder mappings, and the published helper.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Pairing must be explicit, mutually authenticated, scoped to one app installation, one homeserver, and selected folders; it must never be inferred or repeated automatically.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Pairing alone must create no synchronized namespace and provide no upload, download, or roundtrip evidence.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Do not use mDNS, UPnP, Cloud Relay, unauthenticated synchronized files, StoreKit transactions, Syncthing TLS keys, public listeners, or automatic discovery in bootstrap.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Treat old, unpaired, unreachable, downgraded, or revoked helpers as capability unavailable or re-pair required; never convert them into general sync errors, weaker proof, or automatic trust.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Give every app installation a distinct authorization, key, epoch, folder scope, quota, and revocation state; adding an installation requires a new one-time pairing.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Do not automatically recover lost app keys, adopt old helper credentials, escrow credentials, or delete credentials and namespace roots during upgrades, downgrades, or rollbacks.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Keep Cloud Relay, Trigger v1, and existing app/helper compatibility behavior unchanged; the new capability must be additive and dormant until explicitly paired.
Learnt from: CR
Repo: psimaker/vaultsync
Timestamp: 2026-07-12T15:13:03.258Z
Learning: Obtain explicit human approval for the cryptographic suite, storage model, helper state backup policy, QR/pinned-TLS bootstrap, per-installation authorization, downgrade behavior, and lifecycle registry before implementation.
🪛 LanguageTool
docs/decisions/022-diagnostics-helper-credentials-and-mutual-pairing.md
[style] ~122-~122: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...y, and type 8 by the current app key. Type 9 is signed by the current helper key...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
[style] ~122-~122: Three successive sentences begin with the same word. Consider rewording the sentence or use a thesaurus to find a synonym.
Context: ...y and type 10 by the current app key. Type 11 is signed by the current app key; ...
(ENGLISH_WORD_REPEAT_BEGINNING_RULE)
Specify the bootstrap HMAC key and golden vector, stable binding reuse, and authenticated pending/finalize/abort recovery without cross-device atomicity claims.


What changed
Adds proposed Decision 022 for the credential and mutual-pairing gate required by Decision 021. It specifies the cryptographic suite, deterministic pairing encoding and signature domains, iOS/helper key storage, explicit QR plus pinned-TLS bootstrap, homeserver/folder bindings, multi-installation authorization, rotation, revocation, recovery, replay defense, downgrade behavior, compatibility, privacy constraints, and the required test matrix.
Safety boundary
This is design-only. It creates no endpoint, key, pairing record, namespace, probe, installer permission, Relay change, or rollout. The document is explicitly not independently reviewed or approved and lists the human security/product decisions still required.
Validation
git diff --checkSummary
Scope clarification: the decision's invariants and bootstrap prohibitions apply only to the diagnostics capability.