I am running an iOS Multi-Factor Authentication (MFA) app. It is similar to the Microsoft Authenticator and Google Authenticator.
Users of my app can choose to sync all their One Time Passwords (OTP's) to iCloud (via CloudKit), so they do not lose access if their phone dies. If a user chooses to enable this sync my app automatically syncs all OTPs (as CloudKit records), that a user creates or modifies, to CloudKit.
Whenever the app is launched, I also create a CloudKit subscription, to listen for any changes in the CloudKit database. If there are any changes, I retrieve them and store them locally as well. This provides a seamless experience for the user, as all OTP's are synced across all of the Apple devices where the user is logged into iCloud and has the app installed. This has worked very well for years.
However, several users report that sometimes syncing does not work properly. We found out that it specifically concerns users who have Advanced Data Protection enabled. I have debugged the issue and have identified the following (assuming we have two devices, A and B, which are logged in to the same iCloud account):
- Creating or modifying a record in CloudKit works fine on both A & B.
- Retrieving one or multiple records from CloudKit works fine on both A & B.
- Creating the CloudKit subscription to listen for record changes works fine on both A & B.
- I can also see that both device A & B have registered for remote notifications successfully.
- If I create a record on device A, it does not trigger the subscription (remote notification) on device B, and vice versa.
- Only after restarting the app on device B, it will retrieve all records via a query on launch (not via a remote notification), and the added record is retrieved.
Question: Does anyone have any idea why this subscription and its notifications do not work with Advanced Data Protection enabled?
I have included partials of the code below.
CloudKit subscription creation (source on GitHub):
// Private CloudKit database
let cloud = CKContainer.init(identifier: CloudKitSyncer.containerName).privateCloudDatabase
// Create subscription instance
let subscription = CKQuerySubscription(
recordType: Password.TABLE,
predicate: NSPredicate(value: true),
subscriptionID: CKSubscription.ID(idr(CloudKitPasswordSyncer.self)),
options: [
CKQuerySubscription.Options.firesOnRecordCreation,
CKQuerySubscription.Options.firesOnRecordUpdate
]
)
// Set notification options
let notificationInfo = CKSubscription.NotificationInfo()
notificationInfo.shouldSendContentAvailable = true
subscription.notificationInfo = notificationInfo
// Save subscription to CloudKit
cloud.save(subscription) { (subscription, error) in
guard subscription != nil else {
log.error(error?.localizedDescription ?? "Unknown CloudKit error!")
return
}
}
Saving a record (source on GitHub):
// Private CloudKit database
let cloud = CKContainer.init(identifier: CloudKitSyncer.containerName).privateCloudDatabase
// CKRecord
let record = CKRecord(recordType: Password.TABLE, recordID: CKRecord.ID(recordName: password.getRemotePrimaryKey()))
record.setValue(password.id, forKey: "id")
record.setValue(password.kind, forKey: "kind")
-- snip --
// Creation/modification
let modification = CKModifyRecordsOperation(recordsToSave: [record], recordIDsToDelete: nil)
modification.savePolicy = .changedKeys
modification.qualityOfService = .userInteractive
modification.modifyRecordsCompletionBlock = { records, deletedIDs, error in
if records?.count != 1 {
log.error(error?.localizedDescription ?? "Unknown CloudKit error!")
}
-- snip --
}
cloud.add(modification)
}
Application delegate (source on GitHub):
/// Called when your app has received a remote notification.
///
/// - Parameter application: The singleton app object.
/// - Parameter userInfo: A dictionary that contains information related to the remote notification.
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
log.verbose("Received a remote notification")
SyncerHelper.shared.getSyncer().notify(userInfo)
}
didReceiveRemoteNotification
is never called when Advanced Data Protection is enabled, in relation to a CloudKit record change.