diff --git a/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppAccountManager.swift b/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppAccountManager.swift index 8db50ca58..7c5966a02 100644 --- a/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppAccountManager.swift +++ b/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppAccountManager.swift @@ -8,38 +8,23 @@ import Foundation class ClassicAppAccountManager { - static let matrixKitFolder = "MatrixKit" - static let kMXKAccountsKey = "accountsV2" - static let kMXFileStoreFolder = "MXFileStore" - static let kMXFileStoreUsersFolder = "users" - static let cryptoStoreFolder = "MXCryptoStore" + private let cacheFolder: URL + private let aesKey: Data + private let iv: Data + private let cryptoStorePassphrase: Data - let cacheFolder: URL - let iv: Data - let aesKey: Data + private(set) var accounts: [ClassicAppAccount] = [] - private(set) var accounts: [ClassicAppMXAccount] = [] - private var users: [String: ClassicAppMXUser] = [:] - - var activeAccounts: [ClassicAppAccount] { - accounts - .filter { !$0.isDisabled && !$0.isSoftLogout } - .compactMap(activeAccount) - } - - init(cacheFolder: URL, iv: Data, aesKey: Data) { + init(cacheFolder: URL, aesKey: Data, iv: Data, cryptoStorePassphrase: Data) { self.cacheFolder = cacheFolder - self.iv = iv self.aesKey = aesKey - } - - /// Return the path of the file containing stored MXAccounts array - func accountFile() -> URL { - cacheFolder.appending(component: Self.matrixKitFolder).appending(component: Self.kMXKAccountsKey) + self.iv = iv + self.cryptoStorePassphrase = cryptoStorePassphrase } func loadAccounts() { MXLog.info("Loading accounts from Classic app.") + let accountFile = accountFile() if FileManager.default.fileExists(atPath: accountFile.path(percentEncoded: false)) { let startDate = Date() @@ -51,103 +36,58 @@ class ClassicAppAccountManager { let unciphered = try ClassicAppAES.decrypt(fileContent, aesKey: aesKey, iv: iv) let decoder = NSKeyedUnarchiver(forReadingWith: unciphered) decoder.setClass(ClassicAppMXAccount.self, forClassName: "MXKAccount") - decoder.setClass(ClassicAppMXThirdPartyIdentifier.self, forClassName: "MXThirdPartyIdentifier") - decoder.setClass(ClassicAppMXDevice.self, forClassName: "MXDevice") - guard let accounts = decoder.decodeObject(forKey: "mxAccounts") as? [ClassicAppMXAccount] else { + guard let mxAccounts = decoder.decodeObject(forKey: "mxAccounts") as? [ClassicAppMXAccount] else { MXLog.error("Failed to decode accounts.") return } - self.accounts = accounts + MXLog.info("\(mxAccounts.count) accounts loaded in \(Date().timeIntervalSince(startDate) * 1000)ms.") - MXLog.info("[MXKAccountManager] loadAccounts. \(accounts.count) accounts loaded in \(Date().timeIntervalSince(startDate) * 1000)ms") + accounts = mxAccounts + .filter(\.isActive) + .compactMap(makeAccount) + + MXLog.info("\(mxAccounts.count) active accounts available.") } catch { MXLog.error("Failed to load account file: \(error)") } - - for account in activeAccounts { - if let user = loadUsers([account.userID], forAccount: account.userID).first { - users[user.userID] = user - } - } } + } + + // MARK: - Private + + private func makeAccount(for mxAccount: ClassicAppMXAccount) -> ClassicAppAccount? { + let userID = mxAccount.userID + let user = loadUser(for: mxAccount) - if accounts.isEmpty { - MXLog.info("[MXKAccountManager] loadAccounts. No accounts") - } - } - - /// From `MXCryptoMachineStore` - func cryptoStoreURL(for userID: String) -> URL { - cacheFolder.appending(component: Self.cryptoStoreFolder).appending(component: userID) - } - - var fileStorePath: URL { - cacheFolder.appending(component: Self.kMXFileStoreFolder) - } - - func storePath(for userID: String) -> URL { - fileStorePath.appending(component: userID) - } - - /// This store contains all of the users known to the specific user ID. - func storeUsersPath(for userID: String) -> URL { - storePath(for: userID).appending(component: Self.kMXFileStoreUsersFolder) - } - - func loadUsers(_ userIDs: [String], forAccount accountUserID: String) -> [ClassicAppMXUser] { - // Determine which groups to load based on userIds - var groups: [String: [String]] = [:] - for userID in userIDs { - let groupID = String(userID.hash % 100) - - if groups[groupID] != nil { - groups[groupID]?.append(userID) - } else { - groups[groupID] = [userID] - } - } - - let usersFolder = storeUsersPath(for: accountUserID) - - var loadedUsers: [ClassicAppMXUser] = [] - for group in groups.keys { - autoreleasepool { - let groupFile = usersFolder.appendingPathComponent(group) - - // Load stored users in this group - do { - let fileContent = try Data(contentsOf: groupFile) - - let decoder = NSKeyedUnarchiver(forReadingWith: fileContent) - decoder.setClass(ClassicAppMXUser.self, forClassName: "MXUser") - decoder.setClass(ClassicAppMXUser.self, forClassName: "MXMyUser") - - if let groupUsers = decoder.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as? [String: ClassicAppMXUser] { - let usersToLoad = Set(groups[group] ?? []) - for user in groupUsers.values where usersToLoad.contains(user.userID) { - loadedUsers.append(user) - } - } - } catch { - MXLog.warning("[MXFileStore] Warning: MXFileRoomStore file for users group \(group) has been corrupted") - } - } - } - - return loadedUsers - } - - private func activeAccount(mxAccount: ClassicAppMXAccount) -> ClassicAppAccount? { - guard let userID = mxAccount.credentials.userID, let serverName = serverName(for: userID) else { - return nil - } + guard let serverName = serverName(for: userID) else { return nil } return ClassicAppAccount(userID: userID, - displayName: users[userID]?.displayName, + displayName: user?.displayName, + avatarURL: user?.avatarURL.flatMap(URL.init(string:)), serverName: serverName, - cryptoStoreURL: cryptoStoreURL(for: userID)) + cryptoStoreURL: cryptoStoreURL(for: userID), + cryptoStorePassphrase: cryptoStorePassphrase) + } + + private func loadUser(for mxAccount: ClassicAppMXAccount) -> ClassicAppMXUser? { + let userID = mxAccount.userID + let groupID = String(userID.hash % 100) + let groupFile = storeUsersPath(for: userID).appendingPathComponent(groupID) + + do { + let fileContent = try Data(contentsOf: groupFile) + let decoder = NSKeyedUnarchiver(forReadingWith: fileContent) + decoder.setClass(ClassicAppMXUser.self, forClassName: "MXUser") + decoder.setClass(ClassicAppMXUser.self, forClassName: "MXMyUser") + + let groupUsers = decoder.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as? [String: ClassicAppMXUser] + return groupUsers?[userID] + } catch { + MXLog.warning("Users group \(groupID) file for \(mxAccount.userID) has been corrupted.") + return nil + } } /// The server name extracted from the user's ID. @@ -157,43 +97,35 @@ class ClassicAppAccountManager { guard components.count > 1 else { return nil } return components[1] // Directly use [1] as .last may be the port number. } -} - -// MARK: - Probably not needed - -private extension ClassicAppAccountManager { - func loadUsers(forAccount accountUserID: String) { - let startDate = Date() - var users: [String: ClassicAppMXUser] = [:] - - // Load all users which are distributed in several files - let storeUsersPath = storeUsersPath(for: accountUserID) - let groups = try? FileManager.default.contentsOfDirectory(atPath: storeUsersPath.path(percentEncoded: false)) - - if let groups { - for group in groups { - let groupFile = storeUsersPath.appending(path: group) - - // Load stored users in this group - do { - let fileContent = try Data(contentsOf: groupFile) - - let decoder = NSKeyedUnarchiver(forReadingWith: fileContent) - decoder.setClass(ClassicAppMXUser.self, forClassName: "MXUser") - decoder.setClass(ClassicAppMXUser.self, forClassName: "MXMyUser") - - if let groupUsers = decoder.decodeObject(forKey: NSKeyedArchiveRootObjectKey) as? [String: ClassicAppMXUser] { - // Append them - users.merge(groupUsers) { _, new in new } - } - } catch { - MXLog.error("Failed to load users from group \(group): \(error)") - } - } - } - - MXLog.debug("[MXFileStore] Loaded \(users.count) MXUsers in \(Date().timeIntervalSince(startDate) * 1000)ms") - - self.users = users + + // MARK: - File URLs + + private static let matrixKitFolder = "MatrixKit" + private static let cryptoStoreFolder = "MXCryptoStore" + private static let kMXKAccountsKey = "accountsV2" + private static let kMXFileStoreFolder = "MXFileStore" + private static let kMXFileStoreUsersFolder = "users" + + /// The file URL that contains the app's `MXAccounts` array. + private func accountFile() -> URL { + cacheFolder.appending(component: Self.matrixKitFolder).appending(component: Self.kMXKAccountsKey) + } + + /// The database file URL as defined in `MXCryptoMachineStore`. + private func cryptoStoreURL(for userID: String) -> URL { + cacheFolder.appending(component: Self.cryptoStoreFolder).appending(component: userID) + } + + /// This store contains all of the users known to the specific user ID. + private func storeUsersPath(for userID: String) -> URL { + storePath(for: userID).appending(component: Self.kMXFileStoreUsersFolder) + } + + private func storePath(for userID: String) -> URL { + fileStorePath.appending(component: userID) + } + + private var fileStorePath: URL { + cacheFolder.appending(component: Self.kMXFileStoreFolder) } } diff --git a/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppMXAccount.swift b/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppMXAccount.swift index 76ae8d8ef..bfb82b623 100644 --- a/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppMXAccount.swift +++ b/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppMXAccount.swift @@ -10,59 +10,35 @@ import Foundation struct ClassicAppAccount { let userID: String let displayName: String? + let avatarURL: URL? let serverName: String let cryptoStoreURL: URL + let cryptoStorePassphrase: Data } // MARK: NSCoding Types class ClassicAppMXAccount: NSObject, NSCoding { - /// The account's credentials: homeserver, access token, user ID. - private(set) var credentials: Credentials - /// The identity server URL. - var identityServerURL: String - /// The antivirus server URL, if any (nil by default). - /// Set a non-null url to configure the antivirus scanner use. - var antivirusServerURL: String? - /// The Push Gateway URL used to send event notifications to (nil by default). - /// This URL should be over HTTPS and never over HTTP. - var pushGatewayURL: String? - /// The 3PIDs linked to this account. - /// [self load3PIDs] must be called to update the property. - private(set) var threePIDs: [ClassicAppMXThirdPartyIdentifier]? - /// The account user's device. - /// [self loadDeviceInformation] must be called to update the property. - private(set) var device: ClassicAppMXDevice? - /// Transient information storage. - private(set) var others = NSMutableDictionary() - /// Flag to indicate that an APNS pusher has been set on the homeserver for this device. - private(set) var hasPusherForPushNotifications = false + /// The obtained user ID. + var userID: String + /// The homeserver url (ex: "https://matrix.org"). + var homeserver: String? - /// The Push notification activity (based on PushKit) for this account. - /// YES when Push is turned on (locally available and enabled homeserver side). - var isPushKitNotificationActive: Bool { - // This would typically have custom getter logic - hasPusherForPushKitNotifications - } - - /// Flag to indicate that a PushKit pusher has been set on the homeserver for this device. - private(set) var hasPusherForPushKitNotifications = false - /// Enable In-App notifications based on Remote notifications rules. - /// NO by default. - var enableInAppNotifications = false /// Disable the account without logging out (NO by default). /// /// A matrix session is automatically opened for the account when this property is toggled from YES to NO. /// The session is closed when this property is set to YES. - var isDisabled = false - /// Flag indicating if the end user has been warned about encryption and its limitations. - var isWarnedAboutEncryption = false - + let isDisabled: Bool /// Flag to indicate if the account has been logged out by the homeserver admin. - private(set) var isSoftLogout = false + let isSoftLogout: Bool + + /// Whether or not the account is considered active. + var isActive: Bool { + !isDisabled && !isSoftLogout + } // MARK: NSCoding - + enum Keys { static let homeserverURL = "homeserverurl" // String? static let userID = "userid" // String? @@ -85,175 +61,20 @@ class ClassicAppMXAccount: NSObject, NSCoding { static let isWarnedAboutEncryption = "warnedAboutEncryption" // Bool static let others = "others" // NSMutableDictionary } - + required init?(coder: NSCoder) { - let homeserverURL = coder.decodeObject(forKey: Keys.homeserverURL) as? String - let userID = coder.decodeObject(forKey: Keys.userID) as? String - let accessToken = coder.decodeObject(forKey: Keys.accessToken) as? String - - credentials = Credentials(homeserver: homeserverURL, - userID: userID, - accessToken: accessToken) - - credentials.accessTokenExpiresAt = UInt64(coder.decodeInt64(forKey: Keys.accessTokenExpiresAt)) - credentials.refreshToken = coder.decodeObject(forKey: Keys.refreshToken) as? String - credentials.identityServer = coder.decodeObject(forKey: Keys.identityServerURL) as? String - credentials.identityServerAccessToken = coder.decodeObject(forKey: Keys.identityServerAccessToken) as? String - credentials.deviceID = coder.decodeObject(forKey: Keys.deviceID) as? String - credentials.allowedCertificate = coder.decodeObject(forKey: Keys.allowedCertificate) as? Data - - identityServerURL = credentials.identityServer ?? "" - - super.init() - - if let threePIDs = coder.decodeObject(forKey: Keys.threePIDs) as? [ClassicAppMXThirdPartyIdentifier] { - self.threePIDs = threePIDs + guard let userID = coder.decodeObject(forKey: Keys.userID) as? String, + let homeserver = coder.decodeObject(forKey: Keys.homeserverURL) as? String else { + return nil } - if let device = coder.decodeObject(forKey: Keys.device) as? ClassicAppMXDevice { - self.device = device - } - - if let antivirusServerURL = coder.decodeObject(forKey: Keys.antivirusServerURL) as? String { - self.antivirusServerURL = antivirusServerURL - } - - if let pushGatewayURL = coder.decodeObject(forKey: Keys.pushGatewayURL) as? String { - self.pushGatewayURL = pushGatewayURL - } - - hasPusherForPushNotifications = coder.decodeBool(forKey: Keys.hasPusherForPushNotifications) - hasPusherForPushKitNotifications = coder.decodeBool(forKey: Keys.hasPusherForPushKitNotifications) - enableInAppNotifications = coder.decodeBool(forKey: Keys.enableInAppNotifications) + self.userID = userID + self.homeserver = homeserver isDisabled = coder.decodeBool(forKey: Keys.isDisabled) isSoftLogout = coder.decodeBool(forKey: Keys.isSoftLogout) - isWarnedAboutEncryption = coder.decodeBool(forKey: Keys.isWarnedAboutEncryption) - - if let others = coder.decodeObject(forKey: Keys.others) as? NSMutableDictionary { - self.others = others - } - } - - func encode(with coder: NSCoder) { - fatalError("Not available") - } - - /// The `MXCredentials` struct contains credentials to communicate with the Matrix - /// Client-Server API. - struct Credentials { - /// The homeserver url (ex: "https://matrix.org"). - var homeserver: String? - /// The identity server url (ex: "https://vector.im"). - var identityServer: String? - /// The obtained user ID. - var userID: String? - /// The access token to create a MXRestClient - var accessToken: String? - /// The timestamp in milliseconds for when the access token will expire - var accessTokenExpiresAt: UInt64 = 0 - /// The refresh token, which can be used to obtain new access tokens. (optional) - var refreshToken: String? - /// The access token to create a MXIdentityServerRestClient - var identityServerAccessToken: String? - /// The device ID. - var deviceID: String? - /// The server certificate trusted by the user (nil when the server is trusted by the device). - var allowedCertificate: Data? - /// The ignored server certificate (set when the user ignores a certificate change). - var ignoredCertificate: Data? - /// Additional data received during login process - var loginOthers: [String: Any]? - - init(homeserver: String?, userID: String?, accessToken: String?) { - self.homeserver = homeserver - self.userID = userID - self.accessToken = accessToken - } - } -} - -/// `MXThirdPartyIdentifier` represents the response to /account/3pid GET request. -class ClassicAppMXThirdPartyIdentifier: NSObject, NSCoding { - /// The medium of the third party identifier. - var medium: String - /// The third party identifier address. - var address: String - /// The timestamp in milliseconds when this 3PID has been validated. - var validatedAt: UInt64 - /// The timestamp in milliseconds when this 3PID has been added to the user account. - var addedAt: UInt64 - - // MARK: NSCoding - - enum Keys { - static let medium = "medium" // String - static let address = "address" // String - static let validatedAt = "validatedAt" // NSNumber?.uint64Value - static let addedAt = "addedAt" // NSNumber?.uint64Value - } - - required init?(coder aDecoder: NSCoder) { - guard let medium = aDecoder.decodeObject(forKey: Keys.medium) as? String, - let address = aDecoder.decodeObject(forKey: Keys.address) as? String else { - return nil - } - - self.medium = medium - self.address = address - - if let validatedAtNumber = aDecoder.decodeObject(forKey: Keys.validatedAt) as? NSNumber { - validatedAt = validatedAtNumber.uint64Value - } else { - validatedAt = 0 - } - - if let addedAtNumber = aDecoder.decodeObject(forKey: Keys.addedAt) as? NSNumber { - addedAt = addedAtNumber.uint64Value - } else { - addedAt = 0 - } - } - - func encode(with coder: NSCoder) { - fatalError("Not available") - } -} - -/// `MXDevice` represents a device of the current user. -class ClassicAppMXDevice: NSObject, NSCoding { - /// A unique identifier of the device. - var deviceID: String - /// The display name set by the user for this device. Absent if no name has been set. - var displayName: String? - /// The IP address where this device was last seen. (May be a few minutes out of date, for efficiency reasons). - var lastSeenIP: String? - /// The timestamp (in milliseconds since the unix epoch) when this devices was last seen. (May be a few minutes out of date, for efficiency reasons). - var lastSeenTimestamp: UInt64 - /// The latest recorded user agent for the device. - var lastSeenUserAgent: String? - - // MARK: NSCoding - - enum Keys { - static let deviceID = "device_id" // String - static let displayName = "display_name" // String? - static let lastSeenIP = "last_seen_ip" // String? - static let lastSeenTimestamp = "last_seen_ts" // NSNumber?.uint64Value - static let lastSeenUserAgent = "org.matrix.msc3852.last_seen_user_agent" // String? - } - - required init?(coder aDecoder: NSCoder) { - guard let deviceID = aDecoder.decodeObject(forKey: Keys.deviceID) as? String else { - return nil - } - - self.deviceID = deviceID - displayName = aDecoder.decodeObject(forKey: Keys.displayName) as? String - lastSeenIP = aDecoder.decodeObject(forKey: Keys.lastSeenIP) as? String - lastSeenTimestamp = (aDecoder.decodeObject(forKey: Keys.lastSeenTimestamp) as? NSNumber)?.uint64Value ?? 0 - lastSeenUserAgent = aDecoder.decodeObject(forKey: Keys.lastSeenUserAgent) as? String + super.init() } func encode(with coder: NSCoder) { @@ -264,25 +85,14 @@ class ClassicAppMXDevice: NSObject, NSCoding { /// `MXUser` represents a user in Matrix. class ClassicAppMXUser: NSObject, NSCoding { /// The user id. - private(set) var userID: String + let userID: String /// The user display name. - var displayName: String? + let displayName: String? /// The url of the user of the avatar. - var avatarURL: String? - /// The user status. - var statusMessage: String? - - /// Whether the user is currently active. - /// If YES, lastActiveAgo is an approximation and "Now" should be shown instead. - private(set) var currentlyActive = false - /// The time in milliseconds since epoch the last activity by the user has - /// been tracked by the home server. - var lastActiveLocalTimestamp: UInt64 = 0 - /// Only when event.originServerTs > latestUpdateTS, we change displayname and avatarUrl. - var latestUpdateTimestamp: UInt64 = 0 + let avatarURL: String? // MARK: NSCoding - + enum Keys { static let userID = "userId" // String static let displayName = "displayname" // String? @@ -292,7 +102,7 @@ class ClassicAppMXUser: NSObject, NSCoding { static let lastActiveLocalTimestamp = "lastActiveLocalTS" // UInt64 static let latestUpdateTimestamp = "latestUpdateTS" // UInt64 } - + required init?(coder aDecoder: NSCoder) { guard let userID = aDecoder.decodeObject(forKey: Keys.userID) as? String else { return nil @@ -301,10 +111,6 @@ class ClassicAppMXUser: NSObject, NSCoding { self.userID = userID displayName = aDecoder.decodeObject(forKey: Keys.displayName) as? String avatarURL = aDecoder.decodeObject(forKey: Keys.avatarURL) as? String - statusMessage = aDecoder.decodeObject(forKey: Keys.statusMessage) as? String - currentlyActive = aDecoder.decodeBool(forKey: Keys.currentlyActive) - // lastActiveLocalTimestamp = UInt64(aDecoder.decodeInt64(forKey: Keys.lastActiveLocalTimestamp)) - // latestUpdateTimestamp = UInt64(aDecoder.decodeInt64(forKey: Keys.latestUpdateTimestamp)) super.init() } diff --git a/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppManager.swift b/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppManager.swift index 48bf1de66..ce72c1ec8 100644 --- a/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppManager.swift +++ b/ElementX/Sources/Services/Authentication/ClassicApp/ClassicAppManager.swift @@ -48,15 +48,11 @@ final class ClassicAppManager: ClassicAppManagerProtocol { throw ClassicAppManagerError.missingCryptoStorePassphrase } - let accountManager = ClassicAppAccountManager(cacheFolder: url, iv: accountIV, aesKey: accountAESKey) + let accountManager = ClassicAppAccountManager(cacheFolder: url, + aesKey: accountAESKey, + iv: accountIV, + cryptoStorePassphrase: cryptoStorePassphrase) accountManager.loadAccounts() - let activeAccounts = accountManager.activeAccounts - - MXLog.info("Loaded \(accountManager.accounts.count) accounts") - MXLog.verbose("Loaded accounts: \(accountManager.accounts.compactMap(\.credentials.userID).formatted(.list(type: .and)))") - MXLog.info("Found \(activeAccounts.count) active accounts") - MXLog.verbose("Active accounts: \(activeAccounts.compactMap(\.userID).formatted(.list(type: .and)))") - - return activeAccounts + return accountManager.accounts } }