// // Copyright 2022 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 Foundation import Kingfisher import MatrixRustSDK class UserSessionStore: UserSessionStoreProtocol { private let keychainController: KeychainControllerProtocol private let backgroundTaskService: BackgroundTaskServiceProtocol /// Whether or not there are sessions in the store. var hasSessions: Bool { !keychainController.restorationTokens().isEmpty } /// The base directory where all session data is stored. let baseDirectory: URL init(backgroundTaskService: BackgroundTaskServiceProtocol) { keychainController = KeychainController(service: .sessions, accessGroup: InfoPlistReader.main.keychainAccessGroupIdentifier) self.backgroundTaskService = backgroundTaskService baseDirectory = .sessionsBaseDirectory MXLog.info("Setup base directory at: \(baseDirectory)") } /// Deletes all data stored in the shared container and keychain func reset() { try? FileManager.default.removeItem(at: baseDirectory) keychainController.removeAllRestorationTokens() } func restoreUserSession() async -> Result { let availableCredentials = keychainController.restorationTokens() guard let credentials = availableCredentials.first else { return .failure(.missingCredentials) } switch await restorePreviousLogin(credentials) { case .success(let clientProxy): return .success(buildUserSessionWithClient(clientProxy)) case .failure(let error): MXLog.error("Failed restoring login with error: \(error)") // On any restoration failure reset the token and restart keychainController.removeAllRestorationTokens() deleteSessionDirectory(for: credentials.userID) return .failure(error) } } func userSession(for client: Client) async -> Result { switch await setupProxyForClient(client) { case .success(let clientProxy): return .success(buildUserSessionWithClient(clientProxy)) case .failure(let error): MXLog.error("Failed creating user session with error: \(error)") return .failure(error) } } func refreshRestorationToken(for userSession: UserSessionProtocol) -> Result { guard let restorationToken = userSession.clientProxy.restorationToken else { return .failure(.failedRefreshingRestoreToken) } keychainController.setRestorationToken(restorationToken, forUsername: userSession.clientProxy.userID) return .success(()) } func logout(userSession: UserSessionProtocol) { let userID = userSession.clientProxy.userID keychainController.removeRestorationTokenForUsername(userID) deleteSessionDirectory(for: userID) } // MARK: - Private private func buildUserSessionWithClient(_ clientProxy: ClientProxyProtocol) -> UserSessionProtocol { let imageCache = ImageCache.onlyInMemory imageCache.memoryStorage.config.keepWhenEnteringBackground = true return UserSession(clientProxy: clientProxy, mediaProvider: MediaProvider(mediaLoader: clientProxy, imageCache: imageCache, backgroundTaskService: backgroundTaskService)) } private func restorePreviousLogin(_ credentials: KeychainCredentials) async -> Result { let builder = ClientBuilder() .basePath(path: baseDirectory.path) .username(username: credentials.userID) .homeserverUrl(url: credentials.restorationToken.session.homeserverUrl) .userAgent(userAgent: UserAgentBuilder.makeASCIIUserAgent() ?? "unknown") .serverVersions(versions: ["v1.0", "v1.1", "v1.2", "v1.3", "v1.4", "v1.5"]) // FIXME: Quick and dirty fix for stopping version requests on startup https://github.com/matrix-org/matrix-rust-sdk/pull/1376 do { let client: Client = try await Task.dispatch(on: .global()) { let client = try builder.build() try client.restoreSession(session: credentials.restorationToken.session) return client } return await setupProxyForClient(client) } catch { MXLog.error("Failed restoring login with error: \(error)") return .failure(.failedRestoringLogin) } } private func setupProxyForClient(_ client: Client) async -> Result { do { let session = try client.session() let userId = try client.userId() keychainController.setRestorationToken(RestorationToken(session: session), forUsername: userId) } catch { MXLog.error("Failed setting up user session with error: \(error)") return .failure(.failedSettingUpSession) } let clientProxy = await ClientProxy(client: client, backgroundTaskService: backgroundTaskService) return .success(clientProxy) } private func deleteSessionDirectory(for userID: String) { // Rust sanitises the user ID replacing invalid characters with an _ let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_") let url = baseDirectory.appendingPathComponent(sanitisedUserID) do { try FileManager.default.removeItem(at: url) } catch { MXLog.failure("Failed deleting the session data: \(error)") } } }