TeraSessionStore.swift (8401B)
1 import Foundation 2 import RadrootsKit 3 4 final class TeraProtectedDataMonitor: @unchecked Sendable { 5 private let lock = NSLock() 6 private var available: Bool 7 8 init(available: Bool) { 9 self.available = available 10 } 11 12 func update(available: Bool) { 13 lock.withLock { self.available = available } 14 } 15 16 func isAvailable() -> Bool { 17 lock.withLock { available } 18 } 19 } 20 21 enum TeraSessionPhase: Sendable, Equatable { 22 case starting 23 case identityRequired 24 case identityLocked(TeraAppIdentity) 25 case protectedDataUnavailable(TeraAppIdentity) 26 case recoveryRequired(TeraAppIdentity) 27 case corruptIdentity(TeraAppIdentity) 28 case configurationReconfigurationRequired(TeraConfigurationReconfigurationRequirement) 29 case running(TeraRuntimeSnapshot) 30 case stopped 31 case failed(TeraRuntimeFailure) 32 } 33 34 struct TeraConfigurationReconfigurationRequirement: Sendable, Equatable { 35 let generation: UInt64 36 let previousBlossomConfigFingerprint: String? 37 } 38 39 actor TeraSessionStore { 40 let configurationStore: TeraConfigurationStore 41 let identityStore: TeraIdentityStore 42 let runtimeClient: TeraRuntimeClient 43 let roots: RadrootsAppleFileRoots 44 let protectedData: TeraProtectedDataMonitor 45 private let automatesQualificationIdentity: Bool 46 let qualificationEvidenceStore: TeraRemoteQualificationEvidenceStore? 47 var generation = TeraSessionGeneration.initial 48 var phase: TeraSessionPhase = .starting 49 var removalInProgress = false 50 51 init( 52 configurationStore: TeraConfigurationStore, 53 identityStore: TeraIdentityStore, 54 runtimeClient: TeraRuntimeClient, 55 roots: RadrootsAppleFileRoots, 56 protectedData: TeraProtectedDataMonitor, 57 automatesQualificationIdentity: Bool = false, 58 qualificationEvidenceStore: TeraRemoteQualificationEvidenceStore? = nil 59 ) { 60 self.configurationStore = configurationStore 61 self.identityStore = identityStore 62 self.runtimeClient = runtimeClient 63 self.roots = roots 64 self.protectedData = protectedData 65 self.automatesQualificationIdentity = automatesQualificationIdentity 66 self.qualificationEvidenceStore = qualificationEvidenceStore 67 } 68 69 func updateProtectedDataAvailability(_ available: Bool) { 70 protectedData.update(available: available) 71 } 72 73 func currentPhase() -> TeraSessionPhase { 74 phase 75 } 76 77 func start() async -> TeraSessionPhase { 78 guard !removalInProgress else { return phase } 79 return await start(acceptingReconfiguration: false) 80 } 81 82 func applyConfigurationReconfiguration() async -> TeraSessionPhase { 83 guard !removalInProgress else { return phase } 84 return await start(acceptingReconfiguration: true) 85 } 86 87 func suspend() async { 88 generation = generation.invalidated() 89 let requestedGeneration = generation 90 await runtimeClient.suspend() 91 if generation == requestedGeneration, case .starting = phase { 92 phase = .stopped 93 } 94 } 95 96 private func start(acceptingReconfiguration: Bool) async -> TeraSessionPhase { 97 generation = generation.invalidated() 98 let requestedGeneration = generation 99 phase = .starting 100 do { 101 let identity = try await identityStore.loadAndMigrate() 102 guard generation == requestedGeneration else { 103 throw TeraRuntimeClientError.superseded 104 } 105 switch identity.state { 106 case .absent: 107 phase = .identityRequired 108 case .locked: 109 #if DEBUG 110 if automatesQualificationIdentity { 111 return await unlockIdentity() 112 } 113 #endif 114 phase = .identityLocked(identity) 115 case .protectedDataUnavailable: 116 phase = .protectedDataUnavailable(identity) 117 case .recoveryRequired: 118 phase = .recoveryRequired(identity) 119 case .corrupt: 120 phase = .corruptIdentity(identity) 121 case .unlocked: 122 phase = try await startUnlocked( 123 identity, acceptingReconfiguration: acceptingReconfiguration, generation: requestedGeneration 124 ) 125 } 126 } catch TeraRuntimeClientError.superseded { 127 return phase 128 } catch let TeraRuntimeClientError.startup(failure) { 129 guard generation == requestedGeneration else { return phase } 130 phase = .failed(failure) 131 } catch { 132 guard generation == requestedGeneration else { return phase } 133 phase = .failed( 134 .local( 135 operation: "session.start", 136 code: "ios.session.start_failed", 137 safeMessage: TeraUserMessages.text(for: error, fallback: .startupFailed) 138 ) 139 ) 140 } 141 return phase 142 } 143 144 private func startUnlocked( 145 _ identity: TeraAppIdentity, acceptingReconfiguration: Bool, 146 generation requestedGeneration: TeraSessionGeneration 147 ) async throws -> TeraSessionPhase { 148 let configuration = try await configurationStore.load() 149 try ensureCurrent(requestedGeneration) 150 if configuration.activationState == .reconfigurationRequired, 151 !acceptingReconfiguration 152 { 153 return .configurationReconfigurationRequired( 154 TeraConfigurationReconfigurationRequirement( 155 generation: configuration.generation, 156 previousBlossomConfigFingerprint: configuration 157 .previousBlossomConfigFingerprint 158 ) 159 ) 160 } 161 return try await startRuntime( 162 configuration: configuration, 163 identity: identity, 164 generation: requestedGeneration.requireActive(), 165 forceReconfiguration: configuration.activationState 166 == .reconfigurationRequired, 167 adoptBootstrapSettings: configuration.activationState 168 == .reconfigurationRequired 169 ) 170 } 171 172 func createIdentity(label: String? = nil) async -> TeraSessionPhase { 173 await runIdentityOperation { try await self.identityStore.create(label: label) } 174 } 175 176 func importIdentity( 177 _ material: RadrootsIdentitySecretMaterial, 178 label: String? = nil 179 ) async -> TeraSessionPhase { 180 await runIdentityOperation { try await self.identityStore.importIdentity(material, label: label) } 181 } 182 183 func lockIdentity() async -> TeraSessionPhase { 184 generation = generation.invalidated() 185 let requestedGeneration = generation 186 if case .running = phase, 187 let settings = try? await runtimeClient.mobileSettings() 188 { 189 guard isCurrent(requestedGeneration) else { return phase } 190 _ = try? await runtimeClient.applyIdentityCommand( 191 expectedRevision: settings.revision, 192 command: TeraIdentityCommand( 193 kind: .lock, 194 operationID: nil, 195 identityID: nil, 196 publicKeyHex: nil 197 ) 198 ) 199 } 200 guard isCurrent(requestedGeneration) else { return phase } 201 _ = try? await runtimeClient.stop() 202 guard isCurrent(requestedGeneration) else { return phase } 203 await identityStore.lock() 204 guard isCurrent(requestedGeneration) else { return phase } 205 let identity = await identityStore.snapshot() 206 guard isCurrent(requestedGeneration) else { return phase } 207 phase = .identityLocked(identity) 208 return phase 209 } 210 211 func unlockIdentity() async -> TeraSessionPhase { 212 await runIdentityOperation { try await self.identityStore.unlock() } 213 } 214 215 func recoverIdentity() async -> TeraSessionPhase { 216 await runIdentityOperation { try await self.identityStore.recover() } 217 } 218 219 private func runIdentityOperation( 220 _ operation: () async throws -> TeraAppIdentity 221 ) async -> TeraSessionPhase { 222 guard !removalInProgress else { return phase } 223 generation = generation.invalidated() 224 let requestedGeneration = generation 225 do { 226 try ensureCurrent(requestedGeneration) 227 _ = try await operation() 228 try ensureCurrent(requestedGeneration) 229 return await start() 230 } catch { 231 guard generation == requestedGeneration, !Task.isCancelled else { return phase } 232 return failIdentityOperation(error) 233 } 234 } 235 236 func isCurrent(_ requested: TeraSessionGeneration) -> Bool { 237 generation == requested && generation.isActive && !Task.isCancelled 238 } 239 240 func ensureCurrent(_ requested: TeraSessionGeneration) throws { 241 guard isCurrent(requested) else { throw TeraRuntimeClientError.superseded } 242 } 243 244 private func failIdentityOperation(_ error: Error) -> TeraSessionPhase { 245 generation = generation.invalidated() 246 phase = .failed( 247 .local( 248 operation: "identity.operation", 249 code: "ios.identity.operation_failed", 250 safeMessage: TeraUserMessages.text(for: error, fallback: .identityOperationFailed) 251 ) 252 ) 253 return phase 254 } 255 }