Repository navigation
Add the Mac device services: identity storage, pairing and messages - #296
Merged
Merged
Conversation
The Vault stores the Mac's three Secure Enclave key blobs and the paired-device list as App-only items under the App service (accounts device-message-key, device-agreement-key, device-approval-key and paired-devices), each with its own conflict kind and message. The list item is the list's serialization followed by an HMAC-SHA256 tag under a key derived from the vault key (ADR 0013); a list that fails authentication is refused, not repaired. Reading needs no management session; one registry per storage epoch commits every list change, and a save that adds a device needs the management session and the pairing session's authorization for that device. Local erase stops the device services first, then deletes the four items with the other App keychain items; an identity reset closes the epoch and deletes keys, list and inbox state. AskKeyAppKit gains Devices/: the CloudKit gate (Info.plist key plus the signed container and CloudKit entitlements), a backend interface with the real CloudKit backend, and the coordinator. It creates the identity on the first pairing, runs pairing as the initiator (waiting for the iPhone's verified reveal, code entry, system authentication inside the approval signature, add only with both confirmations), delivery checks, removal, the iCloud account and push hints, and publishes the state the Devices section will read. An open pairing sheet holds SessionPolicy's idle expiry. AppRuntimeConfiguration takes an optional device-services factory, and AskKeyTestSupport provides the fake one.
- Each key item is now the Secure Enclave blob followed by an HMAC-SHA256 tag over a label, its account and the blob, under a key derived from the vault key. Any process on the Mac can use a blob it holds, so an identity this library did not store, such as blobs another App planted before the first pairing, is refused and offers the reset. - Only the latest account check applies: an earlier check that answers later no longer overwrites an account change. An unrecorded account is adopted only while the list is empty, and creating the identity fails if its account cannot be recorded. - After the device is added, the open sheet keeps polling and holding idle expiry until its session expires, so a cancellation that undoes the pairing is still fetched; the authorization ends with the add. - A failed management-session check also drops the pairing authorization, and the management observer re-registers before it awaits the cancellation. - Tests wait on time-bounded conditions instead of counted yields.
Vault.endManagementSession() now also drops the pairing authorization, so a new management session never inherits it, whether management was locked, expired in the App or its window closed. The planner approved this one line outside the issue's Vault.swift Scope as a security-boundary fix found in the cross review.
The sheet's polling loop now ends the session's polling and its hold on idle expiry whenever the session can no longer change after a turn, including an expiry that passed while that turn polled; before, such a turn left the loop without releasing the hold. Ending the management session after the expiry also releases it. An account check whose services stopped while it read the list no longer applies.
An account check that ended an open pairing session no longer replaces the messenger when a newer check switched it, and a new session started, while the cancellation was sent. The foreign-key test case now uses the real labels under another key.
|
Cross review summary (Opus 5.5 and GPT-6.1-Sol, xhigh, read-only, two rounds; in round 2 each read the other's round 1 report and reviewed the fixes). No P0 in either round. Round 1: GPT rated the planted identity P1, Opus P2; both independently found it. Round 2: both found one P2 in a round 1 fix (the post-pairing poll loop), now fixed. Fixed
Not fixed, and why
For the planner
Reports stay outside the repository. |
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.
Closes #287
Summary
device-message-key,device-agreement-key,device-approval-key,paired-devices), each with its ownAppKeychainItemKindand conflict message, inAppBoundKeyStore,IsolatedAppKeyStoreand every test conformer. Every device helper passes its account explicitly; the privatedeleteno longer defaults to the vault-key account. The list item is the list's serialization followed by an HMAC-SHA256 tag overAskKey paired devices v1\0and those exact bytes, under HKDF-SHA256(vault key, empty salt,AskKey paired devices key v1\0); the tag is checked before decoding and a list that fails it is refused, never repaired. Each key item is the blob followed by an HMAC-SHA256 tag overAskKey device key blob v1\0, its account name and NUL, and the blob, under HKDF-SHA256(vault key, empty salt,AskKey device key blob key v1\0): any process on the Mac can use a blob it holds, so an identity this library did not store (for example blobs another App planted before the first pairing) is refused and offers the reset.Vault+Devices.swift). Reading the identity and list needs no management session. OnePairedDeviceRegistryper storage epoch commits every list change; a save that adds a device (new device ID or new pairing ID) needs the management session and the authorization recorded byauthorizeDevicePairing(with:using:)for that device, which the write uses up; ending the management session, or a failed management-session check, drops the authorization. Local erase stops the device services in its quiesce step (epoch closed, stop handler called), then deletes the four items with the other App keychain items inside the existing erase sequence and its recovery; a pending erase journal keeps device storage closed. An identity reset closes the epoch and deletes keys, list and inbox state. The inbox token and the iCloud account it and the list belong to live indevice-inbox.jsonin the library directory. Backup export is unchanged.Sources/AskKeyAppKit/Devices/).CloudKitGateallows the CloudKit backend only when the Info.plist declaresCloudKitContainerIdentifierand the signed entitlements contain that container and the CloudKit service; otherwise Devices reportsnotConfiguredand no CloudKit object exists.DeviceCoordinatorcreates the identity on the first pairing (Secure Enclave check, keys in memory, account check, and only then the stored blobs), prepares the inbox, shows the QR code, polls about once a second while the sheet is open and its session can still change (also after the add, until the session expires), waits for the iPhone's verified reveal (logged without content), takes the code, asks for system authentication inside the approval signature (cancelled or failed: nothing sent, no attempt, retry without re-entry), and adds the device only with both confirmations. It runs delivery checks, removal with unpair notices, renaming, push hints (fetch only), the iCloud account (devices unreachable after an account change, also one while the App was not running), the unusable-identity reset and local erase. It never exposes the Mac's code.AppRuntimeConfiguration.makeDeviceServices(the production executable passes none),AppDelegate+Devices.swift(create, start, stop, register for remote notifications, cancel pairing when management locks or closes), one-line calls from launch and termination, andSessionPolicy.holdIdleExpiry(), which an open pairing sheet holds until its session ends or the sheet closes.AskKeyTestSupportgainsFakeDeviceServicesBackendandSimulatedPairingPhonefor Add the Devices section to Mac Settings #288.Diff stat
git diff --stat origin/main: 31 files changed, 3848 insertions(+), 17 deletions(-). Sources: 17 files, +1988/−11 (new:Vault+Devices.swift405 lines,Devices/6 files ≤ 498 lines,AppDelegate+Devices.swift;Vault.swift588 lines). Tests: 13 files, +1852/−3. Commits: c8a5680 (feature), 06d6b34, d5425fd, 57e32f1, 3c211ae (cross-review fixes).Package.swift: the dependency edges the issue allows.Sources/AskKeyDevices/is unchanged.Tests
swift build --build-tests, thenswift test --skip-build --filter <suites>, 0 failed:notConfigured, nothing stored; Mac key protections explicitFinal head 3c211ae: the new suites 16 + 1 + 7 + 17 + 14 + 3 = 58 tests, 0 failed (3 native skipped); the existing suites above 0 failed. The coordinator suites ran twice more with no failure.
Not run locally (per AGENTS.md): the full
swift testsuite, the native keychain tests (CI only) and the desktop flows; CI runs them.Checks
swift build: passes.python3 scripts/check_hygiene.py:Hygiene rules passed: size=0, test-support=0, debug=0, local-path=0, non-ascii-name=0, multica=0, cjk=0.python3 scripts/check_module_deps.py:Module dependency check passed.Deviations and questions
Outside the Scope, approved by the planner.
Vault.endManagementSession()gains one line,deviceState.authorize(nil)(commit d5425fd); the issue allowsVault.swiftchanges only for local erase. Reason: both cross reviewers found that the Vault-level pairing authorization was bound only to the device ID and outlived its management session, so "authorize A → end the management session → begin a new one → add A" succeeded at the Vault layer although the issue requires an authentication obtained for that pairing session. Test:DeviceStorageTests.testAnAuthorizationEndsWithItsManagementSessionEvenWithoutACheckInBetween. Paths checked: locking (VaultViewModel.lock()andVault.lock()), the App's idle expiry (VaultViewModel.renewManagementSessiontimer), closing the management window (ManagementSessionLifecycle→VaultViewModel.lock()) and local erase (VaultViewModel.eraseLocalLibrary→lock()) all callendManagementSession(). Not through it: the Vault's own idle expiry insiderequireManagementSession()and the session reset infinishLocalErase(); the first is covered by the in-scope fallback (a failed management-session check drops the authorization) and in the App by the App-level timer of the same length, the second by the erase's epoch change, which also drops it.Authentication reason. The App's authentication subprocess accepts only the reasons in
ManagementAuthenticationAction, so a new pairing reason would make every real authentication fail. Adding one changesManagementAuthenticationProcess.swiftand needs a translation, both outside this Scope. Pairing therefore uses the management reason (Vault.devicePairingReason == CredentialManagementCopy.manageReason). Handed to Add the Devices section to Mac Settings #288 (Scope and step 7): a dedicated pairing reason inManagementAuthenticationProcess.swiftandAppLanguage.swiftwith its translation, andSources/AskKeyAppKit/Devices/passing it.Strings. The four new conflict messages are English only. Handed to Add the Devices section to Mac Settings #288 (step 7): their Simplified Chinese translations.
Account change with devices listed. While another account is signed in and the list is not empty, pairing reports
accountUnavailable(.accountChanged)until the person removes the devices or signs back in; with an empty list the new account is adopted.Key-item format not in ADR 0013. The tag on each key item (above) is recorded only in code and tests; ADR 0013 says the blobs are stored like the vault key and gives no item layout.
docs/adris outside this Scope, so recording it is left to the planner.Inbox state location. The token and account live in the library directory (
device-inbox.json), which is per storage namespace and goes with a local erase, rather than in UserDefaults.The E2E App does not pass the fake factory yet; Add the Devices section to Mac Settings #288 wires it.
Sources/AskKeyDevices/needed no change.