Allow erasing remote iCloud store (#351)

Convenient for those with privacy concerns.
This commit is contained in:
Davide De Rosa 2023-09-09 21:52:32 +02:00 committed by GitHub
parent a4ca8cc996
commit 791b6be7d5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 106 additions and 26 deletions

View File

@ -45,6 +45,12 @@ extension Constants {
}() }()
} }
enum CloudKit {
static let containerId: String = bundleConfig("cloudkit_id")
static let coreDataZone = "com.apple.coredata.cloudkit.zone"
}
enum Plugins { enum Plugins {
static let macBridgeName = "PassepartoutMac.bundle" static let macBridgeName = "PassepartoutMac.bundle"
} }

View File

@ -137,4 +137,8 @@ extension AppContext {
lastIsCloudSyncingEnabled = isCloudSyncingEnabled lastIsCloudSyncingEnabled = isCloudSyncingEnabled
} }
} }
func eraseCloudKitStore() async {
await coreContext.eraseCloudKitStore()
}
} }

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
import CloudKit
import Combine import Combine
import Foundation import Foundation
import PassepartoutLibrary import PassepartoutLibrary
@ -35,6 +36,8 @@ final class CoreContext {
private let persistenceManager: PersistenceManager private let persistenceManager: PersistenceManager
private(set) var vpnPersistence: VPNPersistence
let upgradeManager: UpgradeManager let upgradeManager: UpgradeManager
let providerManager: ProviderManager let providerManager: ProviderManager
@ -63,7 +66,7 @@ final class CoreContext {
upgradeManager.migrate(toVersion: Constants.Global.appVersionNumber) upgradeManager.migrate(toVersion: Constants.Global.appVersionNumber)
persistenceManager = PersistenceManager(store: store) persistenceManager = PersistenceManager(store: store)
let vpnPersistence = persistenceManager.vpnPersistence( vpnPersistence = persistenceManager.vpnPersistence(
withName: Constants.Persistence.profilesContainerName, withName: Constants.Persistence.profilesContainerName,
cloudKit: store.isCloudSyncingEnabled cloudKit: store.isCloudSyncingEnabled
) )
@ -140,14 +143,17 @@ private extension CoreContext {
extension CoreContext { extension CoreContext {
func reloadCloudKitObjects(isEnabled: Bool) { func reloadCloudKitObjects(isEnabled: Bool) {
let vpnPersistence = persistenceManager.vpnPersistence( vpnPersistence = persistenceManager.vpnPersistence(
withName: Constants.Persistence.profilesContainerName, withName: Constants.Persistence.profilesContainerName,
cloudKit: isEnabled cloudKit: isEnabled
) )
profileManager.swapProfileRepository(vpnPersistence.profileRepository()) profileManager.swapProfileRepository(vpnPersistence.profileRepository())
} }
func eraseCloudKitStore() { func eraseCloudKitStore() async {
// TODO: iCloud, erase remote records await vpnPersistence.eraseCloudKitStore(
fromContainerWithId: Constants.CloudKit.containerId,
zoneId: .init(zoneName: Constants.CloudKit.coreDataZone)
)
} }
} }

View File

@ -101,6 +101,8 @@
<string>$(CFG_APPSTORE_ID)</string> <string>$(CFG_APPSTORE_ID)</string>
<key>group_id</key> <key>group_id</key>
<string>group.$(CFG_GROUP_ID)</string> <string>group.$(CFG_GROUP_ID)</string>
<key>cloudkit_id</key>
<string>iCloud.$(CFG_GROUP_ID)</string>
</dict> </dict>
</dict> </dict>
</plist> </plist>

View File

@ -257,6 +257,8 @@ enum Unlocalized {
enum Other { enum Other {
static let siri = "Siri" static let siri = "Siri"
static let iCloud = "iCloud"
static let totp = "TOTP" static let totp = "TOTP"
} }
} }

View File

@ -37,6 +37,8 @@ struct SettingsView: View {
@Binding private var shouldEnableCloudSyncing: Bool @Binding private var shouldEnableCloudSyncing: Bool
@State private var isErasingCloudStore = false
private let versionString = Constants.Global.appVersionString private let versionString = Constants.Global.appVersionString
init() { init() {
@ -45,14 +47,20 @@ struct SettingsView: View {
_shouldEnableCloudSyncing = .init { _shouldEnableCloudSyncing = .init {
AppContext.shared.shouldEnableCloudSyncing AppContext.shared.shouldEnableCloudSyncing
} set: { } set: { isEnabled in
AppContext.shared.shouldEnableCloudSyncing = $0 withAnimation {
AppContext.shared.shouldEnableCloudSyncing = isEnabled
}
} }
} }
var body: some View { var body: some View {
List { List {
#if !targetEnvironment(macCatalyst)
preferencesSection preferencesSection
#endif
iCloudSection
diagnosticsSection
aboutSection aboutSection
}.toolbar { }.toolbar {
themeCloseItem(presentationMode: presentationMode) themeCloseItem(presentationMode: presentationMode)
@ -66,29 +74,48 @@ struct SettingsView: View {
private extension SettingsView { private extension SettingsView {
var preferencesSection: some View { var preferencesSection: some View {
Section { Section {
#if !targetEnvironment(macCatalyst)
Toggle(L10n.Settings.Items.LocksInBackground.caption, isOn: $locksInBackground) Toggle(L10n.Settings.Items.LocksInBackground.caption, isOn: $locksInBackground)
#endif
Toggle(L10n.Settings.Items.ShouldEnableCloudSyncing.caption, isOn: $shouldEnableCloudSyncing)
} header: { } header: {
Text(L10n.Preferences.title) Text(L10n.Preferences.title)
} }
} }
var iCloudSection: some View {
Section {
Toggle(L10n.Settings.Items.ShouldEnableCloudSyncing.caption, isOn: $shouldEnableCloudSyncing)
Button(L10n.Settings.Items.EraseCloudStore.caption) {
isErasingCloudStore = true
Task {
await AppContext.shared.eraseCloudKitStore()
isErasingCloudStore = false
}
}.withTrailingProgress(when: isErasingCloudStore)
.disabled(shouldEnableCloudSyncing || isErasingCloudStore)
} header: {
Text(Unlocalized.Other.iCloud)
} footer: {
Text(L10n.Settings.Sections.Icloud.footer)
}
}
var diagnosticsSection: some View {
Section {
DiagnosticsRow(currentProfile: profileManager.currentProfile)
}
}
var aboutSection: some View { var aboutSection: some View {
Section { Section {
NavigationLink {
AboutView()
} label: {
Text(L10n.About.title)
}
NavigationLink { NavigationLink {
DonateView() DonateView()
} label: { } label: {
Text(L10n.Settings.Items.Donate.caption) Text(L10n.Settings.Items.Donate.caption)
}.disabled(!productManager.canMakePayments()) }.disabled(!productManager.canMakePayments())
NavigationLink {
DiagnosticsRow(currentProfile: profileManager.currentProfile) AboutView()
} label: {
Text(L10n.About.title)
}
} footer: { } footer: {
HStack { HStack {
Spacer() Spacer()

View File

@ -331,8 +331,10 @@
/* MARK: SettingsView */ /* MARK: SettingsView */
"settings.title" = "Settings"; "settings.title" = "Settings";
"settings.sections.icloud.footer" = "Disable sync to allow erase. To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.";
"settings.items.locks_in_background.caption" = "Lock app access"; "settings.items.locks_in_background.caption" = "Lock app access";
"settings.items.should_enable_cloud_syncing.caption" = "Sync with iCloud"; "settings.items.should_enable_cloud_syncing.caption" = "Sync with iCloud";
"settings.items.erase_cloud_store.caption" = "Erase iCloud store";
"settings.items.donate.caption" = "Make a donation"; "settings.items.donate.caption" = "Make a donation";
/* MARK: AboutView */ /* MARK: AboutView */

View File

@ -921,6 +921,10 @@ internal enum L10n {
/// Make a donation /// Make a donation
internal static let caption = L10n.tr("Localizable", "settings.items.donate.caption", fallback: "Make a donation") internal static let caption = L10n.tr("Localizable", "settings.items.donate.caption", fallback: "Make a donation")
} }
internal enum EraseCloudStore {
/// Erase iCloud store
internal static let caption = L10n.tr("Localizable", "settings.items.erase_cloud_store.caption", fallback: "Erase iCloud store")
}
internal enum LocksInBackground { internal enum LocksInBackground {
/// Lock app access /// Lock app access
internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app access") internal static let caption = L10n.tr("Localizable", "settings.items.locks_in_background.caption", fallback: "Lock app access")
@ -930,6 +934,12 @@ internal enum L10n {
internal static let caption = L10n.tr("Localizable", "settings.items.should_enable_cloud_syncing.caption", fallback: "Sync with iCloud") internal static let caption = L10n.tr("Localizable", "settings.items.should_enable_cloud_syncing.caption", fallback: "Sync with iCloud")
} }
} }
internal enum Sections {
internal enum Icloud {
/// Disable sync to allow erase. To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.
internal static let footer = L10n.tr("Localizable", "settings.sections.icloud.footer", fallback: "Disable sync to allow erase. To erase the iCloud store securely, do so on all your synced devices. This will not affect local profiles.")
}
}
} }
internal enum Shortcuts { internal enum Shortcuts {
internal enum Add { internal enum Add {

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
import CloudKit
import Combine import Combine
import CoreData import CoreData
import Foundation import Foundation
@ -69,7 +70,25 @@ public final class CoreDataPersistentStore {
container.viewContext.transactionAuthor = author container.viewContext.transactionAuthor = author
} }
} }
}
extension CoreDataPersistentStore {
public var context: NSManagedObjectContext {
container.viewContext
}
public var backgroundContext: NSManagedObjectContext {
container.newBackgroundContext()
}
public var coordinator: NSPersistentStoreCoordinator {
container.persistentStoreCoordinator
}
}
// MARK: Development
extension CoreDataPersistentStore {
public var containerURLs: [URL]? { public var containerURLs: [URL]? {
guard let url = container.persistentStoreDescriptions.first?.url else { guard let url = container.persistentStoreDescriptions.first?.url else {
return nil return nil
@ -104,13 +123,3 @@ public final class CoreDataPersistentStore {
} }
} }
} }
extension CoreDataPersistentStore {
public var context: NSManagedObjectContext {
container.viewContext
}
public var coordinator: NSPersistentStoreCoordinator {
container.persistentStoreCoordinator
}
}

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>. // along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
// //
import CloudKit
import CoreData import CoreData
import Foundation import Foundation
import PassepartoutCore import PassepartoutCore
@ -54,4 +55,15 @@ public final class VPNPersistence {
public func profileRepository() -> ProfileRepository { public func profileRepository() -> ProfileRepository {
CDProfileRepository(store.context) CDProfileRepository(store.context)
} }
// WARNING: this is not running on main actor
public func eraseCloudKitStore(fromContainerWithId containerId: String, zoneId: CKRecordZone.ID) async {
do {
let container = CKContainer(identifier: containerId)
let db = container.privateCloudDatabase
try await db.deleteRecordZone(withID: zoneId)
} catch {
pp_log.error("Unable to erase CloudKit store: \(error)")
}
}
} }