From 0966e75492fc94c9013d85f38ea5b35f707b0777 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Thu, 5 Mar 2026 08:53:53 +0100 Subject: [PATCH] Encrypt sqlite data on disk with key stored in Keychain. When signing in initially a random passphrase is generated that is stored in the Keychain together with the session data. This prevents cached data to be extracted from disk without having access to the Keychain item. --- Mactrix/Models/HomeserverLogin.swift | 6 +- Mactrix/Models/MatrixClient.swift | 122 ++++++------------ .../Models/MatrixClientSessionDelegate.swift | 6 +- Mactrix/Models/UserSession.swift | 62 +++++++++ Mactrix/Models/WindowState.swift | 10 ++ 5 files changed, 122 insertions(+), 84 deletions(-) create mode 100644 Mactrix/Models/UserSession.swift diff --git a/Mactrix/Models/HomeserverLogin.swift b/Mactrix/Models/HomeserverLogin.swift index d79fc7d..1f4852d 100644 --- a/Mactrix/Models/HomeserverLogin.swift +++ b/Mactrix/Models/HomeserverLogin.swift @@ -6,11 +6,13 @@ import SwiftUI struct HomeserverLogin { let storeID: String + let storePassphrase: String let unauthenticatedClient: ClientProtocol let loginDetails: HomeserverLoginDetailsProtocol - init(storeID: String, unauthenticatedClient: ClientProtocol, loginDetails: HomeserverLoginDetailsProtocol) { + init(storeID: String, storePassphrase: String, unauthenticatedClient: ClientProtocol, loginDetails: HomeserverLoginDetailsProtocol) { self.storeID = storeID + self.storePassphrase = storePassphrase self.unauthenticatedClient = unauthenticatedClient self.loginDetails = loginDetails } @@ -46,7 +48,7 @@ struct HomeserverLogin { @MainActor fileprivate func onSuccessfullLogin() async throws -> MatrixClient { - let matrixClient = await MatrixClient(storeID: storeID, client: unauthenticatedClient) + let matrixClient = await MatrixClient(storeID: storeID, storePassphrase: storePassphrase, client: unauthenticatedClient) let userSession = try matrixClient.userSession() try userSession.saveUserToKeychain() diff --git a/Mactrix/Models/MatrixClient.swift b/Mactrix/Models/MatrixClient.swift index 8988476..d068729 100644 --- a/Mactrix/Models/MatrixClient.swift +++ b/Mactrix/Models/MatrixClient.swift @@ -3,73 +3,16 @@ import Foundation import KeychainAccess import MatrixRustSDK import OSLog +import Security import SwiftUI import UI import UniformTypeIdentifiers import Utils -struct UserSession: Codable { - let accessToken: String - let refreshToken: String? - let userID: String - let deviceID: String - let homeserverURL: String - let oidcData: String? - let storeID: String - - init(session: Session, storeID: String) { - accessToken = session.accessToken - refreshToken = session.refreshToken - userID = session.userId - deviceID = session.deviceId - homeserverURL = session.homeserverUrl - oidcData = session.oidcData - self.storeID = storeID - } - - var session: Session { - Session(accessToken: accessToken, - refreshToken: refreshToken, - userId: userID, - deviceId: deviceID, - homeserverUrl: homeserverURL, - oidcData: oidcData, - slidingSyncVersion: .native) - } - - fileprivate static var keychainKey: String { "UserSession" } - - func saveUserToKeychain() throws { - let keychainData = try JSONEncoder().encode(self) - let keychain = Keychain(service: applicationID) - try keychain.set(keychainData, key: Self.keychainKey) - } - - static func loadUserFromKeychain() throws -> Self? { - Logger.matrixClient.debug("Load user from keychain") - /* #if DEBUG - if true { - return try JSONDecoder().decode(Self.self, from: DevSecrets.matrixSession.data(using: .utf8)!) - } - #endif */ - let keychain = Keychain(service: applicationID) - guard let keychainData = try keychain.getData(keychainKey) else { return nil } - return try JSONDecoder().decode(Self.self, from: keychainData) - } -} - -enum SelectedScreen { - case joinedRoom(timeline: LiveTimeline) - case loadMatrixUrl(_ url: Utils.MatrixUriScheme) - case previewRoom(_ room: RoomPreview) - case user(profile: UserProfile) - case newRoom - case none -} - @MainActor @Observable class MatrixClient { let storeID: String + let storePassphrase: String var client: ClientProtocol! var rooms: [SidebarRoom] = [] @@ -81,64 +24,83 @@ class MatrixClient { let notifications: MatrixNotifications = .init() - init(storeID: String, clientBuilder: ClientBuilderProtocol) async throws { - self.storeID = storeID + init(userSession: UserSession) async throws { + storeID = userSession.storeID + storePassphrase = userSession.storePassphrase - client = try await clientBuilder + client = try await Self.clientBuilder(homeServer: userSession.homeserverURL, storeId: storeID, storePassphrase: storePassphrase) .enableOidcRefreshLock() .setSessionDelegate(sessionDelegate: self) .build() spaceService = LiveSpaceService(spaceService: await client.spaceService()) - clientDelegateHandle = try? client.setDelegate(delegate: self) } - init(storeID: String, client: ClientProtocol) async { + init(storeID: String, storePassphrase: String, client: ClientProtocol) async { self.storeID = storeID + self.storePassphrase = storePassphrase self.client = client - spaceService = LiveSpaceService(spaceService: await client.spaceService()) + spaceService = LiveSpaceService(spaceService: await client.spaceService()) clientDelegateHandle = try? self.client.setDelegate(delegate: self) } func userSession() throws -> UserSession { - return try UserSession(session: client.session(), storeID: storeID) + return try UserSession(session: client.session(), storeID: storeID, storePassphrase: storePassphrase) } - static func clientBuilder(homeServer: String, storeId: String) -> ClientBuilder { + static func clientBuilder(homeServer: String, storeId: String, storePassphrase: String) -> ClientBuilder { + let sqliteConfig = SqliteStoreBuilder( + dataPath: URL.sessionData(for: storeId).path(percentEncoded: false), + cachePath: URL.sessionCaches(for: storeId).path(percentEncoded: false) + ) + .passphrase(passphrase: storePassphrase) + return ClientBuilder() .serverNameOrHomeserverUrl(serverNameOrUrl: homeServer) - .sessionPaths(dataPath: URL.sessionData(for: storeId).path(percentEncoded: false), - cachePath: URL.sessionCaches(for: storeId).path(percentEncoded: false)) + .sqliteStore(config: sqliteConfig) .slidingSyncVersionBuilder(versionBuilder: .discoverNative) .threadsEnabled(enabled: true, threadSubscriptions: true) .autoEnableCrossSigning(autoEnableCrossSigning: true) .userAgent(userAgent: "Mactrix macOS") } + struct SecureRandomBytesError: LocalizedError { + let code: Int32 + + var errorDescription: String? { + "Failed to generate secure bytes with status code \(code)" + } + } + + static func generateStorePassphrase() throws -> String { + var result = [UInt8](repeating: UInt8.random(in: 0 ..< UInt8.max), count: 32) + let status = unsafe SecRandomCopyBytes(kSecRandomDefault, result.count, &result) + if status != errSecSuccess { + throw SecureRandomBytesError(code: status) + } + + return Data(result).base64EncodedString() + } + static func loginDetails(homeServer: String) async throws -> HomeserverLogin { let storeID = UUID().uuidString + let storePassphrase = try Self.generateStorePassphrase() - let client = try await Self.clientBuilder(homeServer: homeServer, storeId: storeID).build() + let client = try await Self.clientBuilder(homeServer: homeServer, storeId: storeID, storePassphrase: storePassphrase).build() let details = await client.homeserverLoginDetails() - return HomeserverLogin(storeID: storeID, unauthenticatedClient: client, loginDetails: details) + return HomeserverLogin(storeID: storeID, storePassphrase: storePassphrase, unauthenticatedClient: client, loginDetails: details) } static func attemptRestore() async throws -> MatrixClient? { guard let userSession = try UserSession.loadUserFromKeychain() else { return nil } - let session = userSession.session - let storeID = userSession.storeID - - // Build a client for the homeserver. - let clientBuilder = Self.clientBuilder(homeServer: session.homeserverUrl, storeId: storeID) - - let matrixClient = try await MatrixClient(storeID: storeID, clientBuilder: clientBuilder) + let matrixClient = try await MatrixClient(userSession: userSession) // Restore the client using the session. - try await matrixClient.client.restoreSession(session: session) + try await matrixClient.client.restoreSession(session: userSession.session) return matrixClient } @@ -272,7 +234,7 @@ extension MatrixClient: MatrixRustSDK.ClientSessionDelegate { nonisolated func saveSessionInKeychain(session: MatrixRustSDK.Session) { Logger.matrixClient.debug("client session delegate: save session in keychain") do { - try UserSession(session: session, storeID: storeID).saveUserToKeychain() + try UserSession(session: session, storeID: storeID, storePassphrase: storePassphrase).saveUserToKeychain() } catch { Logger.matrixClient.error("failed to save session in keychain: \(error)") } diff --git a/Mactrix/Models/MatrixClientSessionDelegate.swift b/Mactrix/Models/MatrixClientSessionDelegate.swift index e0db0e3..0ba99a7 100644 --- a/Mactrix/Models/MatrixClientSessionDelegate.swift +++ b/Mactrix/Models/MatrixClientSessionDelegate.swift @@ -9,9 +9,11 @@ import Utils final class MatrixClientSessionDelegate: MatrixRustSDK.ClientSessionDelegate { let storeID: String + let storePassphrase: String - init(storeID: String) { + init(storeID: String, storePassphrase: String) { self.storeID = storeID + self.storePassphrase = storePassphrase } func retrieveSessionFromKeychain(userId: String) throws -> MatrixRustSDK.Session { @@ -35,7 +37,7 @@ final class MatrixClientSessionDelegate: MatrixRustSDK.ClientSessionDelegate { func saveSessionInKeychain(session: MatrixRustSDK.Session) { Logger.matrixClient.debug("client session delegate: save session in keychain") do { - try UserSession(session: session, storeID: storeID).saveUserToKeychain() + try UserSession(session: session, storeID: storeID, storePassphrase: storePassphrase).saveUserToKeychain() } catch { Logger.matrixClient.error("failed to save session in keychain: \(error)") } diff --git a/Mactrix/Models/UserSession.swift b/Mactrix/Models/UserSession.swift new file mode 100644 index 0000000..8c9d1a7 --- /dev/null +++ b/Mactrix/Models/UserSession.swift @@ -0,0 +1,62 @@ +import AsyncAlgorithms +import Foundation +import KeychainAccess +import MatrixRustSDK +import OSLog +import Security +import SwiftUI +import UI +import UniformTypeIdentifiers +import Utils + +struct UserSession: Codable { + let accessToken: String + let refreshToken: String? + let userID: String + let deviceID: String + let homeserverURL: String + let oidcData: String? + let storeID: String + let storePassphrase: String + + init(session: Session, storeID: String, storePassphrase: String) { + accessToken = session.accessToken + refreshToken = session.refreshToken + userID = session.userId + deviceID = session.deviceId + homeserverURL = session.homeserverUrl + oidcData = session.oidcData + self.storeID = storeID + self.storePassphrase = storePassphrase + } + + var session: Session { + Session(accessToken: accessToken, + refreshToken: refreshToken, + userId: userID, + deviceId: deviceID, + homeserverUrl: homeserverURL, + oidcData: oidcData, + slidingSyncVersion: .native) + } + + fileprivate static var keychainKey: String { "UserSession" } + + func saveUserToKeychain() throws { + let keychainData = try JSONEncoder().encode(self) + let keychain = Keychain(service: applicationID) + try keychain.set(keychainData, key: Self.keychainKey) + } + + static func loadUserFromKeychain() throws -> Self? { + Logger.matrixClient.debug("Load user from keychain") + /* #if DEBUG + if true { + return try JSONDecoder().decode(Self.self, from: DevSecrets.matrixSession.data(using: .utf8)!) + } + #endif */ + let keychain = Keychain(service: applicationID) + guard let keychainData = try keychain.getData(keychainKey) else { return nil } + return try JSONDecoder().decode(Self.self, from: keychainData) + } +} diff --git a/Mactrix/Models/WindowState.swift b/Mactrix/Models/WindowState.swift index c6ac37c..ae6dccc 100644 --- a/Mactrix/Models/WindowState.swift +++ b/Mactrix/Models/WindowState.swift @@ -2,6 +2,7 @@ import Foundation import MatrixRustSDK import OSLog import SwiftUI +import Utils enum InspectorContent: Equatable { case roomInfo @@ -20,6 +21,15 @@ enum SearchDirectResult { case resolvedUser(profile: MatrixRustSDK.UserProfile) } +enum SelectedScreen { + case joinedRoom(timeline: LiveTimeline) + case loadMatrixUrl(_ url: Utils.MatrixUriScheme) + case previewRoom(_ room: RoomPreview) + case user(profile: UserProfile) + case newRoom + case none +} + @MainActor @Observable final class WindowState { var selectedScreen: SelectedScreen = .none