Files
letro-ios/ElementX/Sources/Services/SecureBackup/SecureBackupController.swift
Stefan Ceriu 89eae00479 Switch license to AGPL (#3237)
* Switch license file to AGPL

* Update file copyright headers

* Update the default project file header
2024-09-06 16:34:30 +03:00

240 lines
8.5 KiB
Swift

//
// Copyright 2023, 2024 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only
// Please see LICENSE in the repository root for full details.
//
import Combine
import Foundation
import MatrixRustSDK
class SecureBackupController: SecureBackupControllerProtocol {
private let encryption: Encryption
private let recoveryStateSubject = CurrentValueSubject<SecureBackupRecoveryState, Never>(.unknown)
private let keyBackupStateSubject = CurrentValueSubject<SecureBackupKeyBackupState, Never>(.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<Void, Error>?
var recoveryState: CurrentValuePublisher<SecureBackupRecoveryState, Never> {
recoveryStateSubject.asCurrentValuePublisher()
}
var keyBackupState: CurrentValuePublisher<SecureBackupKeyBackupState, Never> {
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<Void, SecureBackupControllerError> {
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<Void, SecureBackupControllerError> {
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<String, SecureBackupControllerError> {
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<Void, SecureBackupControllerError> {
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<Void, SecureBackupControllerError> {
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)
}
}