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 {
static let macBridgeName = "PassepartoutMac.bundle"
}

View File

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

View File

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

View File

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

View File

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

View File

@ -331,8 +331,10 @@
/* MARK: SettingsView */
"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.should_enable_cloud_syncing.caption" = "Sync with iCloud";
"settings.items.erase_cloud_store.caption" = "Erase iCloud store";
"settings.items.donate.caption" = "Make a donation";
/* MARK: AboutView */

View File

@ -921,6 +921,10 @@ internal enum L10n {
/// 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 {
/// 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 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 Add {

View File

@ -23,6 +23,7 @@
// along with Passepartout. If not, see <http://www.gnu.org/licenses/>.
//
import CloudKit
import Combine
import CoreData
import Foundation
@ -69,7 +70,25 @@ public final class CoreDataPersistentStore {
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]? {
guard let url = container.persistentStoreDescriptions.first?.url else {
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/>.
//
import CloudKit
import CoreData
import Foundation
import PassepartoutCore
@ -54,4 +55,15 @@ public final class VPNPersistence {
public func profileRepository() -> ProfileRepository {
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)")
}
}
}