mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-02 21:33:26 +00:00
Update the SDK (#3196)
* Update the SDK. * Fix API breaks on send failures and propagate the new type. * Handle new SDK ShieldState. * Set up the ClientBuilder's `cachePath` option. * Delete the cacheDirectory during logout/clearCache. * Add unit tests for RestorationToken decoding and SessionDirectories generation.
This commit is contained in:
@@ -20,7 +20,7 @@ import MatrixRustSDK
|
||||
|
||||
class AuthenticationService: AuthenticationServiceProtocol {
|
||||
private var client: Client?
|
||||
private var sessionDirectory: URL
|
||||
private var sessionDirectories: SessionDirectories
|
||||
private let passphrase: String
|
||||
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
@@ -31,7 +31,7 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
var homeserver: CurrentValuePublisher<LoginHomeserver, Never> { homeserverSubject.asCurrentValuePublisher() }
|
||||
|
||||
init(userSessionStore: UserSessionStoreProtocol, encryptionKeyProvider: EncryptionKeyProviderProtocol, appSettings: AppSettings, appHooks: AppHooks) {
|
||||
sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString)
|
||||
sessionDirectories = .init()
|
||||
passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
|
||||
self.userSessionStore = userSessionStore
|
||||
self.appSettings = appSettings
|
||||
@@ -149,20 +149,24 @@ class AuthenticationService: AuthenticationServiceProtocol {
|
||||
slidingSyncProxy: appSettings.slidingSyncProxyURL,
|
||||
sessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
appHooks: appHooks)
|
||||
.sessionPath(path: sessionDirectory.path(percentEncoded: false))
|
||||
.sessionPaths(dataPath: sessionDirectories.dataPath,
|
||||
cachePath: sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: passphrase)
|
||||
}
|
||||
|
||||
private func rotateSessionDirectory() {
|
||||
if FileManager.default.directoryExists(at: sessionDirectory) {
|
||||
try? FileManager.default.removeItem(at: sessionDirectory)
|
||||
if FileManager.default.directoryExists(at: sessionDirectories.dataDirectory) {
|
||||
try? FileManager.default.removeItem(at: sessionDirectories.dataDirectory)
|
||||
}
|
||||
if FileManager.default.directoryExists(at: sessionDirectories.cacheDirectory) {
|
||||
try? FileManager.default.removeItem(at: sessionDirectories.cacheDirectory)
|
||||
}
|
||||
|
||||
sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString)
|
||||
sessionDirectories = .init()
|
||||
}
|
||||
|
||||
private func userSession(for client: Client) async -> Result<UserSessionProtocol, AuthenticationServiceError> {
|
||||
switch await userSessionStore.userSession(for: client, sessionDirectory: sessionDirectory, passphrase: passphrase) {
|
||||
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
|
||||
case .success(let clientProxy):
|
||||
return .success(clientProxy)
|
||||
case .failure:
|
||||
|
||||
@@ -120,6 +120,7 @@ class KeychainController: KeychainControllerProtocol {
|
||||
}
|
||||
let restorationToken = RestorationToken(session: session,
|
||||
sessionDirectory: oldToken.sessionDirectory,
|
||||
cacheDirectory: oldToken.cacheDirectory,
|
||||
passphrase: oldToken.passphrase,
|
||||
pusherNotificationClientIdentifier: oldToken.pusherNotificationClientIdentifier)
|
||||
setRestorationToken(restorationToken, forUsername: session.userId)
|
||||
|
||||
@@ -20,7 +20,7 @@ import Foundation
|
||||
import MatrixRustSDK
|
||||
|
||||
final class QRCodeLoginService: QRCodeLoginServiceProtocol {
|
||||
private var sessionDirectory: URL
|
||||
private var sessionDirectories: SessionDirectories
|
||||
private let passphrase: String
|
||||
|
||||
private let userSessionStore: UserSessionStoreProtocol
|
||||
@@ -36,7 +36,7 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol {
|
||||
userSessionStore: UserSessionStoreProtocol,
|
||||
appSettings: AppSettings,
|
||||
appHooks: AppHooks) {
|
||||
sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString)
|
||||
sessionDirectories = .init()
|
||||
passphrase = encryptionKeyProvider.generateKey().base64EncodedString()
|
||||
self.userSessionStore = userSessionStore
|
||||
self.appSettings = appSettings
|
||||
@@ -83,20 +83,24 @@ final class QRCodeLoginService: QRCodeLoginServiceProtocol {
|
||||
slidingSyncProxy: appSettings.slidingSyncProxyURL,
|
||||
sessionDelegate: userSessionStore.clientSessionDelegate,
|
||||
appHooks: appHooks)
|
||||
.sessionPath(path: sessionDirectory.path(percentEncoded: false))
|
||||
.sessionPaths(dataPath: sessionDirectories.dataPath,
|
||||
cachePath: sessionDirectories.cachePath)
|
||||
.passphrase(passphrase: passphrase)
|
||||
}
|
||||
|
||||
private func rotateSessionDirectory() {
|
||||
if FileManager.default.directoryExists(at: sessionDirectory) {
|
||||
try? FileManager.default.removeItem(at: sessionDirectory)
|
||||
if FileManager.default.directoryExists(at: sessionDirectories.dataDirectory) {
|
||||
try? FileManager.default.removeItem(at: sessionDirectories.dataDirectory)
|
||||
}
|
||||
if FileManager.default.directoryExists(at: sessionDirectories.cacheDirectory) {
|
||||
try? FileManager.default.removeItem(at: sessionDirectories.cacheDirectory)
|
||||
}
|
||||
|
||||
sessionDirectory = .sessionsBaseDirectory.appending(component: UUID().uuidString)
|
||||
sessionDirectories = .init()
|
||||
}
|
||||
|
||||
private func userSession(for client: Client) async -> Result<UserSessionProtocol, QRCodeLoginServiceError> {
|
||||
switch await userSessionStore.userSession(for: client, sessionDirectory: sessionDirectory, passphrase: passphrase) {
|
||||
switch await userSessionStore.userSession(for: client, sessionDirectories: sessionDirectories, passphrase: passphrase) {
|
||||
case .success(let session):
|
||||
return .success(session)
|
||||
case .failure(let error):
|
||||
|
||||
@@ -28,6 +28,7 @@ enum EncryptionAuthenticity: Hashable {
|
||||
case unknownDevice(color: Color)
|
||||
case unsignedDevice(color: Color)
|
||||
case unverifiedIdentity(color: Color)
|
||||
case previouslyVerified(color: Color)
|
||||
case sentInClear(color: Color)
|
||||
|
||||
var message: String {
|
||||
@@ -40,6 +41,8 @@ enum EncryptionAuthenticity: Hashable {
|
||||
L10n.eventShieldReasonUnsignedDevice
|
||||
case .unverifiedIdentity:
|
||||
L10n.eventShieldReasonUnverifiedIdentity
|
||||
case .previouslyVerified:
|
||||
L10n.eventShieldReasonPreviouslyVerified
|
||||
case .sentInClear:
|
||||
L10n.eventShieldReasonSentInClear
|
||||
}
|
||||
@@ -51,6 +54,7 @@ enum EncryptionAuthenticity: Hashable {
|
||||
.unknownDevice(let color),
|
||||
.unsignedDevice(let color),
|
||||
.unverifiedIdentity(let color),
|
||||
.previouslyVerified(let color),
|
||||
.sentInClear(let color):
|
||||
color
|
||||
}
|
||||
@@ -59,7 +63,7 @@ enum EncryptionAuthenticity: Hashable {
|
||||
var icon: KeyPath<CompoundIcons, Image> {
|
||||
switch self {
|
||||
case .notGuaranteed: \.info
|
||||
case .unknownDevice, .unsignedDevice, .unverifiedIdentity: \.helpSolid
|
||||
case .unknownDevice, .unsignedDevice, .unverifiedIdentity, .previouslyVerified: \.helpSolid
|
||||
case .sentInClear: \.lockOff
|
||||
}
|
||||
}
|
||||
@@ -87,6 +91,8 @@ extension EncryptionAuthenticity {
|
||||
self = .unsignedDevice(color: color)
|
||||
case .unverifiedIdentity:
|
||||
self = .unverifiedIdentity(color: color)
|
||||
case .previouslyVerified:
|
||||
self = .previouslyVerified(color: color)
|
||||
case .sentInClear:
|
||||
self = .sentInClear(color: color)
|
||||
}
|
||||
|
||||
@@ -68,7 +68,20 @@ enum TimelineItemProxy {
|
||||
enum TimelineItemDeliveryStatus: Hashable {
|
||||
case sending
|
||||
case sent
|
||||
case sendingFailed
|
||||
case sendingFailed(SendFailureReason)
|
||||
|
||||
enum SendFailureReason: Hashable {
|
||||
case verifiedUserHasUnsignedDevice(devices: [String: [String]])
|
||||
case verifiedUserChangedIdentity(users: [String])
|
||||
case unknown
|
||||
}
|
||||
|
||||
var isSendingFailed: Bool {
|
||||
switch self {
|
||||
case .sending, .sent: false
|
||||
case .sendingFailed: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A light wrapper around event timeline items returned from Rust.
|
||||
@@ -88,11 +101,15 @@ class EventTimelineItemProxy {
|
||||
|
||||
switch localSendState {
|
||||
case .sendingFailed(_, let isRecoverable):
|
||||
return isRecoverable ? .sending : .sendingFailed
|
||||
return isRecoverable ? .sending : .sendingFailed(.unknown)
|
||||
case .notSentYet:
|
||||
return .sending
|
||||
case .sent:
|
||||
return .sent
|
||||
case .verifiedUserHasUnsignedDevice(devices: let devices):
|
||||
return .sendingFailed(.verifiedUserHasUnsignedDevice(devices: devices))
|
||||
case .verifiedUserChangedIdentity(users: let users):
|
||||
return .sendingFailed(.verifiedUserChangedIdentity(users: users))
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ extension EventBasedTimelineItemProtocol {
|
||||
}
|
||||
|
||||
var hasFailedToSend: Bool {
|
||||
properties.deliveryStatus == .sendingFailed
|
||||
properties.deliveryStatus?.isSendingFailed == true
|
||||
}
|
||||
|
||||
var hasFailedDecryption: Bool {
|
||||
|
||||
@@ -20,6 +20,7 @@ import MatrixRustSDK
|
||||
struct RestorationToken: Equatable {
|
||||
let session: MatrixRustSDK.Session
|
||||
let sessionDirectory: URL
|
||||
let cacheDirectory: URL
|
||||
let passphrase: String?
|
||||
let pusherNotificationClientIdentifier: String?
|
||||
}
|
||||
@@ -29,10 +30,22 @@ extension RestorationToken: Codable {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
let session = try container.decode(MatrixRustSDK.Session.self, forKey: .session)
|
||||
let sessionDirectory = try container.decodeIfPresent(URL.self, forKey: .sessionDirectory)
|
||||
let dataDirectory = try container.decodeIfPresent(URL.self, forKey: .sessionDirectory)
|
||||
let cacheDirectory = try container.decodeIfPresent(URL.self, forKey: .cacheDirectory)
|
||||
|
||||
let sessionDirectories = if let dataDirectory {
|
||||
if let cacheDirectory {
|
||||
SessionDirectories(dataDirectory: dataDirectory, cacheDirectory: cacheDirectory)
|
||||
} else {
|
||||
SessionDirectories(dataDirectory: dataDirectory)
|
||||
}
|
||||
} else {
|
||||
SessionDirectories(userID: session.userId)
|
||||
}
|
||||
|
||||
self = try .init(session: session,
|
||||
sessionDirectory: sessionDirectory ?? .legacySessionDirectory(for: session.userId),
|
||||
sessionDirectory: sessionDirectories.dataDirectory,
|
||||
cacheDirectory: sessionDirectories.cacheDirectory,
|
||||
passphrase: container.decodeIfPresent(String.self, forKey: .passphrase),
|
||||
pusherNotificationClientIdentifier: container.decodeIfPresent(String.self, forKey: .pusherNotificationClientIdentifier))
|
||||
}
|
||||
@@ -66,18 +79,3 @@ extension MatrixRustSDK.Session: Codable {
|
||||
case accessToken, refreshToken, userId, deviceId, homeserverUrl, oidcData, slidingSyncProxy
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Migrations
|
||||
|
||||
private extension URL {
|
||||
/// Gets the store directory of a legacy session that hasn't been migrated to the new token format.
|
||||
///
|
||||
/// This should only be used to fill in the missing value when restoring a token as older versions of
|
||||
/// the SDK set the session directory for us, based on the user's ID. Newer sessions now use a UUID,
|
||||
/// which is generated app side during authentication.
|
||||
static func legacySessionDirectory(for userID: String) -> URL {
|
||||
// Rust sanitises the user ID replacing invalid characters with an _
|
||||
let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_")
|
||||
return .sessionsBaseDirectory.appendingPathComponent(sanitisedUserID)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// Copyright 2024 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
|
||||
|
||||
struct SessionDirectories: Hashable, Codable {
|
||||
let dataDirectory: URL
|
||||
let cacheDirectory: URL
|
||||
|
||||
var dataPath: String { dataDirectory.path(percentEncoded: false) }
|
||||
var cachePath: String { cacheDirectory.path(percentEncoded: false) }
|
||||
}
|
||||
|
||||
extension SessionDirectories {
|
||||
/// Creates a fresh set of session directories for a new user.
|
||||
init() {
|
||||
let sessionDirectoryName = UUID().uuidString
|
||||
dataDirectory = .sessionsBaseDirectory.appending(component: sessionDirectoryName)
|
||||
cacheDirectory = .cachesBaseDirectory.appending(component: sessionDirectoryName)
|
||||
}
|
||||
|
||||
/// Creates the session directories for a user who signed in before the data directory was stored.
|
||||
init(userID: String) {
|
||||
dataDirectory = .legacySessionDirectory(for: userID)
|
||||
cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent)
|
||||
}
|
||||
|
||||
/// Creates the session directories for a user who has a single session directory stored without a separate caches directory.
|
||||
init(dataDirectory: URL) {
|
||||
self.dataDirectory = dataDirectory
|
||||
cacheDirectory = .cachesBaseDirectory.appending(component: dataDirectory.lastPathComponent)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Migrations
|
||||
|
||||
private extension URL {
|
||||
/// Gets the store directory of a legacy session that hasn't been migrated to the new token format.
|
||||
///
|
||||
/// This should only be used to fill in the missing value when restoring a token as older versions of
|
||||
/// the SDK set the session directory for us, based on the user's ID. Newer sessions now use a UUID,
|
||||
/// which is generated app side during authentication.
|
||||
static func legacySessionDirectory(for userID: String) -> URL {
|
||||
// Rust sanitises the user ID replacing invalid characters with an _
|
||||
let sanitisedUserID = userID.replacingOccurrences(of: ":", with: "_")
|
||||
return .sessionsBaseDirectory.appendingPathComponent(sanitisedUserID)
|
||||
}
|
||||
}
|
||||
@@ -64,20 +64,21 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
|
||||
// On any restoration failure reset the token and restart
|
||||
keychainController.removeRestorationTokenForUsername(credentials.userID)
|
||||
deleteSessionDirectory(for: credentials)
|
||||
deleteSessionDirectories(for: credentials)
|
||||
|
||||
return .failure(error)
|
||||
}
|
||||
}
|
||||
|
||||
func userSession(for client: Client, sessionDirectory: URL, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError> {
|
||||
do {
|
||||
let session = try client.session()
|
||||
let userID = try client.userId()
|
||||
let clientProxy = await setupProxyForClient(client)
|
||||
|
||||
keychainController.setRestorationToken(RestorationToken(session: session,
|
||||
sessionDirectory: sessionDirectory,
|
||||
sessionDirectory: sessionDirectories.dataDirectory,
|
||||
cacheDirectory: sessionDirectories.cacheDirectory,
|
||||
passphrase: passphrase,
|
||||
pusherNotificationClientIdentifier: clientProxy.pusherNotificationClientIdentifier),
|
||||
forUsername: userID)
|
||||
@@ -95,7 +96,7 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
keychainController.removeRestorationTokenForUsername(userID)
|
||||
|
||||
if let credentials {
|
||||
deleteSessionDirectory(for: credentials)
|
||||
deleteSessionDirectories(for: credentials)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +134,8 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
slidingSync: appSettings.simplifiedSlidingSyncEnabled ? .simplified : .restored,
|
||||
sessionDelegate: keychainController,
|
||||
appHooks: appHooks)
|
||||
.sessionPath(path: credentials.restorationToken.sessionDirectory.path(percentEncoded: false))
|
||||
.sessionPaths(dataPath: credentials.restorationToken.sessionDirectory.path(percentEncoded: false),
|
||||
cachePath: credentials.restorationToken.cacheDirectory.path(percentEncoded: false))
|
||||
.username(username: credentials.userID)
|
||||
.homeserverUrl(url: homeserverURL)
|
||||
.passphrase(passphrase: credentials.restorationToken.passphrase)
|
||||
@@ -156,22 +158,36 @@ class UserSessionStore: UserSessionStoreProtocol {
|
||||
appSettings: appSettings)
|
||||
}
|
||||
|
||||
private func deleteSessionDirectory(for credentials: KeychainCredentials) {
|
||||
private func deleteSessionDirectories(for credentials: KeychainCredentials) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: credentials.restorationToken.sessionDirectory)
|
||||
} catch {
|
||||
MXLog.failure("Failed deleting the session data: \(error)")
|
||||
}
|
||||
do {
|
||||
try FileManager.default.removeItem(at: credentials.restorationToken.cacheDirectory)
|
||||
} catch {
|
||||
MXLog.failure("Failed deleting the session caches: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteCaches(for credentials: KeychainCredentials) {
|
||||
do {
|
||||
let sessionDirectoryContents = try FileManager.default.contentsOfDirectory(at: credentials.restorationToken.sessionDirectory, includingPropertiesForKeys: nil)
|
||||
for url in sessionDirectoryContents where url.path.contains(matrixSDKStateKey) {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
try deleteContentsOfDirectory(at: credentials.restorationToken.sessionDirectory)
|
||||
} catch {
|
||||
MXLog.failure("Failed clearing caches: \(error)")
|
||||
MXLog.failure("Failed clearing state store: \(error)")
|
||||
}
|
||||
do {
|
||||
try deleteContentsOfDirectory(at: credentials.restorationToken.cacheDirectory)
|
||||
} catch {
|
||||
MXLog.failure("Failed clearing event cache store: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteContentsOfDirectory(at url: URL) throws {
|
||||
let sessionDirectoryContents = try FileManager.default.contentsOfDirectory(at: url, includingPropertiesForKeys: nil)
|
||||
for url in sessionDirectoryContents where url.path.contains(matrixSDKStateKey) {
|
||||
try FileManager.default.removeItem(at: url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ protocol UserSessionStoreProtocol {
|
||||
func restoreUserSession() async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
/// Creates a user session for a new client from the SDK along with the passphrase used for the data stores.
|
||||
func userSession(for client: Client, sessionDirectory: URL, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
func userSession(for client: Client, sessionDirectories: SessionDirectories, passphrase: String?) async -> Result<UserSessionProtocol, UserSessionStoreError>
|
||||
|
||||
/// Logs out of the specified session.
|
||||
func logout(userSession: UserSessionProtocol)
|
||||
|
||||
Reference in New Issue
Block a user