Add support for reading accounts from Element Classic.

This commit is contained in:
Mauro Romito
2025-10-10 16:44:26 +02:00
committed by Doug
parent ac56a0ba43
commit 95b3a8fbb1
18 changed files with 775 additions and 3 deletions
@@ -15,8 +15,9 @@ class AuthenticationService: AuthenticationServiceProtocol {
private var sessionDirectories: SessionDirectories
private let passphrase: String
private let clientFactory: AuthenticationClientFactoryProtocol
private let userSessionStore: UserSessionStoreProtocol
private let classicAppManager: ClassicAppManagerProtocol?
private let clientFactory: AuthenticationClientFactoryProtocol
private let appSettings: AppSettings
private let appHooks: AppHooks
@@ -29,16 +30,31 @@ class AuthenticationService: AuthenticationServiceProtocol {
init(userSessionStore: UserSessionStoreProtocol,
encryptionKeyProvider: EncryptionKeyProviderProtocol,
classicAppManager: ClassicAppManagerProtocol?,
clientFactory: AuthenticationClientFactoryProtocol = AuthenticationClientFactory(),
appSettings: AppSettings,
appHooks: AppHooks) {
sessionDirectories = .init()
passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
self.clientFactory = clientFactory
self.userSessionStore = userSessionStore
self.classicAppManager = classicAppManager
self.clientFactory = clientFactory
self.appSettings = appSettings
self.appHooks = appHooks
do {
if let classicAppManager {
// Just let the app manager log the detected account for now.
_ = try classicAppManager.loadAccounts()
} else {
MXLog.info("Classic App not configured, skipping loadAccounts.")
}
} catch {
// This should show an alert: "We have detected an older version of Element Classic, but no bueno!"
MXLog.error("Failed loading accounts from the Classic app: \(error)")
}
// When updating these, don't forget to update the reset method too.
homeserverSubject = .init(LoginHomeserver(address: appSettings.accountProviders[0], loginMode: .unknown))
flow = .login
@@ -277,6 +293,7 @@ extension AuthenticationService {
static var mock: AuthenticationService {
AuthenticationService(userSessionStore: UserSessionStoreMock(configuration: .init()),
encryptionKeyProvider: EncryptionKeyProvider(),
classicAppManager: nil,
clientFactory: AuthenticationClientFactoryMock(configuration: .init()),
appSettings: ServiceLocator.shared.settings,
appHooks: AppHooks())
@@ -0,0 +1,78 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import CommonCrypto
import Foundation
enum ClassicAppAES {
enum Error: Swift.Error {
case cannotInitializeCryptor
case decryptionFailed(CCCryptorStatus)
}
/// Decrypt data using AES-256 in CTR mode.
/// - Parameters:
/// - data: The data to decrypt
/// - aesKey: The AES decryption key (256-bit)
/// - iv: The initialization vector
/// - Returns: The decrypted data, or nil if decryption fails
/// - Throws: An error if decryption fails
static func decrypt(_ data: Data, aesKey: Data, iv: Data) throws -> Data {
var cryptor: CCCryptorRef?
var status: CCCryptorStatus
// Create the cryptor
status = iv.withUnsafeBytes { ivBytes in
aesKey.withUnsafeBytes { keyBytes in
CCCryptorCreateWithMode(CCOperation(kCCDecrypt),
CCMode(kCCModeCTR),
CCAlgorithm(kCCAlgorithmAES),
CCPadding(ccNoPadding),
ivBytes.baseAddress,
keyBytes.baseAddress,
kCCKeySizeAES256,
nil,
0,
0,
CCModeOptions(kCCModeOptionCTR_BE),
&cryptor)
}
}
guard status == kCCSuccess, let cryptor else {
MXLog.error("Failed to create cryptor: \(status)")
throw Error.cannotInitializeCryptor
}
// Get the output buffer size
let bufferLength = CCCryptorGetOutputLength(cryptor, data.count, false)
var buffer = Data(count: bufferLength)
var outLength = 0
status = data.withUnsafeBytes { dataBytes in
let count = buffer.count
return buffer.withUnsafeMutableBytes { bufferBytes in
CCCryptorUpdate(cryptor,
dataBytes.baseAddress,
data.count,
bufferBytes.baseAddress,
count,
&outLength)
}
}
let releaseStatus = CCCryptorRelease(cryptor)
status = (status == kCCSuccess && releaseStatus == kCCSuccess) ? CCCryptorStatus(kCCSuccess) : status
guard status == kCCSuccess else {
MXLog.error("Decryption failed: \(status)")
throw Error.decryptionFailed(status)
}
return buffer
}
}
@@ -0,0 +1,199 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
class ClassicAppAccountManager {
static let matrixKitFolder = "MatrixKit"
static let kMXKAccountsKey = "accountsV2"
static let kMXFileStoreFolder = "MXFileStore"
static let kMXFileStoreUsersFolder = "users"
static let cryptoStoreFolder = "MXCryptoStore"
let cacheFolder: URL
let iv: Data
let aesKey: Data
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) {
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)
}
func loadAccounts() {
MXLog.info("Loading accounts from Classic app.")
let accountFile = accountFile()
if FileManager.default.fileExists(atPath: accountFile.path(percentEncoded: false)) {
let startDate = Date()
do {
let fileContent = try Data(contentsOf: accountFile, options: [.alwaysMapped, .uncached])
// Decrypt data if encryption method is provided
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 {
MXLog.error("Failed to decode accounts.")
return
}
self.accounts = accounts
MXLog.info("[MXKAccountManager] loadAccounts. \(accounts.count) accounts loaded in \(Date().timeIntervalSince(startDate) * 1000)ms")
} 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
}
}
}
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
}
return ClassicAppAccount(userID: userID,
displayName: users[userID]?.displayName,
serverName: serverName,
cryptoStoreURL: cryptoStoreURL(for: userID))
}
/// The server name extracted from the user's ID.
private func serverName(for userID: String) -> String? {
#warning("Expose a serverName method for this from the SDK?")
let components = userID.components(separatedBy: ":")
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
}
}
@@ -0,0 +1,315 @@
//
// Copyright 2026 Element Creations Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
// Please see LICENSE files in the repository root for full details.
//
import Foundation
struct ClassicAppAccount {
let userID: String
let displayName: String?
let serverName: String
let cryptoStoreURL: URL
}
// 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 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
/// Flag to indicate if the account has been logged out by the homeserver admin.
private(set) var isSoftLogout = false
// MARK: NSCoding
enum Keys {
static let homeserverURL = "homeserverurl" // String?
static let userID = "userid" // String?
static let accessToken = "accesstoken" // String?
static let accessTokenExpiresAt = "accessTokenExpiresAt" // UInt64
static let refreshToken = "refreshToken" // String?
static let identityServerURL = "identityserverurl" // String?
static let identityServerAccessToken = "identityserveraccesstoken" // String?
static let deviceID = "deviceId" // String?
static let allowedCertificate = "allowedCertificate" // Data?
static let threePIDs = "threePIDs" // [MXThirdPartyIdentifier]?
static let device = "device" // MXDevice?
static let antivirusServerURL = "antivirusserverurl" // String?
static let pushGatewayURL = "pushgatewayurl" // String?
static let hasPusherForPushNotifications = "_enablePushNotifications" // Bool
static let hasPusherForPushKitNotifications = "enablePushKitNotifications" // Bool
static let enableInAppNotifications = "enableInAppNotifications" // Bool
static let isDisabled = "disabled" // Bool
static let isSoftLogout = "isSoftLogout" // Bool
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
}
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)
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
}
func encode(with coder: NSCoder) {
fatalError("Not available")
}
}
/// `MXUser` represents a user in Matrix.
class ClassicAppMXUser: NSObject, NSCoding {
/// The user id.
private(set) var userID: String
/// The user display name.
var 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
// MARK: NSCoding
enum Keys {
static let userID = "userId" // String
static let displayName = "displayname" // String?
static let avatarURL = "avatarUrl" // String?
static let statusMessage = "statusMsg" // String?
static let currentlyActive = "currentlyActive" // Bool
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
}
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()
}
func encode(with coder: NSCoder) {
fatalError("Not available")
}
}
@@ -0,0 +1,62 @@
//
// Copyright 2025 New Vector Ltd.
//
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
// Please see LICENSE files in the repository root for full details.
//
import Foundation
import KeychainAccess
import MatrixRustSDK
protocol ClassicAppManagerProtocol {
func loadAccounts() throws -> [ClassicAppAccount]
}
enum ClassicAppManagerError: Error {
case invalidAppGroupIdentifier(String)
case missingAccountKeys
case missingCryptoStorePassphrase
}
final class ClassicAppManager: ClassicAppManagerProtocol {
private enum KeychainKeys: String {
case cryptoSDKStoreKey
case accountIV = "accountIv"
case accountAESKey = "accountAesKey"
}
private let classicAppGroupIdentifier: String
private let keychain: Keychain
init(classicAppGroupIdentifier: String, classicAppKeychainServiceIdentifier: String, classicAppKeychainAccessGroupIdentifier: String) {
self.classicAppGroupIdentifier = classicAppGroupIdentifier
keychain = Keychain(service: classicAppKeychainServiceIdentifier, accessGroup: classicAppKeychainAccessGroupIdentifier)
}
func loadAccounts() throws -> [ClassicAppAccount] {
guard let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: classicAppGroupIdentifier) else {
throw ClassicAppManagerError.invalidAppGroupIdentifier(classicAppGroupIdentifier)
}
guard let accountIV = try keychain.getData(KeychainKeys.accountIV.rawValue),
let accountAESKey = try keychain.getData(KeychainKeys.accountAESKey.rawValue) else {
throw ClassicAppManagerError.missingAccountKeys
}
guard let cryptoStorePassphrase = try keychain.getData(KeychainKeys.cryptoSDKStoreKey.rawValue) else {
throw ClassicAppManagerError.missingCryptoStorePassphrase
}
let accountManager = ClassicAppAccountManager(cacheFolder: url, iv: accountIV, aesKey: accountAESKey)
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
}
}