Skip to content

Commit 462844a

Browse files
fix: EAR Security Tests - WPB-23474 πŸ’ (#4452)
Co-authored-by: David-Henner <david.henner@wire.com>
1 parent e185b1a commit 462844a

File tree

16 files changed

+379
-253
lines changed

16 files changed

+379
-253
lines changed

β€ŽWireDomain/Sources/WireDomain/Notifications/Components/NSEUserScope.swiftβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ final class NSEUserScope: Component<NSEUserScopeDependency> {
155155
throw Failure.mainAppRequired(message: "no self client id")
156156
}
157157

158-
let earService = await EARService(
158+
let earService = await EARServiceFactory.createEARService(
159159
accountID: accountID,
160160
coreDataStack: coreDataStack,
161161
sharedUserDefaults: dependency.sharedUserDefaults,

β€Žwire-ios-data-model/Source/Authentication/EAR/EARService.swiftβ€Ž

Lines changed: 9 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -50,54 +50,17 @@ public class EARService: EARServiceInterface {
5050

5151
// MARK: - Life cycle
5252

53-
/// Create a new `EARService`.
54-
///
55-
/// - Parameters:
56-
/// - accountID: The id of the self user.
57-
/// - databaseContexts: A list of database contexts that require access to the database key.
58-
/// - canPerformKeyMigration: Whether key migration can be performed. Key migration should not be performed when
59-
/// the service is running in app extensions.
60-
/// - sharedUserDefaults: The shared user defaults in which to keep track of whether EAR is enabled.
61-
/// - authenticationContext: The authentication context used to access encryption keys.
62-
63-
public convenience init(
64-
accountID: UUID,
65-
databaseContexts: [NSManagedObjectContext] = [],
66-
coreDataStack: CoreDataStackProtocol,
67-
canPerformKeyMigration: Bool = false,
68-
sharedUserDefaults: UserDefaults,
69-
authenticationContext: any AuthenticationContextProtocol
70-
) async {
71-
let earStorage = EARStorage(userID: accountID, sharedUserDefaults: sharedUserDefaults)
72-
let messageEncryptionService = EARMessageEncryptionService(earStorage: earStorage)
73-
let migrator = EARMigrator(messageEncryptionService: messageEncryptionService)
74-
75-
await self.init(
76-
accountID: accountID,
77-
keyRepository: EARKeyRepository(),
78-
keyEncryptor: EARKeyEncryptor(),
79-
databaseContexts: databaseContexts,
80-
coreDataStack: coreDataStack,
81-
canPerformKeyMigration: canPerformKeyMigration,
82-
earStorage: earStorage,
83-
messageEncryptionService: messageEncryptionService,
84-
migrator: migrator,
85-
authenticationContext: authenticationContext
86-
)
87-
}
88-
8953
init(
9054
accountID: UUID,
9155
keyRepository: EARKeyRepositoryInterface = EARKeyRepository(),
9256
keyEncryptor: EARKeyEncryptorInterface = EARKeyEncryptor(),
93-
databaseContexts: [NSManagedObjectContext],
9457
coreDataStack: CoreDataStackProtocol,
9558
canPerformKeyMigration: Bool,
9659
earStorage: EARStorage,
9760
messageEncryptionService: EARMessageEncryptionServiceProtocol,
9861
migrator: EARMigratorProtocol,
9962
authenticationContext: AuthenticationContextProtocol
100-
) async {
63+
) {
10164
self.accountID = accountID
10265
self.keyRepository = keyRepository
10366
self.keyEncryptor = keyEncryptor
@@ -114,17 +77,19 @@ public class EARService: EARServiceInterface {
11477

11578
coreDataStack.setEARMessageEncryptionService(messageEncryptionService)
11679

117-
for context in databaseContexts {
118-
await context.perform {
119-
context.earMessageEncryptionService = messageEncryptionService
120-
}
121-
}
122-
12380
if canPerformKeyMigration {
12481
migrateKeysIfNeeded()
12582
}
12683
}
12784

85+
func setupDatabaseContexts(databaseContexts: [NSManagedObjectContext]) async {
86+
for context in databaseContexts {
87+
await context.perform { [service = earMessageEncryptionService] in
88+
context.earMessageEncryptionService = service
89+
}
90+
}
91+
}
92+
12893
// MARK: - Lock status
12994

13095
public var isLocked: Bool {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//
2+
// Wire
3+
// Copyright (C) 2026 Wire Swiss GmbH
4+
//
5+
// This program is free software: you can redistribute it and/or modify
6+
// it under the terms of the GNU General Public License as published by
7+
// the Free Software Foundation, either version 3 of the License, or
8+
// (at your option) any later version.
9+
//
10+
// This program is distributed in the hope that it will be useful,
11+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
// GNU General Public License for more details.
14+
//
15+
// You should have received a copy of the GNU General Public License
16+
// along with this program. If not, see http://www.gnu.org/licenses/.
17+
//
18+
19+
public enum EARServiceFactory {
20+
21+
// MARK: - Public Interface
22+
23+
/// Create a new `EARService`.
24+
///
25+
/// - Parameters:
26+
/// - accountID: The id of the self user.
27+
/// - databaseContexts: The managed object contexts on which to set the `EARMessageEncryptionService`
28+
/// - coreDataStack: The core data stack on which the `EARMessageEncryptionService` will be set.
29+
/// - canPerformKeyMigration: Whether key migration can be performed. Key migration should not be performed when
30+
/// the service is running in app extensions.
31+
/// - sharedUserDefaults: The shared user defaults in which to keep track of whether EAR is enabled.
32+
/// - authenticationContext: The authentication context used to access encryption keys.
33+
/// - Returns: a newly created `EARService` instance
34+
///
35+
public static func createEARService(
36+
accountID: UUID,
37+
databaseContexts: [NSManagedObjectContext] = [],
38+
coreDataStack: CoreDataStackProtocol,
39+
canPerformKeyMigration: Bool = false,
40+
sharedUserDefaults: UserDefaults,
41+
authenticationContext: any AuthenticationContextProtocol
42+
) async -> EARServiceInterface {
43+
let earStorage = EARStorage(userID: accountID, sharedUserDefaults: sharedUserDefaults)
44+
let messageEncryptionService = EARMessageEncryptionService(earStorage: earStorage)
45+
let migrator = EARMigrator(messageEncryptionService: messageEncryptionService)
46+
47+
let earService = EARService(
48+
accountID: accountID,
49+
keyRepository: EARKeyRepository(),
50+
keyEncryptor: EARKeyEncryptor(),
51+
coreDataStack: coreDataStack,
52+
canPerformKeyMigration: canPerformKeyMigration,
53+
earStorage: earStorage,
54+
messageEncryptionService: messageEncryptionService,
55+
migrator: migrator,
56+
authenticationContext: authenticationContext
57+
)
58+
59+
await internalSetupDatabaseContexts(
60+
databaseContexts: databaseContexts,
61+
onEARService: earService
62+
)
63+
64+
return earService
65+
}
66+
67+
// MARK: - Tests Interface
68+
69+
#if DEBUG
70+
71+
/// Synchronously creates an `EARService` instance
72+
///
73+
/// - Parameters:
74+
/// - accountID: The id of the self user.
75+
/// - coreDataStack: The core data stack on which the `EARMessageEncryptionService` will be set.
76+
/// - canPerformKeyMigration: Whether key migration can be performed. Key migration should not be performed
77+
/// when
78+
/// the service is running in app extensions.
79+
/// - sharedUserDefaults: The shared user defaults in which to keep track of whether EAR is enabled.
80+
/// - authenticationContext: The authentication context used to access encryption keys.
81+
/// - Returns: a newly created `EARService` instance
82+
///
83+
public static func createEARService(
84+
accountID: UUID,
85+
coreDataStack: CoreDataStackProtocol,
86+
canPerformKeyMigration: Bool = false,
87+
sharedUserDefaults: UserDefaults,
88+
authenticationContext: any AuthenticationContextProtocol
89+
) -> EARService {
90+
let earStorage = EARStorage(userID: accountID, sharedUserDefaults: sharedUserDefaults)
91+
let messageEncryptionService = EARMessageEncryptionService(earStorage: earStorage)
92+
let migrator = EARMigrator(messageEncryptionService: messageEncryptionService)
93+
94+
return EARService(
95+
accountID: accountID,
96+
keyRepository: EARKeyRepository(),
97+
keyEncryptor: EARKeyEncryptor(),
98+
coreDataStack: coreDataStack,
99+
canPerformKeyMigration: canPerformKeyMigration,
100+
earStorage: earStorage,
101+
messageEncryptionService: messageEncryptionService,
102+
migrator: migrator,
103+
authenticationContext: authenticationContext
104+
)
105+
}
106+
107+
/// Sets the `EARMessageEncryptionService` on the given managed object contexts
108+
///
109+
/// - Parameters:
110+
/// - databaseContexts: The managed object contexts on which to set the `EARMessageEncryptionService`
111+
/// - earService: The `EARService` instance to setup
112+
///
113+
public static func setupDatabaseContexts(
114+
databaseContexts: [NSManagedObjectContext],
115+
onEARService earService: EARService
116+
) async {
117+
await internalSetupDatabaseContexts(
118+
databaseContexts: databaseContexts,
119+
onEARService: earService
120+
)
121+
}
122+
123+
#endif
124+
125+
// MARK: - Private Helpers
126+
127+
private static func internalSetupDatabaseContexts(
128+
databaseContexts: [NSManagedObjectContext],
129+
onEARService earService: EARService
130+
) async {
131+
guard !databaseContexts.isEmpty else { return }
132+
133+
await earService.setupDatabaseContexts(
134+
databaseContexts: databaseContexts
135+
)
136+
}
137+
}

β€Žwire-ios-data-model/Tests/Authentication/EAR/EARServiceIntegrationTests.swiftβ€Ž

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,19 +67,20 @@ final class EARServiceIntegrationTests: EARServiceTestsBase, @MainActor EARServi
6767
canPerformMigration: Bool = false,
6868
contexts: [NSManagedObjectContext]? = nil
6969
) async -> EARService {
70-
let sut = await EARService(
70+
let sut = EARService(
7171
accountID: userID,
7272
keyRepository: keyRepository,
7373
keyEncryptor: keyEncryptor,
74-
databaseContexts: contexts ?? [uiMOC, syncMOC],
7574
coreDataStack: coreDataStack,
7675
canPerformKeyMigration: canPerformMigration,
7776
earStorage: earStorage,
7877
messageEncryptionService: messageEncryptionService,
7978
migrator: migrator,
8079
authenticationContext: MockAuthenticationContextProtocol()
8180
)
82-
81+
await sut.setupDatabaseContexts(
82+
databaseContexts: contexts ?? [uiMOC, syncMOC]
83+
)
8384
sut.delegate = self
8485
return sut
8586
}
@@ -391,18 +392,18 @@ final class EARServiceIntegrationTests: EARServiceTestsBase, @MainActor EARServi
391392
let messageEncryptionService = EARMessageEncryptionService(earStorage: earStorage)
392393
let migrator = EARMigrator(messageEncryptionService: messageEncryptionService)
393394

394-
let sut = await EARService(
395+
let sut = EARService(
395396
accountID: userID,
396397
keyRepository: EARKeyRepository(), // Real keychain access
397398
keyEncryptor: EARKeyEncryptor(), // Real crypto
398-
databaseContexts: [uiMOC],
399399
coreDataStack: coreDataStack,
400400
canPerformKeyMigration: false,
401401
earStorage: earStorage,
402402
messageEncryptionService: messageEncryptionService,
403403
migrator: migrator,
404404
authenticationContext: MockAuthenticationContextProtocol()
405405
)
406+
await sut.setupDatabaseContexts(databaseContexts: [uiMOC])
406407
sut.delegate = self
407408

408409
let oldDatabaseKey = try sut.generateKeys()

β€Žwire-ios-data-model/Tests/Authentication/EAR/EARServiceTests.swiftβ€Ž

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,10 @@ final class EARServiceTests: EARServiceTestsBase, @MainActor EARServiceDelegate
6161
func createSUT(
6262
canPerformMigration: Bool = false
6363
) async -> EARService {
64-
let sut = await EARService(
64+
let sut = EARService(
6565
accountID: userID,
6666
keyRepository: keyRepository,
6767
keyEncryptor: keyEncryptor,
68-
databaseContexts: [uiMOC, syncMOC],
6968
coreDataStack: coreDataStack,
7069
canPerformKeyMigration: canPerformMigration,
7170
earStorage: earStorage,
@@ -74,6 +73,9 @@ final class EARServiceTests: EARServiceTestsBase, @MainActor EARServiceDelegate
7473
authenticationContext: MockAuthenticationContextProtocol()
7574
)
7675

76+
await sut.setupDatabaseContexts(
77+
databaseContexts: [uiMOC, syncMOC]
78+
)
7779
sut.delegate = self
7880
return sut
7981
}

β€Žwire-ios-data-model/WireDataModel.xcodeproj/project.pbxprojβ€Ž

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,7 @@
386386
882B86BE2E8441AA008A50CA /* RemoveCoreCryptoKeysUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 882B86BD2E84419A008A50CA /* RemoveCoreCryptoKeysUseCase.swift */; };
387387
8838CFA42EC4CB1B00495012 /* NSManagedObjectContext+AccountDirectory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8838CFA32EC4CB0D00495012 /* NSManagedObjectContext+AccountDirectory.swift */; };
388388
884371662EB8E9F3003DA083 /* store2-132-0.wiredatabase in Resources */ = {isa = PBXBuildFile; fileRef = 884371652EB8E9F3003DA083 /* store2-132-0.wiredatabase */; };
389+
885B4BC12F6062B9001D7D72 /* EARServiceFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885B4BC02F6062B4001D7D72 /* EARServiceFactory.swift */; };
389390
88A80B632F320E9200F7E372 /* EARMessageEncryptionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A80B622F320E7500F7E372 /* EARMessageEncryptionService.swift */; };
390391
88A80B652F3381ED00F7E372 /* EARMigrator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 88A80B642F3381DF00F7E372 /* EARMigrator.swift */; };
391392
88B81BD62EC232C600248D7A /* store2-133-0.wiredatabase in Resources */ = {isa = PBXBuildFile; fileRef = 88B81BD52EC232C600248D7A /* store2-133-0.wiredatabase */; };
@@ -1123,6 +1124,7 @@
11231124
882B86BD2E84419A008A50CA /* RemoveCoreCryptoKeysUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveCoreCryptoKeysUseCase.swift; sourceTree = "<group>"; };
11241125
8838CFA32EC4CB0D00495012 /* NSManagedObjectContext+AccountDirectory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSManagedObjectContext+AccountDirectory.swift"; sourceTree = "<group>"; };
11251126
884371652EB8E9F3003DA083 /* store2-132-0.wiredatabase */ = {isa = PBXFileReference; lastKnownFileType = file; path = "store2-132-0.wiredatabase"; sourceTree = "<group>"; };
1127+
885B4BC02F6062B4001D7D72 /* EARServiceFactory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EARServiceFactory.swift; sourceTree = "<group>"; };
11261128
88A80B622F320E7500F7E372 /* EARMessageEncryptionService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EARMessageEncryptionService.swift; sourceTree = "<group>"; };
11271129
88A80B642F3381DF00F7E372 /* EARMigrator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EARMigrator.swift; sourceTree = "<group>"; };
11281130
88B81BD52EC232C600248D7A /* store2-133-0.wiredatabase */ = {isa = PBXFileReference; lastKnownFileType = file; path = "store2-133-0.wiredatabase"; sourceTree = "<group>"; };
@@ -2088,6 +2090,7 @@
20882090
E6E504282BC542C5004948E7 /* EARPrivateKeys.swift */,
20892091
E6E504292BC542C5004948E7 /* EARPublicKeys.swift */,
20902092
E6E5042A2BC542C5004948E7 /* EARService.swift */,
2093+
885B4BC02F6062B4001D7D72 /* EARServiceFactory.swift */,
20912094
E6E5042B2BC542C5004948E7 /* EARServiceDelegate.swift */,
20922095
E6E5042C2BC542C5004948E7 /* EARServiceFailure.swift */,
20932096
E6E5042D2BC542C5004948E7 /* EARStorage.swift */,
@@ -3572,6 +3575,7 @@
35723575
16CDEBFB2209D13B00E74A41 /* ZMMessage+Quotes.swift in Sources */,
35733576
EE997A1425062295008336D2 /* Logging.swift in Sources */,
35743577
F9331C841CB4191B00139ECC /* NSPredicate+ZMSearch.m in Sources */,
3578+
885B4BC12F6062B9001D7D72 /* EARServiceFactory.swift in Sources */,
35753579
BFCD502D21511D58008CD845 /* DraftMessage.swift in Sources */,
35763580
63B1336B29A503D100009D84 /* MLSGroupStatus.swift in Sources */,
35773581
EE3EFE9725305A84009499E5 /* ModifiedObjects+Mergeable.swift in Sources */,

β€Žwire-ios-share-engine/Sources/SharingSession.swiftβ€Ž

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ public final class SharingSession {
244244
featureRepository: featureRepository
245245
)
246246
let contextStorage = LAContextStorage()
247-
let earService = await EARService(
247+
248+
let earService = await EARServiceFactory.createEARService(
248249
accountID: accountIdentifier,
249250
databaseContexts: [
250251
coreDataStack.viewContext,

β€Žwire-ios-share-engine/Sources/SharingSessionLoader.swiftβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ public struct SharingSessionLoader {
318318
transportSession: transportSession
319319
)
320320
let contextStorage = LAContextStorage()
321-
let earService = await EARService(
321+
let earService = await EARServiceFactory.createEARService(
322322
accountID: accountID,
323323
databaseContexts: [
324324
coreDataStack.viewContext,

β€Žwire-ios-share-engine/WireShareEngineTests/BaseSharingSessionTests.swiftβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ class BaseTest: ZMTBaseTest {
176176
}
177177

178178
func createSharingSession() async throws -> SharingSession {
179-
let earService = await EARService(
179+
let earService = await EARServiceFactory.createEARService(
180180
accountID: accountIdentifier,
181181
databaseContexts: [coreDataStack.viewContext, coreDataStack.syncContext],
182182
coreDataStack: coreDataStack,

β€Žwire-ios-sync-engine/Source/SessionManager/SessionFactories.swiftβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ open class AuthenticatedSessionFactory {
118118
localDomain: BackendInfo.domain
119119
)
120120
let contextStorage = LAContextStorage()
121-
let earService = await EARService(
121+
let earService = await EARServiceFactory.createEARService(
122122
accountID: coreDataStack.account.userIdentifier,
123123
databaseContexts: [
124124
coreDataStack.viewContext,

0 commit comments

Comments
Β (0)