// // Copyright 2023 New Vector Ltd // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // import Combine import Foundation import MatrixRustSDK class SecureBackupController: SecureBackupControllerProtocol { private let encryption: Encryption private let recoveryStateSubject = CurrentValueSubject(.unknown) private let keyBackupStateSubject = CurrentValueSubject(.unknown) // periphery:ignore - retaining purpose private var backupStateListenerTaskHandle: TaskHandle? // periphery:ignore - retaining purpose private var recoveryStateListenerTaskHandle: TaskHandle? // periphery:ignore - auto cancels when reassigned /// Used to dedupe remote backup state requests @CancellableTask private var remoteBackupStateTask: Task? var recoveryState: CurrentValuePublisher { recoveryStateSubject.asCurrentValuePublisher() } var keyBackupState: CurrentValuePublisher { keyBackupStateSubject.asCurrentValuePublisher() } init(encryption: Encryption) { self.encryption = encryption backupStateListenerTaskHandle = encryption.backupStateListener(listener: SecureBackupControllerBackupStateListener { [weak self] state in guard let self else { return } switch state { case .unknown: keyBackupStateSubject.send(.unknown) case .creating: keyBackupStateSubject.send(.enabling) case .enabling: keyBackupStateSubject.send(.enabling) case .resuming: keyBackupStateSubject.send(.enabled) case .enabled: keyBackupStateSubject.send(.enabled) case .downloading: keyBackupStateSubject.send(.enabled) case .disabling: keyBackupStateSubject.send(.disabling) } MXLog.info("Key backup state changed to: \(state), setting local state to \(keyBackupStateSubject.value)") if case .unknown = state { updateBackupStateFromRemote() } }) recoveryStateListenerTaskHandle = encryption.recoveryStateListener(listener: SecureBackupRecoveryStateListener { [weak self] state in guard let self else { return } switch state { case .unknown: recoveryStateSubject.send(.unknown) case .enabled: recoveryStateSubject.send(.enabled) case .disabled: recoveryStateSubject.send(.disabled) case .incomplete: recoveryStateSubject.send(.incomplete) } MXLog.info("Recovery state changed to: \(state), setting local state to \(recoveryStateSubject.value)") }) updateBackupStateFromRemote() } func enable() async -> Result { MXLog.info("Enabling secure backup") do { try await encryption.enableBackups() } catch { MXLog.error("Failed enabling secure backup with error: \(error)") return .failure(.failedEnablingBackup) } return .success(()) } func disable() async -> Result { MXLog.info("Disabling secure backup") do { try await encryption.disableRecovery() } catch { MXLog.error("Failed disabling secure backup with error: \(error)") return .failure(.failedDisablingBackup) } return .success(()) } func generateRecoveryKey() async -> Result { do { guard recoveryState.value == .disabled else { MXLog.info("Resetting recovery key") let key = try await encryption.resetRecoveryKey() return .success(key) } MXLog.info("Enabling recovery") var keyUploadErrored = false let recoveryKey = try await encryption.enableRecovery(waitForBackupsToUpload: false, progressListener: SecureBackupEnableRecoveryProgressListener { [weak self] state in guard let self else { return } switch state { case .starting, .creatingBackup, .creatingRecoveryKey, .backingUp: recoveryStateSubject.send(.settingUp) case .done: recoveryStateSubject.send(.enabled) case .roomKeyUploadError: MXLog.error("Failed enabling recovery: room key upload error") keyUploadErrored = true } }) return keyUploadErrored ? .failure(.failedGeneratingRecoveryKey) : .success(recoveryKey) } catch { MXLog.error("Failed generating recovery key with error: \(error)") return .failure(.failedGeneratingRecoveryKey) } } func confirmRecoveryKey(_ key: String) async -> Result { do { MXLog.info("Confirming recovery key") try await encryption.recover(recoveryKey: key) return .success(()) } catch { MXLog.info("Failed confirming recovery key with error: \(error)") return .failure(.failedConfirmingRecoveryKey) } } func waitForKeyBackupUpload() async -> Result { do { MXLog.info("Waiting for backup upload steady state") try await encryption.waitForBackupUploadSteadyState(progressListener: nil) return .success(()) } catch let error as SteadyStateError { MXLog.error("Failed waiting for backup upload steady state with error: \(error)") switch error { case .BackupDisabled: MXLog.error("Key backup disabled, continuing logout.") return .success(()) case .Connection, .Lagged: MXLog.error("Key backup upload failure: \(error)") return .failure(.failedUploadingForBackup) } } catch { MXLog.error("Unknown key backup upload failure") return .failure(.failedUploadingForBackup) } } // MARK: - Private private func updateBackupStateFromRemote(retry: Bool = true) { remoteBackupStateTask = Task { do { MXLog.info("Checking if backup exists on the server") let backupExists = try await self.encryption.backupExistsOnServer() if Task.isCancelled { return } if !backupExists { keyBackupStateSubject.send(.unknown) } } catch { MXLog.error("Failed retrieving remote backup state with error: \(error)") if retry { updateBackupStateFromRemote(retry: false) } } } } } private final class SecureBackupControllerBackupStateListener: BackupStateListener { private let onUpdateClosure: (BackupState) -> Void init(_ onUpdateClosure: @escaping (BackupState) -> Void) { self.onUpdateClosure = onUpdateClosure } func onUpdate(status: BackupState) { onUpdateClosure(status) } } private final class SecureBackupRecoveryStateListener: RecoveryStateListener { private let onUpdateClosure: (RecoveryState) -> Void init(_ onUpdateClosure: @escaping (RecoveryState) -> Void) { self.onUpdateClosure = onUpdateClosure } func onUpdate(status: RecoveryState) { onUpdateClosure(status) } } private final class SecureBackupEnableRecoveryProgressListener: EnableRecoveryProgressListener { private let onUpdateClosure: (EnableRecoveryProgress) -> Void init(_ onUpdateClosure: @escaping (EnableRecoveryProgress) -> Void) { self.onUpdateClosure = onUpdateClosure } func onUpdate(status: EnableRecoveryProgress) { onUpdateClosure(status) } }