Skip to content

feat: add optional iCloud Keychain sync for connection passwords#364

Merged
datlechin merged 3 commits intomainfrom
feat/icloud-keychain-password-sync
Mar 18, 2026
Merged

feat: add optional iCloud Keychain sync for connection passwords#364
datlechin merged 3 commits intomainfrom
feat/icloud-keychain-password-sync

Conversation

@datlechin
Copy link
Copy Markdown
Collaborator

@datlechin datlechin commented Mar 18, 2026

Summary

  • Add opt-in Passwords toggle under Connections in Sync settings that syncs all connection credentials (DB password, SSH password, key passphrase, TOTP secret) via Apple's iCloud Keychain
  • KeychainHelper conditionally sets kSecAttrSynchronizable on save, uses kSecAttrSynchronizableAny on load/delete for seamless access regardless of sync state
  • Safe migration between local and synchronizable items using add-first-then-delete pattern (same as existing legacy migration)
  • Startup reconciliation ensures UserDefaults flag stays consistent with persisted SyncSettings
  • Password sync is off by default, requires explicit opt-in on each Mac

Test plan

  • Toggle password sync ON/OFF, verify migration logs in Console.app (com.TablePro > KeychainHelper)
  • Create connection with password, toggle sync ON, reconnect -- password still loads
  • Toggle sync OFF, reconnect -- password still loads
  • Disable Connections toggle -- verify Passwords toggle disappears and migrates back to local
  • Quit and relaunch with sync ON -- verify startup reconciliation sets UserDefaults flag
  • Cross-Mac: enable on Mac A, verify password arrives on Mac B via iCloud Keychain
  • Verify Keychain Access.app shows "Synchronizable: Yes" on items when sync is ON

Summary by CodeRabbit

  • New Features

    • Optional iCloud Keychain sync for connection passwords. When Connection sync is enabled, a Passwords toggle appears (default off); toggling migrates password items between local and iCloud Keychain and persists the choice per Mac.
  • Documentation

    • Updated iCloud Sync docs to describe the Passwords toggle, opt-in behavior, migration details, and end-to-end encryption assurances.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 18, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: bccd092a-3493-4d7b-8e1b-96ed97652b0c

📥 Commits

Reviewing files that changed from the base of the PR and between 04e496a and e6c630c.

📒 Files selected for processing (3)
  • TablePro/AppDelegate.swift
  • TablePro/Core/Storage/KeychainHelper.swift
  • TablePro/Views/Settings/SyncSettingsView.swift

📝 Walkthrough

Walkthrough

Adds an opt-in iCloud Keychain password sync: new SyncSettings flag and UI toggle, AppDelegate startup handling to persist expected state and trigger migration, KeychainHelper support for kSecAttrSynchronizable with a migratePasswordSyncState routine, and updated docs in English and Vietnamese.

Changes

