From 51ff6ef915d2e6562fcc415d87ed5cca9c2c6ea5 Mon Sep 17 00:00:00 2001 From: Narasimha-sc <166327228+Narasimha-sc@users.noreply.github.com> Date: Thu, 1 Oct 2026 22:59:28 +0000 Subject: [PATCH] ios: check database encryption for backup at launch, re-apply backup rule after restoring database backup --- apps/ios/Shared/Views/Database/DatabaseErrorView.swift | 1 + apps/ios/Shared/Views/Database/DatabaseView.swift | 3 ++- apps/ios/spec/database.md | 2 +- plans/ios-icloud-backup.md | 4 ++-- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift index f7f253a617..36da0d4d5a 100644 --- a/apps/ios/Shared/Views/Database/DatabaseErrorView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseErrorView.swift @@ -263,6 +263,7 @@ struct DatabaseErrorView: View { private func restoreDb() { do { try restoreBackup() + updateAppDataBackup() showRestoreDbButton = false encryptionStartedDefault.set(false) } catch { diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index b6f53f9955..4dbfcf2e6c 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -62,7 +62,8 @@ func updateAppDataBackup() { @MainActor func updateAppDataBackupOnLaunch() { if UIApplication.shared.isProtectedDataAvailable { - excludeAppDataFromBackup(!iCloudBackupDefault.get() || dbContainerGroupDefault.get() != .group || initialRandomDBPassphraseGroupDefault.get()) + let dbEncrypted = !storeDBPassphraseGroupDefault.get() || kcDatabasePassword.get()?.isEmpty == false + excludeAppDataFromBackup(!iCloudBackupDefault.get() || iCloudBackupBlock(dbEncrypted) != nil) } } diff --git a/apps/ios/spec/database.md b/apps/ios/spec/database.md index 39e14e0036..12cf9cc764 100644 --- a/apps/ios/spec/database.md +++ b/apps/ios/spec/database.md @@ -80,7 +80,7 @@ The container choice is stored in `dbContainerGroupDefault` (`GroupDefaults`). The app group container is included in backup when the "Enable iCloud backup" toggle in "Database passphrase & export" is on (`DEFAULT_ICLOUD_BACKUP`, on by default) and [`iCloudBackupBlock`](../Shared/Views/Database/DatabaseView.swift) returns `nil`: the database is in the app group container, encrypted, and its passphrase was set by the user, not the initial random one. Otherwise the toggle is disabled and shown off, and its footer states the reason. The keychain passphrase is stored with `kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, so on another device the user has to enter it after a restore. -[`updateAppDataBackup()`](../Shared/Views/Database/DatabaseView.swift) applies the rule in [`initializeChat`](../Shared/Model/SimpleXAPI.swift) after the database is opened and the file paths are set (the core re-creates `temp_files` then), after a passphrase change in [`DatabaseEncryptionView`](../Shared/Views/Database/DatabaseEncryptionView.swift) (the core makes new `.bak` copies), and when the toggle changes. [`prepareForLaunch()`](../Shared/AppDelegate.swift#L124-L127) applies it with `updateAppDataBackupOnLaunch()` without the encryption check, as the database is not open yet; while protected data is unavailable it does nothing, keeping the last applied state. +[`updateAppDataBackup()`](../Shared/Views/Database/DatabaseView.swift) applies the rule in [`initializeChat`](../Shared/Model/SimpleXAPI.swift) after the database is opened and the file paths are set (the core re-creates `temp_files` then), after a passphrase change in [`DatabaseEncryptionView`](../Shared/Views/Database/DatabaseEncryptionView.swift) (the core makes new `.bak` copies), after restoring the database backup in [`DatabaseErrorView`](../Shared/Views/Database/DatabaseErrorView.swift), and when the toggle changes. [`prepareForLaunch()`](../Shared/AppDelegate.swift#L124-L127) applies it with `updateAppDataBackupOnLaunch()` before the database is opened, treating it as encrypted when its passphrase is not stored in the keychain (it was set by the user) or a key is stored there, as `chatMigrateInit` opens the database with the stored key (importing an archive removes it, so an unencrypted imported database is excluded); while protected data is unavailable it does nothing, keeping the last applied state. Accepted gaps: after deleting or importing a database, the previous decision stays until the app restarts; `.bak` copies made during a passphrase change or migration, and `temp_files` re-created by the NSE or SE, are excluded only at the next update; files in `app_files` are backed up as stored (unencrypted when "Encrypt local files" is off). diff --git a/plans/ios-icloud-backup.md b/plans/ios-icloud-backup.md index a0d48d401b..059b746014 100644 --- a/plans/ios-icloud-backup.md +++ b/plans/ios-icloud-backup.md @@ -22,9 +22,9 @@ The app group container is included in backup when the toggle is on and `iCloudB | the database is not encrypted | Database is not encrypted. Set passphrase to enable iCloud backup. | | the database uses the initial random passphrase | Database is encrypted using a random passphrase. Set passphrase to enable iCloud backup. | -`excludeAppDataFromBackup(_:)` always excludes the Documents directory (legacy database, exported archives, migration files), `temp_files` (decrypted copies of files) and the `.bak` copies of the database (made by the core on passphrase changes and migrations, possibly unencrypted or with an old passphrase). It clears the attribute on the database files, because restoring a backup in `DatabaseErrorView` copies them from the excluded `.bak` files with `FileManager.copyItem`, which keeps the attribute. +`excludeAppDataFromBackup(_:)` always excludes the Documents directory (legacy database, exported archives, migration files), `temp_files` (decrypted copies of files) and the `.bak` copies of the database (made by the core on passphrase changes and migrations, possibly unencrypted or with an old passphrase). It clears the attribute on the database files, because restoring a backup in `DatabaseErrorView` copies them from the excluded `.bak` files with `FileManager.copyItem`, which keeps the attribute; the rule is applied right after the restore. -The rule is applied when the database is opened (after the file paths are set, as the core re-creates `temp_files`), after a passphrase change, when the toggle changes, and at launch from the stored flags (skipped while the device is locked, as they cannot be read). +The rule is applied when the database is opened (after the file paths are set, as the core re-creates `temp_files`), after a passphrase change, when the toggle changes, and at launch, before the database is opened: it is treated as encrypted when the passphrase is not stored in the keychain (it was set by the user) or a key is stored there, as `chatMigrateInit` opens it with that key — importing an archive removes the stored key, so an unencrypted import stays excluded even if the database then fails to open. This is skipped while the device is locked, as the stored flags cannot be read. ## Accepted gaps