diff --git a/WireDomain/Sources/WireDomain/Account/AccountManager.swift b/WireDomain/Sources/WireDomain/Account/AccountManager.swift index df9319a1bf1..c93b51d1ddb 100644 --- a/WireDomain/Sources/WireDomain/Account/AccountManager.swift +++ b/WireDomain/Sources/WireDomain/Account/AccountManager.swift @@ -179,7 +179,7 @@ public final class AccountManager: NSObject, Sendable { public static func delete(at root: URL) { AccountStore.delete(directory: AccountURLs(root: root).accounts) - UserDefaults.shared().selectedAccountIdentifier = nil + UserDefaults.shared()!.selectedAccountIdentifier = nil } // MARK: - Retrieve diff --git a/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift b/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift index aefe69845f3..09187207d07 100644 --- a/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift +++ b/WireDomain/Sources/WireDomain/Components/ClientSessionComponent.swift @@ -20,6 +20,7 @@ import Combine import Foundation import WireCoreCrypto import WireDataModel +import WireFoundation import WireLogging import WireNetwork @@ -54,7 +55,7 @@ public final class ClientSessionComponent { private let isMLSEnabled: Bool - private let cookieStorage: any CookieStorageProtocol + private let cookieStorage: any WireNetwork.CookieStorageProtocol private let sharedContainerURL: URL? private let sharedUserDefaults: UserDefaults private let syncContext: NSManagedObjectContext @@ -76,7 +77,7 @@ public final class ClientSessionComponent { websocketNetworkService: NetworkService, backendMetadata: ResolvedBackendMetadata, isMLSEnabled: Bool, - cookieStorage: any CookieStorageProtocol, + cookieStorage: any WireNetwork.CookieStorageProtocol, sharedContainerURL: URL?, sharedUserDefaults: UserDefaults, syncContext: NSManagedObjectContext, @@ -110,6 +111,7 @@ public final class ClientSessionComponent { } public private(set) lazy var authenticationManager = AuthenticationManager( + userID: selfUserID, clientID: selfClientID, cookieStorage: cookieStorage, networkService: restNetworkService, diff --git a/WireDomain/Sources/WireDomain/Components/UserSessionComponent.swift b/WireDomain/Sources/WireDomain/Components/UserSessionComponent.swift index 487abdb34a2..8940b1646e6 100644 --- a/WireDomain/Sources/WireDomain/Components/UserSessionComponent.swift +++ b/WireDomain/Sources/WireDomain/Components/UserSessionComponent.swift @@ -49,7 +49,7 @@ public final class UserSessionComponent { public init( currentBuildNumber: String, selfUserID: UUID, - cookieStorage: any CookieStorageProtocol, + cookieStorage: any WireNetwork.CookieStorageProtocol, restNetworkService: NetworkService, websocketNetworkService: NetworkService, blacklistNetworkService: NetworkService, @@ -86,7 +86,7 @@ public final class UserSessionComponent { self.faultyMLSRemovalKeysByDomain = faultyMLSRemovalKeysByDomain } - private let cookieStorage: any CookieStorageProtocol + private let cookieStorage: any WireNetwork.CookieStorageProtocol // MARK: - Children diff --git a/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift b/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift index 0690f2a5046..aa0fee4ced6 100644 --- a/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift +++ b/WireDomain/Sources/WireDomain/Notifications/Components/NSEClientScope.swift @@ -208,6 +208,7 @@ final class NSEClientScope: Component { private var authenticationManager: AuthenticationManager { shared { AuthenticationManager( + userID: dependency.accountID, clientID: clientID, cookieStorage: dependency.cookieStorage, networkService: restNetworkService, diff --git a/WireDomain/Sources/WireDomain/Notifications/Components/NSEUserScope.swift b/WireDomain/Sources/WireDomain/Notifications/Components/NSEUserScope.swift index 28bad39147c..2753afe50b5 100644 --- a/WireDomain/Sources/WireDomain/Notifications/Components/NSEUserScope.swift +++ b/WireDomain/Sources/WireDomain/Notifications/Components/NSEUserScope.swift @@ -77,9 +77,7 @@ final class NSEUserScope: Component { public var cookieStorage: CookieStorage { shared { CookieStorage( - userID: accountID, - cookieEncryptionKey: dependency.cookieEncryptionKey, - keychain: Keychain() + cookieEncryptionKey: dependency.cookieEncryptionKey ) } } @@ -286,7 +284,7 @@ final class NSEUserScope: Component { private func isAuthenticated() async throws -> Bool { let cookies: [HTTPCookie] do { - cookies = try await cookieStorage.fetchCookies() + cookies = try cookieStorage.fetchCookies(userID: accountID) } catch { throw Failure.failedToFetchCookies(error) } diff --git a/WireDomain/Sources/WireDomain/Notifications/VerifyUserSessionUseCase.swift b/WireDomain/Sources/WireDomain/Notifications/VerifyUserSessionUseCase.swift index 367bca46ac3..62d5129e710 100644 --- a/WireDomain/Sources/WireDomain/Notifications/VerifyUserSessionUseCase.swift +++ b/WireDomain/Sources/WireDomain/Notifications/VerifyUserSessionUseCase.swift @@ -17,6 +17,7 @@ // import WireDataModel +import WireFoundation import WireLogging import WireNetwork @@ -39,16 +40,19 @@ struct VerifyUserSessionUseCase { // MARK: - Properties + private let userID: UUID private let journal: any JournalProtocol - private let cookieStorage: any CookieStorageProtocol + private let cookieStorage: any WireNetwork.CookieStorageProtocol private let coreData: any CoreDataStackProtocol private let logger = WireLogger.notifications init( + userID: UUID, journal: any JournalProtocol, - cookieStorage: any CookieStorageProtocol, + cookieStorage: any WireNetwork.CookieStorageProtocol, coreData: any CoreDataStackProtocol ) { + self.userID = userID self.journal = journal self.cookieStorage = cookieStorage self.coreData = coreData @@ -74,7 +78,7 @@ struct VerifyUserSessionUseCase { attributes: .newNSE ) - let cookies = try await cookieStorage.fetchCookies() + let cookies = try cookieStorage.fetchCookies(userID: userID) for cookie in cookies where cookie.name == Constants.cookieName { if let cookieExpirationDate = cookie.expiresDate { diff --git a/WireDomain/Tests/WireDomainTests/Notifications/VerifyUserSessionUseCaseTests.swift b/WireDomain/Tests/WireDomainTests/Notifications/VerifyUserSessionUseCaseTests.swift index 2e59c2bd070..a127ad20e9b 100644 --- a/WireDomain/Tests/WireDomainTests/Notifications/VerifyUserSessionUseCaseTests.swift +++ b/WireDomain/Tests/WireDomainTests/Notifications/VerifyUserSessionUseCaseTests.swift @@ -41,6 +41,7 @@ final class VerifyUserSessionUseCaseTests: XCTestCase { cookieStorage = MockCookieStorageProtocol() sut = VerifyUserSessionUseCase( + userID: Scaffolding.userID, journal: journal, cookieStorage: cookieStorage, coreData: stack @@ -64,13 +65,13 @@ final class VerifyUserSessionUseCaseTests: XCTestCase { stack.needsMigration = false stack.load_MockMethod = {} let validCookie = try XCTUnwrap(Scaffolding.validCookie) - cookieStorage.fetchCookies_MockValue = [validCookie] + cookieStorage.fetchCookiesUserID_MockValue = [validCookie] // When try await sut.invoke() // Then - XCTAssertEqual(cookieStorage.fetchCookies_Invocations.count, 1) + XCTAssertEqual(cookieStorage.fetchCookiesUserID_Invocations.count, 1) } @@ -90,7 +91,7 @@ final class VerifyUserSessionUseCaseTests: XCTestCase { func testVerify_It_Throws_User_Unauthenticated_Error() async throws { // Mock - cookieStorage.fetchCookies_MockValue = [.init()] + cookieStorage.fetchCookiesUserID_MockValue = [.init()] // Then await XCTAssertThrowsErrorAsync(VerifyUserSessionUseCase.Failure.userUnauthenticated) { [self] in @@ -104,7 +105,7 @@ final class VerifyUserSessionUseCaseTests: XCTestCase { // Mock let expiredCookie = try XCTUnwrap(Scaffolding.expiredCookie) - cookieStorage.fetchCookies_MockValue = [expiredCookie] + cookieStorage.fetchCookiesUserID_MockValue = [expiredCookie] // Then await XCTAssertThrowsErrorAsync(VerifyUserSessionUseCase.Failure.userUnauthenticated) { [self] in @@ -117,7 +118,7 @@ final class VerifyUserSessionUseCaseTests: XCTestCase { func testVerify_It_Throws_User_Unauthenticated_Error_When_No_Cookies_Found() async throws { // Mock - cookieStorage.fetchCookies_MockValue = [] + cookieStorage.fetchCookiesUserID_MockValue = [] // Then await XCTAssertThrowsErrorAsync(VerifyUserSessionUseCase.Failure.userUnauthenticated) { [self] in @@ -130,7 +131,7 @@ final class VerifyUserSessionUseCaseTests: XCTestCase { func testStartSyncingEvents_It_Throws_Core_Data_Missing_Shared_Container() async throws { // Mock let validCookie = try XCTUnwrap(Scaffolding.validCookie) - cookieStorage.fetchCookies_MockValue = [validCookie] + cookieStorage.fetchCookiesUserID_MockValue = [validCookie] stack.storesExists = false // Then @@ -145,7 +146,7 @@ final class VerifyUserSessionUseCaseTests: XCTestCase { stack.storesExists = true stack.needsMigration = true let validCookie = try XCTUnwrap(Scaffolding.validCookie) - cookieStorage.fetchCookies_MockValue = [validCookie] + cookieStorage.fetchCookiesUserID_MockValue = [validCookie] // Then await XCTAssertThrowsErrorAsync(VerifyUserSessionUseCase.Failure.coreDataMigrationRequired) { [self] in diff --git a/WireFoundation/Sources/WireFoundation/SDKAbstractions/Keychain.swift b/WireFoundation/Sources/WireFoundation/SDKAbstractions/Keychain.swift index edc5b47c57c..a30657f4c65 100644 --- a/WireFoundation/Sources/WireFoundation/SDKAbstractions/Keychain.swift +++ b/WireFoundation/Sources/WireFoundation/SDKAbstractions/Keychain.swift @@ -32,7 +32,7 @@ public struct Keychain: KeychainProtocol { public func addItem( query: Set - ) async throws { + ) throws { let status = SecItemAdd( query.toCFDictionary(), nil @@ -50,7 +50,7 @@ public struct Keychain: KeychainProtocol { public func updateItem( query: Set, attributesToUpdate: Set - ) async throws { + ) throws { let status = SecItemUpdate( query.toCFDictionary(), attributesToUpdate.toCFDictionary() @@ -67,7 +67,7 @@ public struct Keychain: KeychainProtocol { public func fetchItem( query: Set - ) async throws -> T? { + ) throws -> T? { var result: CFTypeRef? let status = SecItemCopyMatching( @@ -96,7 +96,7 @@ public struct Keychain: KeychainProtocol { public func deleteItem( query: Set - ) async throws { + ) throws { let status = SecItemDelete( query.toCFDictionary() ) @@ -106,6 +106,27 @@ public struct Keychain: KeychainProtocol { } } + /// Delete all items from the keychain. + + public func reset() throws { + let items: [KeychainQueryItem] = [ + .itemClass(.genericPassword), + .itemClass(.internetPassword), + .itemClass(.certificate), + .itemClass(.key), + .itemClass(.identity) + ] + + for item in items { + let query = Set([item]).toCFDictionary() + let status = SecItemDelete(query) + guard status == errSecSuccess || status == errSecItemNotFound else { + throw KeychainError.errorStatus(status) + } + + } + } + } private extension Set { diff --git a/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift b/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift index 3becec1507b..74d60f76bab 100644 --- a/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift +++ b/WireFoundation/Sources/WireFoundation/SDKAbstractions/KeychainProtocol.swift @@ -23,20 +23,20 @@ public protocol KeychainProtocol: Sendable { func addItem( query: Set - ) async throws + ) throws func updateItem( query: Set, attributesToUpdate: Set - ) async throws + ) throws func fetchItem( query: Set - ) async throws -> T? + ) throws -> T? func deleteItem( query: Set - ) async throws + ) throws } @@ -51,14 +51,20 @@ public enum KeychainQueryItem: Hashable, Equatable, Sendable { case service(String) case account(String) + case generic(Data) case itemClass(ItemClass) case accessible(ItemAccessibility) case returningData(Bool) case data(Data) + case returningAttributes(Bool) public enum ItemClass: Equatable, Sendable { case genericPassword + case internetPassword + case certificate + case key + case identity } @@ -76,9 +82,24 @@ public enum KeychainQueryItem: Hashable, Equatable, Sendable { case let .account(string): (kSecAttrAccount, string) + case let .generic(data): + (kSecAttrGeneric, data) + case .itemClass(.genericPassword): (kSecClass, kSecClassGenericPassword) + case .itemClass(.internetPassword): + (kSecClass, kSecClassInternetPassword) + + case .itemClass(.certificate): + (kSecClass, kSecClassCertificate) + + case .itemClass(.key): + (kSecClass, kSecClassKey) + + case .itemClass(.identity): + (kSecClass, kSecClassIdentity) + case .accessible(.afterFirstUnlock): (kSecAttrAccessible, kSecAttrAccessibleAfterFirstUnlock) @@ -87,6 +108,9 @@ public enum KeychainQueryItem: Hashable, Equatable, Sendable { case let .data(data): (kSecValueData, data) + + case let .returningAttributes(bool): + (kSecReturnAttributes, bool) } } diff --git a/WireFoundation/Sources/WireFoundationSupport/Sourcery/AutoMockable.manual.swift b/WireFoundation/Sources/WireFoundationSupport/Sourcery/AutoMockable.manual.swift index 16c0f16809f..ad877a47bb8 100644 --- a/WireFoundation/Sources/WireFoundationSupport/Sourcery/AutoMockable.manual.swift +++ b/WireFoundation/Sources/WireFoundationSupport/Sourcery/AutoMockable.manual.swift @@ -18,7 +18,7 @@ public import WireFoundation -public actor KeychainProtocolMock: KeychainProtocol { +public final class KeychainProtocolMock: KeychainProtocol, @unchecked Sendable { // MARK: - Init @@ -33,12 +33,12 @@ public actor KeychainProtocolMock: KeychainProtocol { addItemQuery_MockError = error } - var addItemQuery_MockMethod: ((Set) async throws -> Void)? - public func setAddItemQuery_MockMethod(_ method: @escaping (Set) async throws -> Void) { + var addItemQuery_MockMethod: ((Set) throws -> Void)? + public func setAddItemQuery_MockMethod(_ method: @escaping (Set) throws -> Void) { addItemQuery_MockMethod = method } - public func addItem(query: Set) async throws { + public func addItem(query: Set) throws { addItemQuery_Invocations.append(query) if let error = addItemQuery_MockError { @@ -49,7 +49,7 @@ public actor KeychainProtocolMock: KeychainProtocol { fatalError("no mock for `addItemQuery`") } - try await mock(query) + try mock(query) } // MARK: - updateItem @@ -61,12 +61,12 @@ public actor KeychainProtocolMock: KeychainProtocol { updateItemQueryAttributesToUpdate_MockError = error } - var updateItemQueryAttributesToUpdate_MockMethod: ((Set, Set) async throws -> Void)? - public func setUpdateItemQueryAttributesToUpdate_MockMethod(_ method: @escaping (Set, Set) async throws -> Void) { + var updateItemQueryAttributesToUpdate_MockMethod: ((Set, Set) throws -> Void)? + public func setUpdateItemQueryAttributesToUpdate_MockMethod(_ method: @escaping (Set, Set) throws -> Void) { updateItemQueryAttributesToUpdate_MockMethod = method } - public func updateItem(query: Set, attributesToUpdate: Set) async throws { + public func updateItem(query: Set, attributesToUpdate: Set) throws { updateItemQueryAttributesToUpdate_Invocations.append((query: query, attributesToUpdate: attributesToUpdate)) if let error = updateItemQueryAttributesToUpdate_MockError { @@ -77,7 +77,7 @@ public actor KeychainProtocolMock: KeychainProtocol { fatalError("no mock for `updateItemQueryAttributesToUpdate`") } - try await mock(query, attributesToUpdate) + try mock(query, attributesToUpdate) } // MARK: - fetchItem @@ -89,19 +89,19 @@ public actor KeychainProtocolMock: KeychainProtocol { fetchItemQuery_MockError = error } - var fetchItemQuery_MockMethod: ((Set) async throws -> (any Sendable)?)? + var fetchItemQuery_MockMethod: ((Set) throws -> (any Sendable)?)? public func setFetchItemQuery_MockMethod( - _ method: @escaping (Set) async throws -> (any Sendable)? + _ method: @escaping (Set) throws -> (any Sendable)? ) { fetchItemQuery_MockMethod = method } var fetchItemQuery_MockValue: (any Sendable)?? - public func setFetchItemQuery_MockValue(_ value: (any Sendable)?) async { + public func setFetchItemQuery_MockValue(_ value: (any Sendable)?) { fetchItemQuery_MockValue = value } - public func fetchItem(query: Set) async throws -> T? { + public func fetchItem(query: Set) throws -> T? { fetchItemQuery_Invocations.append(query) if let error = fetchItemQuery_MockError { @@ -109,7 +109,7 @@ public actor KeychainProtocolMock: KeychainProtocol { } if let mock = fetchItemQuery_MockMethod { - return try await mock(query) as? T + return try mock(query) as? T } else if let mock = fetchItemQuery_MockValue { return mock as? T } else { @@ -126,12 +126,12 @@ public actor KeychainProtocolMock: KeychainProtocol { deleteItemQuery_MockError = error } - var deleteItemQuery_MockMethod: ((Set) async throws -> Void)? - public func setDeleteItemQuery_MockMethod(_ method: @escaping (Set) async throws -> Void) { + var deleteItemQuery_MockMethod: ((Set) throws -> Void)? + public func setDeleteItemQuery_MockMethod(_ method: @escaping (Set) throws -> Void) { deleteItemQuery_MockMethod = method } - public func deleteItem(query: Set) async throws { + public func deleteItem(query: Set) throws { deleteItemQuery_Invocations.append(query) if let error = deleteItemQuery_MockError { @@ -142,7 +142,7 @@ public actor KeychainProtocolMock: KeychainProtocol { fatalError("no mock for `deleteItemQuery`") } - try await mock(query) + try mock(query) } } diff --git a/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveDeletePublicLinkPasswordUseCase.swift b/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveDeletePublicLinkPasswordUseCase.swift index 179e2ab2b8c..77df2e82286 100644 --- a/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveDeletePublicLinkPasswordUseCase.swift +++ b/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveDeletePublicLinkPasswordUseCase.swift @@ -42,7 +42,7 @@ package struct WireDriveDeletePublicLinkPasswordUseCase { ] do { - try await keychain.deleteItem(query: query) + try keychain.deleteItem(query: query) } catch let error as KeychainError { switch error { case let .errorStatus(oSstatus) where oSstatus == errSecItemNotFound: diff --git a/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveGetPublicLinkPasswordUseCase.swift b/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveGetPublicLinkPasswordUseCase.swift index c2b907ca9d5..4b4d9346028 100644 --- a/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveGetPublicLinkPasswordUseCase.swift +++ b/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveGetPublicLinkPasswordUseCase.swift @@ -43,7 +43,7 @@ package struct WireDriveGetPublicLinkPasswordUseCase { ] do { - let data: Data? = try await keychain.fetchItem(query: query) + let data: Data? = try keychain.fetchItem(query: query) guard let data else { throw Failure.itemNotFound } return String(decoding: data, as: UTF8.self) } catch { diff --git a/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveStorePublicLinkPasswordUseCase.swift b/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveStorePublicLinkPasswordUseCase.swift index d5a20bc18a6..728f059d0fc 100644 --- a/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveStorePublicLinkPasswordUseCase.swift +++ b/WireMessaging/Sources/WireMessagingDomain/WireDrive/UseCases/Keychain/WireDriveStorePublicLinkPasswordUseCase.swift @@ -45,12 +45,12 @@ package struct WireDriveStorePublicLinkPasswordUseCase { ] do { - try await keychain.addItem(query: query) + try keychain.addItem(query: query) } catch let error as KeychainError { switch error { case let .errorStatus(oSstatus) where oSstatus == errSecDuplicateItem: let updateQuery: Set = [.data(data)] - try await keychain.updateItem(query: query, attributesToUpdate: updateQuery) + try keychain.updateItem(query: query, attributesToUpdate: updateQuery) default: return WireLogger.wireDrive.error("Failed to store password in keychain: \(error)") } diff --git a/WireMessaging/Tests/WireMessagingTests/WireDrive/UITests/Components/Files/FilesViewModelTests.swift b/WireMessaging/Tests/WireMessagingTests/WireDrive/UITests/Components/Files/FilesViewModelTests.swift index 40f7da6b241..827e0d1804d 100644 --- a/WireMessaging/Tests/WireMessagingTests/WireDrive/UITests/Components/Files/FilesViewModelTests.swift +++ b/WireMessaging/Tests/WireMessagingTests/WireDrive/UITests/Components/Files/FilesViewModelTests.swift @@ -47,6 +47,7 @@ final class FilesViewModelTests { editingURLRepository.getEditorURLId_MockValue = nil networkMonitor.currentStatus = .connected + localAssetRepository.offlineAssetsConversationNameAssetsPath_MockValue = [] self.sut = FilesViewModel( useCases: .init( diff --git a/WireNetwork/Sources/WireNetwork/Assembly.swift b/WireNetwork/Sources/WireNetwork/Assembly.swift deleted file mode 100644 index fd815ada807..00000000000 --- a/WireNetwork/Sources/WireNetwork/Assembly.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -public import Foundation -import WireFoundation - -public final class Assembly { - - let userID: UUID - let clientID: String - let backendEnvironment: BackendEnvironment - let minTLSVersion: TLSVersion - let cookieEncryptionKey: Data - - public init( - userID: UUID, - clientID: String, - backendEnvironment: BackendEnvironment, - minTLSVersion: TLSVersion, - cookieEncryptionKey: Data - ) { - self.userID = userID - self.clientID = clientID - self.backendEnvironment = backendEnvironment - self.minTLSVersion = minTLSVersion - self.cookieEncryptionKey = cookieEncryptionKey - } - - private lazy var keychain: some KeychainProtocol = Keychain() - private lazy var urlSessionConfigurationFactory = URLSessionConfigurationFactory( - minTLSVersion: minTLSVersion, - proxySettings: backendEnvironment.proxySettings - ) - - private lazy var apiService: some APIServiceProtocol = APIService( - networkService: apiNetworkService, - authenticationManager: authenticationManager - ) - - public lazy var apiNetworkService = NetworkService( - baseURL: backendEnvironment.url, - urlSessionConfiguration: urlSessionConfigurationFactory.makeRESTAPISessionConfiguration(), - serverTrustValidator: serverTrustValidator - ) - - public lazy var authenticationManager: some AuthenticationManagerProtocol = AuthenticationManager( - clientID: clientID, - cookieStorage: cookieStorage, - networkService: apiNetworkService, - onAuthenticationFailure: {} - ) - - private lazy var cookieStorage: some CookieStorageProtocol = CookieStorage( - userID: userID, - cookieEncryptionKey: cookieEncryptionKey, - keychain: keychain - ) - - private lazy var serverTrustValidator = ServerTrustValidator( - pinnedKeys: backendEnvironment.pinnedKeys, - currentDateProvider: .system - ) - -} diff --git a/WireNetwork/Sources/WireNetwork/Authentication/AuthenticationManager.swift b/WireNetwork/Sources/WireNetwork/Authentication/AuthenticationManager.swift index 957f73cc243..e2bf3765557 100644 --- a/WireNetwork/Sources/WireNetwork/Authentication/AuthenticationManager.swift +++ b/WireNetwork/Sources/WireNetwork/Authentication/AuthenticationManager.swift @@ -44,17 +44,20 @@ public actor AuthenticationManager: AuthenticationManagerProtocol { } private var currentToken: CurrentToken? + private let userID: UUID private let clientID: String? private let cookieStorage: any CookieStorageProtocol private let networkService: any NetworkServiceProtocol private let onAuthenticationFailure: () -> Void public init( + userID: UUID, clientID: String?, cookieStorage: any CookieStorageProtocol, networkService: any NetworkServiceProtocol, onAuthenticationFailure: @escaping () -> Void ) { + self.userID = userID self.clientID = clientID self.cookieStorage = cookieStorage self.networkService = networkService @@ -123,7 +126,7 @@ public actor AuthenticationManager: AuthenticationManagerProtocol { switch authenticationError { case .invalidCredentials: // can't recover, deleting cookies and logging out - try await cookieStorage.removeCookies() + try cookieStorage.removeCookies(userID: userID) WireLogger.authentication.info( "Removed cookies (invalidCredentials)", attributes: .safePublic ) @@ -143,7 +146,7 @@ public actor AuthenticationManager: AuthenticationManagerProtocol { lastKnownToken: AccessToken? ) -> Task { Task { - let cookies = try await cookieStorage.fetchCookies() + let cookies = try cookieStorage.fetchCookies(userID: userID) var requestBuilder = try URLRequestBuilder(path: "/access") .withMethod(.post) diff --git a/WireNetwork/Sources/WireNetwork/Authentication/CookieStorage.swift b/WireNetwork/Sources/WireNetwork/Authentication/CookieStorage.swift index b8aead647ec..f5790befcbe 100644 --- a/WireNetwork/Sources/WireNetwork/Authentication/CookieStorage.swift +++ b/WireNetwork/Sources/WireNetwork/Authentication/CookieStorage.swift @@ -17,6 +17,7 @@ // public import Foundation +import os public import WireFoundation import WireCrypto @@ -24,72 +25,112 @@ import WireCrypto // sourcery: AutoMockable public protocol CookieStorageProtocol: Sendable { - func storeCookies(_ cookies: [HTTPCookie]) async throws - func fetchCookies() async throws -> [HTTPCookie] - func removeCookies() async throws + func storeCookies(_ cookies: [HTTPCookie], userID: UUID) throws + func fetchCookies(userID: UUID) throws -> [HTTPCookie] + func removeCookies(userID: UUID) throws } -public actor CookieStorage: CookieStorageProtocol { +/// A cache for cookies, keyed by user ID. +/// +/// This class is thread-safe and can be shared across multiple `CookieStorage` instances. +/// It is intended to be used as a singleton within a process to avoid redundant keychain reads. + +public final class CookieStorageCache: Sendable { + + public struct Item: Sendable { + let cookies: [HTTPCookie] + let epoch: UUID + } + + public static let sharedStorage = OSAllocatedUnfairLock<[UUID: Item]>(initialState: [:]) + + private let cache: OSAllocatedUnfairLock<[UUID: Item]> + + public init(sharedStorage: OSAllocatedUnfairLock<[UUID: Item]> = CookieStorageCache.sharedStorage) { + self.cache = sharedStorage + } + + func get(for userID: UUID) -> Item? { + cache.withLock { $0[userID] } + } + + func set(_ item: Item, for userID: UUID) { + cache.withLock { $0[userID] = item } + } + + func remove(for userID: UUID) { + cache.withLock { $0[userID] = nil } + } + + func removeAll() { + cache.withLock { $0.removeAll() } + } + +} + +public struct CookieStorage: CookieStorageProtocol, Sendable { enum Failure: Error { case malformedCookieData - case failedToDecodeCookieData(any Error) - case missingCookieEncryptionKey case failedToEncryptCookie(any Error) case failedToDecryptCookie(any Error) } - private let userID: UUID + private static let lock = OSAllocatedUnfairLock() + private let cookieEncryptionKey: Data private let keychain: any KeychainProtocol + private let cache: CookieStorageCache - private lazy var baseQuery: Set = [ - .service("Wire: Credentials for wire.com"), - .account(userID.uuidString), - .itemClass(.genericPassword) - ] - - private lazy var fetchQuery: Set = { - var result = baseQuery - result.insert(.returningData(true)) - return result - }() - - private func addQuery(cookieData: Data) -> Set { - var result = updateQuery(cookieData: cookieData) - result.insert(.accessible(.afterFirstUnlock)) - return result - } - - private func updateQuery(cookieData: Data) -> Set { - var result = baseQuery - result.insert(.data(cookieData.base64EncodedData())) - return result - } + /// Creates a new `CookieStorage`. + /// + /// - Parameters: + /// - cookieEncryptionKey: A key used to encrypt and decrypt cookie data. This key should be stored in defaults + /// so that it is destroyed when the app is deleted. public init( - userID: UUID, - cookieEncryptionKey: Data, - keychain: any KeychainProtocol + cookieEncryptionKey: Data ) { - self.userID = userID self.cookieEncryptionKey = cookieEncryptionKey - self.keychain = keychain + self.keychain = Keychain() + self.cache = CookieStorageCache() } + #if DEBUG + /// Creates a new `CookieStorage` with injected dependencies for testing purposes only. + public init( + cookieEncryptionKey: Data, + keychain: any KeychainProtocol, + cache: CookieStorageCache + ) { + self.cookieEncryptionKey = cookieEncryptionKey + self.keychain = keychain + self.cache = cache + } + #endif + /// Store cookies. /// /// Cookie data is stored in the device keychain and may persist across /// different installations of the application, such as when the app is /// deleted without the user logging out. /// - /// - Parameter cookies: The cookies to store. + /// - Parameters: + /// - cookies: The cookies to store. + /// - userID: The unique identifier for the user whose cookies are being stored. - public func storeCookies(_ cookies: [HTTPCookie]) async throws { - let cookieData = try HTTPCookieCodec.encodeCookies(cookies) - try await storeCookieData(cookieData) + public func storeCookies(_ cookies: [HTTPCookie], userID: UUID) throws { + try storeCookies(cookies, userID: userID, epoch: UUID()) + } + + /// Store cookies with a specific epoch. This is intended for testing purposes only. + + func storeCookies(_ cookies: [HTTPCookie], userID: UUID, epoch: UUID) throws { + try Self.lock.withLock { + try makeStorage(userID: userID).storeCookies(cookies, epoch: epoch) + } } /// Fetch stored cookies. @@ -98,85 +139,192 @@ public actor CookieStorage: CookieStorageProtocol { /// account, however it is likely that fetching an old cookie would result /// in a decoding error. /// + /// - Parameter userID: The unique identifier for the user whose cookies are being fetched. /// - Returns: The stored cookies. - public func fetchCookies() async throws -> [HTTPCookie] { - guard let cookieData = try await fetchCookieData() else { - return [] + public func fetchCookies(userID: UUID) throws -> [HTTPCookie] { + try Self.lock.withLock { + try makeStorage(userID: userID).fetchCookies() } - - return try HTTPCookieCodec.decodeData(cookieData) } /// Remove stored cookies from the keychain. /// - /// This will delete any cookie data associated with the current user ID + /// This will delete any cookie data associated with the given user ID /// from the device keychain. This operation is irreversible and is typically /// used during logout or account removal. /// + /// - Parameter userID: The unique identifier for the user whose cookies are being removed. /// - Throws: An error if the keychain deletion fails. - public func removeCookies() async throws { - try await keychain.deleteItem(query: baseQuery) + public func removeCookies(userID: UUID) throws { + try Self.lock.withLock { + try makeStorage(userID: userID).removeCookies() + } + } + + // MARK: - Helper + + private func makeStorage(userID: UUID) -> _CookieStorage { + _CookieStorage( + userID: userID, + cookieEncryptionKey: cookieEncryptionKey, + keychain: keychain, + cache: cache + ) } - // MARK: - Cookie data +} + +// MARK: - Private implementation + +/// To allow for simple locking of the cookie storage operations, we encapsulate the actual storage logic in a separate +/// class. + +private final class _CookieStorage: Sendable { + + private let userID: UUID + private let cookieEncryptionKey: Data + private let keychain: any KeychainProtocol + private let cache: CookieStorageCache + + init( + userID: UUID, + cookieEncryptionKey: Data, + keychain: any KeychainProtocol, + cache: CookieStorageCache + ) { + self.userID = userID + self.cookieEncryptionKey = cookieEncryptionKey + self.keychain = keychain + self.cache = cache + } + + func storeCookies(_ cookies: [HTTPCookie], epoch: UUID) throws { + let newEpoch = epoch.data + let cookieData = try Self.encodeAndEncryptCookies(cookies, key: cookieEncryptionKey) - private func storeCookieData(_ cookieData: Data) async throws { - let encryptedCookieData: Data do { - encryptedCookieData = try AES256Crypto.encryptAllAtOnceWithPrefixedIV( - plaintext: cookieData, - key: cookieEncryptionKey - ).data - } catch { - throw Failure.failedToEncryptCookie(error) + // The typical case is updating so try that first. + try keychain.updateItem(query: baseQuery(), attributesToUpdate: [.data(cookieData), .generic(newEpoch)]) + } catch let KeychainError.errorStatus(status) where status == errSecItemNotFound { + try keychain.addItem(query: addQuery(cookieData: cookieData, epoch: newEpoch)) } + } - if try await fetchCookieData() != nil { - try await updateCookieInKeychain(encryptedCookieData) + func fetchCookies() throws -> [HTTPCookie] { + if let cached = cache.get(for: userID), let epoch = try fetchEpochFromKeychain(), cached.epoch == epoch { + return cached.cookies + } + + guard let cookiesAndEpoch = try fetchCookiesFromKeychain() else { return [] } + cache.set(.init(cookies: cookiesAndEpoch.cookies, epoch: cookiesAndEpoch.epoch), for: userID) + return cookiesAndEpoch.cookies + } + + func removeCookies() throws { + try keychain.deleteItem(query: baseQuery()) + cache.remove(for: userID) + } + + // MARK: - Fetching + + private func fetchCookiesFromKeychain() throws -> (cookies: [HTTPCookie], epoch: UUID)? { + guard let data: Data = try keychain.fetchItem(query: fetchValueQuery()) else { return nil } + let cookies = try Self.decryptAndDecodeCookies(data, key: cookieEncryptionKey) + + if let epoch = try fetchEpochFromKeychain() { + return (cookies, epoch) } else { - try await addCookieToKeychain(encryptedCookieData) + let newEpoch = UUID() + try storeCookies(cookies, epoch: newEpoch) + return (cookies, newEpoch) } } - private func fetchCookieData() async throws -> Data? { - guard let encryptedCookieData = try await fetchCookieDataFromKeychain() else { + private func fetchEpochFromKeychain() throws -> UUID? { + guard let attributes: [String: Any] = try keychain.fetchItem(query: fetchAttributesQuery()), + let epochData = attributes[kSecAttrGeneric as String] as? Data, + epochData.count == MemoryLayout.size else { return nil } - do { - return try AES256Crypto.decryptAllAtOnceWithPrefixedIV( - ciphertext: AES256Crypto.PrefixedData(data: encryptedCookieData), - key: cookieEncryptionKey - ) - } catch { - throw Failure.failedToDecryptCookie(error) - } + return epochData.withUnsafeBytes { $0.load(as: UUID.self) } } - // MARK: - Keychain + // MARK: - Queries - private func addCookieToKeychain(_ cookieData: Data) async throws { - let query = addQuery(cookieData: cookieData) - try await keychain.addItem(query: query) + private func baseQuery() -> Set { + [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword) + ] } - private func updateCookieInKeychain(_ cookieData: Data) async throws { - let updateQuery: Set = [.data(cookieData.base64EncodedData())] - try await keychain.updateItem(query: baseQuery, attributesToUpdate: updateQuery) + private func fetchValueQuery() -> Set { + var query = baseQuery() + query.insert(.returningData(true)) + return query } - private func fetchCookieDataFromKeychain() async throws -> Data? { - guard let base64CookieData: Data = try await keychain.fetchItem(query: fetchQuery) else { - return nil + private func addQuery(cookieData: Data, epoch: Data) -> Set { + var query = baseQuery() + query.insert(.data(cookieData)) + query.insert(.accessible(.afterFirstUnlock)) + query.insert(.generic(epoch)) + return query + } + + private func fetchAttributesQuery() -> Set { + var query = baseQuery() + query.insert(.returningData(false)) + query.insert(.returningAttributes(true)) + return query + } + + // MARK: - Cookie encoding / decoding + + private static func encodeAndEncryptCookies(_ cookies: [HTTPCookie], key: Data) throws -> Data { + let cookieData = try HTTPCookieCodec.encodeCookies(cookies) + + let encryptedData: Data + do { + encryptedData = try AES256Crypto.encryptAllAtOnceWithPrefixedIV( + plaintext: cookieData, + key: key + ).data + } catch { + throw CookieStorage.Failure.failedToEncryptCookie(error) } - guard let cookieData = Data(base64Encoded: base64CookieData) else { - throw Failure.malformedCookieData + return encryptedData.base64EncodedData() + } + + private static func decryptAndDecodeCookies(_ base64Data: Data, key: Data) throws -> [HTTPCookie] { + guard let encryptedData = Data(base64Encoded: base64Data) else { + throw CookieStorage.Failure.malformedCookieData + } + + let cookieData: Data + do { + cookieData = try AES256Crypto.decryptAllAtOnceWithPrefixedIV( + ciphertext: AES256Crypto.PrefixedData(data: encryptedData), + key: key + ) + } catch { + throw CookieStorage.Failure.failedToDecryptCookie(error) } - return cookieData + return try HTTPCookieCodec.decodeData(cookieData) + } + +} + +private extension UUID { + + var data: Data { + withUnsafeBytes(of: uuid) { Data($0) } } } diff --git a/WireNetwork/Sources/WireNetwork/Authentication/HTTPCodecError.swift b/WireNetwork/Sources/WireNetwork/Authentication/HTTPCodecError.swift index 0505ce6dabe..37f2ee47fc8 100644 --- a/WireNetwork/Sources/WireNetwork/Authentication/HTTPCodecError.swift +++ b/WireNetwork/Sources/WireNetwork/Authentication/HTTPCodecError.swift @@ -18,7 +18,7 @@ import Foundation -enum HTTPCookieCodecError: Error { +enum HTTPCookieCodecError: Error, Equatable { case invalidCookies case invalidCookieData(reason: String) diff --git a/WireNetwork/Sources/WireNetwork/Network/NetworkStack/ProxyCredentialStore.swift b/WireNetwork/Sources/WireNetwork/Network/NetworkStack/ProxyCredentialStore.swift index 52e6b1fed88..803289f30de 100644 --- a/WireNetwork/Sources/WireNetwork/Network/NetworkStack/ProxyCredentialStore.swift +++ b/WireNetwork/Sources/WireNetwork/Network/NetworkStack/ProxyCredentialStore.swift @@ -29,13 +29,13 @@ public struct ProxyCredentialStore { host: String, port: Int ) async throws -> (username: String, password: String)? { - let usernameData: Data? = try await keychain.fetchItem(query: [ + let usernameData: Data? = try keychain.fetchItem(query: [ .itemClass(.genericPassword), .account("proxy-\(host):\(port)-username"), .returningData(true) ]) - let passwordData: Data? = try await keychain.fetchItem(query: [ + let passwordData: Data? = try keychain.fetchItem(query: [ .itemClass(.genericPassword), .account("proxy-\(host):\(port)-password"), .returningData(true) @@ -60,26 +60,26 @@ public struct ProxyCredentialStore { username: String, password: String ) async throws { - try? await keychain.deleteItem( + try? keychain.deleteItem( query: getUsernameQuery( host: host, port: port ) ) - try? await keychain.deleteItem( + try? keychain.deleteItem( query: getPasswordQuery( host: host, port: port ) ) - try await keychain.addItem( + try keychain.addItem( query: setUsernameQuery( host: host, port: port, username: username ) ) - try await keychain.addItem( + try keychain.addItem( query: setPasswordQuery( host: host, port: port, diff --git a/WireNetwork/Tests/WireNetworkTests/Authentication/AuthenticationManagerTests.swift b/WireNetwork/Tests/WireNetworkTests/Authentication/AuthenticationManagerTests.swift index 924f6fdc153..62bbe436252 100644 --- a/WireNetwork/Tests/WireNetworkTests/Authentication/AuthenticationManagerTests.swift +++ b/WireNetwork/Tests/WireNetworkTests/Authentication/AuthenticationManagerTests.swift @@ -46,6 +46,7 @@ final class AuthenticationManagerTests: XCTestCase { ) sut = AuthenticationManager( + userID: Scaffolding.userID, clientID: Scaffolding.clientID, cookieStorage: cookieStorage, networkService: networkService, @@ -65,7 +66,7 @@ final class AuthenticationManagerTests: XCTestCase { func testGetValidAccessToken_CacheIsEmpty() async throws { // Mock valid cookie. - cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] + cookieStorage.fetchCookiesUserID_MockValue = [try Scaffolding.cookie()] // Mock successful token response. var receivedRequests = [URLRequest]() @@ -133,7 +134,7 @@ final class AuthenticationManagerTests: XCTestCase { } private func setCachedExpiringAccessToken() async throws -> AccessToken { - cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] + cookieStorage.fetchCookiesUserID_MockValue = [try Scaffolding.cookie()] URLProtocolMock.mockHandler = { try $0.mockResponse( @@ -147,7 +148,7 @@ final class AuthenticationManagerTests: XCTestCase { func testGetValidAccessToken_AwaitTokenRefresh() async throws { // Mock valid cookie. - cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] + cookieStorage.fetchCookiesUserID_MockValue = [try Scaffolding.cookie()] // Mock successful token response. var receivedRequests = [URLRequest]() @@ -185,8 +186,8 @@ final class AuthenticationManagerTests: XCTestCase { func testRefreshAccessToken_AfterAnError_WeCanStillRefresh() async throws { // Mock token refresh error. - cookieStorage.fetchCookies_MockValue = [try Scaffolding.cookie()] - cookieStorage.removeCookies_MockMethod = {} + cookieStorage.fetchCookiesUserID_MockValue = [try Scaffolding.cookie()] + cookieStorage.removeCookiesUserID_MockMethod = { _ in } URLProtocolMock.mockHandler = { try $0.mockErrorResponse( statusCode: .forbidden, diff --git a/WireNetwork/Tests/WireNetworkTests/Authentication/CookieStorageTests.swift b/WireNetwork/Tests/WireNetworkTests/Authentication/CookieStorageTests.swift deleted file mode 100644 index a1fe50a644a..00000000000 --- a/WireNetwork/Tests/WireNetworkTests/Authentication/CookieStorageTests.swift +++ /dev/null @@ -1,295 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import WireCrypto -import WireFoundation -import WireTestingPackage -import XCTest - -@testable import WireFoundationSupport -@testable import WireNetwork - -final class CookieStorageTests: XCTestCase { - - var sut: CookieStorage! - var cookieEncryptionKey: Data! - var keychain: KeychainProtocolMock! - - override func setUpWithError() throws { - cookieEncryptionKey = try Scaffolding.cookieEncryptionKey() - keychain = KeychainProtocolMock() - sut = CookieStorage( - userID: Scaffolding.userID, - cookieEncryptionKey: cookieEncryptionKey, - keychain: keychain - ) - } - - override func tearDown() { - cookieEncryptionKey = nil - keychain = nil - sut = nil - } - - // MARK: - Cookies - - func testStoreCookies_No_Cookies() async throws { - // Given - let cookies = [HTTPCookie]() - - // Then - await XCTAssertThrowsErrorAsync { - // When - try await sut.storeCookies(cookies) - } errorHandler: { error in - guard case HTTPCookieCodecError.invalidCookies = error else { - XCTFail("unexpected error: \(error)") - return - } - } - } - - func testStoreCookies_Invalid_Cookie() async throws { - // Given - let invalidCookie = try XCTUnwrap(Scaffolding.invalidCookie) - - // Then - await XCTAssertThrowsErrorAsync { - // When - try await sut.storeCookies([invalidCookie]) - } errorHandler: { error in - guard case HTTPCookieCodecError.invalidCookies = error else { - XCTFail("unexpected error: \(error)") - return - } - } - } - - func testStoreCookies_Adds_To_Keychain() async throws { - // Given - let validCookie = try XCTUnwrap(Scaffolding.validCookie) - - // Mock no existing cookie. - await keychain.setFetchItemQuery_MockValue(nil) - - // Mock successul add. - await keychain.setAddItemQuery_MockMethod { _ in } - - // When - try await sut.storeCookies([validCookie]) - - // Then first we tried to fetch an existing cookie. - let fetchInvocations = await keychain.fetchItemQuery_Invocations - try XCTAssertCount(fetchInvocations, count: 1) - XCTAssertEqual(fetchInvocations[0], Scaffolding.fetchQuery) - - // Then we added the new cookie. - let addInvocations = await keychain.addItemQuery_Invocations - try XCTAssertCount(addInvocations, count: 1) - try assertAddQuery(addInvocations[0], addedCookie: validCookie) - } - - func testStoreCookies_Updates_Keychain() async throws { - // Given - let validCookie = try XCTUnwrap(Scaffolding.validCookie) - - // Mock existing cookie. - let data = Data("raw cookie".utf8).base64EncodedData() - await keychain.setFetchItemQuery_MockValue(data) - - // Mock successul update. - await keychain.setUpdateItemQueryAttributesToUpdate_MockMethod { _, _ in } - - // When - try await sut.storeCookies([validCookie]) - - // Then first we tried to fetch an existing cookie. - let fetchInvocations = await keychain.fetchItemQuery_Invocations - try XCTAssertCount(fetchInvocations, count: 1) - XCTAssertEqual(fetchInvocations[0], Scaffolding.fetchQuery) - - // Then we updated the keychain with the new cookie. - let updateInvocations = await keychain.updateItemQueryAttributesToUpdate_Invocations - try XCTAssertCount(updateInvocations, count: 1) - - XCTAssertEqual(updateInvocations[0].query, Scaffolding.baseQuery) - try assertUpdateQuery(updateInvocations[0].attributesToUpdate, updatedCookie: validCookie) - } - - func testFetchCookies_No_Cookies_EXist() async throws { - // Mock no existing cookie. - await keychain.setFetchItemQuery_MockValue(nil) - - // When - let cookies = try await sut.fetchCookies() - - // Then - XCTAssertTrue(cookies.isEmpty) - } - - func testFetchCookies() async throws { - // Given - let validCookie = try XCTUnwrap(Scaffolding.validCookie) - let storedCookieData = try Scaffolding.encodeAndEncryptCookieData( - for: [validCookie], - encryptionKey: cookieEncryptionKey - ) - - // Mock existing cookie. - await keychain.setFetchItemQuery_MockValue(storedCookieData) - - // When - let cookies = try await sut.fetchCookies() - - // Then - try assertCookies( - cookies, - equals: validCookie - ) - } - - // MARK: - Helpers - - private func assertAddQuery( - _ query: Set, - addedCookie: HTTPCookie, - file: StaticString = #file, - line: UInt = #line - ) throws { - XCTAssertTrue(query.contains(.accessible(.afterFirstUnlock)), file: file, line: line) - try assertUpdateQuery(query, updatedCookie: addedCookie, file: file, line: line) - } - - private func assertUpdateQuery( - _ query: Set, - updatedCookie: HTTPCookie, - file: StaticString = #file, - line: UInt = #line - ) throws { - var storedData: Data? - for item in query { - if case let .data(data) = item { - storedData = data - break - } - } - - let encryptedCookieData = try XCTUnwrap(storedData, file: file, line: line) - assertStoredCookieData(encryptedCookieData, equals: updatedCookie, file: file, line: line) - } - - private func assertStoredCookieData( - _ storedCookieData: Data, - equals cookie: HTTPCookie, - file: StaticString = #file, - line: UInt = #line - ) { - do { - let actualHTTPCookies = try Scaffolding.decryptAndDecodeCookieData( - storedCookieData, - encryptionKey: cookieEncryptionKey - ) - try assertCookies( - actualHTTPCookies, - equals: cookie, - file: file, - line: line - ) - } catch { - XCTFail( - "failed to assert cookie data: \(error)", - file: file, - line: line - ) - } - } - - private func assertCookies( - _ cookies: [HTTPCookie], - equals cookie: HTTPCookie, - file: StaticString = #file, - line: UInt = #line - ) throws { - try XCTAssertCount(cookies, count: 1, file: file, line: line) - XCTAssertEqual(cookies[0].name, cookie.name, file: file, line: line) - XCTAssertEqual(cookies[0].value, cookie.value, file: file, line: line) - XCTAssertEqual(cookies[0].path, cookie.path, file: file, line: line) - XCTAssertEqual(cookies[0].domain, cookie.domain, file: file, line: line) - } - -} - -private enum Scaffolding { - - static let userID = UUID() - - static func cookieEncryptionKey() throws -> Data { - try AES256Crypto.generateRandomEncryptionKey() - } - - static var baseQuery: Set { - [ - .service("Wire: Credentials for wire.com"), - .account(userID.uuidString), - .itemClass(.genericPassword) - ] - } - - static var fetchQuery: Set { - baseQuery.union([.returningData(true)]) - } - - static let invalidCookie = HTTPCookie(properties: [ - .name: "invalid-name", - .path: "some path", - .value: "some value", - .domain: "some domain" - ]) - - static let validCookie = HTTPCookie(properties: [ - .name: "zuid", - .path: "some path", - .value: "some value", - .domain: "some domain" - ]) - - static func encodeAndEncryptCookieData( - for cookies: [HTTPCookie], - encryptionKey: Data - ) throws -> Data { - let encodedData = try HTTPCookieCodec.encodeCookies(cookies) - let encryptedData = try AES256Crypto.encryptAllAtOnceWithPrefixedIV( - plaintext: encodedData, - key: encryptionKey - ) - return encryptedData.data.base64EncodedData() - } - - static func decryptAndDecodeCookieData( - _ base64CookieData: Data, - encryptionKey: Data - ) throws -> [HTTPCookie] { - let encryptedData = try XCTUnwrap(Data(base64Encoded: base64CookieData)) - let decryptedData = try AES256Crypto.decryptAllAtOnceWithPrefixedIV( - ciphertext: AES256Crypto.PrefixedData(data: encryptedData), - key: encryptionKey - ) - return try HTTPCookieCodec.decodeData(decryptedData) - } - -} diff --git a/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.h b/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.h index efff622d4d7..7e96deb87f3 100644 --- a/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.h +++ b/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.h @@ -45,7 +45,6 @@ @property (nonatomic, readonly, nonnull) NSManagedObjectContext *uiMOC; @property (nonatomic, readonly, nonnull) NSManagedObjectContext *syncMOC; -@property (nonatomic, readonly) BOOL shouldUseRealKeychain; @property (nonatomic, readonly) BOOL shouldUseInMemoryStore; /// reset ui and sync contexts diff --git a/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.m b/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.m index 97dbc41dbe6..c29c63375b9 100644 --- a/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.m +++ b/wire-ios-data-model/Tests/Model/ZMBaseManagedObjectTest.m @@ -43,11 +43,6 @@ @interface ZMBaseManagedObjectTest () @implementation ZMBaseManagedObjectTest -- (BOOL)shouldUseRealKeychain; -{ - return NO; -} - - (BOOL)shouldUseInMemoryStore; { return YES; @@ -81,8 +76,6 @@ - (void)setUp; { [super setUp]; - [ZMPersistentCookieStorage setDoNotPersistToKeychain:!self.shouldUseRealKeychain]; - self.originalConversationLastReadTimestampTimerValue = ZMConversationDefaultLastReadTimestampSaveDelay; ZMConversationDefaultLastReadTimestampSaveDelay = 0.02; diff --git a/wire-ios-mocktransport/Source/Login/MockTransportSession+login.m b/wire-ios-mocktransport/Source/Login/MockTransportSession+login.m index 2e5ba2f8dd0..b37ba5ac2de 100644 --- a/wire-ios-mocktransport/Source/Login/MockTransportSession+login.m +++ b/wire-ios-mocktransport/Source/Login/MockTransportSession+login.m @@ -87,9 +87,10 @@ - (ZMTransportResponse *)processLoginRequest:(ZMTransportRequest *)request; NSString *cookiesValue = @"zuid=something; Path=/access; Expires=Tue, 06-Oct-2099 11:46:18 GMT; HttpOnly; Secure"; - if ([ZMPersistentCookieStorage cookiesPolicy] != NSHTTPCookieAcceptPolicyNever) { - self.cookieStorage.authenticationCookieData = [NSHTTPCookie validCookieDataWithString:cookiesValue]; - } + NSDictionary *cookieHeaders = @{@"Set-Cookie": cookiesValue}; + NSURL *url = [NSURL URLWithString:@"https://example.com"]; + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:cookieHeaders forURL:url]; + [self.cookieStorage storeCookies:cookies error:nil]; self.selfUser = user; self.clientCompletedLogin = YES; diff --git a/wire-ios-mocktransport/Source/MockTransportSession.h b/wire-ios-mocktransport/Source/MockTransportSession.h index e9b7fe5424b..c8dc838d6aa 100644 --- a/wire-ios-mocktransport/Source/MockTransportSession.h +++ b/wire-ios-mocktransport/Source/MockTransportSession.h @@ -51,7 +51,7 @@ typedef ZMTransportResponse * _Nullable (^ZMCustomResponseGeneratorBlock)(ZMTran @property (nonatomic) NSURL *baseURL; @property (nonatomic) NSString *clientID; -@property (nonatomic) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic) LegacyCookieStorage *cookieStorage; @property (readonly, nonatomic) NSManagedObjectContext *managedObjectContext; @property (nonatomic, readonly, copy) ZMCompletionHandlerBlock accessTokenFailureHandler; @property (nonatomic, readonly, copy) ZMAccessTokenHandlerBlock accessTokenSuccessHandler; diff --git a/wire-ios-mocktransport/Source/Registration/MockTransportSession+registration.m b/wire-ios-mocktransport/Source/Registration/MockTransportSession+registration.m index 08cb01e833e..fa9e3cd3af5 100644 --- a/wire-ios-mocktransport/Source/Registration/MockTransportSession+registration.m +++ b/wire-ios-mocktransport/Source/Registration/MockTransportSession+registration.m @@ -124,9 +124,10 @@ - (ZMTransportResponse *)processRegistrationRequest:(ZMTransportRequest *)reques NSString *cookiesValue = @"zuid=something; Path=/access; Expires=Tue, 06-Oct-2099 11:46:18 GMT; HttpOnly; Secure"; - if ([ZMPersistentCookieStorage cookiesPolicy] != NSHTTPCookieAcceptPolicyNever) { - self.cookieStorage.authenticationCookieData = [NSHTTPCookie validCookieDataWithString:cookiesValue]; - } + NSDictionary *headers = @{@"Set-Cookie": cookiesValue}; + NSURL *url = [NSURL URLWithString:@"https://example.com"]; + NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers forURL:url]; + [self.cookieStorage storeCookies:cookies error:nil]; return [ZMTransportResponse responseWithPayload:payload HTTPStatus:200 transportSessionError:nil headers:@{@"Set-Cookie": cookiesValue} apiVersion:request.apiVersion]; } diff --git a/wire-ios-mocktransport/Tests/Source/Login/MockTransportSessionLoginTests.m b/wire-ios-mocktransport/Tests/Source/Login/MockTransportSessionLoginTests.m index caadea5573b..df01914309d 100644 --- a/wire-ios-mocktransport/Tests/Source/Login/MockTransportSessionLoginTests.m +++ b/wire-ios-mocktransport/Tests/Source/Login/MockTransportSessionLoginTests.m @@ -40,8 +40,8 @@ - (void)testThatLoginSucceedsAndSetsTheCookieWithEmail }]; WaitForAllGroupsToBeEmpty(0.5); - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage expect] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage expect] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN NSString *path = @"/login"; @@ -70,8 +70,8 @@ - (void)testThatLoginSucceedsAndSetsTheCookieWithPhoneNumberAfterRequestingALogi }]; WaitForAllGroupsToBeEmpty(0.5); - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage expect] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage expect] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN [self responseForPayload:@{@"phone":phone} path:@"/login/send" method:ZMTransportRequestMethodPost apiVersion:0]; @@ -104,8 +104,8 @@ - (void)testThatLoginSucceedsAndSetsTheCookie_WhenLoggingInWithCorrectEmailVerif }]; WaitForAllGroupsToBeEmpty(0.5); - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage expect] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage expect] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN ZMTransportResponse *verificationCodeSendResponse = [self responseForPayload:@{@"email":email, @"action":action} @@ -145,8 +145,8 @@ - (void)testThatLoginFails_WhenLoggingInWithMissingEmailVerificationCode }]; WaitForAllGroupsToBeEmpty(0.5); - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage expect] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage expect] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN ZMTransportResponse *verificationCodeSendResponse = [self responseForPayload:@{ @@ -184,8 +184,8 @@ - (void)testThatPhoneLoginFailsIfTheLoginCodeIsWrong }]; WaitForAllGroupsToBeEmpty(0.5); - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage reject] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage reject] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN [self responseForPayload:@{@"phone":phone} path:@"/login/send" method:ZMTransportRequestMethodPost apiVersion:0]; @@ -218,8 +218,8 @@ - (void)testThatLoginFails_WhenLoggingInWithIncorrectEmailVerificationCode selfUser.password = password; }]; - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage reject] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage reject] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN ZMTransportResponse *verificationCodeSendResponse = [self responseForPayload:@{@"email":email, @"action":action} @@ -258,8 +258,8 @@ - (void)testThatPhoneLoginFailsIfThereIsNoUserWithSuchPhone }]; WaitForAllGroupsToBeEmpty(0.5); - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage reject] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage reject] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN [self responseForPayload:@{@"phone":phone} path:@"/login/send" method:ZMTransportRequestMethodPost apiVersion:0]; @@ -333,8 +333,8 @@ - (void)testThatPhoneLoginFailsIfNoVerificationCodeWasRequested }]; WaitForAllGroupsToBeEmpty(0.5); - self.sut.cookieStorage = [OCMockObject mockForClass:[ZMPersistentCookieStorage class]]; - [[(id) self.sut.cookieStorage reject] setAuthenticationCookieData:OCMOCK_ANY]; + self.sut.cookieStorage = [OCMockObject mockForClass:[LegacyCookieStorage class]]; + [[(id) self.sut.cookieStorage reject] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN ZMTransportResponse *response = [self responseForPayload:@{ @@ -393,7 +393,7 @@ - (void)testThatLoginFailsAndDoesNotSetTheCookie selfUser.password = password; }]; WaitForAllGroupsToBeEmpty(0.5); - [[(id) self.cookieStorage reject] setAuthenticationCookieData:OCMOCK_ANY]; + [[(id) self.cookieStorage reject] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN NSString *path = @"/login"; @@ -425,7 +425,7 @@ - (void)testThatLoginFailsForWrongEmailAndDoesNotSetTheCookie selfUser.password = password; }]; WaitForAllGroupsToBeEmpty(0.5); - [[(id) self.cookieStorage reject] setAuthenticationCookieData:OCMOCK_ANY]; + [[(id) self.cookieStorage reject] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; // WHEN diff --git a/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.h b/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.h index a8e70f49607..4da247efd09 100644 --- a/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.h +++ b/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.h @@ -40,7 +40,7 @@ @interface MockTransportSessionTests : ZMTBaseTest @property (nonatomic) MockTransportSession *sut; -@property (nonatomic) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic) LegacyCookieStorage *cookieStorage; /// Array of TestPushChannelEvent @property (nonatomic) NSMutableArray *pushChannelReceivedEvents; @property (nonatomic) NSUInteger pushChannelDidOpenCount; diff --git a/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.m b/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.m index dbfe105ea8b..bb7675774df 100644 --- a/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.m +++ b/wire-ios-mocktransport/Tests/Source/MockTransportSessionTests.m @@ -110,7 +110,7 @@ - (void)setUp { [super setUp]; self.pushChannelReceivedEvents = [NSMutableArray array]; - self.cookieStorage = [OCMockObject niceMockForClass:[ZMPersistentCookieStorage class]]; + self.cookieStorage = [OCMockObject niceMockForClass:[LegacyCookieStorage class]]; self.sut = [[MockTransportSession alloc] initWithDispatchGroup:self.dispatchGroup]; self.sut.cookieStorage = self.cookieStorage; } diff --git a/wire-ios-mocktransport/Tests/Source/Registration/MockTransportSessionRegistrationTests.m b/wire-ios-mocktransport/Tests/Source/Registration/MockTransportSessionRegistrationTests.m index 8b87919467a..77de00ce5d1 100644 --- a/wire-ios-mocktransport/Tests/Source/Registration/MockTransportSessionRegistrationTests.m +++ b/wire-ios-mocktransport/Tests/Source/Registration/MockTransportSessionRegistrationTests.m @@ -126,47 +126,24 @@ - (void)testThatRegistrationWithEmailReturnsCookies XCTAssertEqualObjects([(NSHTTPCookie *)cookies.firstObject name], @"zuid"); } -- (void)testThatRegistrationWithEmailStoresCookiesIfPolicyIsAlways +- (void)testThatRegistrationWithEmailStoresCookies { // GIVEN - [ZMPersistentCookieStorage setCookiesPolicy:NSHTTPCookieAcceptPolicyAlways]; NSDictionary *payload = @{ @"name" : @"Someone someone", @"email" : @"someone@example.com", @"password" : @"supersecure", }; - - // WHEN - __unused ZMTransportResponse *response = [self responseForPayload:payload path:@"/register" method:ZMTransportRequestMethodPost apiVersion:0]; - - // expect - __block NSData *cookieData; - [[(id) self.sut.cookieStorage expect] setAuthenticationCookieData:ZM_ARG_SAVE(cookieData)]; - - WaitForAllGroupsToBeEmpty(0.5); -} -- (void)testThatRegistrationWithEmailDoesNotStoreCookiesIfPolicyIsNever -{ - // GIVEN - [ZMPersistentCookieStorage setCookiesPolicy:NSHTTPCookieAcceptPolicyAlways]; - NSDictionary *payload = @{ - @"name" : @"Someone someone", - @"email" : @"someone@example.com", - @"password" : @"supersecure", - }; - // WHEN __unused ZMTransportResponse *response = [self responseForPayload:payload path:@"/register" method:ZMTransportRequestMethodPost apiVersion:0]; - + // expect - __block NSData *cookieData; - [[(id) self.sut.cookieStorage reject] setAuthenticationCookieData:ZM_ARG_SAVE(cookieData)]; - + [[(id) self.sut.cookieStorage expect] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; + WaitForAllGroupsToBeEmpty(0.5); } - - (void)testThatRegistrationCreatesAUserWithNoValidatedEmail { // GIVEN @@ -374,17 +351,16 @@ - (void)testThatRegistrationWithPhoneNumberReturns201AndSetsTheCookie @"phone_code" : self.sut.phoneVerificationCodeForRegistration, }; [self requestVerificationCodeForPhone:phone]; - + // expect - __block NSData *cookieData; - [[(id) self.sut.cookieStorage expect] setAuthenticationCookieData:ZM_ARG_SAVE(cookieData)]; - + [[(id) self.sut.cookieStorage expect] storeCookies:OCMOCK_ANY error:[OCMArg anyObjectRef]]; + // WHEN ZMTransportResponse *response = [self responseForPayload:payload path:@"/register" method:ZMTransportRequestMethodPost apiVersion:0]; - + // THEN XCTAssertEqual(response.HTTPStatus, 200); - XCTAssertNotNil(cookieData); + [(id)self.sut.cookieStorage verify]; } - (void)testThatRegistrationWithPhoneNumberReturns409ItThereIsAlreadyAUserWithThatPhone diff --git a/wire-ios-share-engine/Sources/BackendEnvironmentProvider+CookieStorage.swift b/wire-ios-share-engine/Sources/BackendEnvironmentProvider+CookieStorage.swift index d6112d04b1d..af179b6e2fb 100644 --- a/wire-ios-share-engine/Sources/BackendEnvironmentProvider+CookieStorage.swift +++ b/wire-ios-share-engine/Sources/BackendEnvironmentProvider+CookieStorage.swift @@ -17,15 +17,18 @@ // import WireDataModel +import WireFoundation +import WireNetwork import WireTransport extension BackendEnvironmentProvider { - func cookieStorage(for account: Account) -> ZMPersistentCookieStorage { - let backendURL = backendURL.host! - return ZMPersistentCookieStorage( - forServerName: backendURL, + func cookieStorage(for account: Account) -> LegacyCookieStorage { + let cookieStorage = CookieStorage( + cookieEncryptionKey: UserDefaults.cookiesKey() + ) + return LegacyCookieStorage( userIdentifier: account.userIdentifier, - useCache: false + cookieStorage: cookieStorage ) } @@ -33,3 +36,5 @@ extension BackendEnvironmentProvider { cookieStorage(for: account).hasAuthenticationCookie } } + +extension CookieStorage: WireTransport.CookieStorageProtocol {} diff --git a/wire-ios-share-engine/Sources/SharingSession.swift b/wire-ios-share-engine/Sources/SharingSession.swift index 949ca64c16e..af9172943dc 100644 --- a/wire-ios-share-engine/Sources/SharingSession.swift +++ b/wire-ios-share-engine/Sources/SharingSession.swift @@ -292,9 +292,7 @@ public final class SharingSession { let networkServices = try await networkStack.networkServices let metadata = try await networkStack.resolvedBackendMetadata() let cookieStorage = CookieStorage( - userID: accountIdentifier, - cookieEncryptionKey: UserDefaults.cookiesKey(), - keychain: Keychain() + cookieEncryptionKey: UserDefaults.cookiesKey() ) let isMLSEnabled = journal[.isBackendMLSEnabled] diff --git a/wire-ios-share-engine/Sources/SharingSessionLoader.swift b/wire-ios-share-engine/Sources/SharingSessionLoader.swift index 4314a72ff1a..333ab3b257d 100644 --- a/wire-ios-share-engine/Sources/SharingSessionLoader.swift +++ b/wire-ios-share-engine/Sources/SharingSessionLoader.swift @@ -260,12 +260,11 @@ public struct SharingSessionLoader { coreDataStack: CoreDataStack ) async throws -> SharingSession { let legacyEnvironment = BackendEnvironment(environment) - // Don't cache the cookie because if the user logs out and back in again in the main app - // process, then the cached cookie will be invalid. - let legacyCookieStorage = ZMPersistentCookieStorage( - forServerName: legacyEnvironment.backendURL.host!, + let legacyCookieStorage = LegacyCookieStorage( userIdentifier: accountID, - useCache: false + cookieStorage: CookieStorage( + cookieEncryptionKey: UserDefaults.cookiesKey() + ) ) guard legacyCookieStorage.hasAuthenticationCookie else { throw Failure.mainAppRequired(message: "no authentication cookie") @@ -358,9 +357,7 @@ public struct SharingSessionLoader { localDomain: backendMetadata.domain ) let cookieStorage = CookieStorage( - userID: accountID, - cookieEncryptionKey: UserDefaults.cookiesKey(), - keychain: Keychain() + cookieEncryptionKey: UserDefaults.cookiesKey() ) let userSessionComponent = UserSessionComponent( currentBuildNumber: buildNumber, diff --git a/wire-ios-sync-engine/Source/SessionManager/BackendEnvironmentProvider+Cookie.swift b/wire-ios-sync-engine/Source/SessionManager/BackendEnvironmentProvider+Cookie.swift index 86a48c365a5..30f8b06c5fa 100644 --- a/wire-ios-sync-engine/Source/SessionManager/BackendEnvironmentProvider+Cookie.swift +++ b/wire-ios-sync-engine/Source/SessionManager/BackendEnvironmentProvider+Cookie.swift @@ -16,15 +16,18 @@ // along with this program. If not, see http://www.gnu.org/licenses/. // +import WireFoundation +import WireNetwork import WireTransport extension BackendEnvironmentProvider { - func cookieStorage(for account: Account) -> ZMPersistentCookieStorage { - let backendURL = backendURL.host! - return ZMPersistentCookieStorage( - forServerName: backendURL, + func cookieStorage(for account: Account) -> LegacyCookieStorage { + let cookieStorage = CookieStorage( + cookieEncryptionKey: UserDefaults.cookiesKey() + ) + return LegacyCookieStorage( userIdentifier: account.userIdentifier, - useCache: true + cookieStorage: cookieStorage ) } @@ -36,3 +39,5 @@ extension BackendEnvironmentProvider { return expirationDate.timeIntervalSinceNow > 0 } } + +extension CookieStorage: WireTransport.CookieStorageProtocol {} diff --git a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift index 26d73ba0574..b008b718bde 100644 --- a/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift +++ b/wire-ios-sync-engine/Source/SessionManager/SessionManager.swift @@ -337,6 +337,7 @@ public final class SessionManager: NSObject, SessionManagerType { var notificationCenter: UserNotificationCenterAbstraction = .wrapper(.current()) let unauthenticatedSessionFactory: UnauthenticatedSessionFactory + private let cookieStorage: CookieStorage private let sessionLoadingQueue: DispatchQueue = .init(label: "sessionLoadingQueue") @@ -413,6 +414,7 @@ public final class SessionManager: NSObject, SessionManagerType { maxNumberAccounts: Int = defaultMaxNumberAccounts, currentAppVersion: String, currentBuildNumber: String, + cookieStorage: CookieStorage, mediaManager: MediaManagerType, delegate: SessionManagerDelegate?, application: ZMApplication, @@ -454,6 +456,7 @@ public final class SessionManager: NSObject, SessionManagerType { maxNumberAccounts: maxNumberAccounts, currentAppVersion: currentAppVersion, currentBuildNumber: currentBuildNumber, + cookieStorage: cookieStorage, mediaManager: mediaManager, unauthenticatedSessionFactory: unauthenticatedSessionFactory, reachability: reachability, @@ -518,6 +521,7 @@ public final class SessionManager: NSObject, SessionManagerType { maxNumberAccounts: Int = defaultMaxNumberAccounts, currentAppVersion: String, currentBuildNumber: String, + cookieStorage: CookieStorage, mediaManager: MediaManagerType, unauthenticatedSessionFactory: UnauthenticatedSessionFactory, reachability: ReachabilityWrapper, @@ -545,6 +549,7 @@ public final class SessionManager: NSObject, SessionManagerType { self.environment = environment self.currentAppVersion = currentAppVersion self.currentBuildNumber = currentBuildNumber + self.cookieStorage = cookieStorage self.application = application self.delegate = delegate self.dispatchGroup = dispatchGroup @@ -1005,6 +1010,7 @@ public final class SessionManager: NSObject, SessionManagerType { let loader = try UserSessionLoader( account: account, accountManager: accountManager, + cookieStorage: cookieStorage, sharedContainerURL: sharedContainerURL, defaultEnvironment: defaultEnvironment, legacyEnvironment: environment, @@ -1147,7 +1153,11 @@ public final class SessionManager: NSObject, SessionManagerType { fileprivate func deleteAccountData(for account: Account) { WireLogger.sessionManager.debug("Deleting the data for \(account.userName) -- \(account.userIdentifier)") WireLogger.session.debug("Deleting the data for account \(account)") - environment.cookieStorage(for: account).deleteKeychainItems() + do { + try environment.cookieStorage(for: account).removeCookies() + } catch { + WireLogger.sessionManager.error("Failed to remove cookies: \(error)") + } account.deleteKeychainItems() clearCRLExpirationDates(for: account) diff --git a/wire-ios-sync-engine/Source/SessionManager/URLActions.swift b/wire-ios-sync-engine/Source/SessionManager/URLActions.swift index e646afe3e56..d1bea47f878 100644 --- a/wire-ios-sync-engine/Source/SessionManager/URLActions.swift +++ b/wire-ios-sync-engine/Source/SessionManager/URLActions.swift @@ -202,13 +202,12 @@ extension URLAction { throw CompanyLoginError.missingRequiredParameter } - guard let cookieData = HTTPCookie.extractCookieData(from: cookieString, url: url) else { + let cookies = HTTPCookie.cookies(from: cookieString, for: url) + guard !cookies.isEmpty else { throw CompanyLoginError.invalidCookie } - let cookies = HTTPCookie.cookies(from: cookieString, for: url) - - let userInfo = UserInfo(identifier: userID, cookieData: cookieData, cookies: cookies) + let userInfo = UserInfo(identifier: userID, cookies: cookies) self = .companyLoginSuccess(userInfo: userInfo) case URL.Path.failure: diff --git a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift index 247ab7286a0..031a0b5160c 100644 --- a/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift +++ b/wire-ios-sync-engine/Source/SessionManager/UserSessionLoader.swift @@ -32,6 +32,7 @@ final class UserSessionLoader { private let account: Account private let accountManager: AccountManager + private let cookieStorage: CookieStorage private let sharedContainerURL: URL private let defaultEnvironment: BackendEnvironment2 private let legacyEnvironment: WireTransport.BackendEnvironment @@ -56,6 +57,7 @@ final class UserSessionLoader { init( account: Account, accountManager: AccountManager, + cookieStorage: CookieStorage, sharedContainerURL: URL, defaultEnvironment: BackendEnvironment2, legacyEnvironment: WireTransport.BackendEnvironment, @@ -73,6 +75,7 @@ final class UserSessionLoader { ) throws { self.account = account self.accountManager = accountManager + self.cookieStorage = cookieStorage self.sharedContainerURL = sharedContainerURL self.defaultEnvironment = defaultEnvironment self.legacyEnvironment = legacyEnvironment @@ -185,18 +188,13 @@ final class UserSessionLoader { let networkServices = try await networkStack.networkServices // Store any new cookies. - let cookieStorage = CookieStorage( - userID: accountID, - cookieEncryptionKey: UserDefaults.cookiesKey(), - keychain: Keychain() - ) - if let cookies = newEnvironment?.cookies { - try await cookieStorage.storeCookies(cookies) + try cookieStorage.storeCookies(cookies, userID: accountID) } // Check if this backend supports MLS. if let isBackendMLSEnabled = try await isBackendMLSEnabled( + accountID: accountID, networkService: networkServices.rest, cookieStorage: cookieStorage, apiVersion: metadata.apiVersion @@ -578,12 +576,14 @@ final class UserSessionLoader { } private func isBackendMLSEnabled( + accountID: UUID, networkService: NetworkService, cookieStorage: CookieStorage, apiVersion: WireNetwork.APIVersion ) async throws -> Bool? { do { let authenticationManager = AuthenticationManager( + userID: accountID, clientID: nil, cookieStorage: cookieStorage, networkService: networkService, diff --git a/wire-ios-sync-engine/Source/Synchronization/ApplicationStatusDirectory.swift b/wire-ios-sync-engine/Source/Synchronization/ApplicationStatusDirectory.swift index 51c2cd3409b..66e7d746a6b 100644 --- a/wire-ios-sync-engine/Source/Synchronization/ApplicationStatusDirectory.swift +++ b/wire-ios-sync-engine/Source/Synchronization/ApplicationStatusDirectory.swift @@ -38,7 +38,7 @@ public final class ApplicationStatusDirectory: NSObject, ApplicationStatus { public init( withManagedObjectContext managedObjectContext: NSManagedObjectContext, - cookieStorage: ZMPersistentCookieStorage, + cookieStorage: any CookieProvider, requestCancellation: ZMRequestCancellation, application: ZMApplication, coreCryptoProvider: CoreCryptoProviderProtocol, diff --git a/wire-ios-sync-engine/Source/Synchronization/Strategies/DeleteAccountRequestStrategy.swift b/wire-ios-sync-engine/Source/Synchronization/Strategies/DeleteAccountRequestStrategy.swift index 8c4018e2a24..d67d43dbe82 100644 --- a/wire-ios-sync-engine/Source/Synchronization/Strategies/DeleteAccountRequestStrategy.swift +++ b/wire-ios-sync-engine/Source/Synchronization/Strategies/DeleteAccountRequestStrategy.swift @@ -25,14 +25,11 @@ public final class DeleteAccountRequestStrategy: AbstractRequestStrategy, ZMSing fileprivate static let path: String = "/self" public static let userDeletionInitiatedKey: String = "ZMUserDeletionInitiatedKey" fileprivate(set) var deleteSync: ZMSingleRequestSync! - let cookieStorage: ZMPersistentCookieStorage - public init( + public override init( withManagedObjectContext moc: NSManagedObjectContext, - applicationStatus: ApplicationStatus, - cookieStorage: ZMPersistentCookieStorage + applicationStatus: ApplicationStatus ) { - self.cookieStorage = cookieStorage super.init(withManagedObjectContext: moc, applicationStatus: applicationStatus) self.configuration = [ .allowsRequestsWhileUnauthenticated, diff --git a/wire-ios-sync-engine/Source/Synchronization/StrategyDirectory.swift b/wire-ios-sync-engine/Source/Synchronization/StrategyDirectory.swift index 7a7b9c69374..f7dca4d55bc 100644 --- a/wire-ios-sync-engine/Source/Synchronization/StrategyDirectory.swift +++ b/wire-ios-sync-engine/Source/Synchronization/StrategyDirectory.swift @@ -42,7 +42,6 @@ public class StrategyDirectory: NSObject, StrategyDirectoryProtocol { init( contextProvider: ContextProvider, applicationStatusDirectory: ApplicationStatusDirectory, - cookieStorage: ZMPersistentCookieStorage, pushMessageHandler: PushMessageHandler, flowManager: FlowManagerType, localNotificationDispatcher: LocalNotificationDispatcher, @@ -56,7 +55,6 @@ public class StrategyDirectory: NSObject, StrategyDirectoryProtocol { self.strategies = Self.buildStrategies( contextProvider: contextProvider, applicationStatusDirectory: applicationStatusDirectory, - cookieStorage: cookieStorage, pushMessageHandler: pushMessageHandler, flowManager: flowManager, localNotificationDispatcher: localNotificationDispatcher, @@ -90,7 +88,6 @@ public class StrategyDirectory: NSObject, StrategyDirectoryProtocol { static func buildStrategies( contextProvider: ContextProvider, applicationStatusDirectory: ApplicationStatusDirectory, - cookieStorage: ZMPersistentCookieStorage, pushMessageHandler: PushMessageHandler, flowManager: FlowManagerType, localNotificationDispatcher: LocalNotificationDispatcher, @@ -133,8 +130,7 @@ public class StrategyDirectory: NSObject, StrategyDirectoryProtocol { ), DeleteAccountRequestStrategy( withManagedObjectContext: syncMOC, - applicationStatus: applicationStatusDirectory, - cookieStorage: cookieStorage + applicationStatus: applicationStatusDirectory ), AssetV3UploadRequestStrategy( withManagedObjectContext: syncMOC, diff --git a/wire-ios-sync-engine/Source/Synchronization/ZMOperationLoop.h b/wire-ios-sync-engine/Source/Synchronization/ZMOperationLoop.h index 76178bd08e8..d1868ccd96e 100644 --- a/wire-ios-sync-engine/Source/Synchronization/ZMOperationLoop.h +++ b/wire-ios-sync-engine/Source/Synchronization/ZMOperationLoop.h @@ -25,7 +25,7 @@ @protocol RequestStrategy; @protocol UpdateEventProcessor; -@class ZMPersistentCookieStorage; +@class LegacyCookieStorage; @class OperationStatus; @class ZMSyncStrategy; diff --git a/wire-ios-sync-engine/Source/UnauthenticatedSession/UnauthenticatedSession.swift b/wire-ios-sync-engine/Source/UnauthenticatedSession/UnauthenticatedSession.swift index 47409f32acb..4928ea4b869 100644 --- a/wire-ios-sync-engine/Source/UnauthenticatedSession/UnauthenticatedSession.swift +++ b/wire-ios-sync-engine/Source/UnauthenticatedSession/UnauthenticatedSession.swift @@ -17,6 +17,7 @@ // import Foundation +import WireLogging import WireNetwork import WireUtilities @@ -162,8 +163,13 @@ extension UnauthenticatedSession: UserInfoParser { public func upgradeToAuthenticatedSession(with userInfo: UserInfo) { let account = Account(userName: "", userIdentifier: userInfo.identifier) let cookieStorage = transportSession.environment.cookieStorage(for: account) - cookieStorage.authenticationCookieData = userInfo.cookieData - authenticationStatus.authenticationCookieData = userInfo.cookieData + do { + try cookieStorage.storeCookies(userInfo.cookies) + } catch { + let errorDescription = (error as NSError).safeForLoggingDescription + WireLogger.authentication.critical("Failed to store cookies: \(errorDescription)", attributes: .safePublic) + } + authenticationStatus.didReceiveAuthenticationCookies = true delegate?.session( session: self, createdAccount: account, @@ -177,8 +183,13 @@ extension UnauthenticatedSession: UserInfoParser { ) { let account = Account(userName: "", userIdentifier: userInfo.identifier) let cookieStorage = transportSession.environment.cookieStorage(for: account) - cookieStorage.authenticationCookieData = userInfo.cookieData - authenticationStatus.authenticationCookieData = userInfo.cookieData + do { + try cookieStorage.storeCookies(userInfo.cookies) + } catch { + let errorDescription = (error as NSError).safeForLoggingDescription + WireLogger.authentication.critical("Failed to store cookies: \(errorDescription)", attributes: .safePublic) + } + authenticationStatus.didReceiveAuthenticationCookies = true delegate?.session( session: self, createdAccount: account, diff --git a/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.h b/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.h index 0f4df095563..79fe89ad561 100644 --- a/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.h +++ b/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.h @@ -25,7 +25,7 @@ @class UserCredentials; @class UserEmailCredentials; @class UserPhoneCredentials; -@class ZMPersistentCookieStorage; +@class LegacyCookieStorage; @class ZMTransportResponse; @protocol UserInfoParser; @protocol NotificationContext; @@ -79,7 +79,7 @@ typedef NS_ENUM(NSUInteger, ZMAuthenticationPhase) { @property (nonatomic, readonly) ZMAuthenticationPhase currentPhase; @property (nonatomic, readonly) NSUUID *authenticatedUserIdentifier; -@property (nonatomic) NSData *authenticationCookieData; +@property (nonatomic) BOOL didReceiveAuthenticationCookies; - (instancetype)initWithDelegate:(id)delegate groupQueue:(id)groupQueue diff --git a/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.m b/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.m index b866703d9d7..38da8d960e9 100644 --- a/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.m +++ b/wire-ios-sync-engine/Source/UnauthenticatedSession/ZMAuthenticationStatus.m @@ -94,7 +94,6 @@ - (void)setLoginCredentials:(UserCredentials *)credentials { if(credentials != self.internalLoginCredentials) { self.internalLoginCredentials = credentials; - [ZMPersistentCookieStorage setCookiesPolicy:NSHTTPCookieAcceptPolicyAlways]; [[[NotificationInContext alloc] initWithNotificationCenter:[NSNotificationCenter defaultCenter] name:AuthenticationCenterDataChangeNotificationName context:self @@ -143,7 +142,7 @@ - (BOOL)needsCredentialsToLogin - (BOOL)isLoggedIn { - return nil != self.authenticationCookieData; + return self.didReceiveAuthenticationCookies; } - (void)startLoginTimer @@ -170,7 +169,7 @@ - (void)timerDidFire:(ZMTimer *)timer - (void)prepareForLoginWithCredentials:(UserCredentials *)credentials { ZMLogDebug(@"%@", NSStringFromSelector(_cmd)); - self.authenticationCookieData = nil; + self.didReceiveAuthenticationCookies = NO; [self resetLoginAndRegistrationStatus]; self.loginCredentials = credentials; self.isWaitingForLogin = YES; @@ -308,13 +307,6 @@ - (void)cancelWaitingForEmailVerification ZMLogDebug(@"current phase: %lu", (unsigned long)self.currentPhase); } -- (void)setAuthenticationCookieData:(NSData *)data; -{ - ZMLogDebug(@"Setting cookie data: %@", @(data.length)); - _authenticationCookieData = data; - ZMLogDebug(@"current phase: %lu", (unsigned long)self.currentPhase); -} - - (void)didCompleteRequestForLoginCodeSuccessfully { ZMLogDebug(@"%@", NSStringFromSelector(_cmd)); diff --git a/wire-ios-transport/Source/Public/CookieProvider.swift b/wire-ios-sync-engine/Source/UserSession/CookieProvider.swift similarity index 85% rename from wire-ios-transport/Source/Public/CookieProvider.swift rename to wire-ios-sync-engine/Source/UserSession/CookieProvider.swift index 57225492d4f..6fbe3be1d24 100644 --- a/wire-ios-transport/Source/Public/CookieProvider.swift +++ b/wire-ios-sync-engine/Source/UserSession/CookieProvider.swift @@ -20,11 +20,11 @@ import Foundation public protocol CookieProvider { var isAuthenticated: Bool { get } - func setRequestHeaderFieldsOn(_ request: NSMutableURLRequest) - func deleteKeychainItems() + func setRequestHeaderFields(on request: NSMutableURLRequest) + func removeCookies() throws } -extension ZMPersistentCookieStorage: CookieProvider { +extension LegacyCookieStorage: CookieProvider { public var isAuthenticated: Bool { hasAuthenticationCookie diff --git a/wire-ios-sync-engine/Source/UserSession/ZMClientRegistrationStatus.swift b/wire-ios-sync-engine/Source/UserSession/ZMClientRegistrationStatus.swift index 74a3b0ac875..3a86ffd8877 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMClientRegistrationStatus.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMClientRegistrationStatus.swift @@ -387,7 +387,12 @@ public class ZMClientRegistrationStatus: NSObject, ClientRegistrationDelegate { @objc public func invalidateCookieAndNotify() { emailCredentials = nil - cookieProvider.deleteKeychainItems() + do { + try cookieProvider.removeCookies() + } catch { + let errorDescription = (error as NSError).safeForLoggingDescription + WireLogger.authentication.error("Failed to remove cookies: \(errorDescription)", attributes: .safePublic) + } let selfUser = ZMUser.selfUser(in: managedObjectContext) let outError = NSError.userSessionError( diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift index 4fdd9ed13c1..3edb9736dd7 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession+Authentication.swift @@ -17,6 +17,7 @@ // import Foundation +import WireLogging extension ZMUserSession { @@ -60,7 +61,12 @@ extension ZMUserSession { /// This will delete user data stored by WireSyncEngine in the keychain. func deleteUserKeychainItems() { - transportSession.cookieStorage.deleteKeychainItems() + do { + try transportSession.cookieStorage.removeCookies() + } catch { + let errorDescription = (error as NSError).safeForLoggingDescription + WireLogger.authentication.error("Failed to remove cookies: \(errorDescription)", attributes: .safePublic) + } } /// Logout the current user diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift index 0ef3592b47b..45fd4c2d267 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSession.swift @@ -470,7 +470,7 @@ public final class ZMUserSession: NSObject { dependencies: UserSessionDependencies, journal: Journal, logFilesProvider: LogFilesProviding, - cookieStorage: any CookieStorageProtocol, + cookieStorage: any WireNetwork.CookieStorageProtocol, faultyMLSRemovalKeysByDomain: [String: [String]], updateBackendMetadataUseCase: any UpdateBackendMetadataUseCaseProtocol ) { @@ -825,7 +825,6 @@ public final class ZMUserSession: NSObject { StrategyDirectory( contextProvider: coreDataStack, applicationStatusDirectory: applicationStatusDirectory, - cookieStorage: transportSession.cookieStorage, pushMessageHandler: localNotificationDispatcher!, flowManager: flowManager, localNotificationDispatcher: localNotificationDispatcher!, @@ -1550,7 +1549,15 @@ extension ZMUserSession { let clientUpdateStatus = applicationStatusDirectory.clientUpdateStatus clientRegistrationStatus.emailCredentials = nil - clientRegistrationStatus.cookieProvider.deleteKeychainItems() + do { + try clientRegistrationStatus.cookieProvider.removeCookies() + } catch { + let errorDescription = (error as NSError).safeForLoggingDescription + WireLogger.authentication.error( + "Failed to remove cookies: \(errorDescription)", + attributes: .safePublic + ) + } let selfUser = ZMUser.selfUser(in: syncContext) let clientDeletedRemotelyError = NSError.userSessionError( diff --git a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift index 89bd5863580..cd2027fc6d3 100644 --- a/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift +++ b/wire-ios-sync-engine/Source/UserSession/ZMUserSession/ZMUserSessionBuilder.swift @@ -102,9 +102,7 @@ struct ZMUserSessionBuilder { let keychain = WireFoundation.Keychain() let cookieStorage = CookieStorage( - userID: userId, - cookieEncryptionKey: UserDefaults.cookiesKey(), - keychain: keychain + cookieEncryptionKey: UserDefaults.cookiesKey() ) let serverTrustValidator = ServerTrustValidator( diff --git a/wire-ios-sync-engine/Tests/Source/E2EE/UserClientRequestStrategyTests.swift b/wire-ios-sync-engine/Tests/Source/E2EE/UserClientRequestStrategyTests.swift index ba1a51040ca..f830d5ec0cb 100644 --- a/wire-ios-sync-engine/Tests/Source/E2EE/UserClientRequestStrategyTests.swift +++ b/wire-ios-sync-engine/Tests/Source/E2EE/UserClientRequestStrategyTests.swift @@ -63,7 +63,7 @@ final class UserClientRequestStrategyTests: RequestStrategyTestBase { var clientUpdateStatus: ZMMockClientUpdateStatus! let fakeCredentialsProvider = FakeCredentialProvider() - var cookieStorage: ZMPersistentCookieStorage! + var cookieStorage: LegacyCookieStorage! var proteusService: MockProteusServiceInterface! var coreCryptoProvider: MockCoreCryptoProviderProtocol! @@ -78,10 +78,8 @@ final class UserClientRequestStrategyTests: RequestStrategyTestBase { self.setupProteusService() self.coreCryptoProvider = MockCoreCryptoProviderProtocol() - self.cookieStorage = ZMPersistentCookieStorage( - forServerName: "myServer", - userIdentifier: self.userIdentifier, - useCache: true + self.cookieStorage = LegacyCookieStorage( + testingWithUserIdentifier: self.userIdentifier ) self.mockClientRegistrationStatusDelegate = MockClientRegistrationStatusDelegate() self.clientRegistrationStatus = ZMMockClientRegistrationStatus( @@ -413,7 +411,7 @@ extension UserClientRequestStrategyTests { // given self.clientRegistrationStatus.prekeys = [(UInt16(1), "prekey1")] self.clientRegistrationStatus.lastResortPrekey = (ushort.max, "last-resort-prekey") - self.cookieStorage.authenticationCookieData = HTTPCookie.validCookieData() + try? self.cookieStorage.storeCookies(HTTPCookie.validCookies()) self.clientRegistrationStatus.mockPhase = .unregistered let client = self.createSelfClient(self.syncMOC) diff --git a/wire-ios-sync-engine/Tests/Source/MessagingTest.m b/wire-ios-sync-engine/Tests/Source/MessagingTest.m index 5c7de75226a..fc1332d6020 100644 --- a/wire-ios-sync-engine/Tests/Source/MessagingTest.m +++ b/wire-ios-sync-engine/Tests/Source/MessagingTest.m @@ -92,11 +92,6 @@ - (void)performPretendingUiMocIsSyncMoc:(void(^)(void))block; [self.uiMOC markAsUIContext]; } -- (BOOL)shouldUseRealKeychain; -{ - return NO; -} - - (BOOL)shouldUseInMemoryStore; { return YES; @@ -153,23 +148,6 @@ - (void)setUp; [self setupCaches]; - if (self.shouldUseRealKeychain) { - [ZMPersistentCookieStorage setDoNotPersistToKeychain:NO]; - -#if ! TARGET_IPHONE_SIMULATOR - // On the Xcode Continuous Intergration server the tests run as a user whose username starts with an underscore. - BOOL const runningOnIntegrationServer = [[[NSProcessInfo processInfo] environment][@"USER"] hasPrefix:@"_"]; - if (runningOnIntegrationServer) { - [ZMPersistentCookieStorage setDoNotPersistToKeychain:YES]; - } -#endif - } else { - [ZMPersistentCookieStorage setDoNotPersistToKeychain:YES]; - } - - ZMPersistentCookieStorage *cookieStorage = [[ZMPersistentCookieStorage alloc] init]; - [cookieStorage deleteKeychainItems]; - self.mockTransportSession = [[MockTransportSession alloc] initWithDispatchGroup:self.dispatchGroup]; Require([self waitForAllGroupsToBeEmptyWithTimeout:5]); diff --git a/wire-ios-sync-engine/Tests/Source/RecordingMockTransportSession.swift b/wire-ios-sync-engine/Tests/Source/RecordingMockTransportSession.swift index 111c9f212c6..c7270567a70 100644 --- a/wire-ios-sync-engine/Tests/Source/RecordingMockTransportSession.swift +++ b/wire-ios-sync-engine/Tests/Source/RecordingMockTransportSession.swift @@ -22,7 +22,7 @@ import WireTransport @objcMembers class RecordingMockTransportSession: NSObject, TransportSessionType { - var cookieStorage: ZMPersistentCookieStorage + var cookieStorage: LegacyCookieStorage var requestLoopDetectionCallback: ((String) -> Void)? let mockReachability = MockReachability() @@ -30,7 +30,7 @@ class RecordingMockTransportSession: NSObject, TransportSessionType { mockReachability } - init(cookieStorage: ZMPersistentCookieStorage) { + init(cookieStorage: LegacyCookieStorage) { self.cookieStorage = cookieStorage super.init() diff --git a/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/DeleteAccountRequestStrategyTests.swift b/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/DeleteAccountRequestStrategyTests.swift index ba7d4cc2cf8..d74729a0fc2 100644 --- a/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/DeleteAccountRequestStrategyTests.swift +++ b/wire-ios-sync-engine/Tests/Source/Synchronization/Strategies/DeleteAccountRequestStrategyTests.swift @@ -24,7 +24,6 @@ class DeleteAccountRequestStrategyTests: MessagingTest, AccountDeletedObserver { fileprivate var sut: DeleteAccountRequestStrategy! fileprivate var mockApplicationStatus: MockApplicationStatus! - fileprivate let cookieStorage = ZMPersistentCookieStorage() private var accountDeleted: Bool = false var observers: [Any] = [] @@ -33,8 +32,7 @@ class DeleteAccountRequestStrategyTests: MessagingTest, AccountDeletedObserver { mockApplicationStatus = MockApplicationStatus() sut = DeleteAccountRequestStrategy( withManagedObjectContext: uiMOC, - applicationStatus: mockApplicationStatus, - cookieStorage: cookieStorage + applicationStatus: mockApplicationStatus ) } diff --git a/wire-ios-sync-engine/Tests/Source/Synchronization/SynchronizationMocks.swift b/wire-ios-sync-engine/Tests/Source/Synchronization/SynchronizationMocks.swift index 9a1a708c7a1..bbf2926788d 100644 --- a/wire-ios-sync-engine/Tests/Source/Synchronization/SynchronizationMocks.swift +++ b/wire-ios-sync-engine/Tests/Source/Synchronization/SynchronizationMocks.swift @@ -19,6 +19,7 @@ import avs import Foundation import WireDataModel +import WireTransport @testable import WireSyncEngine @objcMembers @@ -198,8 +199,6 @@ class FakeCredentialProvider: NSObject, ZMCredentialProvider { } } -class FakeCookieStorage: ZMPersistentCookieStorage {} - @objc public class MockPushMessageHandler: NSObject, PushMessageHandler { diff --git a/wire-ios-sync-engine/Tests/Source/Synchronization/Transcoders/ZMLoginTranscoderTests.m b/wire-ios-sync-engine/Tests/Source/Synchronization/Transcoders/ZMLoginTranscoderTests.m index bbd0a145737..d5794769667 100644 --- a/wire-ios-sync-engine/Tests/Source/Synchronization/Transcoders/ZMLoginTranscoderTests.m +++ b/wire-ios-sync-engine/Tests/Source/Synchronization/Transcoders/ZMLoginTranscoderTests.m @@ -284,7 +284,7 @@ - (void)testThatItDoesNotGenerateADuplicateLoginRequestInTheRaceWindowWhileFirst - (void)testThatItDoesNotGenerateALoginRequestWhenTheUserSessionIsLoggedIn { // given - [self.authenticationStatus setAuthenticationCookieData:[@"foo" dataUsingEncoding:NSUTF8StringEncoding]]; + self.authenticationStatus.didReceiveAuthenticationCookies = YES; // when ZMTransportRequest *request = [self.sut nextRequestForAPIVersion:APIVersionV0]; @@ -455,7 +455,7 @@ - (void)testThatItDoesNotDeleteCredentialsButSwitchesToStateAuthenticatedOnLogin [[self.sut nextRequestForAPIVersion:APIVersionV0] completeWithResponse:response]; WaitForAllGroupsToBeEmpty(0.5); [self.authenticationStatus continueAfterBackupImportStep]; - [self.authenticationStatus setAuthenticationCookieData:[@"foo" dataUsingEncoding:NSUTF8StringEncoding]]; + self.authenticationStatus.didReceiveAuthenticationCookies = YES; WaitForAllGroupsToBeEmpty(0.5); }]; diff --git a/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.h b/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.h index 8d3b7597601..6b68963d026 100644 --- a/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.h +++ b/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.h @@ -26,13 +26,12 @@ @class MockUpdateEventProcessor; @class MockRequestCancellation; @class MockPushChannel; -@class ZMPersistentCookieStorage; @class RecordingMockTransportSession; @interface ZMOperationLoopTests : MessagingTest @property (nonatomic) ZMOperationLoop *sut; -@property (nonatomic) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic) LegacyCookieStorage *cookieStorage; @property (nonatomic) RecordingMockTransportSession *mockTransportSesssion; @property (nonatomic) ApplicationStatusDirectory *applicationStatusDirectory; @property (nonatomic) OperationStatus *operationStatus; diff --git a/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.m b/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.m index fcc32a1efe2..18a891ccce1 100644 --- a/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.m +++ b/wire-ios-sync-engine/Tests/Source/Synchronization/ZMOperationLoopTests.m @@ -25,13 +25,15 @@ #import "ZMSyncStrategy+ManagedObjectChanges.h" #import "ZMOperationLoopTests.h" +@import WireTransportSupport; + @implementation ZMOperationLoopTests; - (void)setUp { [super setUp]; - self.cookieStorage = [[FakeCookieStorage alloc] init]; + self.cookieStorage = [[LegacyCookieStorage alloc] initWithTestingWithUserIdentifier:[[NSUUID alloc] init]]; self.mockTransportSesssion = [[RecordingMockTransportSession alloc] initWithCookieStorage:self.cookieStorage]; self.mockRequestStrategy = [[MockRequestStrategy alloc] init]; diff --git a/wire-ios-sync-engine/Tests/Source/UnauthenticatedSession/URLActionProcessors/CompanyLoginURLActionProcessorTests.swift b/wire-ios-sync-engine/Tests/Source/UnauthenticatedSession/URLActionProcessors/CompanyLoginURLActionProcessorTests.swift index 551e4256dce..5425e86e56f 100644 --- a/wire-ios-sync-engine/Tests/Source/UnauthenticatedSession/URLActionProcessors/CompanyLoginURLActionProcessorTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UnauthenticatedSession/URLActionProcessors/CompanyLoginURLActionProcessorTests.swift @@ -51,8 +51,7 @@ final class CompanyLoginURLActionProcessorTests: ZMTBaseTest, UnauthenticatedSes func testThatAuthenticationStatusIsInformed_OnCompanyLoginSuccessAction() { // given let accountId = UUID() - let cookieData = Data("cookie".utf8) - let userInfo = UserInfo(identifier: accountId, cookieData: cookieData, cookies: []) + let userInfo = UserInfo(identifier: accountId, cookies: []) let action: URLAction = .companyLoginSuccess(userInfo: userInfo) // when diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/ApplicationStatusDirectoryTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/ApplicationStatusDirectoryTests.swift index f103b66dc6b..f5ab4c50eee 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/ApplicationStatusDirectoryTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/ApplicationStatusDirectoryTests.swift @@ -19,6 +19,7 @@ import Foundation import WireDataModelSupport +import WireTransportSupport @testable import WireSyncEngine class ApplicationStatusDirectoryTests: MessagingTest { @@ -28,7 +29,7 @@ class ApplicationStatusDirectoryTests: MessagingTest { override func setUp() { super.setUp() - let cookieStorage = ZMPersistentCookieStorage() + let cookieStorage = LegacyCookieStorage(testingWithUserIdentifier: UUID()) let mockApplication = ApplicationMock() syncMOC.performAndWait { diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/UnauthenticatedSessionTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/UnauthenticatedSessionTests.swift index b5859f81455..ee3fd1071fb 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/UnauthenticatedSessionTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/UnauthenticatedSessionTests.swift @@ -17,12 +17,13 @@ // import WireTesting +import WireTransportSupport import XCTest @testable import WireSyncEngine final class TestUnauthenticatedTransportSession: NSObject, UnauthenticatedTransportSessionProtocol { - public var cookieStorage = ZMPersistentCookieStorage() + public var cookieStorage = LegacyCookieStorage(testingWithUserIdentifier: UUID()) var nextEnqueueResult: EnqueueResult = .nilRequest var lastEnqueuedRequest: ZMTransportRequest? @@ -210,7 +211,7 @@ public final class UnauthenticatedSessionTests: ZMTBaseTest { // then XCTAssertEqual(account.userIdentifier, userId) - XCTAssertNotNil(transportSession.environment.cookieStorage(for: account).authenticationCookieData) + XCTAssertTrue(transportSession.environment.cookieStorage(for: account).hasAuthenticationCookie) } func testThatItParsesCookieDataAndDoesCallTheDelegateIfTheCookieIsValidAndThereIsAUserIdKeyId() throws { @@ -224,7 +225,7 @@ public final class UnauthenticatedSessionTests: ZMTBaseTest { // then XCTAssertEqual(account.userIdentifier, userId) - XCTAssertNotNil(transportSession.environment.cookieStorage(for: account).authenticationCookieData) + XCTAssertTrue(transportSession.environment.cookieStorage(for: account).hasAuthenticationCookie) } func testThatItDoesNotParseAnAccountWithWrongUserIdKey() { diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/ZMAuthenticationStatusTests.m b/wire-ios-sync-engine/Tests/Source/UserSession/ZMAuthenticationStatusTests.m index 5f761b4328e..f3d5bd0992a 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/ZMAuthenticationStatusTests.m +++ b/wire-ios-sync-engine/Tests/Source/UserSession/ZMAuthenticationStatusTests.m @@ -88,7 +88,7 @@ - (void)testThatAllValuesAreEmptyAfterInit - (void)testThatItIsLoggedInWhenThereIsAuthenticationDataSelfUserSyncedAndClientIsAlreadyRegistered { // when - self.sut.authenticationCookieData = [NSHTTPCookie validCookieData]; + self.sut.didReceiveAuthenticationCookies = YES; [self.uiMOC setPersistentStoreMetadata:@"someID" forKey:ZMPersistedClientIdKey]; ZMUser *selfUser = [ZMUser selfUserInContext:self.uiMOC]; selfUser.remoteIdentifier = [NSUUID new]; @@ -221,7 +221,7 @@ - (void)testThatItAsksForUserInfoParserIfAccountForBackupExists NSString *email = @"gfdgfgdfg@fds.sgf"; NSString *password = @"#$4tewt343$"; - UserInfo *info = [[UserInfo alloc] initWithIdentifier:NSUUID.createUUID cookieData:NSData.data cookies:@[]]; + UserInfo *info = [[UserInfo alloc] initWithIdentifier:NSUUID.createUUID cookies:@[]]; self.userInfoParser.existingAccounts = [self.userInfoParser.existingAccounts arrayByAddingObject:info]; // when @@ -247,8 +247,8 @@ @implementation ZMAuthenticationStatusTests (CredentialProvider) - (void)testThatItDoesNotReturnCredentialsIfItIsNotLoggedIn { // given - [self.sut setAuthenticationCookieData:nil]; - + self.sut.didReceiveAuthenticationCookies = NO; + // then XCTAssertNil(self.sut.emailCredentials); } @@ -256,7 +256,7 @@ - (void)testThatItDoesNotReturnCredentialsIfItIsNotLoggedIn - (void)testThatItReturnsCredentialsIfLoggedIn { // given - [self.sut setAuthenticationCookieData:[NSData data]]; + self.sut.didReceiveAuthenticationCookies = YES; [self performPretendingUiMocIsSyncMoc:^{ [self.sut prepareForLoginWithCredentials:[UserEmailCredentials credentialsWithEmail:@"foo@example.com" password:@"boo"]]; //XCTAssert((self.sut.emailCredentials) == nil); @@ -272,8 +272,8 @@ - (void)testThatItClearsCredentialsIfInPhaseAuthenticated [self performPretendingUiMocIsSyncMoc:^{ [self.sut prepareForLoginWithCredentials:[UserEmailCredentials credentialsWithEmail:@"foo@example.com" password:@"boo"]]; }]; - [self.sut setAuthenticationCookieData:[NSData data]]; - + self.sut.didReceiveAuthenticationCookies = YES; + XCTAssertNotNil(self.sut.loginCredentials); // when diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/ZMClientRegistrationStatusTests.swift b/wire-ios-sync-engine/Tests/Source/UserSession/ZMClientRegistrationStatusTests.swift index a87b5e99153..21f5c660b14 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/ZMClientRegistrationStatusTests.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/ZMClientRegistrationStatusTests.swift @@ -26,11 +26,11 @@ final class MockCookieStorage: CookieProvider { var isAuthenticated: Bool = true - func setRequestHeaderFieldsOn(_ request: NSMutableURLRequest) {} + func setRequestHeaderFields(on request: NSMutableURLRequest) {} - var didCallDeleteKeychainItems: Bool = false - func deleteKeychainItems() { - didCallDeleteKeychainItems = true + var didCallRemoveCookies: Bool = false + func removeCookies() throws { + didCallRemoveCookies = true } } @@ -566,7 +566,7 @@ final class ZMClientRegistrationStatusTests: MessagingTest { // then syncMOC.performAndWait { XCTAssertNil(syncMOC.persistentStoreMetadata(forKey: ZMPersistedClientIdKey)) - XCTAssertTrue(mockCookieStorage.didCallDeleteKeychainItems) + XCTAssertTrue(mockCookieStorage.didCallRemoveCookies) } } diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTestsBase.swift b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTestsBase.swift index 00eef256582..be94d58d0cc 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTestsBase.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTestsBase.swift @@ -37,8 +37,8 @@ class ZMUserSessionTestsBase: MessagingTest { var backendEnvironment: WireTransport.BackendEnvironment! var wireAPIBackendEnvironment: WireNetwork.BackendEnvironment! var transportSession: RecordingMockTransportSession! - var cookieStorage: ZMPersistentCookieStorage! - var validCookie: Data! + var cookieStorage: LegacyCookieStorage! + var validCookies: [HTTPCookie]! var baseURL: URL! var mediaManager: MediaManagerType! var flowManagerMock: FlowManagerMock! @@ -89,10 +89,8 @@ class ZMUserSessionTestsBase: MessagingTest { proxySettings: nil ) - cookieStorage = ZMPersistentCookieStorage( - forServerName: "usersessiontest.example.com", - userIdentifier: .create(), - useCache: true + cookieStorage = LegacyCookieStorage( + testingWithUserIdentifier: .create() ) transportSession = RecordingMockTransportSession(cookieStorage: cookieStorage) @@ -121,7 +119,7 @@ class ZMUserSessionTestsBase: MessagingTest { _ = waitForAllGroupsToBeEmpty(withTimeout: 0.5) - validCookie = HTTPCookie.validCookieData() + validCookies = HTTPCookie.validCookies() } override func tearDown() { @@ -134,7 +132,7 @@ class ZMUserSessionTestsBase: MessagingTest { wireAPIBackendEnvironment = nil baseURL = nil cookieStorage = nil - validCookie = nil + validCookies = nil mockSessionManager = nil mockMLSService = nil transportSession = nil @@ -226,7 +224,7 @@ class ZMUserSessionTestsBase: MessagingTest { syncMOC.performAndWait { syncMOC.setPersistentStoreMetadata("clientID", key: ZMPersistedClientIdKey) ZMUser.selfUser(in: syncMOC).remoteIdentifier = UUID.create() - cookieStorage.authenticationCookieData = validCookie + try? cookieStorage.storeCookies(validCookies) } } @@ -235,7 +233,7 @@ class ZMUserSessionTestsBase: MessagingTest { syncMOC.setPersistentStoreMetadata("clientID", key: ZMPersistedClientIdKey) ZMUser.selfUser(in: syncMOC).remoteIdentifier = UUID.create() } - cookieStorage.authenticationCookieData = validCookie + try? cookieStorage.storeCookies(validCookies) } private func clearCache() { diff --git a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests_NetworkState.swift b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests_NetworkState.swift index d00cb1f8c54..11c534199d5 100644 --- a/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests_NetworkState.swift +++ b/wire-ios-sync-engine/Tests/Source/UserSession/ZMUserSessionTests_NetworkState.swift @@ -34,10 +34,8 @@ final class ZMUserSessionTests_NetworkState: ZMUserSessionTestsBase { // given let userId = NSUUID.create()! - cookieStorage = ZMPersistentCookieStorage( - forServerName: "usersessiontest.example.com", - userIdentifier: userId, - useCache: true + cookieStorage = LegacyCookieStorage( + testingWithUserIdentifier: userId ) let transportSession = RecordingMockTransportSession(cookieStorage: cookieStorage) let mockCoreCrypto = MockCoreCryptoProtocol() diff --git a/wire-ios-transport/Source/Authentication/LegacyCookieStorage.swift b/wire-ios-transport/Source/Authentication/LegacyCookieStorage.swift new file mode 100644 index 00000000000..54a61881f62 --- /dev/null +++ b/wire-ios-transport/Source/Authentication/LegacyCookieStorage.swift @@ -0,0 +1,115 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import WireLogging + +public protocol CookieStorageProtocol: Sendable { + + func storeCookies(_ cookies: [HTTPCookie], userID: UUID) throws + func fetchCookies(userID: UUID) throws -> [HTTPCookie] + func removeCookies(userID: UUID) throws +} + +@objc +public class LegacyCookieStorage: NSObject { + + private static let cookieName = "zuid" + + @objc public let userIdentifier: UUID + + private let cookieStorage: any CookieStorageProtocol + + public init( + userIdentifier: UUID, + cookieStorage: any CookieStorageProtocol + ) { + self.userIdentifier = userIdentifier + self.cookieStorage = cookieStorage + super.init() + } + + // MARK: - Public API + + /// Stores the given cookies. + @objc + public func storeCookies(_ cookies: [HTTPCookie]) throws { + try cookieStorage.storeCookies(cookies, userID: userIdentifier) + } + + /// Removes all stored cookies for the user. + @objc + public func removeCookies() throws { + try cookieStorage.removeCookies(userID: userIdentifier) + } + + /// The expiration date of the authentication cookie, if it exists. + public var authenticationCookieExpirationDate: Date? { + for cookie in fetchCookies() where cookie.name == Self.cookieName { + return cookie.expiresDate + } + return nil + } + + /// Whether there is an authentication cookie stored. + /// + /// - warning: This only checks for the presence of the cookie, not whether it is valid or not. + @objc public var hasAuthenticationCookie: Bool { + authenticationCookieExpirationDate != nil + } + + // MARK: - HTTPCookie + + /// Extracts cookies from the given HTTP response and stores them if they match the expected cookie name. + @objc(setCookieDataFromResponse:forURL:) + public func setCookieData(from response: HTTPURLResponse, for url: URL) { + let headerFields = response.allHeaderFields as? [String: String] ?? [:] + let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url) + + guard cookies.first?.name == Self.cookieName else { return } + + do { + try storeCookies(cookies) + } catch { + let errorDescription = (error as NSError).safeForLoggingDescription + WireLogger.authentication.error("Failed to store cookies: \(errorDescription)", attributes: .safePublic) + } + } + + /// Adds store cookies on the given request. + @objc(setRequestHeaderFieldsOnRequest:) + public func setRequestHeaderFields(on request: NSMutableURLRequest) { + for (field, value) in HTTPCookie.requestHeaderFields(with: fetchCookies()) { + request.addValue(value, forHTTPHeaderField: field) + } + } + + // MARK: - Helpers + + private func fetchCookies() -> [HTTPCookie] { + do { + return try cookieStorage.fetchCookies(userID: userIdentifier) + } catch { + let errorDescription = (error as NSError).safeForLoggingDescription + WireLogger.authentication.error("Failed to fetch cookies: \(errorDescription)", attributes: .safePublic) + return [] + } + + } + +} diff --git a/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.h b/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.h index 49346ab61ff..0cc5fcd1b60 100644 --- a/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.h +++ b/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.h @@ -19,7 +19,7 @@ #import @class ZMAccessToken; -@class ZMPersistentCookieStorage; +@class LegacyCookieStorage; @class ZMAccessTokenHandler; @class ZMURLSession; @class ZMExponentialBackoff; @@ -38,7 +38,7 @@ @interface ZMAccessTokenHandler : NSObject - (instancetype)initWithBaseURL:(NSURL *)baseURL - cookieStorage:(ZMPersistentCookieStorage *)cookieStorage + cookieStorage:(LegacyCookieStorage *)cookieStorage delegate:(id)delegate queue:(NSOperationQueue *)queue group:(ZMSDispatchGroup *)group diff --git a/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.m b/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.m index 2df3c6e9c95..656b165dad2 100644 --- a/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.m +++ b/wire-ios-transport/Source/Authentication/ZMAccessTokenHandler.m @@ -24,7 +24,6 @@ #import "ZMTransportData.h" #import "ZMTransportCodec.h" #import -#import "ZMPersistentCookieStorage.h" #import "NSError+ZMTransportSession.h" #import "ZMUserAgent.h" #import "ZMTransportRequest.h" @@ -54,7 +53,7 @@ @interface ZMAccessTokenHandler () @property (nonatomic) NSURL *baseURL; -@property (nonatomic) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic) LegacyCookieStorage *cookieStorage; @property (nonatomic, weak) id delegate; @property (nonatomic) ZMExponentialBackoff *backoff; @@ -68,7 +67,7 @@ @interface ZMAccessTokenHandler () @implementation ZMAccessTokenHandler - (instancetype)initWithBaseURL:(NSURL *)baseURL - cookieStorage:(ZMPersistentCookieStorage *)cookieStorage + cookieStorage:(LegacyCookieStorage *)cookieStorage delegate:(id)delegate queue:(NSOperationQueue *)queue group:(ZMSDispatchGroup *)group @@ -323,7 +322,7 @@ - (BOOL)processAccessTokenResponse:(ZMTransportResponse *)response if (didFail) { [self logError:[NSString stringWithFormat:@"Failed to process access token response... clearing access token and cookie. Response result: %d, response status: %ld", response.result, (long)response.HTTPStatus]]; self.accessToken = nil; - self.cookieStorage.authenticationCookieData = nil; + [self.cookieStorage removeCookiesAndReturnError:nil]; [self notifyTokenFailure:response]; } diff --git a/wire-ios-transport/Source/Authentication/ZMPersistentCookieStorage+Testing.h b/wire-ios-transport/Source/Authentication/ZMPersistentCookieStorage+Testing.h deleted file mode 100644 index c80337f4dbb..00000000000 --- a/wire-ios-transport/Source/Authentication/ZMPersistentCookieStorage+Testing.h +++ /dev/null @@ -1,33 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -@interface ZMPersistentCookieStorage (Testing) - -/** - Disable/enable keychain access. This method should be called for testing only - - @param disabled true if not persist to keychain - */ -+ (void)setDoNotPersistToKeychain:(BOOL)disabled; - -/** - This method should be called for testing only - */ -- (BOOL)isCacheEmpty; - -@end diff --git a/wire-ios-transport/Source/Authentication/ZMPersistentCookieStorage.m b/wire-ios-transport/Source/Authentication/ZMPersistentCookieStorage.m deleted file mode 100644 index 049f822ec95..00000000000 --- a/wire-ios-transport/Source/Authentication/ZMPersistentCookieStorage.m +++ /dev/null @@ -1,466 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -@import Security; -@import WireSystem; -@import WireUtilities; -@import UIKit; - -#import "ZMTLogging.h" -#import "ZMPersistentCookieStorage.h" -#import -#import "ZMKeychain.h" - -static NSString * const CookieName = @"zuid"; -static NSString * const LegacyAccountName = @"User"; -static NSString* ZMLogTag ZM_UNUSED = ZMT_LOG_TAG_NETWORK; -static BOOL KeychainDisabled = NO; -static NSMutableDictionary *NonPersistedPassword; -static NSHTTPCookieAcceptPolicy cookiesPolicy = NSHTTPCookieAcceptPolicyAlways; - - -static dispatch_queue_t isolationQueue(void) -{ - static dispatch_queue_t queue; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - queue = dispatch_queue_create("ZMPersistentCookieStorage.isolation", 0); - }); - return queue; -} - - -@interface ZMPersistentCookieStorage () - -@property (nonatomic, readonly) NSString *serverName; -@property (nonatomic, readonly) NSArray *authenticationCookies; -@property (nonatomic, readonly) BOOL useCache; - -@end - - - -@interface ZMPersistentCookieStorage (Keychain) - -- (BOOL)findItemWithPassword:(NSData **)passwordP; -- (void)setItem:(NSData *)item; - -- (void)deleteItem; - -@end - - - -@implementation ZMPersistentCookieStorage - -#pragma mark - Creation - -+ (instancetype)storageForServerName:(NSString *)serverName userIdentifier:(NSUUID *)userIdentifier useCache:(BOOL)useCache -{ - return [[self alloc] initWithServerName:serverName userIdentifier:userIdentifier useCache:useCache]; -} - -- (instancetype)init -{ - return nil; -} - -- (instancetype)initWithServerName:(NSString *)serverName userIdentifier:(NSUUID *)userIdentifier useCache:(BOOL)useCache -{ - self = [super init]; - if (self) { - _serverName = [serverName copy]; - _userIdentifier = [userIdentifier copy]; - _useCache = useCache; - } - return self; -} - -#pragma mark - Private API for tests - -+ (void)setDoNotPersistToKeychain:(BOOL)disabled; -{ - KeychainDisabled = disabled; -} - -- (BOOL)isCacheEmpty -{ - return [NonPersistedPassword count] == 0; -} - -#pragma mark - Public API - -- (NSData *)authenticationCookieData; -{ - NSData *result = nil; - if ([self findItemWithPassword:&result]) { - return result; - } - return nil; -} - -- (void)setAuthenticationCookieData:(NSData *)data; -{ - if (data == nil) { - [self deleteItem]; - } else { - [self setItem:data]; - } -} - -- (NSDate *)authenticationCookieExpirationDate -{ - for (NSHTTPCookie *cookie in self.authenticationCookies) { - if ([cookie.name isEqualToString:CookieName]) { - return cookie.expiresDate; - } - } - - return nil; -} - -- (NSString *)cookieKey -{ - if (nil != self.accountName) { - return [[self.accountName stringByAppendingString:@"_"] stringByAppendingString:self.serverName]; - } else { - return self.serverName; // Legacy and migration support - } -} - -- (NSString *)accountName -{ - if (nil != self.userIdentifier) { - return self.userIdentifier.UUIDString; - } else { - return LegacyAccountName; // Legacy and migration support - } -} - -- (void)deleteKeychainItems -{ - dispatch_sync(isolationQueue(), ^{ - NonPersistedPassword[self.cookieKey] = nil; - - [ZMKeychain deleteAllKeychainItemsWithAccountName:self.accountName]; - }); -} - -+ (void)deleteAllKeychainItems -{ - dispatch_sync(isolationQueue(), ^{ - NonPersistedPassword = nil; - - if (KeychainDisabled) { - return; - } - - [ZMKeychain deleteAllKeychainItems]; - }); -} - -+ (BOOL)hasAccessibleAuthenticationCookieData -{ - __block BOOL success = NO; - dispatch_sync(isolationQueue(), ^{ - success = [ZMKeychain hasAccessibleAccountData]; - }); - - return success; -} - -#pragma mark - Private API - -- (NSArray *)authenticationCookies -{ - NSData *data = self.authenticationCookieData; - if (data == nil) { - return nil; - } - data = [[NSData alloc] initWithBase64EncodedData:data options:0]; - if (data == nil) { - return nil; - } - if (TARGET_OS_IPHONE) { - NSData *secretKey = [NSUserDefaults cookiesKey]; - data = [data zmDecryptPrefixedIVWithKey:secretKey]; - } - - NSKeyedUnarchiver *unarchiver; - @try { - NSError *error = nil; - unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&error]; - - if (error != nil || unarchiver == nil) { - ZMLogError(@"Unable to parse stored cookie data."); - self.authenticationCookieData = nil; - return nil; - } - } @catch (id) { - ZMLogError(@"Unable to parse stored cookie data."); - self.authenticationCookieData = nil; - return nil; - } - - unarchiver.requiresSecureCoding = YES; - NSArray *properties = [unarchiver decodePropertyListForKey:@"properties"]; - - if (![properties isKindOfClass:[NSArray class]]) { - return nil; - } - - NSArray *cookies = [properties mapWithBlock:^id(NSDictionary *p) { - return [[NSHTTPCookie alloc] initWithProperties:p]; - }]; - - return cookies; -} - -@end - - - -@implementation ZMPersistentCookieStorage (Keychain) - -- (BOOL)findItemWithPassword:(NSData * __autoreleasing *)passwordP -{ - __block BOOL success = NO; - dispatch_sync(isolationQueue(), ^{ - - NSData *password = NonPersistedPassword[self.cookieKey]; - BOOL const fetchFromKeychain = (password == nil); - *passwordP = (password == (id) [NSNull null]) ? nil : password; - - if (KeychainDisabled) { - success = YES; - return; - } - - if (fetchFromKeychain) { - id result = nil; - if (passwordP == nil) { - result = [ZMKeychain stringForAccount:self.accountName fallbackToDefaultGroup:YES]; - } - else { - result = [ZMKeychain dataForAccount:self.accountName fallbackToDefaultGroup:YES]; - } - - if (result != nil) { - if (passwordP != nil) { - *passwordP = [result isKindOfClass:[NSData class]] ? result : nil; - } - [self addNonPersistedPassword:*passwordP]; - success = YES; - } - } else { - success = (*passwordP != nil); - } - }); - return success; -} - -- (void)addNonPersistedPassword:(NSData *)password -{ - if (!_useCache) { - return; - } - - if (NonPersistedPassword == nil) { - NonPersistedPassword = [NSMutableDictionary dictionary]; - } - NonPersistedPassword[self.cookieKey] = password ?: [NSNull null]; -} - -- (void)setItem:(NSData *)data -{ - if (![self updateItemWithPassword:data]) { - [self addItemWithPassword:data]; - } -} - -- (BOOL)addItemWithPassword:(NSData *)password -{ - Require(password != nil); - __block BOOL success = NO; - dispatch_sync(isolationQueue(), ^{ - - [self addNonPersistedPassword:password]; - - if (KeychainDisabled) { - success = YES; - return; - } - success = [ZMKeychain setData:password forAccount:self.accountName]; - }); - return success; -} - -- (BOOL)updateItemWithPassword:(NSData *)password -{ - __block BOOL success = NO; - dispatch_sync(isolationQueue(), ^{ - - BOOL hasItem = ((NonPersistedPassword[self.cookieKey] != nil) && - (NonPersistedPassword[self.cookieKey] != [NSNull null])); - if (hasItem) { - NonPersistedPassword[self.cookieKey] = password ?: [NSNull null]; - } - - if (KeychainDisabled) { - success = hasItem; - return; - } - - success = [ZMKeychain setData:password forAccount:self.accountName]; - }); - - // now try to read. If we fail to read, it means that the keychain is blocked and it always return success on an update (I guess it's a security feature?) - NSData *readPassword; - BOOL read = [self findItemWithPassword:&readPassword]; - - if(!read || (![readPassword isEqualToData:password])) { - dispatch_async(isolationQueue(), ^{ - [self addNonPersistedPassword:password]; - }); - - } - - return success; -} - -- (void)deleteItem -{ - dispatch_sync(isolationQueue(), ^{ - - [NonPersistedPassword removeObjectForKey:self.cookieKey]; - - if (KeychainDisabled) { - return; - } - - [ZMKeychain deleteAllKeychainItemsWithAccountName:self.accountName]; - }); -} - -@end - - -@implementation ZMPersistentCookieStorage (HTTPCookie) - -+ (void)setCookiesPolicy:(NSHTTPCookieAcceptPolicy)policy -{ - cookiesPolicy = policy == NSHTTPCookieAcceptPolicyNever ? NSHTTPCookieAcceptPolicyNever : NSHTTPCookieAcceptPolicyAlways; -} - -+ (NSHTTPCookieAcceptPolicy)cookiesPolicy; -{ - return cookiesPolicy; -} - -- (void)setCookieDataFromResponse:(NSHTTPURLResponse *)response forURL:(NSURL *)URL; -{ - if (cookiesPolicy == NSHTTPCookieAcceptPolicyNever) { - return; - } - - NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:response.allHeaderFields forURL:URL]; - if (cookies.count == 0) { - return; - } - ZMLogDebug(@"Cookie received."); - - NSArray *properties = [cookies mapWithBlock:^id(NSHTTPCookie *cookie) { - return cookie.properties; - }]; - - if (![[properties.firstObject valueForKey:@"Name"] isEqual:CookieName]) { - return; - } - - NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initRequiringSecureCoding:YES]; - [archiver encodeObject:properties forKey:@"properties"]; - [archiver finishEncoding]; - - NSData *data = [archiver encodedData]; - - if (TARGET_OS_IPHONE) { - NSData *secretKey = [NSUserDefaults cookiesKey]; - data = [[data zmEncryptPrefixingIVWithKey:secretKey] mutableCopy]; - } - - self.authenticationCookieData = [data base64EncodedDataWithOptions:0]; -} - -- (void)setRequestHeaderFieldsOnRequest:(NSMutableURLRequest *)request; -{ - NSArray *cookies = [self authenticationCookies]; - - if (cookies == nil) { - return; - } - - [[NSHTTPCookie requestHeaderFieldsWithCookies:cookies] enumerateKeysAndObjectsUsingBlock:^(NSString *field, NSString *value, BOOL *stop) { - NOT_USED(stop); - [request addValue:value forHTTPHeaderField:field]; - }]; -} - -@end - -#pragma mark – Legacy Storage Migration - - -@interface ZMPersistentCookieStorageMigrator () -@property (nonatomic, readonly) NSUUID *userIdentifier; -@property (nonatomic, readonly) NSString *serverName; -@end - -@implementation ZMPersistentCookieStorageMigrator - -+ (instancetype)migratorWithUserIdentifier:(NSUUID *)userIdentifier serverName:(NSString *)serverName -{ - return [[self alloc] initWithUserIdentifier:userIdentifier serverName:serverName]; -} - -- (instancetype)initWithUserIdentifier:(NSUUID *)userIdentifier serverName:(NSString *)serverName -{ - self = [super init]; - if (self) { - _userIdentifier = userIdentifier; - _serverName = serverName; - } - return self; -} - -- (ZMPersistentCookieStorage *)createStoreMigratingLegacyStoreIfNeeded -{ - ZMPersistentCookieStorage *oldStorage = [ZMPersistentCookieStorage storageForServerName:self.serverName userIdentifier:(NSUUID *_Nonnull)nil useCache:YES]; - ZMPersistentCookieStorage *newStorage = [ZMPersistentCookieStorage storageForServerName:self.serverName userIdentifier:self.userIdentifier useCache:YES]; - NSData *cookieData = oldStorage.authenticationCookieData; - - if (nil != cookieData) { - // Migrate cookie data to the new storage - newStorage.authenticationCookieData = oldStorage.authenticationCookieData; - [oldStorage deleteKeychainItems]; - } - - return newStorage; -} - - -@end diff --git a/wire-ios-transport/Source/Public/ZMPersistentCookieStorage+Label.swift b/wire-ios-transport/Source/Public/CookieLabel.swift similarity index 100% rename from wire-ios-transport/Source/Public/ZMPersistentCookieStorage+Label.swift rename to wire-ios-transport/Source/Public/CookieLabel.swift diff --git a/wire-ios-transport/Source/Public/WireTransport.h b/wire-ios-transport/Source/Public/WireTransport.h index e4e6912b9b3..37084594fa8 100644 --- a/wire-ios-transport/Source/Public/WireTransport.h +++ b/wire-ios-transport/Source/Public/WireTransport.h @@ -36,7 +36,6 @@ FOUNDATION_EXPORT const unsigned char TransportVersionString[]; #import #import #import -#import #import #import #import diff --git a/wire-ios-transport/Source/Public/ZMPersistentCookieStorage.h b/wire-ios-transport/Source/Public/ZMPersistentCookieStorage.h deleted file mode 100644 index 4044b4d6b30..00000000000 --- a/wire-ios-transport/Source/Public/ZMPersistentCookieStorage.h +++ /dev/null @@ -1,86 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -#import - - -NS_ASSUME_NONNULL_BEGIN - -/// This overrides the @c NSHTTPCookieStorage and adds convenience to check for the cookies relevant for our backend. -/// -/// We will only store cookies relevant to our backend. They'll be persisted in the keychain. -@interface ZMPersistentCookieStorage : NSObject - -+ (instancetype)storageForServerName:(NSString *)serverName userIdentifier:(NSUUID *)userIdentifier useCache:(BOOL) useCache; - -/// Looks up if there's any accessible authentication cookie data for any user -/// -/// - Returns: True if it's possible access any authentication cookie data -+ (BOOL)hasAccessibleAuthenticationCookieData; - -/// Delete all keychain items for for all servers and users -+ (void)deleteAllKeychainItems; - -/// Delete all keychain items for current the user and server -- (void)deleteKeychainItems; - -/// Date and time when the authentication cookie is no longer valid -@property (nonatomic, nullable) NSDate *authenticationCookieExpirationDate; - -/// Authentication cookie available in the storage -/// -/// - warning: This is encrypted data stored in the keychain. It may not be possible to decrypt it. For example, if the -/// app is deleted then reinstalled this data may still exist but the encryption key will be different. -@property (nonatomic, nullable) NSData *authenticationCookieData; - -/// User identifier associated with the storage -@property (nonatomic, readonly) NSUUID *userIdentifier; - -@end - - - - - - -@interface ZMPersistentCookieStorage (HTTPCookie) - -//If you try to set it to something different than NSHTTPCookieAcceptPolicyNever it will be set to NSHTTPCookieAcceptPolicyAlways -+ (void)setCookiesPolicy:(NSHTTPCookieAcceptPolicy)policy; -+ (NSHTTPCookieAcceptPolicy)cookiesPolicy; - -- (void)setCookieDataFromResponse:(NSHTTPURLResponse *)response forURL:(NSURL *)URL; -- (void)setRequestHeaderFieldsOnRequest:(NSMutableURLRequest *)request; - -@end - - -/// The @c ZMPersistentCookieStorageMigrator class should be used to migrate cookies from an -/// old legacy store to the multi account stores. Callers should use this class to create a cookie store -/// for the currently logged in user for the first time after upgrading. After the initial migration -/// callers should use the initializers of @c ZMPersistentCookieStorage directly. -/// The migrator will migrate the legacy data to the store with the identifier specified in @c init, -/// meaning callers need to ensure these to match (which will always be the case when called in a single account setup). -@interface ZMPersistentCookieStorageMigrator : NSObject - -+ (instancetype)migratorWithUserIdentifier:(NSUUID *)userIdentifier serverName:(NSString *)serverName; -- (ZMPersistentCookieStorage *)createStoreMigratingLegacyStoreIfNeeded; - -@end - -NS_ASSUME_NONNULL_END diff --git a/wire-ios-transport/Source/Public/ZMPersistentCookieStorage.swift b/wire-ios-transport/Source/Public/ZMPersistentCookieStorage.swift deleted file mode 100644 index ff485883254..00000000000 --- a/wire-ios-transport/Source/Public/ZMPersistentCookieStorage.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -import Foundation - -public extension ZMPersistentCookieStorage { - - /// Returns true if `self` has an authentication cookie that can be **decrypted**. - /// - /// - note: This should generally be used in favor of `ZMPersistentCookieStorage.authenticationCookieData` which - /// makes no guarantees about whether it's returned value can be decrypted. - @objc var hasAuthenticationCookie: Bool { - authenticationCookieExpirationDate != nil - } - -} diff --git a/wire-ios-transport/Source/Public/ZMTransportSession.h b/wire-ios-transport/Source/Public/ZMTransportSession.h index 932eec51e0c..b0cee6033cf 100644 --- a/wire-ios-transport/Source/Public/ZMTransportSession.h +++ b/wire-ios-transport/Source/Public/ZMTransportSession.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN @class UIApplication; @class ZMTransportRequest; -@class ZMPersistentCookieStorage; +@class LegacyCookieStorage; @class ZMAccessTokenHandler; @class ZMTransportRequestScheduler; @protocol ZMSGroupQueue; @@ -70,7 +70,7 @@ extern NSString * const ZMTransportSessionReachabilityIsEnabled; @property (nonatomic, readonly) NSURL *baseURL; @property (nonatomic, readonly) NSOperationQueue *workQueue; @property (nonatomic, assign) NSInteger maximumConcurrentRequests; -@property (nonatomic, readonly) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic, readonly) LegacyCookieStorage *cookieStorage; @property (nonatomic, readonly) ZMAccessTokenHandler *accessTokenHandler; @property (nonatomic, readonly) id sessionsDirectory; @property (nonatomic, copy, nullable) void (^requestLoopDetectionCallback)(NSString*); @@ -79,7 +79,7 @@ extern NSString * const ZMTransportSessionReachabilityIsEnabled; - (instancetype)initWithEnvironment:(id)environment proxyUsername:(nullable NSString *)proxyUsername proxyPassword:(nullable NSString *)proxyPassword - cookieStorage:(ZMPersistentCookieStorage *)cookieStorage + cookieStorage:(LegacyCookieStorage *)cookieStorage reachability:(id)reachability initialAccessToken:(nullable ZMAccessToken *)initialAccessToken applicationGroupIdentifier:(nullable NSString *)applicationGroupIdentifier diff --git a/wire-ios-transport/Source/TransportSession/TransportSession.swift b/wire-ios-transport/Source/TransportSession/TransportSession.swift index 149fe087202..6747d144916 100644 --- a/wire-ios-transport/Source/TransportSession/TransportSession.swift +++ b/wire-ios-transport/Source/TransportSession/TransportSession.swift @@ -25,7 +25,7 @@ public protocol TransportSessionType: ZMBackgroundable, ZMRequestCancellation, T var accessTokenHandler: ZMAccessTokenHandler { get } - var cookieStorage: ZMPersistentCookieStorage { get } + var cookieStorage: LegacyCookieStorage { get } var requestLoopDetectionCallback: ((_ path: String) -> Void)? { get set } diff --git a/wire-ios-transport/Source/TransportSession/Unauthenticated Session/UnauthenticatedTransportSession.swift b/wire-ios-transport/Source/TransportSession/Unauthenticated Session/UnauthenticatedTransportSession.swift index e8fe1ccba26..4baf18bc4b2 100644 --- a/wire-ios-transport/Source/TransportSession/Unauthenticated Session/UnauthenticatedTransportSession.swift +++ b/wire-ios-transport/Source/TransportSession/Unauthenticated Session/UnauthenticatedTransportSession.swift @@ -38,25 +38,23 @@ public protocol UnauthenticatedTransportSessionProtocol: TearDownCapable { @objcMembers public final class UserInfo: NSObject { public let identifier: UUID - public let cookieData: Data public let cookies: [HTTPCookie] - public init(identifier: UUID, cookieData: Data, cookies: [HTTPCookie]) { + public init(identifier: UUID, cookies: [HTTPCookie]) { self.identifier = identifier - self.cookieData = cookieData self.cookies = cookies } public override func isEqual(_ object: Any?) -> Bool { guard let other = object as? UserInfo else { return false } - return other.cookieData == cookieData && other.identifier == identifier + return other.identifier == identifier } } /// The `UnauthenticatedTransportSession` class should be used instead of `ZMTransportSession` /// until a user has been authenticated. Consumers should set themselves as delegate to /// be notified when a cookie was parsed from a response of a request made using this transport session. -/// When cookie data became available it should be used to create a `ZMPersistentCookieStorage` and +/// When cookie data became available it should be used to create a `LegacyCookieStorage` and /// to create a regular transport session with it. public final class UnauthenticatedTransportSession: NSObject, UnauthenticatedTransportSessionProtocol { @@ -280,13 +278,13 @@ extension ZMTransportResponse { @objc public func extractUserInfo() -> UserInfo? { guard - let data = extractCookies().data, + extractCookies().data != nil, // TODO: [WPB-24887] Validate this in another way. let cookies = extractCookies().array, let id = extractUserIdentifier() else { return nil } - return .init(identifier: id, cookieData: data, cookies: cookies) + return .init(identifier: id, cookies: cookies) } } diff --git a/wire-ios-transport/Source/TransportSession/ZMTransportSession+internal.h b/wire-ios-transport/Source/TransportSession/ZMTransportSession+internal.h index 50b7be059ef..280a99c0add 100644 --- a/wire-ios-transport/Source/TransportSession/ZMTransportSession+internal.h +++ b/wire-ios-transport/Source/TransportSession/ZMTransportSession+internal.h @@ -38,7 +38,7 @@ environment:(id)environment proxyUsername:(NSString *)proxyUsername proxyPassword:(NSString *)proxyPassword - cookieStorage:(ZMPersistentCookieStorage *)cookieStorage + cookieStorage:(LegacyCookieStorage *)cookieStorage initialAccessToken:(ZMAccessToken *)initialAccessToken userAgent:(NSString *)userAgent minTLSVersion:(NSString *)minTLSVersion diff --git a/wire-ios-transport/Source/TransportSession/ZMTransportSession.m b/wire-ios-transport/Source/TransportSession/ZMTransportSession.m index 4024d39e4d0..8055d1c865d 100644 --- a/wire-ios-transport/Source/TransportSession/ZMTransportSession.m +++ b/wire-ios-transport/Source/TransportSession/ZMTransportSession.m @@ -26,7 +26,6 @@ #import "ZMTransportSession+internal.h" #import "ZMTransportCodec.h" #import "ZMTransportRequest+Internal.h" -#import "ZMPersistentCookieStorage.h" #import "ZMTaskIdentifierMap.h" #import "ZMReachability.h" #import "Collections+ZMTSafeTypes.h" @@ -56,7 +55,7 @@ @interface ZMTransportSession () @property (nonatomic) NSURL *websocketURL; @property (nonatomic) id environment; @property (nonatomic) NSOperationQueue *workQueue; -@property (nonatomic) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic) LegacyCookieStorage *cookieStorage; @property (nonatomic) BOOL tornDown; @property (nonatomic) NSString *applicationGroupIdentifier; @@ -103,16 +102,16 @@ - (instancetype)init - (instancetype)initWithEnvironment:(id)environment proxyUsername:(NSString *) proxyUsername proxyPassword:(NSString *) proxyPassword - cookieStorage:(ZMPersistentCookieStorage *)cookieStorage + cookieStorage:(LegacyCookieStorage *)cookieStorage reachability:(id)reachability initialAccessToken:(ZMAccessToken *)initialAccessToken applicationGroupIdentifier:(NSString *)applicationGroupIdentifier - applicationVersion:(NSString *)appliationVersion + applicationVersion:(NSString *)applicationVersion minTLSVersion:(NSString * _Nullable)minTLSVersion selfClientID:(nullable NSString *)selfClientID isSyncV2Enabled:(bool)isSyncV2Enabled { - NSString *userAgent = [ZMUserAgent userAgentWithAppVersion:appliationVersion]; + NSString *userAgent = [ZMUserAgent userAgentWithAppVersion:applicationVersion]; NSUUID *userIdentifier = cookieStorage.userIdentifier; NSOperationQueue *queue = [NSOperationQueue zm_serialQueueWithName:[ZMTransportSession identifierWithPrefix:@"ZMTransportSession" userIdentifier:userIdentifier]]; ZMSDispatchGroup *group = [[ZMSDispatchGroup alloc] initWithLabel:[ZMTransportSession identifierWithPrefix:@"ZMTransportSession init" userIdentifier:userIdentifier]]; @@ -181,7 +180,7 @@ - (instancetype)initWithURLSessionsDirectory:(id)environment proxyUsername:(NSString *)proxyUsername proxyPassword:(NSString *)proxyPassword - cookieStorage:(ZMPersistentCookieStorage *)cookieStorage + cookieStorage:(LegacyCookieStorage *)cookieStorage initialAccessToken:(ZMAccessToken *)initialAccessToken userAgent:(NSString *)userAgent minTLSVersion:(NSString * _Nullable)minTLSVersion @@ -509,7 +508,13 @@ - (ZMTransportResponse *)transportResponseFromURLResponse:(NSURLResponse *)URLRe - (void)processCookieResponse:(NSHTTPURLResponse *)HTTPResponse; { - [self.cookieStorage setCookieDataFromResponse:HTTPResponse forURL:HTTPResponse.URL]; + NSURL *URL = HTTPResponse.URL; + + if (URL == nil) { + return; + } + + [self.cookieStorage setCookieDataFromResponse:HTTPResponse forURL:URL]; } - (void)handlerDidReceiveAccessToken:(ZMAccessTokenHandler *)handler diff --git a/wire-ios-transport/Tests/Source/Authentication/LegacyCookieStorageTests.swift b/wire-ios-transport/Tests/Source/Authentication/LegacyCookieStorageTests.swift new file mode 100644 index 00000000000..22c8fcfdba5 --- /dev/null +++ b/wire-ios-transport/Tests/Source/Authentication/LegacyCookieStorageTests.swift @@ -0,0 +1,198 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Testing +import WireTransport +import WireTransportSupport + +struct LegacyCookieStorageTests { + + let cookiesStorage = StubCookieStorage() + let sut: LegacyCookieStorage + + init() { + self.sut = LegacyCookieStorage(userIdentifier: UUID(), cookieStorage: cookiesStorage) + } + + @Test + func `storeCookies updates the underlying cookie storage`() throws { + // given + let cookies = HTTPCookie.validCookies() + cookiesStorage.cookies = [] + + // when + try sut.storeCookies(cookies) + + // then + #expect(cookiesStorage.cookies == cookies) + } + + @Test + func `removeCookies clears the underlying cookie storage`() throws { + // given + cookiesStorage.cookies = HTTPCookie.validCookies() + + // when + try sut.removeCookies() + + // then + #expect(cookiesStorage.cookies.isEmpty) + } + + @Test + func `authenticationCookieExpirationDate returns the expiry date of the first authentication cookie`() throws { + // given + cookiesStorage.cookies = [ + HTTPCookie.validCookies(string: "yuid=aaa; Expires=Thu, 08-Apr-2026 14:00:00 GMT"), + HTTPCookie.validCookies(string: "zuid=bbb; Expires=Thu, 09-Apr-2026 15:00:00 GMT"), // <--- This cookie + HTTPCookie.validCookies(string: "zuid=ccc; Expires=Fri, 10-Apr-2026 16:00:00 GMT") + ].flatMap(\.self) + + // when + let expiration = sut.authenticationCookieExpirationDate + + // then + #expect(expiration == ISO8601DateFormatter().date(from: "2026-04-09T15:00:00Z")!) + } + + @Test(arguments: [ + [], + HTTPCookie.validCookies(string: "yuid=aaa; Expires=Thu, 08-Apr-2026 14:00:00 GMT") + ]) + func `authenticationCookieExpirationDate returns nil when no authentication cookie`(cookies: [HTTPCookie]) throws { + // given + cookiesStorage.cookies = cookies + + // when, then + #expect(sut.authenticationCookieExpirationDate == nil) + } + + @Test + func `hasAuthenticationCookie returns true when a zuid cookie is stored`() { + // given + cookiesStorage.cookies = HTTPCookie.validCookies() + + // when, then + #expect(sut.hasAuthenticationCookie) + } + + @Test + func `hasAuthenticationCookie returns true even when the cookie has expired`() { + // given + cookiesStorage.cookies = HTTPCookie.validCookies( + string: "zuid=expired; Expires=Thu, 01-Jan-2020 00:00:00 GMT" + ) + + // when, then + #expect(sut.hasAuthenticationCookie) + } + + @Test(arguments: [ + [], + HTTPCookie.validCookies(string: "yuid=aaa; Expires=Thu, 08-Apr-2026 14:00:00 GMT") + ]) + func `hasAuthenticationCookie returns false when no zuid cookie is stored`(cookies: [HTTPCookie]) { + // given + cookiesStorage.cookies = cookies + + // when, then + #expect(!sut.hasAuthenticationCookie) + } + + @Test + func `setRequestHeaderFields sets the Cookie header from stored cookies`() { + // given + cookiesStorage.cookies = HTTPCookie.validCookies(string: "zuid=bbb; Expires=Thu, 09-Apr-2026 15:00:00 GMT") + let request = NSMutableURLRequest(url: URL(string: "https://example.com/access")!) + + // when + sut.setRequestHeaderFields(on: request) + + // then + #expect(request.value(forHTTPHeaderField: "Cookie") == "zuid=bbb") + } + + @Test + func `setRequestHeaderFields does nothing when no cookies are stored`() { + // given + cookiesStorage.cookies = [] + let request = NSMutableURLRequest(url: URL(string: "https://example.com/access")!) + + // when + sut.setRequestHeaderFields(on: request) + + // then + #expect(request.value(forHTTPHeaderField: "Cookie") == nil) + } + + @Test + func `setCookieData stores cookies from a response with a zuid cookie`() { + // given + let url = URL(string: "https://example.com/login")! + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Set-Cookie": "zuid=abc123; Expires=Sun, 21-Jul-2030 09:06:45 GMT; Domain=wire.com"] + )! + + // when + sut.setCookieData(from: response, for: url) + + // then + #expect(cookiesStorage.cookies.first?.name == "zuid") + #expect(cookiesStorage.cookies.first?.value == "abc123") + } + + @Test + func `setCookieData does nothing when response has no Set-Cookie header`() { + // given + let url = URL(string: "https://example.com/login")! + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Content-Type": "application/json"] + )! + + // when + sut.setCookieData(from: response, for: url) + + // then + #expect(cookiesStorage.cookies.isEmpty) + } + + @Test + func `setCookieData does nothing when response has a non zuid cookie`() { + // given + let url = URL(string: "https://example.com/login")! + let response = HTTPURLResponse( + url: url, + statusCode: 200, + httpVersion: "HTTP/1.1", + headerFields: ["Set-Cookie": "foo=bar; Expires=Sun, 21-Jul-2030 09:06:45 GMT; Domain=wire.com"] + )! + + // when + sut.setCookieData(from: response, for: url) + + // then + #expect(cookiesStorage.cookies.isEmpty) + } + +} diff --git a/wire-ios-transport/Tests/Source/Authentication/ZMAccessTokenHandlerTests.m b/wire-ios-transport/Tests/Source/Authentication/ZMAccessTokenHandlerTests.m index a59e1caa2d6..ce4e780ee82 100644 --- a/wire-ios-transport/Tests/Source/Authentication/ZMAccessTokenHandlerTests.m +++ b/wire-ios-transport/Tests/Source/Authentication/ZMAccessTokenHandlerTests.m @@ -22,8 +22,8 @@ @import OCMock; #import "ZMAccessTokenHandler.h" -#import "ZMPersistentCookieStorage.h" @import WireTransport.Testing; +@import WireTransportSupport; #import "ZMURLSession.h" #import "NSError+ZMTransportSession.h" #import "Fakes.h" @@ -35,7 +35,7 @@ @interface ZMAccessTokenHandlerTests : ZMTBaseTest @property (nonatomic) ZMAccessToken *expiredAccessToken; @property (nonatomic) id urlSession; -@property (nonatomic) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic) LegacyCookieStorage *cookieStorage; @property (nonatomic) NSOperationQueue *queue; @property (nonatomic) FakeExponentialBackoff *backoff; @property (nonatomic) FakeDelegate *delegate; @@ -64,10 +64,6 @@ @implementation ZMAccessTokenHandlerTests - (void)setUp { [super setUp]; -#if TARGET_IPHONE_SIMULATOR - [ZMPersistentCookieStorage setDoNotPersistToKeychain:YES]; -#endif - NSURL *baseURL = [NSURL URLWithString:@"https://www.example.com"]; self.taskCount = 0; self.failureCount = 0; @@ -77,7 +73,7 @@ - (void)setUp { self.userIdentifier = [NSUUID createUUID]; self.urlSession = [OCMockObject niceMockForClass:[ZMURLSession class]]; - self.cookieStorage = [ZMPersistentCookieStorage storageForServerName:baseURL.host userIdentifier:self.userIdentifier useCache:YES]; + self.cookieStorage = [[LegacyCookieStorage alloc] initWithTestingWithUserIdentifier:self.userIdentifier]; [self setAuthenticationCookieData]; self.queue = [NSOperationQueue mainQueue]; @@ -112,10 +108,6 @@ - (void)tearDown { self.recordedResponse = nil; self.userIdentifier = nil; -#if TARGET_IPHONE_SIMULATOR - [ZMPersistentCookieStorage setDoNotPersistToKeychain:NO]; -#endif - [super tearDown]; } @@ -129,7 +121,7 @@ - (void)setAuthenticationCookieData; HTTPVersion:@"" headerFields:headers]; [self.cookieStorage setCookieDataFromResponse:response forURL:URL]; - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); } - (void)invokeAccessTokenRenewalFailureHandler:(ZMTransportResponse *)response @@ -387,7 +379,7 @@ - (void)testThatItDoesNotDeleteTheCookieIfRequestingTheAccessTokenFailsBecauseOf [self.sut consumeRequestWithTask:task data:nil session:self.urlSession shouldRetry:YES apiVersion:0]; // then - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); } @@ -633,7 +625,7 @@ - (void)testThatItReturns_NO_IfItReceivesA_ZMTransportResponseStatusPermanentErr - (void)testThatIt_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusPermanentError_Not420Or429Status { // given - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); FakeTransportResponse *testResponse = [FakeTransportResponse testResponse]; [testResponse setResult:ZMTransportResponseStatusPermanentError]; @@ -642,13 +634,13 @@ - (void)testThatIt_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusPermanent [self.sut processAccessTokenResponse:(id)testResponse]; // then - XCTAssertNil(self.cookieStorage.authenticationCookieData); + XCTAssertFalse(self.cookieStorage.hasAuthenticationCookie); } - (void)testThatIt_DoesNot_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusPermanentError_429Status { // given - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); FakeTransportResponse *testResponse = [FakeTransportResponse testResponse]; [testResponse setResult:ZMTransportResponseStatusPermanentError]; @@ -658,13 +650,13 @@ - (void)testThatIt_DoesNot_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusP [self.sut processAccessTokenResponse:(id)testResponse]; // then - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); } - (void)testThatIt_DoesNot_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusPermanentError_420Status { // given - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); FakeTransportResponse *testResponse = [FakeTransportResponse testResponse]; [testResponse setResult:ZMTransportResponseStatusPermanentError]; @@ -674,13 +666,13 @@ - (void)testThatIt_DoesNot_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusP [self.sut processAccessTokenResponse:(id)testResponse]; // then - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); } - (void)testThatIt_DoesNot_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusTemporaryError { // given - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); FakeTransportResponse *testResponse = [FakeTransportResponse testResponse]; [testResponse setResult:ZMTransportResponseStatusTemporaryError]; @@ -689,13 +681,13 @@ - (void)testThatIt_DoesNot_ClearsCookie_IfItReceivesA_ZMTransportResponseStatusT [self.sut processAccessTokenResponse:(id)testResponse]; // then - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); } - (void)testThatItDeletesTheCookieDataIfItDoesNotReceiveANewToken { // given - XCTAssertNotNil(self.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.cookieStorage.hasAuthenticationCookie); FakeTransportResponse *response = [FakeTransportResponse testResponse]; [response setResult:ZMTransportResponseStatusSuccess]; @@ -704,7 +696,7 @@ - (void)testThatItDeletesTheCookieDataIfItDoesNotReceiveANewToken [self.sut processAccessTokenResponse:(id)response]; // then - XCTAssertNil(self.cookieStorage.authenticationCookieData); + XCTAssertFalse(self.cookieStorage.hasAuthenticationCookie); } - (void)testThatItForwardsTheResponseToTheFailureHandlerIfItDoesNotReceiveANewToken diff --git a/wire-ios-transport/Tests/Source/Authentication/ZMPersistentCookieStorageTests.m b/wire-ios-transport/Tests/Source/Authentication/ZMPersistentCookieStorageTests.m deleted file mode 100644 index c5809e19124..00000000000 --- a/wire-ios-transport/Tests/Source/Authentication/ZMPersistentCookieStorageTests.m +++ /dev/null @@ -1,404 +0,0 @@ -// -// Wire -// Copyright (C) 2026 Wire Swiss GmbH -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see http://www.gnu.org/licenses/. -// - -@import XCTest; -@import WireTesting; -@import WireTransport.Testing; - -#import "ZMPersistentCookieStorage.h" - - -@interface ZMPersistentCookieStorageTests : XCTestCase - -@property (nonatomic, readonly) NSUUID *userIdentifier; -@property (nonatomic) ZMPersistentCookieStorage *sut; - -@end - -@interface ZMPersistentCookieStorageTests (HTTPCookie) -@end - -@implementation ZMPersistentCookieStorageTests - -- (BOOL)shouldUseRealKeychain; -{ - return (TARGET_IPHONE_SIMULATOR) || !(TARGET_OS_IPHONE); -} - -- (void)setUp -{ - [super setUp]; - [ZMPersistentCookieStorage deleteAllKeychainItems]; - _userIdentifier = NSUUID.createUUID; - self.sut = [ZMPersistentCookieStorage storageForServerName:@"1.example.com" userIdentifier:self.userIdentifier useCache:YES]; -} - -- (void)tearDown -{ - _userIdentifier = nil; - [super tearDown]; - [self.sut deleteKeychainItems]; - self.sut = nil; -} - -- (void)testThatItDoesNotHaveACookie; -{ - XCTAssertNil(self.sut.authenticationCookieData); -} - -- (void)testThatItStoresTheCookie; -{ - XCTAssertNil(self.sut.authenticationCookieData); - NSData *data = [NSData dataWithBytes:(char []){'a'} length:1]; - [self.sut setAuthenticationCookieData:data]; - XCTAssertNotNil(self.sut.authenticationCookieData); - XCTAssertEqualObjects(self.sut.authenticationCookieData, data); -} - -- (void)testThatItUpdatesTheCookie; -{ - XCTAssertNil(self.sut.authenticationCookieData); - - NSData *data1 = [NSData dataWithBytes:(char []){'a'} length:1]; - [self.sut setAuthenticationCookieData:data1]; - XCTAssertEqualObjects(self.sut.authenticationCookieData, data1); - - NSData *data2 = [NSData dataWithBytes:(char []){'B'} length:1]; - [self.sut setAuthenticationCookieData:data2]; - XCTAssertEqualObjects(self.sut.authenticationCookieData, data2); -} - -- (void)testThatItIsUniqueForServerName; -{ - ZMPersistentCookieStorage *sut1 = self.sut; - ZMPersistentCookieStorage *sut2 = [ZMPersistentCookieStorage storageForServerName:@"2.example.com" userIdentifier:self.userIdentifier useCache:YES]; - - XCTAssertNil([sut1 authenticationCookieData]); - XCTAssertNil([sut2 authenticationCookieData]); - - NSData *data1 = [NSData dataWithBytes:(char []){'a'} length:1]; - [sut1 setAuthenticationCookieData:data1]; - NSData *data2 = [NSData dataWithBytes:(char []){'b'} length:1]; - [sut2 setAuthenticationCookieData:data2]; - - XCTAssertEqualObjects([sut1 authenticationCookieData], data1); - XCTAssertEqualObjects([sut2 authenticationCookieData], data2); - [sut2 deleteKeychainItems]; -} - -- (void)testThatItCanDeleteCookies; -{ - XCTAssertNil(self.sut.authenticationCookieData); - - NSData *data = [NSData dataWithBytes:(char []){'a'} length:1]; - [self.sut setAuthenticationCookieData:data]; - XCTAssertNotNil(self.sut.authenticationCookieData); - [self.sut setAuthenticationCookieData:nil]; - XCTAssertNil(self.sut.authenticationCookieData); -} - -- (void)testThatItPersistsCookies; -{ - NSData *data = [NSData dataWithBytes:(char []){'a'} length:1]; - @autoreleasepool { - ZMPersistentCookieStorage *sut1 = self.sut; - [sut1 setAuthenticationCookieData:data]; - } - { - ZMPersistentCookieStorage *sut2 = self.sut; - XCTAssertEqualObjects([sut2 authenticationCookieData], data); - } -} - -- (void)testThatItCachesCookies; -{ - // given - NSData *data = [NSData dataWithBytes:(char []){'a'} length:1]; - ZMPersistentCookieStorage *sut1 = [ZMPersistentCookieStorage storageForServerName:@"z1.example.com" userIdentifier:self.userIdentifier useCache:YES]; - XCTAssertTrue([sut1 isCacheEmpty]); - - // when - [sut1 setAuthenticationCookieData:data]; - - // then - XCTAssertFalse([sut1 isCacheEmpty]); -} - -- (void)testThatItDoesNotCacheCookies; -{ - // given - NSData *data = [NSData dataWithBytes:(char []){'a'} length:1]; - ZMPersistentCookieStorage *sut1 = [ZMPersistentCookieStorage storageForServerName:@"z1.example.com" userIdentifier:self.userIdentifier useCache:NO]; - XCTAssertTrue([sut1 isCacheEmpty]); - - // when - [sut1 setAuthenticationCookieData:data]; - - // then - XCTAssertTrue([sut1 isCacheEmpty]); -} - -- (void)testThatItCanDeleteCookiesForASpecificCookieStorage -{ - // given - NSUUID *otherUserIdentifier = NSUUID.createUUID; - ZMPersistentCookieStorage *sut1 = [ZMPersistentCookieStorage storageForServerName:@"z1.example.com" userIdentifier:self.userIdentifier useCache:YES]; - ZMPersistentCookieStorage *sut2 = [ZMPersistentCookieStorage storageForServerName:@"z1.example.com" userIdentifier:otherUserIdentifier useCache:YES]; - - NSData *data1 = [@"This is the first cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - NSData *data2 = [@"This is the second cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - [sut1 setAuthenticationCookieData:data1]; - XCTAssertNotNil(sut1.authenticationCookieData); - [sut2 setAuthenticationCookieData:data2]; - XCTAssertNotNil(sut2.authenticationCookieData); - - // when - [sut1 deleteKeychainItems]; - - // then - XCTAssertNil(sut1.authenticationCookieData); - XCTAssertEqualObjects(sut2.authenticationCookieData, data2); - - // when - [sut2 deleteKeychainItems]; - XCTAssertNil(sut1.authenticationCookieData); - XCTAssertNil(sut2.authenticationCookieData); -} - -- (void)testThatItCanDeleteAllCookies -{ - // given - NSUUID *otherUserIdentifier = NSUUID.createUUID; - ZMPersistentCookieStorage *sut1 = [ZMPersistentCookieStorage storageForServerName:@"z1.example.com" userIdentifier:self.userIdentifier useCache:YES]; - ZMPersistentCookieStorage *sut2 = [ZMPersistentCookieStorage storageForServerName:@"z1.example.com" userIdentifier:otherUserIdentifier useCache:YES]; - - NSData *data1 = [@"This is the first cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - NSData *data2 = [@"This is the second cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - [sut1 setAuthenticationCookieData:data1]; - XCTAssertNotNil(sut1.authenticationCookieData); - [sut2 setAuthenticationCookieData:data2]; - XCTAssertNotNil(sut2.authenticationCookieData); - - // when - [sut1 deleteKeychainItems]; - [sut2 deleteKeychainItems]; - - // then - XCTAssertNil(sut1.authenticationCookieData); - XCTAssertNil(sut2.authenticationCookieData); -} - -- (void)testThatItMigratesAnDeletesOldCookieData -{ - // given - NSUUID *otherUserIdentifier = NSUUID.createUUID; - NSString *serverName = @"z1.example.com"; - ZMPersistentCookieStorage *legacySut = [ZMPersistentCookieStorage storageForServerName:serverName userIdentifier:(NSUUID *_Nonnull)nil useCache:YES]; - ZMPersistentCookieStorage *sut2 = [ZMPersistentCookieStorage storageForServerName:serverName userIdentifier:otherUserIdentifier useCache:YES]; - - NSData *data1 = [@"This is the first cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - NSData *data2 = [@"This is the second cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - [legacySut setAuthenticationCookieData:data1]; - XCTAssertNotNil(legacySut.authenticationCookieData); - [sut2 setAuthenticationCookieData:data2]; - XCTAssertNotNil(sut2.authenticationCookieData); - - // when - ZMPersistentCookieStorageMigrator *migrator = [ZMPersistentCookieStorageMigrator migratorWithUserIdentifier:self.userIdentifier serverName:serverName]; - ZMPersistentCookieStorage *sut1 = [migrator createStoreMigratingLegacyStoreIfNeeded]; - - // then - XCTAssertNil(legacySut.authenticationCookieData); - XCTAssertEqualObjects(sut1.authenticationCookieData, data1); - XCTAssertEqualObjects(sut2.authenticationCookieData, data2); -} - -- (void)testThatItDoesNotMigrateIfThereIsNoOldCookieData -{ - // given - NSString *serverName = @"z1.example.com"; - NSUUID *otherUserIdentifier = NSUUID.createUUID; - ZMPersistentCookieStorage *sut2 = [ZMPersistentCookieStorage storageForServerName:serverName userIdentifier:otherUserIdentifier useCache:YES]; - ZMPersistentCookieStorage *legacySut = [ZMPersistentCookieStorage storageForServerName:serverName userIdentifier:(NSUUID *_Nonnull)nil useCache:YES]; - NSData *data1 = [@"This is the first cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - NSData *data2 = [@"This is the second cookie data" dataUsingEncoding:NSUTF8StringEncoding]; - - { - ZMPersistentCookieStorage *sut1 = [ZMPersistentCookieStorage storageForServerName:serverName userIdentifier:self.userIdentifier useCache:YES]; - [sut1 setAuthenticationCookieData:data1]; - [sut2 setAuthenticationCookieData:data2]; - XCTAssertNil(legacySut.authenticationCookieData); - XCTAssertEqualObjects(sut1.authenticationCookieData, data1); - XCTAssertEqualObjects(sut2.authenticationCookieData, data2); - } - - { - // when - ZMPersistentCookieStorageMigrator *migrator = [ZMPersistentCookieStorageMigrator migratorWithUserIdentifier:self.userIdentifier serverName:serverName]; - ZMPersistentCookieStorage *sut1 = [migrator createStoreMigratingLegacyStoreIfNeeded]; - - // then - XCTAssertNil(legacySut.authenticationCookieData); - XCTAssertEqualObjects(sut1.authenticationCookieData, data1); - XCTAssertEqualObjects(sut2.authenticationCookieData, data2); - } -} - -- (void)testThatItHasAccessibleAuthenticationCookieData_WhenAuthenticationCookieDataIsAvailable -{ - // given - ZMPersistentCookieStorage *sut = [ZMPersistentCookieStorage storageForServerName:@"z1.example.com" userIdentifier:self.userIdentifier useCache:YES]; - [sut setAuthenticationCookieData:[@"This is a cookie" dataUsingEncoding:NSUTF8StringEncoding]]; - - // then - XCTAssertTrue([ZMPersistentCookieStorage hasAccessibleAuthenticationCookieData]); -} - -- (void)testThatItDoesNotHaveAccessibleAuthenticationCookieData_WhenAuthenticationCookieDataIsNotAvailable -{ - XCTAssertFalse([ZMPersistentCookieStorage hasAccessibleAuthenticationCookieData]); -} - -@end - - - -@implementation ZMPersistentCookieStorageTests (HTTPCookie) - -- (void)testThatWeCanRetrieveTheCookie; -{ - // given - XCTAssertNil(self.sut.authenticationCookieData); - - NSDictionary *headerFields = @{@"Date": @"Thu, 24 Jul 2014 09:06:45 GMT", - @"Content-Encoding": @"gzip", - @"Server": @"nginx", - @"Content-Type": @"application/json", - @"Access-Control-Allow-Origin": @"file://", - @"Connection": @"keep-alive", - @"Set-Cookie": @"zuid=wjCWn1Y1pBgYrFCwuU7WK2eHpAVY8Ocu-rUAWIpSzOcvDVmYVc9Xd6Ovyy-PktFkamLushbfKgBlIWJh6ZtbAA==.1721442805.u.7eaaa023.08326f5e-3c0f-4247-a235-2b4d93f921a4; Expires=Sun, 21-Jul-2024 09:06:45 GMT; Domain=wire.com; HttpOnly; Secure", - @"Content-Length": @"214"}; - NSURL *URL = [NSURL URLWithString:@"https://zeta.example.com/login"]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headerFields]; - [self.sut setCookieDataFromResponse:response forURL:URL]; - XCTAssertNotNil(self.sut.authenticationCookieData); - - // when - NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; - [request setURL:URL]; - [self.sut setRequestHeaderFieldsOnRequest:request]; - - // then - XCTAssertEqualObjects([request valueForHTTPHeaderField:@"Cookie"], - @"zuid=wjCWn1Y1pBgYrFCwuU7WK2eHpAVY8Ocu-rUAWIpSzOcvDVmYVc9Xd6Ovyy-PktFkamLushbfKgBlIWJh6ZtbAA==.1721442805.u.7eaaa023.08326f5e-3c0f-4247-a235-2b4d93f921a4"); -} - -- (void)testThatWeRetrieveCookieExpirationDate -{ - // given - XCTAssertNil(self.sut.authenticationCookieData); - - NSDictionary *headerFields = @{@"Date": @"Thu, 24 Jul 2014 09:06:45 GMT", - @"Content-Encoding": @"gzip", - @"Server": @"nginx", - @"Content-Type": @"application/json", - @"Access-Control-Allow-Origin": @"file://", - @"Connection": @"keep-alive", - @"Set-Cookie": @"zuid=wjCWn1Y1pBgYrFCwuU7WK2eHpAVY8Ocu-rUAWIpSzOcvDVmYVc9Xd6Ovyy-PktFkamLushbfKgBlIWJh6ZtbAA==.1721442805.u.7eaaa023.08326f5e-3c0f-4247-a235-2b4d93f921a4; Expires=Sun, 21-Jul-2024 09:06:45 GMT; Domain=wire.com; HttpOnly; Secure", - @"Content-Length": @"214"}; - NSURL *URL = [NSURL URLWithString:@"https://zeta.example.com/login"]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headerFields]; - [self.sut setCookieDataFromResponse:response forURL:URL]; - XCTAssertNotNil(self.sut.authenticationCookieData); - - // when - NSISO8601DateFormatter *dateFormatter = [[NSISO8601DateFormatter alloc] init]; - XCTAssertEqualObjects([dateFormatter stringFromDate:self.sut.authenticationCookieExpirationDate], @"2024-07-21T09:06:45Z"); -} - -- (void)testThatItDoesNotSetACookieDataIfNewCookieIsInvalid; -{ - // given - XCTAssertNil(self.sut.authenticationCookieData); - self.sut.authenticationCookieData = [@"previous-cookie" dataUsingEncoding:NSUTF8StringEncoding]; - - NSDictionary *headerFields = @{@"Date": @"Thu, 24 Jul 2014 09:06:45 GMT", - @"Content-Encoding": @"gzip", - @"Server": @"nginx", - @"Content-Type": @"application/json", - @"Access-Control-Allow-Origin": @"file://", - @"Connection": @"keep-alive", - @"Set-Cookie": @"UTTER GARBAGE", - @"Content-Length": @"214"}; - NSURL *URL = [NSURL URLWithString:@"https://zeta.example.com/login"]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headerFields]; - - // when - [self.sut setCookieDataFromResponse:response forURL:URL]; - - // then - XCTAssertNotNil(self.sut.authenticationCookieData); -} - -- (void)testThatItDoesNotStoreNotAuthCookies -{ - // given - XCTAssertNil(self.sut.authenticationCookieData); - - NSDictionary *headerFields = @{@"Date": @"Thu, 24 Jul 2014 09:06:45 GMT", - @"Content-Encoding": @"gzip", - @"Server": @"nginx", - @"Content-Type": @"application/json", - @"Access-Control-Allow-Origin": @"file://", - @"Connection": @"keep-alive", - @"Set-Cookie": @"zuid.challenge=wjCWn1Y1pBgYrFCwuU7WK2eHpAVY8Ocu-rUAWIpSzOcvDVmYVc9Xd6Ovyy-PktFkamLushbfKgBlIWJh6ZtbAA==.1721442805.u.7eaaa023.08326f5e-3c0f-4247-a235-2b4d93f921a4; Expires=Sun, 21-Jul-2024 09:06:45 GMT; Domain=wire.com; HttpOnly; Secure", - @"Content-Length": @"214"}; - NSURL *URL = [NSURL URLWithString:@"https://zeta.example.com/login"]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headerFields]; - - // when - [self.sut setCookieDataFromResponse:response forURL:URL]; - - // then - XCTAssertNil(self.sut.authenticationCookieData); -} - -- (void)testThatItStoresAuthCookies -{ - // given - XCTAssertNil(self.sut.authenticationCookieData); - - NSDictionary *headerFields = @{@"Date": @"Thu, 24 Jul 2014 09:06:45 GMT", - @"Content-Encoding": @"gzip", - @"Server": @"nginx", - @"Content-Type": @"application/json", - @"Access-Control-Allow-Origin": @"file://", - @"Connection": @"keep-alive", - @"Set-Cookie": @"zuid=wjCWn1Y1pBgYrFCwuU7WK2eHpAVY8Ocu-rUAWIpSzOcvDVmYVc9Xd6Ovyy-PktFkamLushbfKgBlIWJh6ZtbAA==.1721442805.u.7eaaa023.08326f5e-3c0f-4247-a235-2b4d93f921a4; Expires=Sun, 21-Jul-2024 09:06:45 GMT; Domain=wire.com; HttpOnly; Secure", - @"Content-Length": @"214"}; - NSURL *URL = [NSURL URLWithString:@"https://zeta.example.com/login"]; - NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:URL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headerFields]; - - // when - [self.sut setCookieDataFromResponse:response forURL:URL]; - - // then - XCTAssertNotNil(self.sut.authenticationCookieData); -} - -@end diff --git a/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.m b/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.m index e310d17c373..a54864fd6b5 100644 --- a/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.m +++ b/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.m @@ -35,13 +35,11 @@ #import "ZMTransportSession+internal.h" #import "ZMTransportCodec.h" #import "ZMTransportRequest+Internal.h" -#import "ZMPersistentCookieStorage.h" #import "ZMReachability.h" #import "NSError+ZMTransportSession.h" #import "ZMUserAgent.h" #import "ZMURLSession.h" #import "Fakes.h" -#import "ZMPersistentCookieStorage.h" #import "WireTransport_ios_tests-Swift.h" /// the JSON Content-Type header @@ -231,7 +229,7 @@ @interface ZMTransportSessionTests : ZMTBaseTest @property (nonatomic) FakeTransportRequestScheduler *scheduler; @property (nonatomic) MockSessionsDirectory *sessionsDirectory; @property (nonatomic) NSUUID *userIdentifier; -@property (nonatomic) ZMPersistentCookieStorage *cookieStorage; +@property (nonatomic) LegacyCookieStorage *cookieStorage; @property (nonatomic) FakeReachability *reachability; @property (nonatomic) MockBackgroundActivityManager *activityManager; @property (nonatomic) MockEnvironment *environment; @@ -298,7 +296,7 @@ - (void)setUp self.environment = [[MockEnvironment alloc] init]; self.environment.backendURL = [NSURL URLWithString:@"https://base.example.com"]; self.environment.backendWSURL = [NSURL URLWithString:@"https://websocket.example.com"]; - self.cookieStorage = [ZMPersistentCookieStorage storageForServerName:self.environment.backendURL.host userIdentifier:self.userIdentifier useCache:YES]; + self.cookieStorage = [[LegacyCookieStorage alloc] initWithTestingWithUserIdentifier:self.userIdentifier]; self.reachability = [[FakeReachability alloc] init]; self.activityManager = [[MockBackgroundActivityManager alloc] init]; @@ -359,7 +357,6 @@ - (void)tearDown self.scheduler = nil; self.sessionsDirectory = nil; - [ZMPersistentCookieStorage deleteAllKeychainItems]; self.cookieStorage = nil; [super tearDown]; currentTestCase = nil; @@ -378,7 +375,7 @@ - (void)setAuthenticationCookieData; HTTPVersion:@"" headerFields:headers]; [self.sut.cookieStorage setCookieDataFromResponse:response forURL:URL]; - XCTAssertNotNil(self.sut.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.sut.cookieStorage.hasAuthenticationCookie); } - (BOOL)requestMethodShouldHavePayload:(ZMTransportRequestMethod)method { @@ -1419,7 +1416,7 @@ - (void)testThatItDoesNotDeleteTheCookieWhenEncounteringANetworkErrorAfterLogin WaitForAllGroupsToBeEmpty(0.5); // then - XCTAssertNotNil(self.sut.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.sut.cookieStorage.hasAuthenticationCookie); } @@ -1427,7 +1424,7 @@ - (void)testThatItFailsARequestWith_TryAgainLater_IfTheAccessTokenWasInvalid { // given self.sut.accessToken = self.validAccessToken; - self.sut.cookieStorage.authenticationCookieData = [NSHTTPCookie validCookieData]; + [self.sut.cookieStorage storeCookies:[NSHTTPCookie validCookies] error:nil]; // The request will fail with a 401: NSDictionary *dummyPayload = @{@"b": @"B"}; [self mockURLSessionTaskWithResponseGenerator:^TestResponse *(NSURLRequest *request, NSData *data ZM_UNUSED) { @@ -1465,7 +1462,7 @@ - (void)testThatItStoresCookiesFor_ZMTransportRequestAuthCreatesCookieAndAccessT { // given NSData *cookieData = [@"valid-cookie" dataUsingEncoding:NSUTF8StringEncoding]; - self.sut.cookieStorage.authenticationCookieData = [NSHTTPCookie validCookieData]; + [self.sut.cookieStorage storeCookies:[NSHTTPCookie validCookies] error:nil]; [self mockURLSessionTaskWithResponseGenerator:^TestResponse *(NSURLRequest *request ZM_UNUSED, NSData *data ZM_UNUSED) { TestResponse *testResponse = [TestResponse testResponse]; @@ -1482,8 +1479,7 @@ - (void)testThatItStoresCookiesFor_ZMTransportRequestAuthCreatesCookieAndAccessT WaitForAllGroupsToBeEmpty(0.5); // then - AssertNotEqualData(self.sut.cookieStorage.authenticationCookieData, cookieData); - XCTAssertNotNil(self.sut.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.sut.cookieStorage.hasAuthenticationCookie); } @@ -1491,7 +1487,7 @@ - (void)testThatItStoresCookiesFor_ZMTransportRequestAuthNeedsAccess { // given NSData *cookieData = [@"valid-cookie" dataUsingEncoding:NSUTF8StringEncoding]; - self.sut.cookieStorage.authenticationCookieData = [NSHTTPCookie validCookieData]; + [self.sut.cookieStorage storeCookies:[NSHTTPCookie validCookies] error:nil]; [self mockURLSessionTaskWithResponseGenerator:^TestResponse *(NSURLRequest *request ZM_UNUSED, NSData *data ZM_UNUSED) { TestResponse *testResponse = [TestResponse testResponse]; @@ -1508,8 +1504,7 @@ - (void)testThatItStoresCookiesFor_ZMTransportRequestAuthNeedsAccess WaitForAllGroupsToBeEmpty(0.5); // then - AssertNotEqualData(self.sut.cookieStorage.authenticationCookieData, cookieData); - XCTAssertNotNil(self.sut.cookieStorage.authenticationCookieData); + XCTAssertTrue(self.sut.cookieStorage.hasAuthenticationCookie); } @@ -1518,7 +1513,7 @@ - (void)testThatItStoresCookiesBeforeCallingTheCompletionHandler; // TODO // given self.sut.accessToken = nil; - XCTAssertNil(self.sut.cookieStorage.authenticationCookieData); + XCTAssertFalse(self.sut.cookieStorage.hasAuthenticationCookie); [self mockURLSessionTaskWithResponseGenerator:^TestResponse *(NSURLRequest *request ZM_UNUSED, NSData *data ZM_UNUSED) { TestResponse *testResponse = [TestResponse testResponse]; testResponse.body = [NSJSONSerialization dataWithJSONObject:@{@"a": @"A"} options:0 error:NULL]; @@ -1530,9 +1525,9 @@ - (void)testThatItStoresCookiesBeforeCallingTheCompletionHandler; // when XCTestExpectation *didRun = [self customExpectationWithDescription:@"completion handler"]; ZMTransportRequest *request = [[ZMTransportRequest alloc] initWithPath:self.dummyPath method:ZMTransportRequestMethodGet payload:nil authentication:ZMTransportRequestAuthCreatesCookieAndAccessToken apiVersion:0]; - ZMPersistentCookieStorage *cookieStorage = self.sut.cookieStorage; + LegacyCookieStorage *cookieStorage = self.sut.cookieStorage; [request addCompletionHandler:[ZMCompletionHandler handlerOnGroupQueue:self.fakeUIContext block:^(ZMTransportResponse * ZM_UNUSED r) { - XCTAssertNotNil(cookieStorage.authenticationCookieData); + XCTAssertTrue(cookieStorage.hasAuthenticationCookie); [didRun fulfill]; }]]; diff --git a/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.swift b/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.swift index c207a4d640b..c3dab599418 100644 --- a/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.swift +++ b/wire-ios-transport/Tests/Source/TransportSession/ZMTransportSessionTests.swift @@ -18,7 +18,8 @@ import Foundation import WireTesting -import WireTransport +import WireTransportSupport +@testable import WireTransport @objcMembers public final class FakeReachability: NSObject, ReachabilityProvider, TearDownCapable { @@ -64,7 +65,7 @@ final class ZMTransportSessionTests_Initialization: ZMTBaseTest { var serverName: String! var baseURL: URL! var websocketURL: URL! - var cookieStorage: ZMPersistentCookieStorage! + var cookieStorage: LegacyCookieStorage! var reachability: FakeReachability! var sut: ZMTransportSession! var environment: MockEnvironment! @@ -76,10 +77,8 @@ final class ZMTransportSessionTests_Initialization: ZMTBaseTest { serverName = "https://example.com" baseURL = URL(string: serverName)! websocketURL = URL(string: serverName)!.appendingPathComponent("websocket") - cookieStorage = ZMPersistentCookieStorage( - forServerName: serverName, - userIdentifier: userIdentifier, - useCache: true + cookieStorage = LegacyCookieStorage( + testingWithUserIdentifier: userIdentifier ) reachability = FakeReachability() environment = MockEnvironment() diff --git a/wire-ios-transport/WireTransport.xcodeproj/project.pbxproj b/wire-ios-transport/WireTransport.xcodeproj/project.pbxproj index d494ca7e21b..c1d444a21ff 100644 --- a/wire-ios-transport/WireTransport.xcodeproj/project.pbxproj +++ b/wire-ios-transport/WireTransport.xcodeproj/project.pbxproj @@ -105,9 +105,6 @@ additionalCompilerFlagsByRelativePath = { Requests/ZMTaskIdentifierMap.m = "-fno-objc-arc"; }; - privateHeaders = ( - "Authentication/ZMPersistentCookieStorage+Testing.h", - ); publicHeaders = ( Authentication/ZMAccessTokenHandler.h, "Public/Collections+ZMTSafeTypes.h", @@ -116,7 +113,6 @@ Public/WireTransport.h, Public/ZMBackgroundable.h, Public/ZMKeychain.h, - Public/ZMPersistentCookieStorage.h, Public/ZMReachability.h, Public/ZMRequestCancellation.h, Public/ZMTaskIdentifierMap.h, diff --git a/wire-ios-transport/WireTransportSupport/HTTPCookie+Testing.swift b/wire-ios-transport/WireTransportSupport/HTTPCookie+Testing.swift index ab48c78e881..354aa088e80 100644 --- a/wire-ios-transport/WireTransportSupport/HTTPCookie+Testing.swift +++ b/wire-ios-transport/WireTransportSupport/HTTPCookie+Testing.swift @@ -30,4 +30,14 @@ public extension HTTPCookie { HTTPCookie.extractCookieData(from: string, url: URL(string: "https://example.com")!)! } + @objc + class func validCookies() -> [HTTPCookie] { + validCookies(string: "zuid=something; Path=/access; Expires=Tue, 06-Oct-2099 11:46:18 GMT; HttpOnly; Secure") + } + + @objc + class func validCookies(string: String) -> [HTTPCookie] { + HTTPCookie.cookies(from: string, for: URL(string: "https://example.com")!) + } + } diff --git a/wire-ios-transport/WireTransportSupport/LegacyCookieStorage+Testing.swift b/wire-ios-transport/WireTransportSupport/LegacyCookieStorage+Testing.swift new file mode 100644 index 00000000000..3f0daa1dbd4 --- /dev/null +++ b/wire-ios-transport/WireTransportSupport/LegacyCookieStorage+Testing.swift @@ -0,0 +1,52 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import WireTransport + +/// An in-memory cookie storage for testing purposes. +public final class StubCookieStorage: CookieStorageProtocol { + + public nonisolated(unsafe) var cookies: [HTTPCookie] = [] + + public init() {} + + public func storeCookies(_ cookies: [HTTPCookie], userID: UUID) throws { + self.cookies = cookies + } + + public func fetchCookies(userID: UUID) throws -> [HTTPCookie] { + cookies + } + + public func removeCookies(userID: UUID) throws { + cookies = [] + } + +} + +public extension LegacyCookieStorage { + + @objc + convenience init(testingWithUserIdentifier userIdentifier: UUID) { + self.init( + userIdentifier: userIdentifier, + cookieStorage: StubCookieStorage() + ) + } + +} diff --git a/wire-ios-transport/testing.modulemap b/wire-ios-transport/testing.modulemap index 1b957b7d747..0d802d4fb9e 100644 --- a/wire-ios-transport/testing.modulemap +++ b/wire-ios-transport/testing.modulemap @@ -1,6 +1,4 @@ explicit module WireTransport.Testing { - header "ZMPersistentCookieStorage+Testing.h" - export * } diff --git a/wire-ios/Wire-iOS Tests/Authentication/CookieStorageTests.swift b/wire-ios/Wire-iOS Tests/Authentication/CookieStorageTests.swift new file mode 100644 index 00000000000..80b2919e261 --- /dev/null +++ b/wire-ios/Wire-iOS Tests/Authentication/CookieStorageTests.swift @@ -0,0 +1,459 @@ +// +// Wire +// Copyright (C) 2026 Wire Swiss GmbH +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see http://www.gnu.org/licenses/. +// + +import Foundation +import Testing +import WireCrypto +import WireFoundation +import WireFoundationSupport + +@testable import WireNetwork + +@Suite(.serialized) +final class CookieStorageTests { + + private let encryptionKey: Data + private let keychain = Keychain() + private let sut: CookieStorage + + init() throws { + self.encryptionKey = try AES256Crypto.generateRandomEncryptionKey() + self.sut = CookieStorage(cookieEncryptionKey: encryptionKey) + + try keychain.reset() + } + + deinit { + try? keychain.reset() + } + + // MARK: - Store + + @Test() + func `test store cookies when no existing cookies for that user`() throws { + // Given + let cookie = Scaffolding.validCookieA + let userID = UUID() + let epoch = UUID() + + // When + try sut.storeCookies([cookie], userID: userID, epoch: epoch) + + // Then + let item = try #require(try fetchItemFromKeychain(userID: userID)) + #expect(item.epoch == epoch) + #expect(item.accessible == kSecAttrAccessibleAfterFirstUnlock) + + let cookieData = try #require(item.secureValue) + let decodedCookies = try Self.decryptAndDecodeCookieData(cookieData, encryptionKey: encryptionKey) + #expect(decodedCookies == [cookie]) + } + + @Test() + func `test store cookies when existing cookies for that user`() throws { + // Given + let userID = UUID() + let initialEpoch = UUID() + let updatedEpoch = UUID() + + try sut.storeCookies([Scaffolding.validCookieA], userID: userID, epoch: initialEpoch) + + // When + try sut.storeCookies([Scaffolding.validCookieB], userID: userID, epoch: updatedEpoch) + + // Then + let item = try #require(try fetchItemFromKeychain(userID: userID)) + #expect(item.epoch == updatedEpoch) + #expect(item.accessible == kSecAttrAccessibleAfterFirstUnlock) + + let cookieData = try #require(item.secureValue) + let decodedCookies = try Self.decryptAndDecodeCookieData(cookieData, encryptionKey: encryptionKey) + #expect(decodedCookies == [Scaffolding.validCookieB]) + } + + @Test() + func `test store cookies with invalid cookie`() throws { + // Given + let userID = UUID() + + // When / Then + #expect(throws: HTTPCookieCodecError.invalidCookies) { + try sut.storeCookies([Scaffolding.invalidCookie], userID: userID) + } + + #expect(try fetchItemFromKeychain(userID: userID) == nil) + } + + @Test() + func `test store cookies with empty array`() throws { + // Given + let userID = UUID() + + // When / Then + #expect(throws: HTTPCookieCodecError.invalidCookies) { + try sut.storeCookies([], userID: userID) + } + + #expect(try fetchItemFromKeychain(userID: userID) == nil) + } + + // MARK: - Fetch + + @Test() + func `test fetch cookies when no existing cookies for that user`() throws { + // Given, When + let cookies = try sut.fetchCookies(userID: UUID()) + + // Then + #expect(cookies.isEmpty) + } + + @Test() + func `test fetch cookies when existing cookies for that user`() throws { + // Given + let userID = UUID() + try sut.storeCookies([Scaffolding.validCookieA], userID: userID) + + // When + let cookies = try sut.fetchCookies(userID: userID) + + // Then + #expect(cookies == [Scaffolding.validCookieA]) + } + + @Test() + func `test fetch cookies repairs missing epoch`() throws { + // Given + let userID = UUID() + let cookieData = try Self.encodeAndEncryptCookieData( + for: [Scaffolding.validCookieA], + encryptionKey: encryptionKey + ) + + try keychain.addItem(query: [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword), + .data(cookieData), + .accessible(.afterFirstUnlock) + ]) + + // When + let cookies = try sut.fetchCookies(userID: userID) + + // Then + #expect(cookies == [Scaffolding.validCookieA]) + + let item = try #require(try fetchItemFromKeychain(userID: userID)) + #expect(item.epoch != nil) + + let newCookieData = try #require(item.secureValue) + let decodedCookies = try Self.decryptAndDecodeCookieData(newCookieData, encryptionKey: encryptionKey) + #expect(decodedCookies == [Scaffolding.validCookieA]) + } + + @Test() + func `test fetch cookies with different encryption key throws an error`() throws { + // Given + let userID = UUID() + try sut.storeCookies([Scaffolding.validCookieA], userID: userID) + + let differentEncryptionKey = try AES256Crypto.generateRandomEncryptionKey() + let sutWithDifferentKey = CookieStorage( + cookieEncryptionKey: differentEncryptionKey, + keychain: keychain, + cache: CookieStorageCache(sharedStorage: .init(initialState: [:])) + ) + + #expect { + _ = try sutWithDifferentKey.fetchCookies(userID: userID) + } throws: { error in + switch error { + case HTTPCookieCodecError.invalidCookieData: + true + default: + false + } + } + + } + + // MARK: - Remove + + @Test() + func `test remove cookies deletes existing cookies for that user`() throws { + // Given + let userID = UUID() + try sut.storeCookies([Scaffolding.validCookieA], userID: userID) + + // When + try sut.removeCookies(userID: userID) + + // Then + #expect(try fetchItemFromKeychain(userID: userID) == nil) + } + + @Test() + func `test remove cookies results in empty fetch for that user`() throws { + // Given + let userID = UUID() + try sut.storeCookies([Scaffolding.validCookieA], userID: userID) + + // When + try sut.removeCookies(userID: userID) + let cookies = try sut.fetchCookies(userID: userID) + + // Then + #expect(cookies.isEmpty) + } + + @Test() + func `test remove cookies does not delete cookies for another user`() throws { + // Given + let userA = UUID() + let userB = UUID() + + try sut.storeCookies([Scaffolding.validCookieA], userID: userA) + try sut.storeCookies([Scaffolding.validCookieB], userID: userB) + + // When + try sut.removeCookies(userID: userA) + + // Then + #expect(try sut.fetchCookies(userID: userA).isEmpty) + #expect(try sut.fetchCookies(userID: userB) == [Scaffolding.validCookieB]) + } + + // MARK: - Caching + + @Test() + func `test fetch cookies uses in memory cache`() throws { + // Given + let userID = UUID() + let keychain = KeychainSpy(keychain: keychain) + let sut = CookieStorage( + cookieEncryptionKey: encryptionKey, + keychain: keychain, + cache: CookieStorageCache(sharedStorage: CookieStorageCache.sharedStorage) + ) + try sut.storeCookies([Scaffolding.validCookieA], userID: userID) + + // When - 1st fetch should fetch both data and attributes (for the epoch) + let firstFetch = try sut.fetchCookies(userID: userID) + + // Then + #expect(firstFetch == [Scaffolding.validCookieA]) + #expect(keychain.fetchItem_Invocations == [ + [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword), + .returningData(true) + ], + [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword), + .returningData(false), + .returningAttributes(true) + ] + ]) + + // When - 2nd fetch should fetch only the attributes (for the epoch), not cookie data. + keychain.fetchItem_Invocations.removeAll() + let secondFetch = try sut.fetchCookies(userID: userID) + + // Then + #expect(secondFetch == [Scaffolding.validCookieA]) + #expect(keychain.fetchItem_Invocations == [ + [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword), + .returningData(false), + .returningAttributes(true) + ] + ]) + } + + @Test() + func `test cache is invalidated if item deleted from keychain`() throws { + // Given + let userID = UUID() + try sut.storeCookies([Scaffolding.validCookieA], userID: userID) + + // When + let firstFetch = try sut.fetchCookies(userID: userID) + + // Then + #expect(firstFetch == [Scaffolding.validCookieA]) + + // When + try Keychain().reset() + let secondFetch = try sut.fetchCookies(userID: userID) + + // Then + #expect(secondFetch == []) + } + + @Test() + func `test cache is invalidated if item updated in keychain`() throws { + // Given + let userID = UUID() + let sutA = CookieStorage( + cookieEncryptionKey: encryptionKey, + keychain: keychain, + cache: CookieStorageCache(sharedStorage: .init(initialState: [:])) + ) + let sutB = CookieStorage( + cookieEncryptionKey: encryptionKey, + keychain: keychain, + cache: CookieStorageCache(sharedStorage: .init(initialState: [:])) + ) + try sutA.storeCookies([Scaffolding.validCookieA], userID: userID) + + // When cookies are updated from SUT B + try sutB.storeCookies([Scaffolding.validCookieB], userID: userID) // <- SUT B + + // Then cache is invalidated in SUT A + #expect(try sutA.fetchCookies(userID: userID) == [Scaffolding.validCookieB]) + } + + // MARK: - Helpers + + private func fetchItemFromKeychain(userID: UUID) throws -> [CFString: Any]? { + let query: Set = [ + .service("Wire: Credentials for wire.com"), + .account(userID.uuidString), + .itemClass(.genericPassword), + .returningData(true), + .returningAttributes(true) + ] + + return try keychain.fetchItem(query: query) + } + + private static func encodeAndEncryptCookieData( + for cookies: [HTTPCookie], + encryptionKey: Data + ) throws -> Data { + let encodedData = try HTTPCookieCodec.encodeCookies(cookies) + let encryptedData = try AES256Crypto.encryptAllAtOnceWithPrefixedIV( + plaintext: encodedData, + key: encryptionKey + ) + return encryptedData.data.base64EncodedData() + } + + private static func decryptAndDecodeCookieData( + _ base64CookieData: Data, + encryptionKey: Data + ) throws -> [HTTPCookie] { + let encryptedData = try #require(Data(base64Encoded: base64CookieData)) + let decryptedData = try AES256Crypto.decryptAllAtOnceWithPrefixedIV( + ciphertext: AES256Crypto.PrefixedData(data: encryptedData), + key: encryptionKey + ) + return try HTTPCookieCodec.decodeData(decryptedData) + } + +} + +// MARK: - Helpers + +private enum Scaffolding { + + static let invalidCookie = HTTPCookie(properties: [ + .name: "invalid-name", + .path: "some path", + .value: "some value", + .domain: "some domain" + ])! + + static let validCookieA = HTTPCookie(properties: [ + .name: "zuid", + .path: "some path", + .value: "some value A", + .domain: "some domain" + ])! + + static let validCookieB = HTTPCookie(properties: [ + .name: "zuid", + .path: "some path", + .value: "some value B", + .domain: "some domain" + ])! + +} + +private extension [CFString: Any] { + + var accessible: CFString? { + guard let value = self[kSecAttrAccessible] as? String else { return nil } + return value as CFString + } + + var epoch: UUID? { + guard let epochData = self[kSecAttrGeneric] as? Data else { return nil } + return epochData.withUnsafeBytes { $0.load(as: UUID.self) } + } + + var secureValue: Data? { + self[kSecValueData] as? Data + } + +} + +private extension UUID { + + var data: Data { + withUnsafeBytes(of: uuid) { Data($0) } + } + +} + +private final class KeychainSpy: KeychainProtocol, @unchecked Sendable { + + let keychain: WireFoundation.Keychain + var fetchItem_Invocations: [Set] = [] + + init(keychain: WireFoundation.Keychain) { + self.keychain = keychain + } + + func addItem(query: Set) throws { + try keychain.addItem(query: query) + } + + func updateItem( + query: Set, + attributesToUpdate: Set + ) throws { + try keychain.updateItem(query: query, attributesToUpdate: attributesToUpdate) + } + + func fetchItem(query: Set) throws -> T? { + fetchItem_Invocations.append(query) + return try keychain.fetchItem(query: query) + } + + func deleteItem(query: Set) throws { + try keychain.deleteItem(query: query) + } + +} diff --git a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj index 41a231cbb22..7d938a342a5 100644 --- a/wire-ios/Wire-iOS.xcodeproj/project.pbxproj +++ b/wire-ios/Wire-iOS.xcodeproj/project.pbxproj @@ -527,6 +527,7 @@ AudioRecordViewControllerTests.swift, Authentication/AuthenticationInterfaceBuilderTests.swift, Authentication/AuthenticationStateControllerTests.swift, + Authentication/CookieStorageTests.swift, Authentication/Helpers/RegistrationAnalyticsTrackerTests.swift, Authentication/MockAuthenticationFeatureProvider.swift, AvailabilityLabelTests.swift, diff --git a/wire-ios/Wire-iOS/Sources/AppDelegate.swift b/wire-ios/Wire-iOS/Sources/AppDelegate.swift index 8fc40946d59..751ceb07f74 100644 --- a/wire-ios/Wire-iOS/Sources/AppDelegate.swift +++ b/wire-ios/Wire-iOS/Sources/AppDelegate.swift @@ -46,6 +46,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // MARK: - Private Property + private let cookieStorage = CookieStorage(cookieEncryptionKey: UserDefaults.cookiesKey()) + private lazy var voIPPushManager: VoIPPushManager = .init( application: UIApplication.shared, pushTokenService: pushTokenService @@ -237,8 +239,8 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { DeveloperOverrides.storage = .shared() setupWindowAndRootViewController() - if UIApplication.shared.isProtectedDataAvailable || ZMPersistentCookieStorage - .hasAccessibleAuthenticationCookieData() { + // TODO: [WPB-24600] Use method on CookieStorage instead of ZMKeychain. + if UIApplication.shared.isProtectedDataAvailable || ZMKeychain.hasAccessibleAccountData() { createAppRootRouterAndInitialiazeOperations(launchOptions ?? [:]) } @@ -400,7 +402,10 @@ private extension AppDelegate { let sessionManager: SessionManager do { - sessionManager = try createSessionManager(defaultEnvironment: defaultEnvironment) + sessionManager = try createSessionManager( + defaultEnvironment: defaultEnvironment, + cookieStorage: cookieStorage + ) } catch { fatalError("sessionManager is not created") } @@ -423,7 +428,10 @@ private extension AppDelegate { ) } - private func createSessionManager(defaultEnvironment: BackendEnvironment2) throws -> SessionManager { + private func createSessionManager( + defaultEnvironment: BackendEnvironment2, + cookieStorage: CookieStorage + ) throws -> SessionManager { let infoDictionary = Bundle.main.infoDictionary guard let currentAppVersion = infoDictionary?["CFBundleShortVersionString"] as? String else { @@ -464,6 +472,7 @@ private extension AppDelegate { maxNumberAccounts: maxNumberAccounts, currentAppVersion: currentAppVersion, currentBuildNumber: currentBuildNumber, + cookieStorage: cookieStorage, mediaManager: mediaManager, delegate: appStateCalculator, application: UIApplication.shared, diff --git a/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift b/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift index 9ca6ee4adcd..59862b70017 100644 --- a/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift +++ b/wire-ios/Wire-iOS/Sources/Authentication/Coordinator/AuthenticationCoordinator.swift @@ -329,7 +329,6 @@ extension AuthenticationCoordinator: AuthenticationActioner, SessionManagerCreat let userInfo = UserInfo( identifier: result.userID, - cookieData: HTTPCookie.extractData(from: result.cookies)!, cookies: result.cookies ) diff --git a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift index f39e92558e8..a08ec633d53 100644 --- a/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift +++ b/wire-ios/Wire-iOS/Sources/Developer/DeveloperTools/DebugActions/DeveloperDebugActionsViewModel.swift @@ -238,9 +238,7 @@ final class DeveloperDebugActionsViewModel: ObservableObject { }) else { return } let cookieStorage = CookieStorage( - userID: selfUserID, - cookieEncryptionKey: UserDefaults.cookiesKey(), - keychain: WireFoundation.Keychain() + cookieEncryptionKey: UserDefaults.cookiesKey() ) // Forces the access token request to fail with 403 (invalid credentials) @@ -267,6 +265,7 @@ final class DeveloperDebugActionsViewModel: ObservableObject { networkService.executeRequest_MockValue = (data, httpURLResponse) let authenticationManager = AuthenticationManager( + userID: selfUserID, clientID: UUID().uuidString, cookieStorage: cookieStorage, networkService: networkService diff --git a/wire-ios/WireUITests/FederationTests.swift b/wire-ios/WireUITests/FederationTests.swift index 26c99e6f114..43f7d160db3 100644 --- a/wire-ios/WireUITests/FederationTests.swift +++ b/wire-ios/WireUITests/FederationTests.swift @@ -21,6 +21,7 @@ import XCTest final class FederationTests: WireUITestCase { @MainActor func testConnectFederatedUsers_TC_9459() async throws { + try switchBackend(target: .bella) let bellaTeam = try await UserHelper.instance(backend: .bella).registerTeam(withMemberCount: 0) _ = try await loginToBackend(user: bellaTeam.teamOwner) diff --git a/wire-ios/WireUITests/Helper/UserHelper.swift b/wire-ios/WireUITests/Helper/UserHelper.swift index e0a2260fec7..ff4055851f7 100644 --- a/wire-ios/WireUITests/Helper/UserHelper.swift +++ b/wire-ios/WireUITests/Helper/UserHelper.swift @@ -781,15 +781,15 @@ private final class MockCookieStorage: CookieStorageProtocol { self.cookies = [] } - func storeCookies(_ cookies: [HTTPCookie]) async throws { + func storeCookies(_ cookies: [HTTPCookie], userID: UUID) throws { self.cookies = cookies } - func fetchCookies() async throws -> [HTTPCookie] { + func fetchCookies(userID: UUID) throws -> [HTTPCookie] { cookies } - func removeCookies() async throws { + func removeCookies(userID: UUID) throws { cookies = [] } } diff --git a/wire-ios/WireUITests/MultiBackendSupportTests.swift b/wire-ios/WireUITests/MultiBackendSupportTests.swift index d6805259ae1..6ad2f15a1b5 100644 --- a/wire-ios/WireUITests/MultiBackendSupportTests.swift +++ b/wire-ios/WireUITests/MultiBackendSupportTests.swift @@ -45,6 +45,7 @@ final class MultiBackendSupportTests: WireUITestCase { @MainActor func testAddMultiBackendAccounts_TC_8940() async throws { + var (accountPageBackend1, userBackend1) = try await testLoginToBackend(.staging) _ = try accountPageBackend1 @@ -141,6 +142,67 @@ final class MultiBackendSupportTests: WireUITestCase { XCTAssert(try ConversationsPage().conversationCell(named: conversationC).waitForExistence(timeout: 2.0)) } + @MainActor + func testReLoginWhenMultipleBackends_TC_10550() async throws { + // Login to account A + let userA = try await UserHelper.instance(backend: .staging).createPersonalUser() + _ = try app + .loginUser(email: userA.email, password: userA.password) + .acceptPopup() + + // Go to Anta login + _ = try ConversationsPage() + .openUserProfilePage() + .tapAddAccountOrTeamButton() + + try switchBackend(target: .anta) + + // Login to account B + let userB = try await UserHelper.instance(backend: .anta).createPersonalUser() + _ = try app + .loginUser(email: userB.email, password: userB.password) + .acceptPopup() + + // Switch to account A + _ = try ConversationsPage() + .openUserProfilePage() + .switchUserAccountForUser(withName: userA.name) + + // Switch to account B + _ = try ConversationsPage() + .openUserProfilePage() + .switchUserAccountForUser(withName: userB.name) + + // Logout account B + _ = try ConversationsPage() + .openSettings() + .openAccountSettings() + .logout() + .enterPassword(userB.password, expectWelcomePage: false) + + // Go to Anta login + _ = try ConversationsPage() + .openUserProfilePage() + .tapAddAccountOrTeamButton() + + try switchBackend(target: .anta) + + // Re-login to account B + _ = try app + .loginUser(email: userB.email, password: userB.password) + + // Verify logged into account B + let accountSettingsPage = try ConversationsPage() + .openSettings() + .openAccountSettings() + + try verifySwitchingAccount( + accountPage: accountSettingsPage, + expectedUser: userB, + expectedDomain: BackendTarget.anta.domainInfo + ) + } + private func verifySwitchingAccount( accountPage: AccountSettingsPage, expectedUser: UserInfo,