Cohort / File(s) Summary
Release Notes
CHANGELOG.md
Documented new optional iCloud Keychain sync for connection passwords.
Sync Configuration Model
TablePro/Models/Settings/SyncSettings.swift
Added syncPasswords: Bool to the model, decoder, and initializers to persist the password-sync preference (default false).
Keychain Synchronization
TablePro/Core/Storage/KeychainHelper.swift
Added passwordSyncEnabledKey, migration lock, read/write handling for kSecAttrSynchronizable, permissive queries using kSecAttrSynchronizableAny, duplicate-item update logic, and migratePasswordSyncState(synchronizable:) to bulk-migrate items with per-item logging.
App Initialization
TablePro/AppDelegate.swift
Loads sync settings at launch, computes passwordSyncExpected, persists it in UserDefaults, and conditionally starts a background Task to run migratePasswordSyncState before plugin loading.
Sync Settings UI
TablePro/Views/Settings/SyncSettingsView.swift
Shows Passwords toggle only when Connections is enabled, disables Passwords when Connections is turned off, persists changes, and calls onPasswordSyncChanged(_:)/updatePasswordSyncFlag() to trigger migration and persist effective flag.
Documentation
docs/features/icloud-sync.mdx, docs/vi/features/icloud-sync.mdx
Updated English and Vietnamese docs to mark Passwords as "Optional", added a "Password sync" subsection describing the opt-in behavior, encryption, migration, and macOS iCloud Keychain note.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant UI as SyncSettingsView
    participant App as AppDelegate
    participant UD as UserDefaults
    participant KHelper as KeychainHelper
    participant Keychain as Keychain

    App->>KHelper: loadSync() / compute passwordSyncExpected
    App->>UD: persist passwordSyncEnabled flag
    User->>UI: toggle Passwords
    UI->>UD: persist syncPasswords state
    UI->>KHelper: onPasswordSyncChanged(enabled)
    KHelper->>Keychain: query items (kSecAttrSynchronizableAny)
    loop per item
        KHelper->>Keychain: add item with kSecAttrSynchronizable={enabled}
        KHelper->>Keychain: delete old variant if needed
    end
    KHelper->>UI: return migration summary
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 In orchards of bytes the little passwords hop,
A toggle invites them to climb to the top.
Opt-in they travel, wrapped tight in a key,
Hops and hops—encrypted, safe as can be.
A rabbit cheers: hop on, sync if you agree!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main feature added: optional iCloud Keychain sync for connection passwords, matching the core objective of the pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/icloud-keychain-password-sync
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TablePro/AppDelegate.swift`:
- Around line 56-58: The password-sync UserDefaults flag is set too late and
must be reconciled before legacy migrations run: call
AppSettingsStorage.shared.loadSync() and compute passwordSyncExpected, then set
UserDefaults.standard.set(passwordSyncExpected, forKey:
"com.TablePro.keychainPasswordSyncEnabled") prior to invoking
migrateFromLegacyKeychainIfNeeded(); this ensures KeychainHelper.save (which
reads that key) will choose the correct local vs synchronizable storage during
migration.

In `@TablePro/Core/Storage/KeychainHelper.swift`:
- Line 69: The current keychain queries use kSecAttrSynchronizableAny (notably
in load(key:)) which breaks per-Mac opt-in; update load(key:) and related
methods (save(key:value:), remove(key:), and migrateOptOut) to never query with
kSecAttrSynchronizableAny: first query explicitly for non-synchronizable items
(kSecAttrSynchronizable = kSecAttrSynchronizableFalse) to read local-only
entries, and only if the current Mac is opted-in attempt a separate query for
synchronizable items (kSecAttrSynchronizable = kSecAttrSynchronizableTrue). For
the opt-out migration (migrateOptOut), do not delete the shared synchronizable
item; instead create or update a local non-synchronizable override (or record a
per-device ignore flag) so opted-out Macs ignore synced entries without removing
them for other devices.

In `@TablePro/Views/Settings/SyncSettingsView.swift`:
- Around line 106-120: The current code calls onPasswordSyncChanged(newValue)
and saves only the raw syncSettings.syncPasswords flag, causing the keychain
sync flag to remain true if the master syncConnections switch is turned off;
update both the Passwords toggle handler and the master Connections toggle
handler to compute the effective password-sync value as (enabled &&
syncSettings.syncConnections && syncSettings.syncPasswords) and pass that
derived Bool into onPasswordSyncChanged(_:) after persistSettings(), ensuring
KeychainHelper.save's com.TablePro.keychainPasswordSyncEnabled reflects the
combined state; adjust the Connections Toggle onChange block (where
syncSettings.syncConnections is toggled) to also recalculate and call
onPasswordSyncChanged(false or derived) when turning off the master switch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a9c49324-2f34-416b-946a-a48bf27c54cd

📥 Commits

Reviewing files that changed from the base of the PR and between 4718ff2 and f98b745.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • TablePro/AppDelegate.swift
  • TablePro/Core/Storage/KeychainHelper.swift
  • TablePro/Models/Settings/SyncSettings.swift
  • TablePro/Resources/Localizable.xcstrings
  • TablePro/Views/Settings/SyncSettingsView.swift
  • docs/features/icloud-sync.mdx
  • docs/vi/features/icloud-sync.mdx

@datlechin datlechin merged commit ddcd83c into main Mar 18, 2026
2 checks passed
@datlechin datlechin deleted the feat/icloud-keychain-password-sync branch March 18, 2026 05:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant